Technical Artist的不归路 —— Kajiya-Kay Shading

在游戏中,头发一直是比较难以驾驭。头发难的地方有三块,一在模拟,二在着色,三在工具链适配。目前很多游戏的开发通常都会避免长发和散发,只采用短发。

但是哪怕是短发,其着色也是非常重要的一环。


我自己也看到有不少团队的头发完全使用贴图来进行着色。这样的着色的优点在于在特定的光照与摄像机角度下能够有很精彩的表现,但是如果光照条件与摄像机角度变化后,头发的表现就会非常怪异。

这篇博客介绍了Kajiya-Kay Shading,一种简单而酷炫的头发渲染着色器,原文传送门

Kajiya-Kay Model

Kajiya-Kay Model与其他Shading Model显著不同一点的就是它使用的是发丝的切线而不是这一点的法线来进行计算。在这个模型中,Specular N.H为:

sin(T,H)speculartiy=1dot(T,H)2speculartiy

而不是:
dot(N,H)specularity

但是需要注意的是,Kajiya-Kay的着色模型需要比较正确的自阴影。否则相对来讲会太亮。

Vertex Shader

VS没有什么特别的,仅仅是将T, N, V, L, AO这些数值传递给PS。

Pixel Shader

  • Diffuse Lighting.
    Kajiya-Kay 的diffuse可以使用衰弱的N.L,也可以使用sin(T, L)。正如前面提到的,sin(T, L)需要有自阴影,否则会太亮。

  • Specular Highlights
    在Kajiya-Kay的着色模型中,我们有两个偏移的Specular量,从而表现头发的高光(下图来自本人小妹的自拍)。

    Hair

Specular Highlights

为了产生沿着发丝方向偏移的高光,我们需要将切线沿着法线方向进行一个偏移,如下图:

Nudge

正向的偏移意味着向发根的高光偏移,而负向的偏移则意味着偏向发梢。

在这个着色模型中,我们通过一张纹理来表示偏移量:

ShiftTexture

代码如下:

float3 ShiftTangent(float3 T, float3 N, float shift)
{
    float3 shiftedT = T + (shift * N);
    return normalize(shiftedT);
}

发丝光照

在这个着色模型中,我们使用半角向量(half-angle vector)。用reflectionview向量也可以,但是会使Shader略显复杂。

在两个高光中,可以使用不同的颜色、specular exponent以及不同的切线偏移度。第一层高光可以直接计算,而第二层高光表现的主要是闪耀的情况——通过noise texture进行调整。

代码如下:

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);
}

值得一提的是,对于dirAtten这个变量的意义我一直不太了解,在stackoverflow上面找到了答案,传送门

最终代码

float4 HairLighting (float3 tangent, float3 normal, float3 lightVec, 
                     float3 viewVec, float2 uv, float ambOcc)
{
    // shift tangents
    float shiftTex = tex2D(tSpecShift, uv) - 0.5;
    float3 t1 = ShiftTangent(tangent, normal, primaryShift + shiftTex);
    float3 t2 = ShiftTangent(tangent, normal, secondaryShift + shiftTex);

    // diffuse lighting
    float3 diffuse = saturate(lerp(0.25, 1.0, dot(normal, lightVec)));

    // specular lighting
    float3 specular = specularColor1 * StrandSpecular(t1, viewVec, lightVec, specExp1);
    // add second specular term
    float specMask = tex2D(tSpecMask, uv); 
    specular += specularColor2 * specMask * StrandSpecular(t2, viewVec, lightVec, specExp2);

    // Final color
    float4 o;
    o.rgb = (diffuse + specular) * tex2D(tBase, uv) * lightColor;
    o.rgb *= ambOcc; 
    o.a = tex2D(tAlpha, uv);

    return o;
}

最终效果

Final

以后会在Unreal Engine 4中实现Kajiya-Kay Shading……

<全文完>

  • 13
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
头发各向异性渲染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操作。 至此,头发渲染完毕。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值