双层高光丝绸

N 人看过

记不得是看的哪个武侠MMO的技术分享来着,里面介绍了在各向异性的丝绸高光上再叠一层以法线为轴可旋转的高光,确实看起来会比单层的细节更丰富,遂自己实现一下。先看看最终效果:

已经不记得这个测试模型和贴图是哪里扒过来的了,如有侵权请联系删除。

为了对老项目的更好兼容,故选择在Buildin管线底下实现。各向异性部分直接把HDRP源码搬了过来,同时顺手也把UE的GGX也搬了过来。两种看起来没什么区别,最后选择了hdrp的实现。姑且把两种的代码都端上来罢:

float D_GGXaniso( float roughness, float NoH, float3 H, float3 T, float3 B, float anisoScale)
{
    half aspect = sqrt(1.0f - anisoScale * 0.9f);
    float XR = max(0.001f, roughness / aspect);
    float YR = max(0.001f, roughness * aspect);

    float ax = XR * XR;
    float ay = YR * YR;
    float ToH = dot( T, H );
    float BoH = dot( B, H );
    float d = ToH * ToH / (ax * ax) + BoH * BoH / (ay * ay) + NoH * NoH;
    return 1.0 / ( UNITY_PI * ax * ay * d * d );
}

// Ref: http://jcgt.org/published/0003/02/03/paper.pdf
inline float SmithJointGGXVisibilityTerm (float NdotL, float NdotV, float roughness)
{
#if 0
    // Original formulation:
    //  lambda_v    = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
    //  lambda_l    = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
    //  G           = 1 / (1 + lambda_v + lambda_l);

    // Reorder code to be more optimal
    half a          = roughness;
    half a2         = a * a;

    half lambdaV    = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
    half lambdaL    = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);

    // Simplify visibility term: (2.0f * NdotL * NdotV) /  ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f));
    return 0.5f / (lambdaV + lambdaL + 1e-5f);  // This function is not intended to be running on Mobile,
                                                // therefore epsilon is smaller than can be represented by half
#else
    // Approximation of the above formulation (simplify the sqrt, not mathematically correct but close enough)
    float a = roughness;
    float lambdaV = NdotL * (NdotV * (1 - a) + a);
    float lambdaL = NdotV * (NdotL * (1 - a) + a);

#if defined(SHADER_API_SWITCH)
    return 0.5f / (lambdaV + lambdaL + 1e-4f); // work-around against hlslcc rounding error
#else
    return 0.5f / (lambdaV + lambdaL + 1e-5f);
#endif

#endif
}
UE的GGX

// Inline D_GGXAniso() * V_SmithJointGGXAniso() together for better code generation.
float DV_SmithJointGGXAniso(float TdotH, float BdotH, float NdotH, float NdotV,
                           float TdotL, float BdotL, float NdotL,
                           float roughnessT, float roughnessB, float partLambdaV)
{
    float a2 = roughnessT * roughnessB;
    float3 v = float3(roughnessB * TdotH, roughnessT * BdotH, a2 * NdotH);
    float  s = dot(v, v);

    float lambdaV = NdotL * partLambdaV;
    float lambdaL = NdotV * length(float3(roughnessT * TdotL, roughnessB * BdotL, NdotL));

    float2 D = float2(a2 * a2 * a2, s * s);  // Fraction without the multiplier (1/Pi)
    float2 G = float2(1, lambdaV + lambdaL); // Fraction without the multiplier (1/2)

    // This function is only used for direct lighting.
    // If roughness is 0, the probability of hitting a punctual or directional light is also 0.
    // Therefore, we return 0. The most efficient way to do it is with a max().
    return (UNITY_INV_PI * 0.5) * (D.x * G.x) / max(D.y * G.y, 0.0);
}
HDRP的GGX

#ifdef ANISO_UE_GGX
    float V = SmithJointGGXVisibilityTerm (anisoData.nl, anisoData.nv, anisoData.roughness);
    float D = D_GGXaniso (anisoData.roughness, anisoData.nh, anisoData.halfDir, tangentWorld, binormalWorld, anisoScale);
#elif ANISO_HDRP_GGX
    float LdotV, NdotH, LdotH, invLenLV;
    GetBSDFAngle(viewDir, light.dir, dot(normal, light.dir), max(dot(normal, viewDir), 0.0001), LdotV, NdotH, LdotH, invLenLV);

    // For silk we just use a tinted anisotropy
    float3 H = (light.dir + viewDir) * invLenLV;

    //for anisotropy we must not saturate these values
    float TdotH = dot(tangentWorld, H);
    float TdotL = dot(tangentWorld, light.dir);
    float BdotH = dot(binormalWorld, H);
    float BdotL = dot(binormalWorld, light.dir);
    float TdotV = dot(tangentWorld, viewDir);
    float BdotV = dot(binormalWorld, viewDir);
    float roughnessT = anisoData.roughness * (1 + anisoScale);
    float roughnessB = anisoData.roughness * (1 - anisoScale);

    float V = 1;//fake V, actually is involved in the next attrib
    float D = DV_SmithJointGGXAniso(TdotH, BdotH, NdotH, 
                                    TdotV, BdotV, anisoData.nv, 
                                    TdotL, BdotL, anisoData.nl, 
                                    roughnessT, roughnessB);
光照计算

以本人的实力不足以分出哪个更高效一点(感觉是不是也没必要扣这么细),所以都放着等高手来判断吧。

旋转高光部分,要注意的是得根据法线贴图采样结果进行格拉姆-施密特正交化,在世界空间重新算切线和副切线,然后使用采样完法线重新正交化的TBN矩阵转到切线空间进行旋转。代码如下:

#ifdef _CHANGE_TANGENT
    half3 orthTangentWS = Orthonormalize(s.tangentWorld, s.normalWorld);
    half3 orthBinormalWS = cross(orthTangentWS, s.normalWorld);
    orthBinormalWS = orthBinormalWS * sign (dot (orthBinormalWS, s.binormalWorld));

    #ifdef _DOUBLE_SPECULAR
        half3x3 tbn = half3x3(s.tangentWorld, s.binormalWorld, s.normalWorld);
        half3 secondTangentTS = mul(orthTangentWS, tbn);
        half radian = radians(_AnisoRotate);
        half3x3 tangent_roate = float3x3(cos(radian), -sin(radian), 0,
                                sin(radian), cos(radian),0,
                                0,         0,1
                                  );
        secondTangentTS = mul(tangent_roate, secondTangentTS);

        half3 secondTangentWS = mul(tbn, secondTangentTS);
        half3 secondOrthTangentWS = Orthonormalize(secondTangentWS, s.normalWorld);
        half3 secondOrthBinormalWS = cross(secondOrthTangentWS, s.normalWorld);
        secondOrthBinormalWS = secondOrthBinormalWS * sign(dot(secondOrthBinormalWS, s.binormalWorld));
    #endif
#endif

其他的就是一些传参,参数复用和加上各种scale调节表现的问题了,纯工作量,慢慢捋一下就好。

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。