DX12 实战 法线贴图

前言

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

源代码chenglixue/D3D12 at normalmap

要点

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

    image-20230507173600688

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

压缩和存储法线图

由于纹理图的图像格式范围一般是[0,255],而归一化法向量的范围是[-1,1],因此将法向量存于纹理图中需要进行变换

  • 法向量转换为图像格式::f(x)=(0.5x+0.5)255
  • 图像格式转换为法向量:f1(x)=2x2551

切线空间

  • 切线空间是一个3d纹理坐标系,xy轴分别对应uv轴,z轴为法向量,且这三条轴两两相切。称T轴为切线(tangent)、B轴为副切线( binormal)、N轴为法线
    image-20230507174033343

TBN矩阵

一般来说计算光照是在世界空间中进行计算,但法线图中的法向量定义在切线空间,TBN矩阵定义了将纹理从切线空间转换至世界空间的过程

TBN矩阵仅仅关注向量的变化,因此这是个3×3的矩阵

从uv空间到局部空间

  • 设纹理坐标分别为(u0,v0),(u1,v1),(u2,v2)的顶点v0,v1,v2在切线空间中定义了一个三角形,e0=(Δu0,Δv0)=(u1u0,v1v0),e1=(Δu1,Δv1)=(u2u0,v2v0)

    image-20230507174923997

  • 到局部空间的坐标变化

    [e0,xe0,ye0,ze1,xe1,ye1,z]=[Δu0Δv0Δu1Δv1][TxTyTzBxByBz]

  • 对uv矩阵求逆

    [TxTyTzBxByBz]=[Δu0Δv0Δu1Δv1]1[e0,xe0,ye0,ze1,xe1,ye1,z]=1Δu0Δv1Δv0Δu1[Δv1Δv0Δu1Δu0][e0,xe0,ye0,ze1,xe1,ye1,z]

    这里用到了逆矩阵性质:矩阵A=[abcd],有A1=1adbc[dbca]

平均化法线

  • 大多数情况三角形和三角形之间都会共享顶点。对于不在一个平面的三角形,需要对他们的法线进行平均化达到更加柔和的效果。但对于平行的三角形则无需平均化

流程

  • 美术创造预定的法线图并将其存为图像文件
  • 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

posted @   爱莉希雅  阅读(100)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示