DX12 实战 法线贴图
前言
本篇将展示如何使用DX12 实现normal map
源代码chenglixue/D3D12 at normalmap
要点
-
定义:法线贴图基于凹凸贴图衍生出来的。纹理贴图中的纹素是RGB颜色值,而法线贴图中的纹素是法向量的坐标
-
用途:计算光照,在纹理图中存储法向量,再将其带入光照计算。在避免高模建模的情况下也可以达到理想的效果
压缩和存储法线图
由于纹理图的图像格式范围一般是[0,255],而归一化法向量的范围是[-1,1],因此将法向量存于纹理图中需要进行变换
- 法向量转换为图像格式::
- 图像格式转换为法向量:
切线空间
- 切线空间是一个3d纹理坐标系,xy轴分别对应uv轴,z轴为法向量,且这三条轴两两相切。称T轴为切线(tangent)、B轴为副切线( binormal)、N轴为法线
TBN矩阵
一般来说计算光照是在世界空间中进行计算,但法线图中的法向量定义在切线空间,TBN矩阵定义了将纹理从切线空间转换至世界空间的过程
TBN矩阵仅仅关注向量的变化,因此这是个
从uv空间到局部空间
-
设纹理坐标分别为
的顶点 在切线空间中定义了一个三角形, , -
到局部空间的坐标变化
-
对uv矩阵求逆
这里用到了逆矩阵性质:矩阵
,有
平均化法线
- 大多数情况三角形和三角形之间都会共享顶点。对于不在一个平面的三角形,需要对他们的法线进行平均化达到更加柔和的效果。但对于平行的三角形则无需平均化
流程
- 美术创造预定的法线图并将其存为图像文件
- 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可能不再互切
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?