DX12 实战 法线贴图

发布时间 2023-05-07 22:23:49作者: 爱莉希雅

前言

本篇将展示如何使用DX12 实现normal map

源代码chenglixue/D3D12 at normalmap

要点

  • 定义:法线贴图基于凹凸贴图衍生出来的。纹理贴图中的纹素是RGB颜色值,而法线贴图中的纹素是法向量的坐标

    image-20230507173600688

  • 用途:计算光照,在纹理图中存储法向量,再将其带入光照计算。在避免高模建模的情况下也可以达到理想的效果

压缩和存储法线图

由于纹理图的图像格式范围一般是[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轴为法线
    image-20230507174033343

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)\)

    image-20230507174923997

  • 到局部空间的坐标变化

    \(\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可能不再互切
image-20230507221412696

输出

image-20230507220245511