前言
本篇将展示如何使用DX12 实现normal map
源代码chenglixue/D3D12 at normalmap
要点
-
定义:法线贴图基于凹凸贴图衍生出来的。纹理贴图中的纹素是RGB颜色值,而法线贴图中的纹素是法向量的坐标
-
用途:计算光照,在纹理图中存储法向量,再将其带入光照计算。在避免高模建模的情况下也可以达到理想的效果
压缩和存储法线图
由于纹理图的图像格式范围一般是[0,255],而归一化法向量的范围是[-1,1],因此将法向量存于纹理图中需要进行变换
- 法向量转换为图像格式::\(\large f(x) = (0.5x + 0.5) * 255\)
- 图像格式转换为法向量:\(\large f^{-1}(x) = \frac{2x}{255} - 1\)
切线空间
- 切线空间是一个3d纹理坐标系,xy轴分别对应uv轴,z轴为法向量,且这三条轴两两相切。称T轴为切线(tangent)、B轴为副切线( binormal)、N轴为法线
TBN矩阵
一般来说计算光照是在世界空间中进行计算,但法线图中的法向量定义在切线空间,TBN矩阵定义了将纹理从切线空间转换至世界空间的过程
TBN矩阵仅仅关注向量的变化,因此这是个\(3\times3\)的矩阵
从uv空间到局部空间
-
设纹理坐标分别为\(\large (u_0, v_0),(u_1, v_1),(u_2, v_2)\)的顶点\(\large v_0, v_1, v_2\)在切线空间中定义了一个三角形,\(\large e_0 = (\Delta_{u0} , \Delta v_0) = (u_1 - u_0, v_1 - v_0)\),\(\large e_1 = (\Delta_{u1} , \Delta v_1) = (u_2 - u_0, v_2 - v_0)\)
-
到局部空间的坐标变化
\(\large \left[\begin {array} {cc} e_{0,x} & e_{0,y} & e_{0,z} \\ \large e_{1,x} & e_{1,y} & e_{1,z} \end {array} \right] = \left[\begin {array} {cc} \Delta_{u_0} & \Delta_{v_0} \\ \large \Delta_{u_1} & \Delta_{v_1} \end {array} \right] \left[\begin {array} {cc} T_{x} & T_{y} & T_{z} \\ \large B_{x} & B_{y} & B_{z} \end {array} \right]\)
-
对uv矩阵求逆
\(\large \left[\begin {array} {cc} T_{x} & T_{y} & T_{z} \\ \large B_{x} & B_{y} & B_{z} \end {array} \right] = \large \left[\begin {array} {cc} \Delta_{u_0} & \Delta_{v_0} \\ \large \Delta_{u_1} & \Delta_{v_1} \end {array} \right]^{-1} \large \left[\begin {array} {cc} e_{0,x} & e_{0,y} & e_{0,z} \\ \large e_{1,x} & e_{1,y} & e_{1,z} \end {array} \right] \large = \frac{1}{\Delta_{u_0} \Delta{v_1} - \Delta{v_0}\Delta{u_1}} \left[\begin {array} {cc} \Delta_{v_1} & -\Delta_{v_0} \\ \large -\Delta{u_1} & \Delta_{u_0} \end {array} \right] \left[\begin {array} {cc} e_{0,x} & e_{0,y} & e_{0,z} \\ \large e_{1,x} & e_{1,y} & e_{1,z} \end {array} \right]\)
这里用到了逆矩阵性质:矩阵\(A = \left[\begin {array} {cc} a & b \\ \large c & d \end {array} \right]\),有\(\large A^{-1} = \frac{1}{ad - bc} \left[\begin {array} {cc} d & -b \\ \large -c & a \end {array} \right]\)
平均化法线
- 大多数情况三角形和三角形之间都会共享顶点。对于不在一个平面的三角形,需要对他们的法线进行平均化达到更加柔和的效果。但对于平行的三角形则无需平均化
流程
- 美术创造预定的法线图并将其存为图像文件
- D3D初始化时提取该图像文件
- 计算每一个三角形的切向量T(平均化)
- 在VS中,将顶点的法线和切向量变换至世界空间,并将结果输出至PS
- 通过插值切向量和法向量来构建三角形面每个像素点处的TBN基,再将该TBN基从切线空间变换至世界空间
实现
将normal map纹理贴图绑定至管线的步骤这里就省略咯,和前面纹理贴图中导入纹理一摸一样的
inputlayout
计算TBN需要normal 和 tangent,因此需要在初始化阶段导入模型的normal 和 tangent
m_inputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
shader
struct VSInput
{
float3 position : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD;
float3 tangent : TANGENT;
};
struct PSInput
{
float4 positionH : SV_POSITION;
float3 positionW : POSITION;
float3 normalW : NORMAL;
float3 tangentW : TANGENT;
float2 uv : TEXCOORD;
};
Texture2D g_normalmapTexture : register(t2, space0
PSInput main(VSInput input)
{
PSInput result;
float4 positionW = mul(float4(input.position, 1.f), world);
result.positionW = positionW.xyz;
result.positionH = mul(positionW, viewProjection);
// normal tangent变换至世界空间
result.normalW = mul(input.normal, (float3x3)world);
result.tangentW = mul(input.tangent, (float3x3) world);
result.uv = input.uv;
return result;
}
float4 main(PSInput input) : SV_TARGET
{
float4 textureDiffuseAlbedo = g_diffuseTexture.Sample(g_SamperAnisotropyWrap, input.uv);
float4 textureSpecularAlbedo = g_specularTexture.Sample(g_SamperAnisotropyWrap, input.uv);
// 采样 normalmap texture
float4 normalMapSample = g_normalmapTexture.Sample(g_SamperAnisotropyWrap, input.uv);
// 从uv空间变换至世界空间
float3 averageNormalW = normalmapToWolrd(normalMapSample.rgb, input.normalW, input.tangentW);
input.normalW = normalize(input.normalW);
// 后面光照的计算法线都用averageNormalW
float3 toEyeDirW = eyeWorldPosition - averageNormalW;
float toEyeLength = length(toEyeDirW);
toEyeDirW = normalize(toEyeDirW);
Material material = { textureDiffuseAlbedo, textureSpecularAlbedo, ambientAlbedo, specualrShiness };
float4 resultLightColor = CalcLightColor(lights, material, averageNormalW, toEyeDirW, input.positionW);
resultLightColor.a = textureDiffuseAlbedo.a;
return resultLightColor;
}
// 将法线贴图从uv空间变换至世界空间
float3 normalmapToWolrd(float3 normalmap, float3 normalizeNormalW, float3 tangentW)
{
// [0,1] -> [-1,1]
float3 normalConvert = 2.f * normalmap - 1.f;
// build TBN
float3 N = normalizeNormalW;
// because after lerp T and N may not be orthogonal vectors
float3 T = normalize(tangentW - dot(N, tangentW) * N);
float3 B = cross(N, T);
float3x3 TBN = float3x3(T, B, N);
float3 result = mul(normalConvert, TBN);
return result;
}
“float3 T = normalize(tangentW - dot(N, tangentW) * N);”这里的原因是从uv空间变换至世界空间,normal 和 tangent可能不再互切