法线贴图的混合

        本文考虑多个法线的线性混合,首先需要了解如何正确缩放法线,然后混合要怎么做。同时结合了Unity中的实现。

        比如有两个法线贴图,或者采样同一个法线贴图的不同位置,现在需要将这两个法线混合。这个混合应该具有这样的性质,当一个切线法线是(0,0,1)(垂直向上)时另一个应该不受影响。

        基于这样的考虑,显然不应该做这样的混合:

float3 normal = normalize(normalA * scaleA + normalB * scaleB);

基于法线贴图

        我们应该回到法线的定义上来:法线实际上定义了该点处的偏导数,假设切线空间中的一个曲面Z=f(x,y),该点上的法线其实是:

\vec {n} = (-{f_{x}}', -f_{y}',1)

        将法线转成法线贴图是这样的过程:

normalmap = normalize(\vec {n}) * 0.5 + 0.5

        提取法线贴图其实就是逆向得到N=normalize(n),后文都用大N代表这个值。

        两个法线贴图混合,实际上代表在该点上值的相加。假设切线空间两个法线对应的曲面分别是Z1=f1(x,y)和Z2=f2(x,y),最终值Z=f1(x,y)+f2(x,y),根据偏导数加法原理,合并后的偏导数是两个函数各自偏导数的和,也就是:

\vec {n} = (-{f_{1x}}'-{f_{2x}}', -f_{1y}'-f_{2y}',1)

         我们应该要知道两个贴图中的fx'和fy',但是我们现在只有N,要转换回去的话,就是进行缩放,并且因为缩放后的z为1,所以可以这样转换:

\vec {n} = \frac{N.xyz}{N.z}

         综上可以得到最终的混合式:

\newline \vec {n} =normalize ( \frac{N_{1}.x}{N_{1}.z}+\frac{N_{2}.x}{N_{2}.z}, \frac{N_{1}.y}{N_{1}.z}+\frac{N_{2}.y}{N_{2}.z},1) \newline =normalize \begin{pmatrix} N_{1}.x N_{2}.z+N_{2}.xN_{1}.z\\ N_{1}.yN_{2}.z+N_{2}.yN_{1}.z\\ N_{1}.z * N_{2}.z \end{pmatrix}

        计算x和y时,N1.z和N2.z其实扮演了混合系数的作用,有时可以假设这两个值都为1,这样计算很简单,副作用是对原来的法线做了适当的放大,这个方法名字叫whiteout blending。

\newline \vec {n} =normalize \begin{pmatrix} N_{1}.x +N_{2}.y\\ N_{1}.y+N_{2}.y\\ N_{1}.z * N_{2}.z \end{pmatrix}

        Unity中也定义了这个函数: 

half3 BlendNormals(half3 n1, half3 n2)
{
    return normalize(half3(n1.xy + n2.xy, n1.z*n2.z));
}

        以上都是简单的1+1混合平均,有时我们想要按系数混合,比如混合系数a和b,根据前面的推导有:

\newline \vec {n} =normalize \begin{pmatrix} aN_{1}.x N_{2}.z+bN_{2}.xN_{1}.z\\ aN_{1}.yN_{2}.z+bN_{2}.yN_{1}.z\\ N_{1}.z * N_{2}.z \end{pmatrix}

基于偏导数贴图

        也可以使用另一种方法,直接存储偏导数,也就是f'x和f'y,此时贴图存储的值是:

derivmap= ({f_{x}}', {f_{y}}') * 0.5 + 0.5

        好处是可以直接得到偏导数,并且可以做线性运算。同样假设混合系数a和b,

\vec {n} =normalize (-(a{f_{1x}}'+b{f_{2x}}'), -(af_{1y}'+bf_{2y}'),1)

        注意由于直接存的是偏导数,求法线时要加上负号。

法线缩放

理论分析

       从定义上说,法线缩放也是一种线性运算,也必须在偏导数上使用。但在实际中经常是在法线数值N(x,y,z)的基础上直接缩放xy。

        从本质上说,这样的缩放是有误差的。不妨从N(x,y,z)出发,缩放系数为k,那么先将xy缩放,然后归一化的操作表示为:

\vec {N_{k}} =(kx, ky,\sqrt{1-k^2x^2-k^2y^2})

        将上面的向量转化为偏导数:

\vec {n_{xy}} =\frac{(kx, ky)}{\sqrt{1-k^2x^2-k^2y^2}} = \frac{(x, y)}{\sqrt{\frac{1}{k^2}-x^2-y^2}}

        很显然上式中(x,y)和k并不是线性关系,这意味着这种操作对应到偏导数不是线性的。

        有一个修正的方法,是先归一化,然后缩放xy,这个操作表示为:

z=\sqrt{1-x^2-y^2}

\vec {N_{k}} =normalize(kx, ky,z) = \frac{(kx, ky,z)}{\sqrt{k^2x^2+k^2y^2+z^2}}

        这样再求偏导数: 

\vec {n_{xy}} =\frac{(kx, ky)}{z}

        显然和k成线性关系。

实践

        参考Unity中的实现,有两个相关方法计算:UnpackScaleNormalRGorAG和UnpackNormalAG。UnpackScaleNormalRGorAG是先对xy缩放,然后计算z:

half3 UnpackScaleNormalRGorAG(half4 packednormal, half bumpScale)
{
    // This do the trick
    packednormal.x *= packednormal.w;

    half3 normal;
    normal.xy = (packednormal.xy * 2 - 1);
    #if (SHADER_TARGET >= 30)
        // SM2.0: instruction count limitation
        // SM2.0: normal scaler is not supported
        normal.xy *= bumpScale;
    #endif
    normal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

         UnpackNormalAG是先计算z,然后对xy缩放: 

real3 UnpackNormalAG(real4 packedNormal, real scale = 1.0)
{
    real3 normal;
    normal.xy = packedNormal.ag * 2.0 - 1.0;
    normal.z = max(1.0e-16, sqrt(1.0 - saturate(dot(normal.xy, normal.xy))));

    // must scale after reconstruction of normal.z which also
    // mirrors UnpackNormalRGB(). This does imply normal is not returned
    // as a unit length vector but doesn't need it since it will get normalized after TBN transformation.
    // If we ever need to blend contributions with built-in shaders for URP
    // then we should consider using UnpackDerivativeNormalAG() instead like
    // HDRP does since derivatives do not use renormalization and unlike tangent space
    // normals allow you to blend, accumulate and scale contributions correctly.
    normal.xy *= scale;
    return normal;
}

     这对应了我们前面提到的缩放的两种方法,为了得到线性的法线缩放,推荐使用第二种。但是需要特别注意的是,第二种得到的结果是没有归一化的。但是一般也不需要归一化,因为用TBN矩阵计算后都是要统一归一化一遍的,前面这步就可以省了。

        

猜你喜欢

转载自blog.csdn.net/paserity/article/details/129835886