unity urp 实现头发渲染

此篇文章参考 Hair Rendering and Shading
地址: https://web.engr.oregonstate.edu/~mjb/cs519/Projects/Papers/HairRendering.pdf
另一篇:http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/Scheuermann_HairSketchSlides.pdf
感兴趣的小伙伴可以直接查看原版。

效果:
在这里插入图片描述

概述:
使用多段面片实现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)

高光呢,添加了一个偏移切线位置的方法
在这里插入图片描述
镜面反射
我们使用半角向量和切线方向点乘计算得出方向遮挡
然后使用三角函数求出强度
在这里插入图片描述
当我在项目里面一步步测试的时候,突然发现,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);
}

排序问题
对于半透明的物体,需要从后向前渲染保证渲染的正确性
对于有头发的头,这和头发从内向外渲染很相似。这个可以在制作模型的时候,内部的顶点坐标往前放,外部的顶点坐标向后放,主要是索引的顺序要正确。

接下来看一下他们的解决方案:
按照下面的顺序去渲染,分4个pass (深度写入,不透明,半透明背面,半透明正面)

  1. 第一个pass,使用alphaTest剔除半透明像素,禁用背面剔除,写入深度,深度检测为less,不需要渲染颜色,只需要写入深度。
  2. 第二个pass,进行完整的头发像素渲染,禁用背面剔除,禁用深度写入,深度检测为 Equal,用来绘制不透明部分的表面。
  3. 第三个pass,用于绘制半透明背面,剔除正面渲染,不写入深度,深度检测为less。
  4. 第四个pass,半透明正面渲染,剔除背面,深度写入,深度检测为less,启用深度写入可以防止不正确的深度顺序。

解决方案的优缺点:
优点:
几何复杂度低 深度排序更快 兼容低端设备

缺点:
有动画会出现深度问题
马尾之类的需要单独处理
不适合所有类型的头发

头发各向异性渲染Shader 这个是04年的一个ppt,主要介绍了头发渲染,其追到源头还是要看这个原理。 各向异性的主要计算公式: 主要代码如下: 切线混合扰动部分(这部分也可以用T+k*N,来对切线进行扰动): float3x3 tangentTransform = float3x3(i.tangentDir, i.bitangentDir, i.normalDir); float3 _T_var = UnpackNormal(tex2D(_Tangent, TRANSFORM_TEX(i.uv0, _Tangent))); float3 temp = lerp(_TangentParam.xyz, _T_var, _BlenfTangent); float3 T = normalize(mul(float3(temp.xy,0), tangentTransform)); 主要是通过改变切线的xy值来造成头发高光部分的多样性。 高光部分,按公式计算即可: float StrandSpecular(float3 T, float3 V, float3 L, float exponent) { float3 H = normalize(L + V); float dotTH = dot(T, H); float sinTH = sqrt(1 - dotTH*dotTH); float dirAtten = smoothstep(-1, 0, dotTH); return dirAtten*pow(sinTH, exponent); } 注意,为了模拟的更贴近真实性,应用两层高光,第一层高光代表直射光直接反射出去,第二层代表次表面散射现象具体看代码。 最终渲染部分: float4 HairLighting(float3 T, float3 N, float3 L, float3 V, float2 uv, float3 lightColor) { float diffuse = saturate(lerp(0.25, 1.0, dot(N, L)))*lightColor; float3 indirectDiffuse = float3(0, 0, 0); indirectDiffuse += UNITY_LIGHTMODEL_AMBIENT.rgb; // Ambient Light float3 H = normalize(L + V); float LdotH = saturate(dot(L, H)); float3 specular = _Specular*StrandSpecular(T, V, L, exp2(lerp(1, 11, _Gloss))); //float specMask = tex2D(_SpecMask, TRANSFORM_TEX(uv, _SpecMask)); specular += /*specMask*/_SubColor*StrandSpecular(T, V, L, exp2(lerp(1, 11, _ScatterFactor))); float4 final; float4 base = tex2D(_MainTex, TRANSFORM_TEX(uv, _MainTex)); float3 diffuseColor = (_Color.rgb*base.rgb); //float ao = tex2D(_AO, TRANSFORM_TEX(uv, _AO)).g; final.rgb = (diffuse + indirectDiffuse)*diffuseColor + specular*lightColor* FresnelTerm(_Specular, LdotH); //final.rgb *= ao; final.a = base.a; clip(final.a - _CutOff); return final; } 这里我注释掉了AO和高光遮罩,需要的同学可以加上。 最后一点为了不让头发的边经过clip之后太硬,需要进行两个通道的belnd。 第二个pass使用以下指令: Blend SrcAlpha OneMinusSrcAlpha ZWrite Off 注意第二个通道无需再进行clip操作。 至此,头发渲染完毕。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值