在第8章中,我们介绍了纹理映射,它使我们能够将图像中的细节映射到三角形上。但是,我们的法向矢量仍然定义在较粗糙的顶点级别并在三角形内插。作为本章的一部分,我们研究了一种在较高分辨率下指定曲面法线的流行方法。以更高的分辨率指定曲面法线将增加光照细节,但网格几何细节保持不变。位移映射与曲面细分相结合,使我们能够增加网格的细节。
目标:
1.了解我们为什么需要法线贴图。
2.发现如何存储法线贴图。
3.了解如何创建法线贴图。
4.要了解法线贴图中的坐标系法线矢量是相对于3D三角形的物体空间坐标系而言的,以及它如何与其相关。
5.了解如何在顶点和像素着色器中实现法线贴图。
6.发现位移贴图和镶嵌细分可以如何组合以改善网格细节。
18.1动机
考虑图18.1。 锥形柱上的镜面高光看起来不正确 - 与砖块质地的颠簸相比,它们看起来不自然地光滑。 这是因为底层网格的几何图形是光滑的,而且我们只是将光滑的圆柱形表面上的颠簸砖块的图像应用到了这些地方。 但是,照明计算是基于网格几何(特别是内插顶点法线)执行的,而不是纹理图像。 因此照明与纹理不完全一致。
理想情况下,我们会仔细镶嵌网格几何图形,以便可以通过基础几何图形来模拟砖块的实际凸起和裂缝。 然后可以使照明和纹理保持一致。 硬件细分可以在这方面提供帮助,但我们仍然需要一种方法来指定由细分器生成的顶点的法线(使用插值法线不会增加我们的正常分辨率)。
另一种可能的解决方案是将照明细节直接烘焙成纹理。 但是,如果允许灯光移动,这将不起作用,因为灯光移动时纹理颜色将保持固定。
图18.1 平滑镜面高光。
图18.2 颠簸镜面高光
因此,我们的目标是找到一种实现动态照明的方式,以便纹理贴图中显示的细节也显示在光照中。 由于纹理为我们提供了最初的细节,因此寻找纹理映射解决方案来解决这个问题是很自然的。 图18.2显示了使用法线映射的图18.1所示的相同场景; 我们现在可以看到动态照明与砖块纹理更加一致。
18.2正常图
法线贴图是纹理,但不是将RGB数据存储在每个纹素上,而是分别在红色分量,绿色分量和蓝色分量中存储压缩的x坐标,y坐标和z坐标。 这些坐标定义了一个法线矢量;因此法线贴图在每个像素处存储一个法线矢量。 图18.3显示了一个如何可视化法线贴图的例子。
为了说明,我们将假设一个24位图像格式,它为每个颜色分量保留一个字节,因此,每个颜色分量的范围可以从0-255。 (在alpha组件未被使用或存储一些其他标量值(如高度贴图或高光贴图)的地方可以使用32位格式,并且可以使用浮点格式,其中不需要压缩,但这需要更多 记忆。)
图18.3 相对于由矢量T(x轴),B(y轴)和N(z轴)定义的纹理空间坐标系统,存储在法线贴图中的法线.T矢量水平地垂直于纹理图像运行; B向量向下垂直于纹理图像; N与纹理平面正交。
NOTE:如图18.3所示,矢量通常大多与z轴对齐。 也就是说,z坐标的幅度最大。 因此,当作为彩色图像查看时,法线贴图通常以蓝色显示。 这是因为z坐标存储在蓝色通道中,并且由于它具有最大量级,所以该颜色占主导地位。
那么我们如何将单位矢量压缩为这种格式呢? 首先请注意,对于单位矢量,每个坐标总是位于[-1,1]的范围内。 如果我们将此范围移位并缩放为[0,1]并乘以255并截断小数,则结果将是0-255范围内的整数。 也就是说,如果x是范围[-1,1]中的坐标,则f(x)的整数部分是0-255范围内的整数,其中f由
因此,要将单位矢量存储在24位图像中,我们只需将f应用于每个坐标,并将坐标写入纹理贴图中相应的颜色通道。
接下来的问题是如何扭转压缩过程; 也就是说,给定在0-255范围内的压缩纹理坐标,我们如何在区间[-1,1]中恢复其真实值。 答案是简单地反转函数f,经过一点思考后可以看出:
也就是说,如果x是0-255范围内的整数,则f -1(x)是范围[-1,1]中的浮点数。
我们不需要自己完成压缩过程,因为我们将使用Photoshop插件将图像转换为法线贴图。但是,当我们在像素着色器中对法线贴图进行采样时,我们必须执行逆过程的一部分 解压缩它。 当我们在这样的着色器中采样法线贴图时
float3 normalT = gNormalMap.Sample(gTriLinearSam, pin.Tex);
颜色矢量normalT将具有归一化的分量(r,g,b),使得0≤r,g,b≤1。
因此,该方法已经为我们完成了一部分未压缩的工作(即除以255,这转换了一个整数)。因此,该方法已经为我们完成了部分未压缩工作(即除以255,将整数 在0-255到浮点区间[0,1]的范围内)我们用函数g:[0,1]移动和缩放[0,1]中的每个分量到[-1,1] 1]→[-1,1]定义:
在代码中,我们将这个函数应用于每个颜色组件,如下所示:
// Uncompress each component from [0,1] to [-1,1].
normalT = 2.0f*normalT - 1.0f;
这是可行的,因为标量1.0被扩展到向量(1,1,1),所以表达式是有意义的并且是以分量方式完成的。
NOTE:Photoshop插件可在http://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop获得。 还有其他工具可用于生成法线贴图,例如http://www.crazybump.com/。 此外,还有一些工具可以从高分辨率网格生成法线贴图(请参阅http://www.nvidia.com/object/melody_home.html)。
NOTE:如果您想使用压缩纹理格式存储法线贴图,请使用BC7(DXGI_FORMAT_BC7_UNORM)格式以获得最佳质量,因为它可以显着降低由法线贴图压缩造成的错误。 对于BC6和BC7格式,DirectX SDK有一个名为“BC6HBC7EncoderDecoder11”的样本。该程序可用于将纹理文件转换为BC6或BC7。
18.3纹理/切线空间
考虑3D纹理映射三角形。 为了讨论,假设纹理映射中没有失真; 换句话说,将纹理三角形映射到3D三角形上仅需要刚体变换(平移和旋转)。 现在,假设纹理就像贴花。 所以我们选择贴花,翻译它,并将它旋转到3D三角形上。 所以现在图18.4显示了纹理空间轴与3D三角形的关系:它们与三角形相切并位于三角形的平面内。 三角形的纹理坐标当然是相对于纹理空间坐标系的。 结合三角形面法线N,我们在三角形的平面中获得三维TBN基础,我们称之为纹理空间或切线空间。 请注意,切线空间通常从三角形到三角形变化(见图18.5)。
图18.4 三角形的纹理空间与物体空间之间的关系。三维切线向量T针对纹理坐标系的u轴方向,并且三维切向量B朝向纹理坐标系的v轴方向。
图18.5 盒子的每个面的纹理空间都不相同
现在,如图18.3所示,法线贴图中的法线矢量是相对于纹理空间定义的。但是我们的灯光是在世界空间中定义的。为了进行照明,法线矢量和光线需要处于相同的空间。所以我们的第一步是将切线空间坐标系与三角形顶点相对的物体空间坐标系相关联。一旦我们处于对象空间中,我们可以使用世界矩阵从对象空间到世界空间(详细内容将在下一节中介绍)。让 v0,v1,v2 v 0 , v 1 , v 2 定义三维三角形的三个顶点相应的纹理坐标 (u0,v0),(u1,v1)和(u2,v2),它们定义纹理平面中相对于纹理空间轴(即T和B)的三角形。假设 ( u 0 , v 0 ) , ( u 1 , v 1 ) 和 ( u 2 , v 2 ) , 它 们 定 义 纹 理 平 面 中 相 对 于 纹 理 空 间 轴 ( 即 T 和 B ) 的 三 角 形 。 假 设 e_0=v_1-v_0 和 和 e_1=v_2-v_0 e0=Δu0T+Δv0Be1=Δu1T+Δv1B e 0 = Δ u 0 T + Δ v 0 B e 1 = Δ u 1 T + Δ v 1 B 用相对于对象空间的坐标来表示矢量,我们得到矩阵方程: 用 相 对 于 对 象 空 间 的 坐 标 来 表 示 矢 量 , 我 们 得 到 矩 阵 方 程 : [e0,xe1,xe0,ye1,ye0,ze1,z]=[Δu0Δu1Δv0Δv1][TxBxTyByTzBz] [ e 0 , x e 0 , y e 0 , z e 1 , x e 1 , y e 1 , z ] = [ Δ u 0 Δ v 0 Δ u 1 Δ v 1 ] [ T x T y T z B x B y B z ] 请注意,我们知道三角形顶点的对象空间坐标;因此我们知道边缘向量的对象空间坐标,所以矩阵 请 注 意 , 我 们 知 道 三 角 形 顶 点 的 对 象 空 间 坐 标 ; 因 此 我 们 知 道 边 缘 向 量 的 对 象 空 间 坐 标 , 所 以 矩 阵 [e0,xe1,xe0,ye1,ye0,ze1,z] [ e 0 , x e 0 , y e 0 , z e 1 , x e 1 , y e 1 , z ] 已知。同样,我们知道纹理坐标,所以矩阵 已 知 。 同 样 , 我 们 知 道 纹 理 坐 标 , 所 以 矩 阵 [Δu0Δu1Δv0Δv1] [ Δ u 0 Δ v 0 Δ u 1 Δ v 1 ] 已知。求解T和B对象空间坐标我们得到: 已 知 。 求 解 T 和 B 对 象 空 间 坐 标 我 们 得 到 : [TxBxTyByTzBz]=[Δu0Δu1Δv0Δv1]−1[e0,xe1,xe0,ye1,ye0,ze1,z]=1Δu0Δv1−Δv0Δu1[Δv0−Δu1−Δv0Δu0][e0,xe1,xe0,ye1,ye0,ze1,z] [ T x T y T z B x B y B z ] = [ Δ u 0 Δ v 0 Δ u 1 Δ v 1 ] − 1 [ e 0 , x e 0 , y e 0 , z e 1 , x e 1 , y e 1 , z ] = 1 Δ u 0 Δ v 1 − Δ v 0 Δ u 1 [ Δ v 0 − Δ v 0 − Δ u 1 Δ u 0 ] [ e 0 , x e 0 , y e 0 , z e 1 , x e 1 , y e 1 , z ] 在上面,我们使用了矩阵 在 上 面 , 我 们 使 用 了 矩 阵 A= \left[
注意矢量T和B一般不是物体空间中的单位长度,如果存在纹理失真,它们也不会是正交的。
T,B和N向量通常分别称为切线,副法线(或二值)和法线向量。
18.4 VERTEX切线空间
在前面的章节中,我们得出了每个三角形的切线空间。 然而,如果我们使用这个纹理空间进行法线贴图,我们将得到一个三角形外观,因为切线空间在三角形的面上是恒定的。 因此,我们指定每个顶点的切线向量,并且我们使用与顶点法线相同的平均技巧来逼近光滑曲面:
1.通过对共享顶点v的网格中的每个三角形的切向量进行平均,可以找到网格中任意顶点v的切向量T.
2.通过对共享顶点v的网格中的每个三角形的双切线向量进行平均,找到网格中任意顶点v的双重向量B.
一般来说,平均后,TBN碱基通常需要进行正交归一化处理,以便向量相互正交且单位长度。 这通常使用Gram-Schmidt程序完成。 代码可在Web上为任意三角形网格构建每个顶点切线空间:http://www.terathon.com/code/tangent.html。
在我们的系统中,我们不会将双音节向量B直接存储在内存中。 相反,当我们需要B时,我们将计算B = N×T,其中N是通常的平均顶点法线。 因此,我们的顶点结构如下所示:
namespace Vertex
{
struct NormalMap
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex;
XMFLOAT3 TangentU;
};
}
回想一下,我们由GeometryGenerator创建的程序生成的网格计算与纹理空间的u轴对应的切向量T。 切线向量T的对象空间坐标很容易在每个顶点的盒子和网格网格中指定(见图18.5)。 对于圆柱体和球体,可以通过形成圆柱体/球体的两个变量P(u,v)的矢量估计函数并计算∂P/∂u来找到每个顶点处的切向量T,其中参数u也被用作 utexture坐标。
18.5切换空间和对象空间之间的转换
此时,我们在网格中的每个顶点都有一个正交TBN基。 此外,我们有TBN矢量相对于网格物体空间的坐标。 所以现在我们已经有了TBN坐标系相对于物体空间坐标系的坐标,我们可以用矩阵将坐标从切线空间转换到物体空间:
因为这个矩阵是正交的,所以它的逆是它的转置。 因此,从物空间到切空间的坐标矩阵的变化是:
在我们的着色器程序中,我们实际上想要将法向矢量从切线空间转换为用于照明的世界空间。 一种方法是首先将法线从切线空间转换到对象空间,然后使用世界矩阵从对象空间转换到世界空间:
但是,因为矩阵乘法是关联的,所以我们可以这样做:
注意
其中$T’=T·M_{world},B’=B·M_{world},N’= N·M_{world}。因此,要从切线空间直接到世界空间,我们只需要描述世界坐标中的切线基础,这可以通过将基于TBN的基础从对象空间坐标转换为世界空间坐标来完成。
NOTE:我们只会对转换矢量感兴趣(不是点数)。 因此,我们只需要一个3×3的矩阵。 回想一下仿射矩阵的第四行是用于变换,但我们不变换矢量。
18.6正常映射SHADER CODE
我们总结了法线贴图的一般过程:
1.从一些美术程序或实用程序创建所需的法线贴图并将其存储在图像文件中。程序初始化时,从这些文件创建2D纹理。
2.对于每个三角形,计算切线向量T.通过平均网格中共享顶点v的每个三角形的切向量,获得网格中每个顶点v的每顶点切线向量(在我们的演示中,我们使用简单的几何形状,并且能够直接指定切线向量,但如果使用3D建模程序中制作的任意三角网格,则需要完成此平均过程。)
3.在顶点着色器中,将顶点法线和切线向量转换为世界空间并将结果输出到像素着色器。
4.使用插值切线向量和法向量,我们在三角形表面上的每个像素点处建立TBN基础。我们使用此基础将采样法线向量从法线图从切线空间转换到世界空间。然后,我们从法线贴图中获取世界空间法线矢量,以用于我们通常的照明计算。
为了帮助我们实现法线贴图,我们在lighthelper.fx中添加了以下函数:
//---------------------------------------------------------------------
// 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;
}
This function is used like this in the pixel shader:
float3 normalMapSample = gNormalMap.Sample(samLinear, pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(
normalMapSample,
pin.NormalW,
pin.TangentW);
Two lines that might not be clear are these:
float3 N = unitNormalW;
float3 T = normalize(tangentW - dot(tangentW, N)*N);
图18.6 由于||N||=1,projN(T)=(T·N)N。向量T-projN(T)是与N正交的T的部分。
插值后,切向量和法向量可能不是正交的。 这段代码通过从方向N中减去T的任何分量来确保T对N是正交的(见图18.6)。 请注意,有一个假设unitNormalW被归一化。
一旦我们从法线贴图(我们称之为“碰撞法线贴图”)得到法线,我们将其用于涉及法线矢量的所有后续计算(例如照明,立方体贴图)。 整个法线贴图效果如下所示为完整性,与法线贴图相关的部分以粗体显示。
#include “LightHelper.fx”
cbuffer cbPerFrame
{
DirectionalLight gDirLights[3];
float3 gEyePosW;
float gFogStart;
float gFogRange;
float4 gFogColor;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
float4x4 gTexTransform;
Material gMaterial;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gDiffuseMap;
Texture2D gNormalMap;
TextureCube gCubeMap;
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
float3 TangentL : TANGENT;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 Tex : TEXCOORD;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to world space space.
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// Output vertex attributes for interpolation across triangle.
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
return vout;
}
float4 PS(VertexOut pin,
uniform int gLightCount,
uniform bool gUseTexure,
uniform bool gAlphaClip,
uniform bool gFogEnabled,
uniform bool gReflectionEnabled) : SV_Target
{
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
// The toEye vector is used in lighting.
float3 toEye = gEyePosW - pin.PosW;
// Cache the distance to the eye from this surface point.
float distToEye = length(toEye);
// Normalize.
toEye /= distToEye;
// Default to multiplicative identity.
float4 texColor = float4(1, 1, 1, 1);
if(gUseTexure)
{
// Sample texture.
texColor = gDiffuseMap.Sample(samLinear, pin.Tex);
if(gAlphaClip)
{
// Discard pixel if texture alpha < 0.1. Note
// that we do this test as soon as possible so
// that we can potentially exit the shader early,
// thereby skipping the rest of the shader code.
clip(texColor.a - 0.1f);
}
}
//
// Normal mapping
//
float3 normalMapSample = gNormalMap.Sample(samLinear, pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(
normalMapSample, pin.NormalW, pin.TangentW);
//
// Lighting.
//
float4 litColor = texColor;
if(gLightCount > 0)
{
// Start with a sum of zero.
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// Sum the light contribution from each light source.
[unroll]
for(int i = 0; i < gLightCount; ++i)
{
float4 A, D, S;
ComputeDirectionalLight(gMaterial,
gDirLights[i], bumpedNormalW, toEye,
A, D, S);
ambient += A;
diffuse += D;
spec += S;
}l
itColor = texColor*(ambient + diffuse) + spec;
if(gReflectionEnabled)
{
float3 incident = -toEye;
float3 reflectionVector = reflect(
incident, bumpedNormalW);
float4 reflectionColor = gCubeMap.Sample(
samLinear, reflectionVector);
litColor += gMaterial.Reflect*reflectionColor;
}
}
//
// Fogging
//
if(gFogEnabled)
{
float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
// Blend the fog color and the lit color.
litColor = lerp(litColor, gFogColor, fogLerp);
}
// Common to take alpha from diffuse material and texture.
litColor.a = gMaterial.Diffuse.a * texColor.a;
return litColor;
}
18.7位移映射
现在我们已经理解了法线贴图,我们可以通过将其与曲面细分和位移贴图相结合来提高效果。 这样做的动机是法线贴图只是改善了光线细节,但并没有改善实际几何体的细节。 所以从某种意义上说,法线贴图只是一种照明技巧(见图18.7)。
置换映射的想法是利用称为高度图的额外映射,该映射描述曲面的凸起和裂缝。 换句话说,法线贴图具有三个颜色通道以产生每个像素的法向量(x,y,z),高度图具有单个颜色通道以在每个像素处产生高度值h。 在视觉上,高度图仅仅是一个灰度图像(灰色,因为只有一个颜色通道),其中每个像素被解释为高度值 - 它基本上是2D标量场h = f(x,z)的离散表示。 当我们对网格进行细分时,我们在域着色器中对高度贴图进行采样以偏移法向矢量方向上的顶点以向网格添加几何细节(参见图18.8)。
图18.7 法线贴图造成了凸起和裂缝的错觉,但是如果我们看一下线框,就会发现几何体是光滑的。
镶嵌几何图形添加三角形时,它不会自行添加细节。 也就是说,如果多次细分一个三角形,只会得到更多位于原始三角形平面上的三角形。 要添加细节(例如,凹凸和裂缝),则需要以某种方式偏移细分的顶点。 高度图是一个输入源,可以用来替换细分的顶点。 典型地,下面的公式用于替代顶点位置p,其中我们也使用向外表面法向量n作为位移的方向:
标量h∈[0,1]是从高度图获得的标量值。 我们从h中减去1来移动区间[0,1]→[-1,0]; 因为曲面法线是面向网格的外表面,这意味着我们取代“向外”而不是“向外”。这是常见的惯例,因为“弹出”几何体而不是“弹出”几何体通常更方便。 标量s是将整体高度缩放到某个世界空间值的比例因子。 高度图中的高度值h处于标准化的范围内,其中0表示最低高度(最向内的位移),1表示最高高度(无位移)。 假设我们最多要抵消5个世界单位。 那么我们取s = 5,使s(h - 1)∈[-5,0]。 通常将高度图存储在法线贴图的Alpha通道中。
图18.8 镶嵌阶段创建更多的三角形,以便我们可以在几何体中对小凸起和裂缝进行建模。位移图移动顶点以创建真正的几何凸块和裂缝。
图18.9 (左)法线贴图。(右)存储在法线贴图的Alpha通道中的高度贴图。白色值表示最高高度,黑色值表示最低高度。灰度值代表中间高度。
生成高度贴图是一项艺术任务,纹理艺术家可以绘制高度贴图或使用工具生成它们(例如http://www.crazybump.com/)。
18.8位移映射阴影代码
大部分位移映射代码都出现在顶点着色器,外壳着色器和域着色器中。 像素着色器与我们用于法线贴图的着色器相同。
18.8.1原始类型
为了将位移贴图集成到我们的渲染中,我们需要支持细分,以便我们可以将几何分辨率提高到与位移贴图分辨率更好匹配的几何分辨率。我们可以很容易地将曲面细分纳入现有的三角网格,方法是使用原始类型D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST而不是D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST绘制网格。这样,三角形的三个顶点被解释为三角形贴片的三个控制点,使我们能够镶嵌每个三角形。
18.8.2顶点着色器
在处理曲面细分时,我们必须决定每个三角形镶嵌多少。我们将采用简单的距离度量来确定镶嵌的数量。三角形越接近眼睛,它接收的曲面细分越多。顶点着色器通过计算每个顶点处的曲面细分因子,然后传递给外壳着色器来帮助进行距离计算。
我们在常量缓冲区中引入以下变量来控制距离计算。您为这些变量设置的值非常依赖于场景(您的场景有多大以及需要多少镶嵌细分):
cbuffer cbPerFrame
{
…
float gMaxTessDistance;
float gMinTessDistance;
float gMinTessFactor;
float gMaxTessFactor;
};
1.gMaxTessDistance:实现最大曲面细分的距离眼睛的距离。
2.gMinTessDistance:实现最小曲面细分的距离眼睛的距离。
3.gMinTessFactor:镶嵌的最小量。例如,您可能希望每个三角形至少3次镶嵌,无论距离多远。
4.gMaxTessFactor:镶嵌的最大数量。例如,您可能会发现您不需要镶嵌6次以上,因为不需要额外的细节。 镶嵌细分需要的数量取决于镶嵌之前输入网格有多少个三角形。 另外,请记住第13章的建议,其中三角形覆盖少于8个像素的效率不高。
观察gMaxTessDistance
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
float3 TangentL : TANGENT;
};
struct VertexOut
{
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 Tex : TEXCOORD;
float TessFactor : TESS;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to world space space.
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);
// Output vertex attributes for interpolation across triangle.
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
float d = distance(vout.PosW, gEyePosW);
// Normalized tessellation factor.
// The tessellation is
// 0 if d >= gMinTessDistance and
// 1 if d <= gMaxTessDistance.
float tess = saturate((gMinTessDistance - d) /
(gMinTessDistance - gMaxTessDistance));
// Rescale [0,1] --> [gMinTessFactor, gMaxTessFactor].
vout.TessFactor = gMinTessFactor +
tess*(gMaxTessFactor-gMinTessFactor);
return vout;
}
18.8.3 Hull Shader
回想一下,常量外壳着色器是按照每个补丁评估的,并且负责输出网格的所谓曲面细分因子。曲面细分因子指示曲面细分阶段需要多少细分补丁。 绝大多数确定曲面细分因子的工作都是由顶点着色器完成的,但恒定的外壳着色器仍有一些工作要做。 具体而言,通过平均顶点曲面细分因子来计算边缘曲面细分因子。 对于内部曲面细分因子,我们任意分配第一个边缘曲面细分因子。
struct PatchTess
{
float EdgeTess[3] : SV_TessFactor;
float InsideTess : SV_InsideTessFactor;
};
PatchTess PatchHS(InputPatch<VertexOut,3> patch,
uint patchID : SV_PrimitiveID)
{
PatchTess pt;
// Average vertex tessellation factors along edges.
// It is important to do the tessellation factor
// calculation based on the edge properties so that edges shared by
// more than one triangle will have the same tessellation factor.
// Otherwise, gaps can appear.
pt.EdgeTess[0] = 0.5f*(patch[1].TessFactor + patch[2].TessFactor);
pt.EdgeTess[1] = 0.5f*(patch[2].TessFactor + patch[0].TessFactor);
pt.EdgeTess[2] = 0.5f*(patch[0].TessFactor + patch[1].TessFactor);
// Pick an edge tessellation factor for the interior tessellation.
pt.InsideTess = pt.EdgeTess[0];
return pt;
}
由多个三角形共享的边具有相同的曲面细分系数很重要,否则会出现网格中的裂缝(参见图18.10)。 一种保证共享边的方法具有相同的镶嵌因子,就像我们在前面的代码中一样,仅根据边的属性计算镶嵌因子。 作为不计算曲面细分因子的一种方法,假设我们通过查看眼睛与三角形质心之间的距离来计算内部曲面细分因子。 然后我们将内部曲面细分因子传播到边缘曲面细分因子。 如果两个相邻的三角形具有不同的曲面细分因子,则它们的边缘将具有不同的曲面细分因子,从而形成T形接点,其可能导致位移映射之后的裂缝。
图18.10 (a)两个共享边的三角形。 (b)最上面的三角形得到曲面细分,以便引入沿着边缘的额外顶点。 底部的三角形不会被镶嵌。 (c)从位移映射中,新创建的顶点被移动。 这会在曾经共享边的两个三角形之间创建一个间隙(用阴影区域表示)。
回想一下,控制点外壳着色器输入一些控制点并输出一些控制点。 每个控制点输出调用一次控制点外壳着色器。 在我们的置换映射演示中,控制点外壳着色器只是一个“通过”着色器:
struct HullOut
{
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 Tex : TEXCOORD;
};
[domain(“tri”)]
[partitioning(“fractional_odd”)]
[outputtopology(“triangle_cw”)]
[outputcontrolpoints(3)]
[patchconstantfunc(“PatchHS”)]
HullOut HS(InputPatch<VertexOut,3> p,
uint i : SV_OutputControlPointID,
uint patchId : SV_PrimitiveID)
{
HullOut hout;
// Pass through shader.
hout.PosW = p[i].PosW;
hout.NormalW = p[i].NormalW;
hout.TangentW = p[i].TangentW;
hout.Tex = p[i].Tex;
return hout;
}
18.8.4域着色器
对于镶嵌阶段创建的每个顶点调用域着色器; 外壳着色器实质上是曲面细分的补丁的顶点着色器。 正是在这里我们通过对高度图(我们存储在法线贴图的alpha通道中)进行取样映射,并根据§18.7中讨论的公式对法线方向上的顶点进行偏移。
struct DomainOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 Tex : TEXCOORD;
};
// The domain shader is called for every vertex created by the
tessellator.
// It is like the vertex shader after tessellation.
[domain(“tri”)]
DomainOut DS(PatchTess patchTess,
float3 bary : SV_DomainLocation,
const OutputPatch<HullOut,3> tri)
{
DomainOut dout;
// Interpolate patch attributes to generated vertices.
dout.PosW = bary.x*tri[0].PosW +
bary.y*tri[1].PosW +
bary.z*tri[2].PosW;
dout.NormalW = bary.x*tri[0].NormalW +
bary.y*tri[1].NormalW +
bary.z*tri[2].NormalW;
dout.TangentW = bary.x*tri[0].TangentW +
bary.y*tri[1].TangentW +
bary.z*tri[2].TangentW;
dout.Tex = bary.x*tri[0].Tex +
bary.y*tri[1].Tex +
bary.z*tri[2].Tex;
// Interpolating normal can unnormalize it, so normalize it.
dout.NormalW = normalize(dout.NormalW);
//
// Displacement mapping.
//
// Choose the mipmap level based on distance to the eye;
// specifically, choose the next miplevel every MipInterval units,
// and clamp the miplevel in [0,6].
const float MipInterval = 20.0f;
float mipLevel = clamp(
(distance(dout.PosW, gEyePosW) - MipInterval) / MipInterval, 0.0f, 6.0f);
// Sample height map (stored in alpha channel).
float h = gNormalMap.SampleLevel(samLinear, dout.Tex, mipLevel).a;
// Offset vertex along normal.
dout.PosW += (gHeightScale*(h-1.0))*dout.NormalW;
// Project to homogeneous clip space.
dout.PosH = mul(float4(dout.PosW, 1.0f), gViewProj);
return dout;
}
值得一提的是,我们需要在域着色器中进行自己的mipmap级别选择。 通常的像素着色器方法Texture2D :: Sample在域着色器中不可用,所以我们必须使用Texture2D :: SampleLevel方法。 这种方法要求我们指定mipmap级别。
NOTE:本章的演示具有法线贴图和位移贴图的实现。 关键控制是:
1.按住“1”键进入线框模式。
2.按’2’进行基本渲染。
3.按’3’进行正常映射渲染。
4.按’4’进行位移贴图渲染。
18.9总结
1.法线贴图的策略是用法线贴图来渲染我们的多边形。然后我们获得每像素法线,这些法线可以捕捉曲面的细节,如凸起,划痕和裂缝。然后我们在光照计算中使用法线贴图中的这些每像素法线,而不是插值顶点法线。
2.法线贴图是纹理,但不是在每个纹素上存储RGB数据,而是分别在红色分量,绿色分量和蓝色分量中存储压缩的x坐标,y坐标和z坐标。我们使用各种工具来生成法线贴图,例如http://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop和http://www.crazybump.com。
3.法线贴图中法线的坐标与纹理空间坐标系有关。因此,为了进行光照计算,我们需要将法线从纹理空间转换到世界空间,以使光线和法线处于相同的坐标系中。在每个顶点建立的TBN基地促进了从纹理空间到世界空间的转换。
4.位移贴图的思想是利用一个称为高度贴图的附加贴图,它描述了曲面的凸起和裂缝。当我们细化网格时,我们在域着色器中对高度图进行采样以偏移法向矢量方向上的顶点以向网格添加几何细节。