ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

深入理解法线贴图

2021-05-27 22:35:09  阅读:528  来源: 互联网

标签:贴图 法线 normal HeightMap tangent binormal 深入 float3


高度图转法线

高度图中保存的是物体表面的高度信息,可以利用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方向高度变化程度的向量。即法线可以写成这样的形式:

\[\boldsymbol N = (du, 1, dv) \]

在TBN空间中,则为:

\[\boldsymbol N = (du, dv, 1) \]

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

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

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

\[\boldsymbol 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:

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

标签:贴图,法线,normal,HeightMap,tangent,binormal,深入,float3
来源: https://www.cnblogs.com/back-to-the-past/p/14820087.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有