更新于 

Marschner头发渲染

本文在讲解之前,请您先仔细阅读

《头发着色原理》 理解Marschner渲染模型的基本推导,同时非常感谢雨轩大大提供的关于全局光照和pbr的系列讲解:《Unity PBR Standard Shader》

实验材料

本实验我们将实现一个褐色头发的少女,实现基于物理的头发渲染,因此不再是像上一篇文章那样自己模拟高光,而是要基于bsdf用公式实时渲染形成天使环效果,同时我们还将进一步解决Kajiya-Kay中无法解决的背光透射效果,最后我们还将根据NormalMap进行法线采样贴图,因此我们仍然使用上一个少女模型,只是这次我们使用一个新的头发模型以及所需要的纹理材质可以从下面的网盘中获取:

下面简单介绍一下几张图片的作用:

纹理贴图用来模拟头发效果,不提供颜色,透明度等贡献,采样贴图后可以得到一个类似于头发的蓝图。

头发灰度渐变实现纵向的渐变效果从而得到更加真实的头发效果,采样后主要是最终体现在color的alpha值上。

由于头发建模不可能真的一根一根头发丝构建,因此实际上细看是有多个mesh组成,但是这在高光处会体现的非常不真实,因此这里引入发现贴图来加深头发丝之间的凹凸从而给观察者一种“许多头发丝”的假象。

同样的,头发由于表面粗糙不一会形成表面散射同时对高光有扰动作用,因此这里提供一张粗糙纹理。

反射金属性光泽,由于头发基本上不会呈现非常明显的金属反光属性,因此这里基本上呈现全黑

头发散射纹理,主要会用到全局光照中。

环境光遮蔽,主要是加深头发之间的遮盖阴影效果

最后头发的颜色我们可以直接进行选择,最终根据baseColor图片计算出发射率,其实为了体现头发之间的厚度,应该还要使用一张HeightMap采样高度增加凹凸型,可惜博主在网上找了很多天都没有找到,如果好巧你有的话希望可以分享给我,接下来我们直接先上代码看一下实现思路。

Shader代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
Shader "Custom/Marschner"
{
Properties
{
// 头发主色调
_DiffuseColor("Hair Diffuse Color",Color)= (1,1,1,1)
// 基础反射率与纹理
_MainTex("Main Tex",2D)= "white" {}
// 灰度渐变
_AlphaTex("Alpha Tex",2D) = "white" {}
// 环境光遮蔽增加阴影效果
_AoTex("Ao Tex",2D) = "white" {}
// 粗糙度
_RoughnessTex("Roughness Tex",2D)="white" {}
// 散射能力
_EmissionTex("Emission Tex",2D)="white" {}
// 金属度
_Metallic("Metallic Tex",2D)="white" {}
// 法线贴图
_NormalMap("Nromal Map",2D) = "white" {}
// 设置凹凸程度
_BumpScale("Hair Bump Scale",Range(0,10))=0.2
// 偏移量
_Shift("Specular Shift Scale",Range(0,1))=0.04
}
SubShader
{
Tags { "RenderType" = "Opaque"}
LOD 200
// 关闭面剔除
Cull off

Pass{
// 会调用计算光照
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM

#include "UnityCG.cginc"
#include "Lighting.cginc"
// pbr光照需要
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"

#pragma vertex vert
#pragma fragment frag

#pragma multi_compile_fwdbase


struct appdata{
// 顶点坐标,采样坐标,发现,切线
float4 vertex :POSITION;
float2 uv : TEXCOORD0;
float3 normal :NORMAL;
float4 texcoord1 : TEXCOORD1;
float4 tangent : TANGENT;
};

struct v2f{
// 裁剪坐标,采样坐标,法线,副切线,世界坐标,切线空间转世界空间的矩阵
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 bitangent :TEXCOORD2;
float3 worldPos : TEXCOORD3;
// 变换矩阵
float4 TtoW0 : TEXCOORD4;
float4 TtoW1 : TEXCOORD5;
float4 TtoW2 : TEXCOORD6;

// 光照贴图阴影坐标
UNITY_SHADOW_COORDS(7)
};

struct SurfaceOutputHair{
// 会使用的头发性质,反射率,法线,粗糙度,光照吸收模拟环境光遮蔽,透明度,离心率,金属度,高光偏移量
half3 Albedo;
half3 Normal;
half Roughness;
half AO;
half Alpha;
half3 Emission;
half Metallic;
half Shift;
};

float4 _DiffuseColor;
sampler2D _MainTex;
sampler2D _AlphaTex;
sampler2D _AoTex;
sampler2D _RoughnessTex;
sampler2D _EmissionTex;
sampler2D _MetallicTex;
sampler2D _NormalMap;


fixed _Shift;
fixed _BumpScale;

// π和π^0.5,主要是加速计算
#define PI 3.1415926
#define SQRT2PI 2.50663

v2f vert(appdata v){
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o);//初始化归零
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float3 worldPos=mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal=UnityObjectToWorldNormal(v.normal);
float3 worldTangent=UnityWorldToObjectDir(v.tangent).xyz;
float3 worldBitangent=cross(worldTangent,worldNormal)*v.tangent.w;
o.worldPos = worldPos;
o.normal=UnityObjectToWorldNormal(v.normal);
o.bitangent =cross(v.normal,v.tangent.xyz)*v.tangent.w*unity_WorldTransformParams.w;
o.TtoW0 = float4(worldTangent.x,worldBitangent.x,worldNormal.x,worldPos.x);
o.TtoW1 = float4(worldTangent.y,worldBitangent.y,worldNormal.y,worldPos.y);
o.TtoW2 = float4(worldTangent.z,worldBitangent.z,worldNormal.z,worldPos.z);
// 光照贴图纹理坐标
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
return o;
}

// 采样法线贴图获取法线
float3 getNormal(float4 TtoW0,float4 TtoW1,float4 TtoW2,float2 uv){
// 采样发现,这里注意必须是设置为NromalMap
float4 packedNormal = tex2D(_NormalMap,uv);
//图片没有设置成normal map
//float33 tangentNormal;
//tangentNormal.xy = (packedNormal.xy * 2 - 1)*_BumpScale;
//tangentNormal.z = sqrt(1 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 解码得到采样后的切线坐标系下的法线
float3 tangentNormal=UnpackNormal(packedNormal);
tangentNormal.xy*=_BumpScale;
// 需要转换为世界坐标系下的法线
float3 worldNormal = normalize(float3(dot(TtoW0.xyz,tangentNormal),dot(TtoW1.xyz,tangentNormal),dot(TtoW2.xyz,tangentNormal)));
return worldNormal;

}

inline float square(float x){
// 不用pow加速计算
return x*x;
}

// M项使用的高斯分布计算式
float Hair_g(float B,float Theta){
return exp(-0.5 * square(Theta) / (B * B)) / (SQRT2PI * B);
}

// 菲涅尔项
inline float3 SpecularFresnel(float F0, float x) {
return F0 + (1.0f - F0) * pow(1 - x, 5);
}

// 核心公式,计算BSDF,这里采用UE类似的模拟加速计算,参考文章:
// [Marschner et al. 2003, "Light Scattering from Human Hair Fibers"]
// [Pekelis et al. 2015, "A Data-Driven Light Scattering Model for Hair"]
float3 HairSpecularMarschner(SurfaceOutputHair sh,float3 N,float3 V,float3 L,float Shadow,float Backlit,float Area){
float3 S = 0;
const float dotVL = dot(V,L);
const float SinThetaL = clamp(dot(N,L),-1.f,1.f);
const float SinThetaV = clamp(dot(N,V),-1.f,1.f);
float CosThetaL = sqrt(max(0,1-SinThetaL*SinThetaL));
float CosThetaV = sqrt(max(0,1-SinThetaV*SinThetaV));
float CosThetaD = sqrt((1 + CosThetaL * CosThetaV + SinThetaL * SinThetaV) / 2.0);

const float3 Lp = L-SinThetaL*N;
const float3 Vp = V-SinThetaV*N;
const float CosPhi = dot(Lp,Vp)*rsqrt(dot(Lp,Lp)*dot(Vp,Vp) + 1e-4);
const float CosHalfPhi = sqrt(saturate(0.5+0.5*CosPhi));

/**
* η'的拟合
* 原型:η' = sqrt( η * η - 1 + CosThetaD^2) / CosThetaD;
* float n_prime = sqrt( n*n - 1 + Pow2( CosThetaD ) ) / CosThetaD;
* 拟合思路:η即人类发丝折射率写死为1.55, 拟合后的η'如下:
* η' = 1.19 / CosThetaD + 0.36 * CosThetaD;
*/
float n=1.55;
float n_prime = 1.19/CosThetaD+0.36*CosThetaD;

float Shift=sh.Shift;
float Alpha[] = {
-Shift*2,
Shift,
Shift*4
};

float B[]={
Area+square(sh.Roughness),
Area+square(sh.Roughness)/2,
Area+square(sh.Roughness)*2
};

float F0=square((1-n)/(1+n));

float3 Tp;
float Mp,Np,A,f,Fp,h,a;

// R
// N_R\left(\theta_i, \theta_r, \phi\right)=\left(\frac{1}{4} \Cos \frac{\phi}{2}\right) A(0, h)
Mp=Hair_g(B[0],SinThetaL+SinThetaV-Alpha[0]);
A=SpecularFresnel(F0,sqrt(saturate(0.5 + 0.5 * dotVL)));
Np=0.25*CosHalfPhi*A;
S+=Mp*Np*lerp(1,Backlit,saturate(-dotVL));

// TT
/**
* Step1: 对h的拟合
* h的原型计算公式如下:
* float h = CosHalfPhi * rsqrt( 1 + a*a - 2*a * sqrt( 0.5 - 0.5 * CosPhi ) );
* float h = CosHalfPhi * ( ( 1 - Pow2( CosHalfPhi ) ) * a + 1 );
*
* 最终曲线拟合完的h_tt如下:
*/
Mp=Hair_g(B[1],SinThetaL+SinThetaV-Alpha[1]);
a=1/n_prime;
h=CosHalfPhi*(1+a*(0.6 - 0.8 * CosPhi));

/**
* Step2:η'的拟合
* 原型:η' = sqrt( η * η - 1 + CosThetaD^2) / CosThetaD;
* 拟合思路:η即人类发丝折射率写死为1.55, 拟合后的η'如下:
* η' = 1.19 / CosThetaD + 0.36 * CosThetaD;
* 代码往上翻
*/
f=SpecularFresnel(F0,CosThetaD*sqrt(saturate(1 - h*h)));
Fp=square(1 - f);

/**
* Step3:对于吸收项T的拟合:选择Pixar的方案但没有直接用,还是做了拟合
*
* T与γ_t的计算原型如下:
* T(θ,φ) = e^{-2 * μ_a * (1 + Cos(2γ_t)) / (Cosθt)},其中γt = sin^-1(h / η')
* 代码实现:float yi = asinFast(h); float yt = asinFast(h / n_prime);
*
* 参考Pixar的实现:
* T(θ,φ) = e^{-epsilo(C) * Cosγt / Cosθd}
* 代码实现:float3 Tp = pow( GBuffer.BaseColor, 0.5 * ( 1 + Cos(2*yt) ) / CosThetaD );
*/
// 这里C直接使用折射率进一步简化
Tp = pow(sh.Albedo, 0.5 * sqrt(1 - square((h * a))) / CosThetaD);
/**
* Step4: 对分布项D的拟合
* 技术原型:Pixar's Logistic Distribution Function
* D(φ,s, μ) = (e^{(φ - μ) / s}) / (s^{1 + e^{(φ - μ) / s}}^2)
*
* 考虑s_tt实际上贡献很小,因此近似如下:
* D_TT(φ) = D(φ,0.35,Π) ≈ e^{-3.65Cosφ - 3.98}
*/
Np=exp(-3.65*CosPhi-3.98);
S+=Mp*Np*Fp*Tp*Backlit;

// TRT
/**
* Step1 :对h的拟合
* h_trt = sqrt(3) / 2
* float h = 0.75;
*/
Mp=Hair_g(B[2],SinThetaL+SinThetaV-Alpha[2]);
f=SpecularFresnel(F0,CosThetaD*0.5f);
Fp=square(1 - f)*f;

/**
* Step2:对于吸收项T的拟合
* T_TRT(θ,φ) = C^{0.8 / Cosθd}
*/
Tp=pow(sh.Albedo,0.8/CosThetaD);
Np=exp(17*CosPhi-16.78);
S+=Mp*Np*Fp*Tp;

return S;
}

// Kajiya推导的解析解,主要是模拟头发间的散射
float3 HairDiffuseKajiya(SurfaceOutputHair sh,float3 N,float3 V,float3 L,float Shadow,float Backlit,float Area){
float3 S = 0;
float KajiyaDiffuse = 1-abs(dot(N,L));

float3 FakeNormal = normalize(V-N*dot(V,N));
N=FakeNormal;

// Hack approximation for multiple scattering.
float Wrap=1;
float dotNL=saturate((dot(N,L)+Wrap)/square(1+Wrap));
float DiffuseScatter=( (1 / PI) * lerp(dotNL, KajiyaDiffuse, 0.33))*sh.Metallic;
float Luma=Luminance(sh.Albedo);
float3 ScatterTint=pow(sh.Albedo/Luma,1 - Shadow);
S=sqrt(sh.Albedo)*DiffuseScatter*ScatterTint;

return S;
}

float3 HairShading(SurfaceOutputHair sh,float3 N,float3 V,float3 L,float Shadow,float Backlit,float Area){
float3 S= float3(0,0,0);
// add Specualr
S=HairSpecularMarschner(sh,N,V,L,Shadow,Backlit,Area);
// add Diffuse
S+=HairDiffuseKajiya(sh,N,V,L,Shadow,Backlit,Area);
// 校验一下,保证S不会小于零
S=-min(-S,0.0);
return S;
}

float3 HairBxDF(SurfaceOutputHair sh,float3 N,float3 V,float3 L,float Shadow,float Backlit,float Area){
// 这里sh提供表面材质属性,Shadow提供头发之间的阴影遮蔽强度,Backlit影响透光强度,Area决定散射强度
return HairShading(sh,N,V,L,Shadow,Backlit,Area);
}

inline void LightingHair_GI(SurfaceOutputHair sh,UnityGIInput giInput,inout UnityGI gi){
// 计算全局光照效果,会更新gi中的light和diffuse参数,参考了giInput,法线和环境光遮蔽生成
gi=UnityGlobalIllumination(giInput,sh.AO,sh.Normal);
}

inline fixed4 LightingHair(SurfaceOutputHair sh,float3 viewDir,UnityGI gi){
fixed4 c = fixed4(0,0,0,sh.Alpha);
// 直接光照,由于光照直射强度高,所以是提供透射的主要因素,但是散射能力弱
c.rgb=gi.light.color*HairBxDF(sh,sh.Normal,viewDir,gi.light.dir,0.1f,0.5f,0.0f);
// 间接光照,基本不提供透射,但是简介光照主要提供表面散射,是头发暗部底色的主要贡献
float3 reflect=normalize(viewDir-sh.Normal*dot(sh.Normal,viewDir));
// 此时贡献光源不是来自灯光,而是根据视线反射对应的反射处,别忘了乘直径
c.rgb+=gi.indirect.diffuse*6.28f*HairBxDF(sh,sh.Normal,viewDir,reflect,0.5f,0.0f,0.5f);

return c;
}

fixed4 frag(v2f i) : SV_TARGET{
float3 l=normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 v=normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 n=getNormal(i.TtoW0,i.TtoW1,i.TtoW2,i.uv);
// float3 n = normalize(i.normal);

float3 albedo = tex2D(_MainTex,i.uv);
float ao =tex2D(_AoTex,i.uv);
float alpha=tex2D(_AlphaTex,i.uv);
float roughness=tex2D(_RoughnessTex,i.uv);
float metallic=tex2D(_MetallicTex,i.uv);
float3 emission=tex2D(_EmissionTex,i.uv);

SurfaceOutputHair sh;
UNITY_INITIALIZE_OUTPUT(SurfaceOutputHair, sh);//初始化归零
sh.Normal=n;
sh.AO=ao;
sh.Albedo = fixed4(albedo*_DiffuseColor,alpha);
sh.Roughness=roughness;
sh.Emission=emission;
sh.Alpha=alpha;
sh.Metallic=metallic;
sh.Shift=_Shift;

// compute lighting & shadowing factor
//计算光照衰减和阴影
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)

// 计算全局光照
UnityGI gi;
UNITY_INITIALIZE_OUTPUT(UnityGI, gi);//初始化归零
gi.indirect.diffuse=0;
gi.indirect.specular=0;
gi.light.color = _LightColor0.rgb;
gi.light.dir=l;

UnityGIInput giInput;
UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);//初始化归零
giInput.light=gi.light;
giInput.worldPos=i.worldPos;
giInput.worldViewDir=v;
giInput.atten=atten;
giInput.probeHDR[0]=unity_SpecCube0_HDR;
giInput.probeHDR[1]=unity_SpecCube1_HDR;

// 计算全局光照
LightingHair_GI(sh,giInput,gi);
// 计算最终颜色值
fixed4 col = LightingHair(sh,v,gi);
// fixed4 col = fixed4(gi.light.color,1.0);
return col;
}

ENDCG
}
}

FallBack "Diffuse"
}

实现思路与细节探讨

全局光照

由于我们需要实现基于物理的光照渲染,因此不能直接通过光线与法线求一个三角函数再乘上颜色来实现了,因为这样我们无法实现背光的间接光照效果,会导致背光头发基本上全暗非常不真实,在Kajiya-Kay中我们只是认为添加了一圈镜面反光效果来模拟,但是这会导致背光处的高光基本上和面光处的高光强度一致,这在现实世界中显然是不可能的。

因此在Marschner中的实现,我们首先要调用全局光照实时计算头发不同地方的光照强度,这里我们可以使用globalIllumination来实现,因此我们需要初始化UnityGIInput和UntiyGI传入初始的光照信息计算,这里具体参考代码的358-366行,同时需要添加背光阴影,因此我们还需要使用forwardbase计算光照的衰减和阴影效果,并存储到一个光照贴图中,使用阴影坐标进行采样,具体参考代码的355行和72行。具体的全局光照计算需要在globalIllumination中传入三个参数,见代码的312行:

1
2
3
4
5
inline void  LightingHair_GI(SurfaceOutputHair sh,UnityGIInput giInput,inout UnityGI gi){
// 计算全局光照效果,会更新gi中的light和diffuse参数,参考了giInput,法线和环境光遮蔽生成
gi=UnityGlobalIllumination(giInput,sh.AO,sh.Normal);
}

其中sh使我们自定义的一个结构体用来存储,第二个参数是计算全局光照前的一些列光照信息,我们使用giInput存储,而第三个参数就是最终会将间接光照颜色以及散射系数存储的变量,他既作为输入变量也是最终的输出变量gi。这里我们可以暂时将377行注释掉并将378行解注释查看最终全局光照的效果:

可以看到随着光照方向不同,生成的全局光照贴图也会随之变化,正式基于这种贴图效果我们再通过加上渲染方程着色即可得到真实的背光效果。

着色函数

查看代码317行,我们可以看到g函数主要是通过融合直接光照与间接光照两个部分实现头发额着色,其中BSDF是实现的核心我们会在后面讲解,这里主要注意一下Shadow,Backlit以及Area参数:

1
2
3
4
5
6
7
8
9
10
11
inline fixed4 LightingHair(SurfaceOutputHair sh,float3 viewDir,UnityGI gi){
fixed4 c = fixed4(0,0,0,sh.Alpha);
// 直接光照,由于光照直射强度高,所以是提供透射的主要因素,但是散射能力弱
c.rgb=gi.light.color*HairBxDF(sh,sh.Normal,viewDir,gi.light.dir,0.1f,0.5f,0.0f);
// 间接光照,基本不提供透射,但是简介光照主要提供表面散射,是头发暗部底色的主要贡献
float3 reflect=normalize(viewDir-sh.Normal*dot(sh.Normal,viewDir));
// 此时贡献光源不是来自灯光,而是根据视线反射对应的反射处,别忘了乘直径
c.rgb+=gi.indirect.diffuse*6.28f*HairBxDF(sh,sh.Normal,viewDir,reflect,0.5f,0.0f,0.5f);

return c;
}

其中Shadow主要决定阴影强度,这里为了得到和图片相似的效果设定了直接光照0.1f和间接光照0.5f,我们要注意直接光照是光源直接提供的,而间接光照是光经过多次反弹最终打到渲染物体上的,因此直接光照照射的物体颜色会较明亮,阴影不会非常明显,因此值要小于间接光照。

Backlit主要是用来决定Marschner特有的透光效果,前文我们讲解过头发在背光时并不是全暗的,而是再边缘厚度薄的地方会有透光效果,因此仍能看到亮色,而这里就是决定透光颜色强度的,首先光能透过头发肯定是强光能量光穿过的效果,因此间接光照基本上不提供透光效果,全部由直接光照提供,而具体的透光多少可以调整,这里我们为了最终观察效果明显设定为0.5f。

Area其实在MarschnerSpecular函数中找到,主要是用来计算粗糙度的,我们可以理解为决定散射的强度,因此这里直接光照较少,而间接光照提供主要贡献。

Kajiya-Kay散射

虽然我们是使用Marschner模型计算,但是其主要是计算头发的高光效果,提供了R,TRT,TT等不同的光照反射,但是头发由于表面粗糙还会有散射效果,这里我们也是参考了UE4的实现,加入一种基于Kajiya的散射作为头发的底色,因此我们可以在代码的296行看到无论是直接光照还是间接光照我们都是融合了基于MarschnerSpecular高光和Kajiya_Kay散射效果,那么首先我们查看一下Kajiya_Kay的散射效果,这里我们将299行暂时注释掉然后查看效果:

可以看到结合之前计算的全局光照贴图,我们得到了一个不错的头发效果,但是还有几点问题没有解决,一个是背光时透光效果还未实现,同时并未加入高光的计算因此头发的天使环效果并不明显,接下来我们看一下MarschnerSpecular中对R,TRT,TT的逼近来实现更加真实的高光效果。

Marschner模型的R,TRT,TT近似渲染效果

这里为了减少实时计算开销,我们采用近似逼近的R,TRT,TT的N项和M项计算,同时将三者累加到S中得到最终颜色计算结果,这里我们加入高光以后首先可以看到出现了天使环效果,并且由于是使用R,TRT实时计算的,因此并不向Kajiya-Kay中那样手动移动主高光和次高光得到的,而是根据光照以及不同的头发位置自动计算得到的,因此渐变效果也更加真实:

最后我们再重点看一下Marschner主要解决的透光效果,这里可以注释掉254行查看透光和不透光的效果,这里给出对比图:

思考:什么是基于物理的渲染?

最后参考下面两张对比图我们很容易就可以理解基于物理渲染的优势:

所谓基于物理并不是指完全像真实世界一样获取各种物理量的参数计算,而是指我们是通过一定的数学公式推导得到最终的渲染计算效果,物理量未必数值上和真实世界完全一致,但是由于是以一种物理量带入数学公式计算得到的着色效果,因此当我们更改渲染物体表面材质或者光照角度等环境属性时无需再调整即可得到基本正确的着色效果,这也正是基于物理的渲染相较于非物理渲染的优势,但是缺陷就是实现更为复杂,同时计算开销也会较大,也正是如此实际上最终选择何种方法是仁者见仁,智者见智的,最终的目的都是要能够用最小的成本“以假乱真”

效果图

最终祭出我这几天实现的一个效果图吧,由于本人技术能力太差,最终实现效果并不理想但毕竟是自己第一次实现还是记录一下吧😁