从4.12上手,进行一个阶段性总结,作为复习,也顺便瞻前顾后一下。
(当然,只有暂时记得住的东西)
1:TBN矩阵
TBN矩阵的本质就是沟通起法线贴图和世界空间下的法线的通道。
TBN矩阵毕竟还是一个矩阵
具体构成有:
T(切向方向)
B(副切线方向)
N(世界空间法线方向)
传统意义上的法线贴图里存在了模型的法线方向的各种信息,我们可以在PS中从贴图中找到法线贴图
TBN矩阵的具体使用方向:
是个矩阵,就要获取来自模型中的顶点的各种属性,比如,切线,副切线,还有世界坐标下的法线。
现在构造顶点找色器的结构体中拿去来自Unity的API,构造好几个贴图通道,
分别是世界空间法线位置(normalws),世界顶点位置(positionWS),物体到摄像机的方向向量(viewDirWS),世界空间下顶点的切线向量(tangentWS),世界空间下顶点的副切线方向(bitangentWS)
然后就是顶点着色器的编写:
其中:输入来自结构体的各种变换。
从TBN矩阵中来看就是赋值,给结构体类型的OUTPUT的属性中输入来自模型中的世界空间法线向量:
给切线tangentWS从物体空间转换为世界空间。切线在纹理映射中非常重要,尤其是在法线贴图等效果中,用来影响光照计算。
副切线同理,但副切线是切线与法线的叉积,表示纹理的“垂直”方向。
至此,顶点找色器处理完毕,接下来是片元找色器方面。
获取法线贴图,进行归一化。
菲涅尔效果,Fresnel效果用于模拟表面反射强度随着视角变化的效果。
随后便是TBN矩阵构建。输入来自顶点的三个向量。
viewDir在经过TBN矩阵转化后,其中包含了法线贴图中的方向和位置信息。用viewTS作为名字储存
至此,viewTS便是通过TBN矩阵变换后的ViewDir,里面包含了法线贴图的许多信息,
就可以让法线贴图的作用在模型上得到彰显。
2:RIm边缘光和菲涅尔效应的区别
当初在了解菲涅尔效应后总感觉和边缘光好像,今天就进行一下理解和区分:
菲涅尔公式:
菲涅尔模拟的是,光线在远处的边缘反射,比如这一部分
越远,光线在河上反射的强度就越高,因此,这一效果通常被用来模拟金属边缘或者透明材质;
Rim边缘光:
Rim边缘光指的就是摄像机从正面看角色,看不到任何效果,但是在边缘方向,和摄像机视角越垂直,颜色表现就越明显。
更多的只是在进行一种光照操作,就用最简单的办法解决好模型表面的视觉效果。
但这并不符合现实
所以,往往菲涅尔效果会被运用到现实渲染,而边缘光往往用在非现实渲染上。
3:SDF卡通渲染
模仿原神的卡通渲染,该功能主要是在应对传统的光照模型下对阴影的紊乱,所采取的改进。
简单来讲,就是生成一张纹理阴影贴图,给它贴在角色的表面上。模拟好在光影变化。
逻辑是:
光照-》打在脸上-》和NdotL接近的部分-》阴影相对来说较大,相反,在背光处阴影较小。
1:获取SDF贴图:
2:先模拟好前方和右方的向量:
3:计算好光线打造角色脸上的光线效应:
把光线拉到0到1的范围内,进行平方只是为了让变化更加缓和,这样就能模拟光从右边打过来的效果。
这一段看起来比较复杂,但其实就执行了一个操作,如果光从左边打过来,那就翻转UV,重新执行一次操作就行。
光照模型:
1:CookTorrance光照模型
这一篇我已经做过了,详细得可转到:
在这先简单的说明一下
公式贴在这里了:
这个模型是在维持住现实光照下的能量守恒所模拟的光照模型。所以,实际上这个模型能模拟出更好的现实光照。
D:微平面法线分布函数。
简单来说,就是自己定义表面的粗糙度,所模拟出光线在这个粗糙度下的反射状况。
F:菲涅尔效应。上面已经写清楚了。
G:微平面几何遮挡效应。
描述了表面微结构的遮挡效应,考虑了光线在表面上的反射是否被其他微面阻挡。
三个代码就贴在这里了:
float Fresnelfunction(float fresnelPower,float NdotL,float fresnelIntensity){
fresnelPower=saturate(fresnelPower);
fresnelIntensity=saturate(fresnelIntensity);
return max(0.3,fresnelIntensity+(1-fresnelPower)*pow(abs(1-NdotL),fresnelIntensity));
}
float D_GGX(float3 H,float3 N,float alpha){
float NdotH=max(dot(N,H),0);
float alpha2=alpha*alpha;
float denom=1+(alpha2-1)*NdotH*NdotH;
return alpha2/(PI*denom*denom);
}
float G_SchilickGGX(float3 V,float3 L,float3 N,float alpha){
float k=(alpha+1)*(alpha+1)/8.0;
float NdotL=max(dot(N,L),0);
float NdotV=max(dot(N,V),0);
NdotL=smoothstep(0.4,1.0,NdotL);
NdotV=smoothstep(0.4,1.0,NdotV);
float G=NdotL/(NdotL*(1.0-k)+k)*NdotV/(NdotV*(1.0-k)+k);
return G;
}
float G_Smith(float3 V,float3 L,float3 N,
float alpha,float fresnelPower,
float fresnelIntensity,
float AttenuationIntensity,
float useAniso,float3 T,float3 B,float Anisotropy){
float UseAniso = saturate(useAniso);
float k=(alpha+1)*(alpha+1)/8.0;
float LdotN=max(dot(L,N),0.0);
float VdotN=max(dot(V,N),0.0);
float3 H=normalize(L+V);
float F=Fresnelfunction(fresnelPower,LdotN,fresnelIntensity);
float D=lerp(D_GGX(H,N,alpha),D_anisotropy(alpha,Anisotropy,H,T,B,N),useAniso);
float G=lerp(G_SchilickGGX(V,L,N,alpha),G_anisotropy(T,V,L,B,alpha,Anisotropy,N),useAniso);
float R=F*D*G/(4*LdotN*VdotN);
return max(R,AttenuationIntensity);
}
无各向异性下的具体效果:
无渲染
有光照模型下的结果
2:Kajiya-Kay光照模型
虽然这么说本末倒置,但我的路线确实这样学的。
1:来源可以看这篇文章
2:原理
各向异性(英语:Anisotropy),或作异向性、非均向性,与各向同性相反,指物体的全部或部分物理、化学等性质随方向的不同而有所变化的特性,例如石墨单晶的电导率在不同方向的差异可达数千倍,又如天文学上,宇宙微波背景辐射亦拥有些微的非均向性。
而Kajiya模型确实是比较合理且简单的各向异性模型了。
这是代码:
float StrandSpecular(float3 T, float3 V, float L, float exponent)
{
float3 H = normalize(L + V);
float dotTH = dot(T, H);
float sinTH = sqrt(1.0 - dotTH*dotTH);
float dirAtten = smoothstep(-1.0, 0.0, dot(T, H));
return dirAtten * pow(sinTH, exponent);
}
其中,困惑的其实还是对T和B的判定。
T:物体表面的切线。
B:某一点的副切线。
以下是我的理解:
由于模型和UV的关系,所以,我们在贴UV贴图的时候,总是有两类坐标,模型坐标和纹理坐标,模型坐标定义了模型的起起伏伏,各种变化。
纹理坐标定义了模型的某个点位置该为什么颜色之类的。
重点还是理解dirAtten:
从这个图来看
如果我们把头发看成一个圆柱,我们可以假定B,
B:副切线,意味着和头发丝同样方向。
T:切线,头发丝的切线方向。
具体图片
注意,这个切线在我的理解里在世界坐标并不存在,它和B映射的是纹理坐标,B是世界空间中和纹理空间中都存在的东西。
在世界空间里,B意味着头发的方向。
T和B构成纹理坐标。N代表世界法线。世界法线垂直于T和B构成的纹理坐标系
不妨从代码中来理解原理:
for ( int i=0; i<vertices.size(); i+=3)
{
// Shortcuts for vertices
glm::vec3 & v0 = vertices[i+0];
glm::vec3 & v1 = vertices[i+1];
glm::vec3 & v2 = vertices[i+2];
// Shortcuts for UVs
glm::vec2 & uv0 = uvs[i+0];
glm::vec2 & uv1 = uvs[i+1];
glm::vec2 & uv2 = uvs[i+2];
// Edges of the triangle : postion delta
glm::vec3 deltaPos1 = v1-v0;
glm::vec3 deltaPos2 = v2-v0;
// UV delta
glm::vec2 deltaUV1 = uv1-uv0;
glm::vec2 deltaUV2 = uv2-uv0;
float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
glm::vec3 tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y)*r;
}
这是关于切线的解读,直接看最后一句话。先把和现实坐标关联的pos部分先不看,假如没有pos那部分,那么切线算的就是贴图空间内的切线。也就是他的导数。
理解好在没有涉及世界坐标下的pos后,我们来看看加入Pos后是个什么样子?
当引入位置坐标pos后,切线变得与模型表面的几何形状紧密相关,并且涉及到如何将纹理坐标变化映射到三维模型表面上。
也就是说,uv坐标收到了世界空间的影响。
借此来算出世界空间下的切线。
有了切线和法线,就能进行向量叉乘,得到副切线,而副切线意思就是发丝的方向。
output.tangentWS = float4(T, input.tangentOS.w);
output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangentOS.w;
所以,模型中副切线的变化才是切线变化的根本原因,只要对副切线进行一些改进,就操控副切线的变化。
具体如下:
这是加了法线贴图后的结果。
3:各向异性改进下的CookTorrance光照模型:
为什么要改进?
-
现实世界中的许多表面并不是 各向同性的,例如 布料、丝绸、木材和金属 等表面具有明显的纹理和方向性,这种表面的光反射强度是方向依赖的。
-
传统的 各向同性 Cook-Torrance 模型 在这种表面上会出现一些不真实的光照效果,例如在反射方向上无法产生真实的光泽感。
所以目的确认:为了更加真实。
具体文章参考:
Position-free Multiple-bounce Computations for Smith Microfacet BSDFs笔记 - Pangaolee的文章 - 知乎
实时渲染的门门道道:从基本光照模型到PBR和NPR - 鱼露的文章 - 知乎https://blog.csdn.net/linjf520/article/details/133755168?fromshare=blogdetail&sharetype=blogdetail&sharerId=133755168&sharerefer=PC&sharesource=Kaliaailia&sharefrom=from_link
那么代码如下。
inline float D_anisotropy(half _Anisotropy,float r,float3 H,float3 T,float3 B,float3 N){
half anisotropy=_Anisotropy;
float at=max(pow(r*(1.0+anisotropy),2),0.001);
float ab=max(pow(r*(1.0-anisotropy),2),0.001);
half a_2=at*ab;
half TdotH=dot(T,H);
half BdotH=dot(B,H);
half NdotH=dot(N,H);
float D_2=1/((PI * at * ab) * pow((pow(TdotH, 2) / pow(at, 2) +
pow(BdotH,2)/pow(ab,2) +
pow(NdotH,2)),2));
float normalization=1.0/(at*ab+1e-4);
return D_2*normalization;
}
inline float G_anisotropy(float3 t,float3 v,float3 l,float3 b,float r,float anisotropy,float N)
{
float at=max(pow(r*(1.0+anisotropy),2),0.001);
float ab=max(pow(r*(1.0-anisotropy),2),0.001);
half TdotV = dot(t, v);
half TdotL = dot(t, l);
half BdotV = dot(b, v);
half BdotL = dot(b, l);
half NdotV = dot(N, v);
half NdotL = dot(N, l);
float lambdaV = NdotL * length(half3(at * TdotV, ab * BdotV, NdotV));
float lambdaL = NdotV * length(half3(at * TdotL, ab * BdotL, NdotL));
float v_a = saturate(0.5 / (lambdaV + lambdaL));
return v_a;
}
改进还是对D,微表面平面的改进和对G,微表面遮挡的改进
共同部分:引入各向异性效果,超过0就表示为圆环似的高光,0就表示为点状高光、
at
和 ab
分别表示在切线方向(T)和副切线方向(B)的 微表面法线分布
T,B切线方向和副切线方向,具体以及在上文中有所解释,
效果如下
D:
多了这一项:
float D_2=1/((PI * at * ab) * pow((pow(TdotH, 2) / pow(at, 2) +
pow(BdotH,2)/pow(ab,2) +
pow(NdotH,2)),2));
float normalization=1.0/(at*ab+1e-4);
return D_2*normalization;
G:
inline float G_anisotropy(float3 t,float3 v,float3 l,float3 b,float r,float anisotropy,float N)
{
float at=max(pow(r*(1.0+anisotropy),2),0.001);
float ab=max(pow(r*(1.0-anisotropy),2),0.001);
half TdotV = dot(t, v);
half TdotL = dot(t, l);
half BdotV = dot(b, v);
half BdotL = dot(b, l);
half NdotV = dot(N, v);
half NdotL = dot(N, l);
float lambdaV = NdotL * length(half3(at * TdotV, ab * BdotV, NdotV));
float lambdaL = NdotV * length(half3(at * TdotL, ab * BdotL, NdotL));
float v_a = saturate(0.5 / (lambdaV + lambdaL));
return v_a;
}
问题:
1:在各向异性高光下的部分全黑(全白)
比如这里
对象,Kajiya-Kay模型。
改进:
float StrandSpecular=max(StrangSpecular(shiftedT,viewDir,lightDir,_StrangSpecularexponent,_Specularscale),0);
加个最大值限制就行,因为这是因为高光背面没有被考虑的问题,所以只要限定最小就行了,
同理,发白是最大没有被限制,最小设置为1就行。
对象:改进后的BDRP
R=min(R,1.0);