原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十九章:法线贴图

学习目标

  1. 理解为什么需要法线贴图;
  2. 学习法线贴图如何保存;
  3. 学习法线贴图如何创建;
  4. 学习法线贴图中的法向量的坐标系统是如何与物体空间的三角形的坐标系统关联的;
  5. 学习如何在顶点和像素着色器中实现法线贴图。


1 使用法线贴图的原因

找到一种方法在光滑的平面上,显示出更多的细节(比如粗糙的砖块)。

如果使用曲面细分是可以增加实际的细节的,但是我们还是需要一种方法来指定新增加的顶点的法向量。如果直接根据光照来烘焙纹理,这种方法如果灯光移动后,效果就会出问题。

所以要使用法线贴图:



2 法线贴图

一个法线贴图是一张纹理,其每个通道保存x,y,z坐标值,所以每个像素保存了一个法线向量:



一个单位向量其每个组件值的值域为[−1, 1],我们可以经过下面的运算,将其转换到0-255:



如果要再将其准换回[−1, 1],就对每个通道执行:

我们不需要自己去做压缩操作,PhotoShop的插件可以帮忙把图像转化成法线贴图。但是在着色器中,我们需要自己做解压缩操作:

float3 normalT = gNormalMap.Sample(gTriLinearSam, pin.Tex);

normalT每个组件的值域为0 ≤ r, g, b ≤ 1;所以该函数已经为我们做了一半的解压缩操作,我们只需要再将其转换到[−1, 1]即可:

// Uncompress each component from [0,1] to [-1,1].
normalT = 2.0f*normalT - 1.0f;

Photoshop的插件可以在 https://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop 下载到;还有其它一些创建法线贴图的工具:http://www.crazybump.com/http://shadermap.com/home/ ;还有一些工具可以从高分辨率模型上创建法线贴图:https://www.nvidia.com/object/melody_home.html

如果你要使用压缩纹理格式保存法线贴图,使用BC7 (DXGI_FORMAT_BC7_UNORM)格式是最好的效果,它可以减少由压缩法线贴图造成的错误。对于BC6和BC7格式,DirectX SDK有一个例子叫“BC6HBC7EncoderDecoder11”,它可以将你的法线贴图转换到BC6或者BC7。



3 纹理/切线空间

纹理通过平移和旋转后贴到三角形上后,合并三角形的法向量N,我们在三角形所在的平面上生成一个3D TBN-basis的坐标系,叫做纹理空间或者切线空间。注意该空间对于不同三角形是不一样的。



法线贴图的法向量是在纹理空间定义的,但是灯光是在世界坐标系下的,所以我们需要将它们转换到同一个坐标系下才能正确计算光照。所以首先我们要纹理空间关联到它的物体局部坐标系中。令v0, v1, 和 v2定义一个3D三角形的三个顶点,对应的纹理坐标为(u0, v0), (u1, v1), 和(u2, v2)。令e0 = v1 − v0和e1 = v2 − v0是三角形的两条边,并且对于的纹理三角形的两条边:(Δu0, Δv0) = (u1 − u0, v1 − v0) 和 (Δu1, Δv1) = (u2 − u0, v2 − v0) :



表达了向量坐标关联到物体空间,我们得到矩阵方程:



我们知道三角形顶点的物体空间坐标,也知道边的物体空间坐标:



我们也知道纹理坐标:



解T和B的物体空间坐标:



综上所述,我们使用逆矩阵



向量T和B在物体坐标系中不是单位长度,如果有扭曲,它们也不是正交的。

T,V和N向量代表了切线,次法线和法线向量。



4 顶点的切线空间

上一节,我们衍生出了逐三角形的切线空间,如果我们使用它来进行法线贴图映射,物体表面会产生三角形化的效果。所以我们定义逐顶点的切向量,然后进行均值计算来模拟光滑平面:

1、任意顶点V的切向量T通过所有共享它的三角形切向量的平均值来获取;

2、任意顶点的次切向量B通过所有共享它的三角形次切向量的平均值来获取。

通常情况下,进行均值运算后,TBN-bases需要标准正交化,所以向量要进行正交运算和转换为单位长度。这个通常使用Gram-Schmidt步骤。代码可以在下面网站中找到,对任意三角网格创建逐向量的切线空间:http://www.terathon.com/code/tangent.html

在我们的系统中,我们不需要直接保存次切向量B到内存,可以通过计算获得B = N × T,所以顶点结构为:

struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex;
XMFLOAT3 TangentU;
};

回顾我们在GeometryGenerator中创建网格的步骤,计算纹理空间的切线T。向量Y在盒子或者格子网格中非常容易计算。对于圆柱体和球体,每个顶点的切向量可以通过两个点P(u, v)然后计算∂p/∂u来获得(其中u使用的是u的纹理坐标)。



5 切线空间和物体空间之间的转换

现在网格的每个顶点我们有一个标准正交的TBN-basis,并且关联到物体空间。我们可以通过下面的变换矩阵进行转化:



因为它是标准正交的,所以它的逆矩阵就是它的转置矩阵,所以从物体空间到切线空间为:



在着色器代码中,我们需要将它们转换到世界坐标系中:



因为矩阵的乘法具有结合律,所以:



并且:



所以要从切线空间转换到世界坐标系,我们只需要在世界坐标系下描述切线方向轴,即可得到变换矩阵。

因为我们只需要转换向量,所以我们只需要一个3x3矩阵。



6 法线贴图的着色器代码

我们总结一下实现的步骤:

1、通过各种工具或者软件创建法线贴图并保存到图像文件,在程序初始化的时候读取文件创建纹理;

2、对每个三角形,计算它的切向量T;

3、在顶点着色器中,转换法向量和切向量到世界坐标系中,并且输出到像素着色器;

4、使用差值后的切向量和法向量,我们在三角形表面的每个像素点创建TBN-basis,然后用它们将采样到的法向量变换到世界坐标系。然后就可以使用它来进行光照计算。

为了帮助我们实现法线贴图,我们在Common.hlsl添加了下面的函数:

//--------------------------------------------------------------------
// Transforms a normal map sample to world space.
//--------------------------------------------------------------------
float3 NormalSampleToWorldSpace(float3 normalMapSample,
float3 unitNormalW,
float3 tangentW)
{
// Uncompress each component from [0,1] to [-1,1].
float3 normalT = 2.0f*normalMapSample - 1.0f; // Build orthonormal basis.
float3 N = unitNormalW;
float3 T = normalize(tangentW - dot(tangentW, N)*N);
float3 B = cross(N, T);
float3x3 TBN = float3x3(T, B, N); // Transform from tangent space to world space.
float3 bumpedNormalW = mul(normalT, TBN);
return bumpedNormalW;
}

这个函数在像素着色器中可以这样使用:

float3 normalMapSample = gNormalMap.Sample(samLinear, pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(
normalMapSample,
pin.NormalW,
pin.TangentW);

可能有两行不太好理解的是:

float3 N = unitNormalW;
float3 T = normalize(tangentW - dot(tangentW, N)*N);

结果差值运算后,切向量和法向量可能不是标准正交的,这个代码确保T和N是标准正交的

完整的着色器代码如下:

//*********************************************************************
// Default.hlsl by Frank Luna (C) 2015 All Rights Reserved.
//*********************************************************************
// Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif // Include common HLSL code.
#include “Common.hlsl” struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
float3 TangentU : TANGENT;
}; struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 TexC : TEXCOORD;
}; VertexOut VS(VertexIn vin)
{
VertexOut vout = (VertexOut)0.0f; // Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex]; // Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosW = posW.xyz; // Assumes nonuniform scaling; otherwise, need to use
// inverse-transpose of world matrix.
vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
vout.TangentW = mul(vin.TangentU, (float3x3)gWorld); // Transform to homogeneous clip space.
vout.PosH = mul(posW, gViewProj); // Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, matData.MatTransform).xy; return vout;
} float4 PS(VertexOut pin) : SV_Target
{
// Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex];
float4 diffuseAlbedo = matData.DiffuseAlbedo;
float3 fresnelR0 = matData.FresnelR0;
float roughness = matData.Roughness;
uint diffuseMapIndex = matData.DiffuseMapIndex;
uint normalMapIndex = matData.NormalMapIndex; // Interpolating normal can unnormalize it, so renormalize it.
pin.NormalW = normalize(pin.NormalW);
float4 normalMapSample = gTextureMaps[normalMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
float3 bumpedNormalW = NormalSampleToWorldSpace(
normalMapSample.rgb, pin.NormalW,
pin.TangentW); // Uncomment to turn off normal mapping.
//bumpedNormalW = pin.NormalW;
// Dynamically look up the texture in the array.
diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC); // Vector from point being lit to eye.
float3 toEyeW = normalize(gEyePosW - pin.PosW); // Light terms.
float4 ambient = gAmbientLight*diffuseAlbedo; // Alpha channel stores shininess at per-pixel level.
const float shininess = (1.0f - roughness) * normalMapSample.a;
Material mat = { diffuseAlbedo, fresnelR0, shininess };
float3 shadowFactor = 1.0f;
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
bumpedNormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight; // Add in specular reflections.
float3 r = reflect(-toEyeW, bumpedNormalW);
float4 reflectionColor = gCubeMap.Sample(gsamLinearWrap, r);
float3 fresnelFactor = SchlickFresnel(fresnelR0, bumpedNormalW, r);
litColor.rgb += shininess * fresnelFactor * reflectionColor.rgb; // Common convention to take alpha from diffuse albedo.
litColor.a = diffuseAlbedo.a; return litColor;
}

其中bumpedNormalW不仅用以光照计算,还用以反射计算。另外alpha通道还可以用来保存发光度,用来控制逐像素的发光程度。



7 总结

  1. 法线贴图的策略就是,保存物体的法线到一张纹理中,然后使用逐像素的法线来进行计算;
  2. 法线贴图就是各个通道来分别保存法向量的x y z,它可以通过多种工具制作生成;
  3. 法线贴图中的法向量是在纹理坐标系下的,如果要进行光照计算,需要将它转换到世界坐标系下,TBN-bases可以帮助每个顶点的法向量从纹理坐标转换到世界坐标系。


8 练习

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十九章:法线贴图的更多相关文章

  1. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十二章:四元数(QUATERNIONS)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十二章:四元数(QUATERNIONS) 学习目标 回顾复数,以及 ...

  2. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图 代码工程地址: https://github.c ...

  3. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十六章:实例化和截头锥体裁切

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十六章:实例化和截头锥体裁切 代码工程地址: https://git ...

  4. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引 代码工程地址: https://g ...

  5. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段 代码工程地址: https://github. ...

  6. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十二章:几何着色器(The Geometry Shader)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十二章:几何着色器(The Geometry Shader) 代码工 ...

  7. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- Direct12优化

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- Direct12优化 第一章:向量代数 1.向量计算的时候,使用XMV ...

  8. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 全书总结

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 全书总结 本系列文章中可能有很多翻译有问题或者错误的地方:并且有些章节 ...

  9. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画 学习目标 熟悉蒙皮动画的术语: 学习网格层级变换 ...

随机推荐

  1. 主成分分析(PCA)原理详解_转载

    一.PCA简介 1. 相关背景 在许多领域的研究与应用中,往往需要对反映事物的多个变量进行大量的观测,收集大量数据以便进行分析寻找规律.多变量大样本无疑会为研究和应用提供了丰富的信息,但也在一定程度上 ...

  2. 原声js实现nodejs中qs模块中的parse和stringfiy方法

    function stringify(obj, sep, eq) { sep = sep || '&'; eq = eq || '='; let str = ""; for ...

  3. 2019.10.26 csp-s模拟测试88 反思总结

    今天的主人公是什么? 60.1K!!!! 先扔代码再更新防止我等会儿一上头不打算写完题解 T1: #include<iostream> #include<cstdio> #in ...

  4. 应读的paper

    1.Faster R-CNN:https://arxiv.org/abs/1506.01497(已读) 2.FPN(Feature Pyramid Networks for Object Detect ...

  5. JAVA面试常见问题之设计模式篇

    1.常见的设计模式 单例模式.工厂模式.建造模式.观察者模式.适配器模式.代理模式.装饰模式. 参考:https://www.cnblogs.com/cr330326/p/5627658.html 2 ...

  6. PHP1.9--数组

    1.array_slice()函数作用是在数组中根据条件取出一段值并返回,如果数组有字符串键,所返回的数组将保留健名 array array_slice(array array ,int offset ...

  7. html5绘图工具选择

    1. Chart.js 基于html5, 完全开源免费 功能过于简单,只有6种图,能满足小系统需求,使用简便,效果比较炫. http://www.bootcss.com/p/chart.js/ 2. ...

  8. 移动端适配(rem单位定义方法)

    注:移动端必须写: <meta name="viewport" content="width=device-width, user-scalable=no, ini ...

  9. [转]深入理解客户区尺寸client

    关于元素尺寸,一般地,有偏移大小offset.客户区大小client和滚动大小scroll.前文已经介绍过偏移属性,后文将介绍scroll滚动大小,本文主要介绍客户区大小client 客户区大小 客户 ...

  10. UOJ#428. 【集训队作业2018】普通的计数题

    #428. [集训队作业2018]普通的计数题 模型转化好题 所以变成统计有标号合法的树的个数. 合法限制: 1.根标号比子树都大 2.如果儿子全是叶子,数量B中有 3.如果存在一个儿子不是叶子,数量 ...