• 深入理解法线贴图


    高度图转法线

    高度图中保存的是物体表面的高度信息,可以利用u,v方向上高度变化的斜率,计算出tangent和binormal,然后通过向量叉乘得到normal。我们在fragment shader中计算每个fragment的normal:

    void InitializeFragmentNormal(inout Interpolators i) {
    	// 取两侧点进行采样
    	float2 du = float2(_HeightMap_TexelSize.x * 0.5, 0);
    	float u1 = tex2D(_HeightMap, i.uv - du);
    	float u2 = tex2D(_HeightMap, i.uv + du);
    
    	float2 dv = float2(0, _HeightMap_TexelSize.y * 0.5);
    	float v1 = tex2D(_HeightMap, i.uv - dv);
    	float v2 = tex2D(_HeightMap, i.uv + dv);
    
    	float3 tangent = float3(_HeightMap_TexelSize.x, u2 - u1, 0);
    	float3 binormal = float3(0, v2 - v1, _HeightMap_TexelSize.y);
    
    	// 注意是B x T
    	i.normal = cross(binormal, tangent);
    	i.normal = normalize(i.normal);
    }
    

    可以看到,得到的法线非常锐利,这是因为叉乘得到的原始法线为float3(_HeightMap_TexelSize.y * (u1 - u2), _HeightMap_TexelSize.x * _HeightMap_TexelSize.y, _HeightMap_TexelSize.x * (v1 - v2))。原始法线的y分量过小,导致归一化时x和z方向的值会偏大,从而偏离(0,1,0),而显得效果十分锐利。这里我们可以特殊处理,将得到的tangent和binormal向量先进行缩放,再进行叉乘计算:

    	float3 tangent = float3(1, u2 - u1, 0);
    	float3 binormal = float3(0, v2 - v1, 1);
    

    法线贴图采样

    在Unity中,高度图可以直接导入成法线贴图,只要在导入设置中进行修改即可:

    我们可以使用现成的API函数UnpackScaleNormal提取法线贴图中的normal:

    half3 UnpackScaleNormal(half4 packednormal, half bumpScale)
    {
        return UnpackScaleNormalRGorAG(packednormal, bumpScale);
    }
    
    half3 UnpackScaleNormalRGorAG(half4 packednormal, half bumpScale)
    {
        #if defined(UNITY_NO_DXT5nm)
            half3 normal = packednormal.xyz * 2 - 1;
            #if (SHADER_TARGET >= 30)
                // SM2.0: instruction count limitation
                // SM2.0: normal scaler is not supported
                normal.xy *= bumpScale;
            #endif
            return normal;
        #else
            // 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;
        #endif
    }
    

    可以看到,如果引擎编译shader时发现平台不支持DXT5NM,则会将纹理信息直接按rgb格式解析为法线。bumpScale参数用法就和高度图的时候类似,用来缩放法线的xy分量来调整凹凸的程度。如果支持DXT5NM,那么法线贴图里只用了g通道和a通道来储存法线的y分量和x分量。z分量需要根据向量的归一化手动计算。另外别忘了,这里得到的法线是基于TBN空间的,如果直接拿来用,还需要手动调换一下y分量和z分量的位置:

    	i.normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv), _BumpScale);
    	i.normal = i.normal.xzy;
    	i.normal = normalize(i.normal);
    

    多张法线贴图

    之前我们提到过detail texture,可以与main texture叠加来丰富纹理细节。类似地,我们可以拥有一张detail normal map,与原来的法线贴图进行叠加。normal map在unity导入时也可以设置fade range,完全淡出时的效果就跟没有法线一样。

    那么,怎样对两个法线进行叠加呢?显然,直接加和求平均是不合适的,平均会抵消法线的信息,使得效果变得平整。例如一个法线n1=(0, 1, 0),另外一个法线n2=(0, 0.5, 0.87),平均之后得到的法线n3=(0, 0.75, 0.44),显然与竖直方向更加接近了,这不是我们想要的。我们希望,当有一个法线的效果是完全平整时,也不会影响另外一个法线产生的效果。

    让我们回到之前的高度图中来。我们知道,法线其实是反映高度在uv方向高度变化程度的向量。即法线可以写成这样的形式:

    [oldsymbol N = (du, 1, dv) ]

    在TBN空间中,则为:

    [oldsymbol N = (du, dv, 1) ]

    我们希望法线叠加,就是把uv方向高度变化的量进行叠加。假设从两张法线贴图中取出的法线分别为M和D,那么可得到:

    [oldsymbol M = (m_x, m_y, m_z) = (dfrac{m_x}{m_z}, dfrac{m_y}{m_z}, 1) \ oldsymbol D = (d_x, d_y, d_z) = (dfrac{d_x}{d_z}, dfrac{d_y}{d_z}, 1) \ ]

    那么,最终叠加的法线N为:

    [oldsymbol N = (dfrac{m_x}{m_z} + dfrac{d_x}{d_z}, dfrac{m_y}{m_z} + dfrac{d_y}{d_z}, 1) = (m_x cdot d_z + d_x cdot m_z, m_y cdot d_z + d_y cdot m_z, m_z cdot d_z) ]

    可以看出,M和D的xy分量还是会受到各自z分量的影响,那么直接去掉它:

    [N = (m_x + d_x, m_y + d_y, m_z cdot d_z) ]

    这个就是最终得到的叠加法线。

    当然,我们直接可以使用Unity提供的API函数BlendNormals来进行这个操作:

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

    在使用Unity导入模型时,通常使用MikkTSpace算法来计算切线。MikkTSpace约定了计算binormal的方式为:

    binormal = cross(normal.xyz, tangent.xyz) * tangent.w;
    

    可以发现tangent向量是4维的,其中w分量的值为+1/-1。那么这个w分量是做什么用的呢?

    我们知道,tangent和binormal实际代表了纹理的uv方向。在DirectX和OpenGL平台上,纹理的u方向是一致的,都是从左向右;而v方向却有差别,DirectX上v方向是自顶向下的,原点在左上方;OpenGL上v方向是自底向上的,原点在右下方。因此,为了保证binormal的方向始终与纹理的v方向保持一致,需要引入一个分量w来控制是否翻转binormal。

    此外,如果是镜像模型,那么模型的法线和切线应当是对称的,但binormal应当还是一致的,即模型两侧的TBN空间不是一致的,而是对称的。这时,两边的tangent的w分量就需要不同了。来看一个例子:

    图中是一个镜像模型,让我们导入到Unity中,看看它两边的TBN长啥样:

    其中,红色代表tangent,蓝色代表binormal,绿色代表normal。让我们拉近了来看下:

    可以看到,两边的TBN空间是对称的,为了实现这一点,需要借助tangent的w分量。

    不过在Unity中,我们发现实际计算binormal的方法是这样的:

    float3 CreateBinormal (float3 normal, float3 tangent, float binormalSign) {
    	return cross(normal, tangent.xyz) *
    		(binormalSign * unity_WorldTransformParams.w);
    }
    

    这里多出了一个变量unity_WorldTransformParams。它的w分量与物体transform的scale有关。如果有奇数个scale的值为负数,那么w取值为-1,否则取值为0。其实就是说,在scale为负数的时候,物体的纹理可能会被翻转,导致TBN空间不对,这和前面提到的镜像问题原因类似。来看一个例子:

    当scale.x为-1时,原本向上的法线实际上要变得向下,在tangent不变的情况下,需要翻转binormal:

    当scale.x和scale.z都为-1时,原本向上的法线经过两次翻转之后依旧向上,就无需翻转binormal:

    如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路-

  • 相关阅读:
    在C#中,不安装Oracle客户端如何连接Oracle数据库
    敏捷宣言(四) 猪和鸡的故事
    敏捷宣言(六) 单单有敏捷就够了吗?
    敏捷宣言(五) 看板是另外一种敏捷实践
    敏捷宣言(七) 软件系统
    小白知识摘录__进程和线程
    Linux系统修改/etc/sysconfig/i18n文件,桌面无法正常显示
    小白知识摘录__环境变量
    hive表查询中文显示乱码
    3月10日晚
  • 原文地址:https://www.cnblogs.com/back-to-the-past/p/14820087.html
Copyright © 2020-2023  润新知