效果:
概述:
使用多段面片实现ui头发的渲染
着色器使用Kajiya-Kay和Marschner的物理光照混合实现
简单的近似深度排序方案
头发在视觉上很重要,大部分人头上拥有10w-15w的发丝,许多不同的发型,最终幻想 - 灵魂深处》总渲染时间的约 25% 花费在主角的头发上
为什么我们选择多边形头发模型而不是绘制线?
几何复杂度低于线的渲染
深度排序更快
契合我们使用到的渲染管线
头发模型的创作
使用贴片模拟头发的体积,这样可以节约大量的面数
使用近似自阴影的环境光遮蔽
需要的纹理贴图:
基础纹理贴图,头发的基础色,纵向拉伸的noise贴图
半透明贴图 其实一般都会设置到基础颜色贴图的a通道,用于裁剪剔除
高光偏移贴图和高光扰动noise贴图渲染实现:
基于Kajiya-Kay光照模型
使用了各向异性光照
计算反射的角度不再使用法向N而是改为沿着头发朝向的Bitangent(不是图片中的tangent,tangent在unity中生成,是基于模型uv的u方向,而一般头发图片的制作是垂直排布,也就是v方向)
直接光镜面反射实现使用到了blin-phong 半角向量 (光朝向和眼睛朝向相加,和物体朝向点乘,计算夹角作为高光强度,算头发不用法向用副切线)
要考虑到头发的散射特性
使用了Marschner里面的双高光,主高光接近发梢,次高光接近发根(并且是彩色的)
使用一些特征来模拟观察到的效果。
Shader 实现:
顶点着色器向片元传入 法线 切线 视角方向 主光源方向 环境光遮蔽
片元着色器:
Kajiya-Kay的漫反射实现 sin(T,L)太亮了,我看了一下下面代码,是这么做的
登录后复制
lerp (0.25, 1.0, dot(normal, lightVec)1.
高光呢,添加了一个偏移切线位置的方法
镜面反射
我们使用半角向量和切线方向点乘计算得出方向遮挡
然后使用三角函数求出强度
当我在项目里面一步步测试的时候,突然发现,unity竟然内置了相关代码
所以,我们直接用内置的就行了,下面我贴一下片元着色器代码:
登录后复制
half4 Fragment(Varyings input, float face : VFACE) : SV_TARGET{ UNITY_SETUP_INSTANCE_ID(input); HairSurfaceData sfd = InitializeSurfaceData(input.uv); half3 V = SafeNormalize(input.viewDirWS); half3 N = input.normalWS.xyz; half3 T = input.tangentWS.xyz; float sgn = input.tangentWS.w; // should be either +1 or -1 half3 B = sgn * cross(input.normalWS.xyz, input.tangentWS.xyz); N = TransformTangentToWorld(sfd.normalTS, half3x3(T, B, N)); N = NormalizeNormalPerPixel(N); // N = face > 0 ? N : - N; Light mainLight = GetMainLight(); half3 L = mainLight.direction; half3 LightColor = mainLight.color; half atten = mainLight.shadowAttenuation; //----------------------------------Direct Diffuse 直接光漫反射---------------------------------- half diffTerm = max(0.0, dot(N, L)); half halfLambert = (diffTerm + 1.0) * 0.5; //让主光源也影响一些间接光的效果 half diffuse = saturate(lerp(0.25, 1.0, diffTerm * atten)); half3 directDiffuse = diffuse * LightColor * sfd.albedo; //----------------------------------Direct Specular 直接光镜面反射---------------------------------- half2 anisoUV = input.uv * _AnsioMap_ST.xy + _AnsioMap_ST.zw; half anisoNoise = SAMPLE_TEXTURE2D(_AnsioMap, sampler_AnsioMap, anisoUV).r - 0.5; half3 H = normalize(L + V); //半角向量 //spec1 float3 specColor1 = _SpecColor1.rgb * sfd.albedo * _SpecColor1.a * atten * LightColor; float3 t1 = ShiftTangent(B, N, _SpecOffset1 + anisoNoise * _SpecNoise1); float3 specular1 = specColor1 * max(D_KajiyaKay(t1, H, _SpecShininess1), 0.0); //spec2 float3 specColor2 = _SpecColor2.rgb * sfd.albedo * _SpecColor2.a * atten * LightColor; float3 t2 = ShiftTangent(B, N, _SpecOffset2 + anisoNoise * _SpecNoise2); float3 specular2 = specColor2 * max(D_KajiyaKay(t2, H, _SpecShininess2), 0.0); half3 directSpecular = specular1 + specular2; //环境光 漫反射 half3 env = SampleSH(N) * sfd.albedo; //环境光 镜面反射 half3 reflectVector = reflect(-V, N); half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, _Roughness, 1.0) * sfd.albedo; half3 color = directDiffuse + directSpecular + env + indirectSpecular + sfd.emission; // color.rgb = diffTerm; return half4(color, sfd.alpha);}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.
排序问题
对于半透明的物体,需要从后向前渲染保证渲染的正确性
对于有头发的头,这和头发从内向外渲染很相似。这个可以在制作模型的时候,内部的顶点坐标往前放,外部的顶点坐标向后放,主要是索引的顺序要正确。
接下来看一下他们的解决方案:
按照下面的顺序去渲染,分4个pass (深度写入,不透明,半透明背面,半透明正面)
解决方案的优缺点:
优点:
几何复杂度低 深度排序更快 兼容低端设备
缺点:
有动画会出现深度问题
马尾之类的需要单独处理
不适合所有类型的头发
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删