Bootstrap

Unity Shader入门精要第七章 基础纹理 凹凸映射之在世界空间下计算

Unity系列文章目录

前言

现在,我们来实现第二种方法,即在世界空间下计算光照模型。我们需要在片元着色器中把
法线方向从切线空间变换到世界空间下。这种方法的基本思想是:在顶点着色器中计算从切线空
间到世界空间的变换矩阵,并把它传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切
线和法线在世界空间下的表示来得到。最后,我们只需要在片元着色器中把法线纹理中的法线方
向从切线空间变换到世界空间下即可。尽管这种方法需要更多的计算,但在需要使用Cubemap 进
行环境映射等情况下,我们就需要使用这种方法。

一、pandas是什么?

为此,我们进行如下准备工作。
(1)使用上一节中使用的场景。
(2)新建一个材质。在本书资源中,该材质名为NormalMapWorldSpaceMat。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter7-NormalMapWorldSpace。
把新的Shader 赋给第2 步中创建的材质。
(4)把第2 步中创建的材质赋给胶囊体。
打开Chapter7-NormalMapWorldSpace,把上一节中的代码粘贴进去,并进行如下修改:
(1)我们需要修改顶点着色器的输出结构体v2f,使它包含从切线空间到世界空间的变换矩阵:
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
我们在3.3.2 节中讲到,一个插值寄存器最多只能存储float4 大小的变量,对于矩阵这样的变
量,我们可以把它们按行拆成多个变量再进行存储。上面代码中的TtoW0、TtoW1 和TtoW2 就依
次存储了从切线空间到世界空间的变换矩阵的每一行。实际上,对方向矢量的变换只需要使用3×3
大小的矩阵,也就是说,每一行只需要使用float3 类型的变量即可。但为了充分利用插值寄存器
的存储空间,我们把世界空间下的顶点位置存储在这些变量的w 分量中。

二、使用步骤

(2)修改顶点着色器,计算从切线空间到世界空间的变换矩阵:
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// Compute the matrix that transform directions from tangent space to world space
// Put the world position in w component for optimization
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
在上面的代码中,我们计算了世界空间下的顶点切线、副切线和法线的矢量表示,并把它们
按列摆放得到从切线空间到世界空间的变换矩阵。我们把该矩阵的每一行分别存储在TtoW0、
TtoW1 和TtoW2 中,并把世界空间下的顶点位置的xyz 分量分别存储在了这些变量的w 分量中,
以便充分利用插值寄存器的存储空间。
(3)修改片元着色器,在世界空间下进行光照计算:
fixed4 frag(v2f i) : SV_Target {
// Get the position in world space
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// Compute the light and view dir in world space
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// Get the normal in tangent space
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
// Transform the normal from tangent space to world space
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump),
dot(i.TtoW2.xyz, bump)));

}
我们首先从TtoW0、TtoW1 和TtoW2 的w 分量中构建世界空间下的坐标。然后,使用内置
的UnityWorldSpaceLightDir 和UnityWorldSpaceViewDir 函数得到世界空间下的光照和视角方向。
接着,我们使用内置的UnpackNormal 函数对法线纹理进行采样和解码(需要把法线纹理的格式
标识成Normal map),并使用_BumpScale 对其进行缩放。最后,我们使用TtoW0、TtoW1 和TtoW2
存储的变换矩阵把法线变换到世界空间下。这是通过使用点乘操作来实现矩阵的每一行和法线相
乘来得到的。
从视觉表现上,在切线空间下和在世界空间下计算光照几乎没有任何差别。在Unity 4.x 版
本中,在不需要使用Cubemap 进行环境映射的情况下,内置的Unity Shader 使用的是切线空间
来进行法线映射和光照计算。而在Unity 5.x 中,所有内置的Unity Shader 都使用了世界空间来
进行光照计算。这也是为什么Unity 5.x 中表面着色器更容易报错,因为它们使用了更多的插值
寄存器来存储变换矩阵(还有一些额外的插值寄存器是用来辅助计算雾效的,更多内容可以参
见19.2 节)
在这里插入图片描述

2.Unity 中的法线纹理类型

上面我们提到了当把法线纹理的纹理类型标识成Normal map 时,可以使用Unity 的内置函
数UnpackNormal 来得到正确的法线方向,如图7.16
所示。
当我们需要使用那些包含了法线映射的内置的
Unity Shader 时,必须把使用的法线纹理按上面的方
式标识成Normal map 才能得到正确结果(即便你忘
了这么做,Unity 也会在材质面板中提醒你修正这个
问题),这是因为这些Unity Shader 都使用了内置的
UnpackNormal 函数来采样法线方向。那么,当我们
把纹理类型设置成Normal map 时到底发生了什么
呢?为什么要这么做呢?
简单来说,这么做可以让Unity 根据不同平台对
纹理进行压缩(例如使用DXT5nm 格式,具体的压
缩细节可以参考:http://tech-artists.org/wiki/Normal_map_compression),再通过UnpackNormal 函
数来针对不同的压缩格式对法线纹理进行正确的采样。我们可以在UnityCG.cginc 里找到
UnpackNormal 函数的内部实现:
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz * 2 - 1;
#else
return UnpackNormalDXT5nm(packednormal);
#endif
}
从代码中可以看出,在某些平台上由于使用了DXT5nm 的压缩格式,因此需要针对这种格式
对法线进行解码。在DXT5nm 格式的法线纹理中,纹素的a 通道(即w 分量)对应了法线的x
分量,g 通道对应了法线的y 分量,而纹理的r 和b 通道则会被舍弃,法线的z 分量可以由xy 分
量推导而得。为什么之前的普通纹理不能按这种方式压缩,而法线就需要使用DXT5nm 格式来进
行压缩呢?这是因为,按我们之前的处理方式,法线纹理被当成一个和普通纹理无异的图,但实
际上,它只有两个通道是真正必不可少的,因为第三个通道的值可以用另外两个推导出来(法线
是单位向量,并且切线空间下的法线方向的z 分量始终为正)。使用这种压缩方法就可以减少法线
纹理占用的内存空间。
当我们把纹理类型设置成Normal map 后,还有一个复选框是Create from Grayscale,那么它
是做什么用的呢?读者应该还记得在本节开始我们提到过另一种凹凸映射的方法,即使用高度图,
而这个复选框就是用于从高度图中生成法线纹理的。高度图本身记录的是相对高度,是一张灰度
图,白色表示相对更高,黑色表示相对更低。当我们把一张高度图导入Unity 后,除了需要把它
的纹理类型设置成Normal map 外,还需要勾选Create from Grayscale,这样就可以得到类似图7.17中的结果。然后,我们就可以把它和切线空间下的法线纹理同等对待了。
在这里插入图片描述
在这里插入图片描述

当勾选了Create from Grayscale 后,还多出了两个选项—Bumpiness 和Filtering。其中
Bumpiness 用于控制凹凸程度,而Filtering 决定我们使用哪种方式来计算凹凸程度,它有两种选
项:一种是Smooth,这使得生成后的法线纹理会比较平滑;另一种是Sharp,它会使用Sobel 滤
波(一种边缘检测时使用的滤波器)来生成法线。Sobel 滤波的实现非常简单,我们只需要在一
个3×3 的滤波器中计算x 和y 方向上的导数,然后从中得到法线即可。具体方法是:对于高度图
中的每个像素,我们考虑它与水平方向和竖直方向上的像素差,把它们的差当成该点对应的法线
在x 和y 方向上的位移,然后使用之前提到的映射函数存储成到法线纹理的r 和g 分量即可。

参考

Unity Shader入门精要
冯乐乐

;