第九章 Normal Mapping and Displacement Mapping
本章主要讲述两种图形学技术,支持在不增加objects的poly primitive的情况下,在场景中增加更多的细节。第一种是normal mapping,通过创建一些“fake” geometry(虚设的多边形图元)模拟光照作用。第二种是displacement mapping,根据纹理数据moving vertices actually(与“fake”相对应,这里指真实的移动)来创建凹凸不平的表面。
Normal Mapping(法线贴图)
前面几章,已经讨论了specualr maps,environment maps以及transparency maps,这些texture maps都提供了附加的数据信息。Specular maps中的数据用于限制specular highlight,environment maps则包含了用于reflective surfaces的colors,transparency maps用于控制output-merge阶段的alpha blending。这些额外的信息由每一个pixel提供,比仅由每一个vertex提供,具有更高的精度。同样 ,一个normal map也是对每一个pixel都提供表面的法向量数量,该额外的数据可以应用于多种技术上。
Normal maps的其中一个应用是虚构一个凹凸表面的细节,比如石墙。可以使用足够的geometry来模拟这中凹凸不平的墙面,使用的vertices越多,得到的细节也越多。这样就可以在场景中更好的处理光照,远离光源的石块就会显示为较暗的区域。但是,增加geometry会导致计算成本也增大。相反,对于一个含有少量poly geometry的object(即使是一个flat plane),使用normal map方法,也可以模拟与大量增加geometry情况下同样的光照效果。这种应用就称为normal mapping。
Normal Maps
图9.1中显示了用于一面石墙的color map(左图)和normal map(右图)。相对于color map,normal map看起来有点奇怪。虽然可以把norml map显示出来,但是normal map中存储的是3D directions vectors(3维的方向向量)。对normal map进行采样时,从RGB通道中得到的结果表示方向向量的x,y,z分量。这些法向量可用于一些效果的计算,比如diffuse lighting。
图9.1 A color map (left) and normal map (right) for a stone wall. (Textures by Nick
Zuccarello, Florida Interactive Entertainment Academy.)
RGB texture的每一个channels都存储一个unsigned 8-bit值(unsigned char),该类型的数值范围为[0, 255]。但是一个规范化的方向向量,对应的xyz各个分量值范围是[-1, 1]。因此,法线向量存储到texture之前必须先转换到范围[0, 255],对texture进行采样的时候必须再转换到范围[-1, 1]。可以使用如下的公式,把浮点型向量从范围[-1, 1]转换到范围[0, 255]:
f(x) = (0.5x + 0.5) * 255
再使用下面的方程式变换回来:
实际工作中,一般会使用图像处理软件(比如Adobe Photoshop)把一个normal map编码成RGB texture格式。但是采样texture时需要在shader中手动计算,把数据从范围[0, 255]转换回[-1, 1]。在采样过程中已经执行了浮点除法(除以255)操作,所以得到的采样结果在范围[0, 1]之间。因此,只需要使用下面的方程函数把范围[0, 1]转换到范围[1, 1]:
f(x) = 2x − 1
或者,也可以使用16位或32位浮点型数值作为normal maps的texture格式,这样可以产生更好的细节效果,但会牺牲一些性能。
Tangent Space(切空间)
一般使用每一个vertex的法线来计算diffuse,同样也可以使用每一个pixel的法线。但是法线必须与light处于同一个坐标空间。对于per-vertex法线,由object space提供。但是normal maps的法线值处于tangent space。
Tangent space(或者texture space)是一个相对于纹理的坐标系,由三个相互正交的向量确定:surface normal向量,tangent向量以及binormal向量。图9.2描述了这三个向量。
图9.2 An illustration of tangent space. (Texture by Nick Zuccarello, Florida Interactive Entertainment Academy.)
其中normal向量,N,是一个vertex的表面法向量。Tangent向量,T,与表面的法线垂直,与指向texture的u轴方向。Binormal向量,B,则指向texture的v轴方向。
可以使用这三个向量创建一个TBN(tangent, binormal, normal)变换矩阵,如下所示:
可以使用这个矩阵把向量由tangent space变换到object space中。但是,由于light vector通常是在world space中,因此需要把从normal map中采样得到的noraml从tangent space变换到world space。或者换一种方式,直接使用已经处于world space中的向量创建TBN矩阵。
注意
可以把normals直接编码到world space中,就可以省去从tangent space到world space的变换。但是,使用这些normals的objects只能保持静止不动(不能执行变换)。另外,这些normals也不能简单地在多个ojbects之间重用(因为这些normals不能再进行变换)。
TBN矩阵有一个非常值得关注的特性,该矩阵由三个orthonormal向量(相互正交的单位向量)构成,并形成了一个正交基(定义了一个坐标系)。也就说该矩阵是一个正交矩阵,而正交矩阵的逆等于其转置。因此,把一个向量从object或者world space变换回tangent space(inverse mapping反向映射),只需要把该向量与TBN矩阵的转置相乘即可。另外,根据TBN矩阵的orthonormal性质,如果已经知道了任意两个向量,就可以推导出第三个向量。通常情况下,normal和tangent向量与geometry一起存储,而binormal向量(在vertex shader中)由这两个向量进行cross product计算得出。执行这种计算是对GPU运算和数据传输(在CPU和GPU之间)高成本之间的一种折衷的方法。
A Normal Mapping Effect
根据上面所讨论的方法,列表9.1列出了一种normal mapping effect的代码。
列表9.1 NormalMapping.fx
#include "include\\Common.fxh"
cbuffer CBufferPerFrame
{
float4 AmbientColor : AMBIENT <
string UIName = "Ambient Light";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float4 LightColor : COLOR <
string Object = "LightColor0";
string UIName = "Light Color";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float3 LightDirection : DIRECTION <
string Object = "DirectionalLight0";
string UIName = "Light Direction";
string Space = "World";
> = {0.0f, 0.0f, -1.0f};
float3 CameraPosition : CAMERAPOSITION < string UIWidget="None"; >;
}
cbuffer CBufferPerObject
{
float4x4 WorldViewProjection : WORLDVIEWPROJECTION < string UIWidget="None"; >;
float4x4 World : WORLD < string UIWidget="None"; >;
float4 SpecularColor : SPECULAR <
string UIName = "Specular Color";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float SpecularPower : SPECULARPOWER <
string UIName = "Specular Power";
string UIWidget = "slider";
float UIMin = 1.0;
float UIMax = 255.0;
float UIStep = 1.0;
> = {25.0f};
}
Texture2D ColorTexture <
string ResourceName = "default_color.dds";
string UIName = "Color Texture";
string ResourceType = "2D";
>;
Texture2D NormalMap <
string ResourceName = "default_bump_normal.dds";
string UIName = "Normap Map";
string ResourceType = "2D";
>;
SamplerState TrilinearSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
RasterizerState DisableCulling
{
CullMode = NONE;
};
/************* Data Structures *************/
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
float3 Normal : NORMAL;
float3 Tangent : TANGENT;
};
struct VS_OUTPUT
{
float4 Position : SV_Position;
float3 Normal : NORMAL;
float3 Tangent : TANGENT;
float3 Binormal : BINORMAL;
float2 TextureCoordinate : TEXCOORD0;
float3 LightDirection : TEXCOORD1;
float3 ViewDirection : TEXCOORD2;
};
/************* Vertex Shader *************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
VS_OUTPUT OUT = (VS_OUTPUT)0;
OUT.Position = mul(IN.ObjectPosition, WorldViewProjection);
OUT.Normal = normalize(mul(float4(IN.Normal, 0), World).xyz);
OUT.Tangent = normalize(mul(float4(IN.Tangent, 0), World).xyz);
OUT.Binormal = cross(OUT.Normal, OUT.Tangent);
OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate);
OUT.LightDirection = normalize(-LightDirection);
float3 worldPosition = mul(IN.ObjectPosition, World).xyz;
float3 viewDirection = CameraPosition - worldPosition;
OUT.ViewDirection = normalize(viewDirection);
return OUT;
}
/************* Pixel Shader *************/
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 OUT = (float4)0;
float3 sampledNormal = (2 * NormalMap.Sample(TrilinearSampler, IN.TextureCoordinate).xyz) - 1.0; // Map normal from [0..1] to [-1..1]
float3x3 tbn = float3x3(IN.Tangent, IN.Binormal, IN.Normal);
sampledNormal = mul(sampledNormal, tbn); // Transform normal to world space
float3 viewDirection = normalize(IN.ViewDirection);
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb);
LIGHT_CONTRIBUTION_DATA lightContributionData;
lightContributionData.Color = color;
lightContributionData.Normal = sampledNormal;
lightContributionData.ViewDirection = viewDirection;
lightContributionData.LightDirection = float4(IN.LightDirection, 1);
lightContributionData.SpecularColor = SpecularColor;
lightContributionData.SpecularPower = SpecularPower;
lightContributionData.LightColor = LightColor;
float3 light_contribution = get_light_contribution(lightContributionData);
OUT.rgb = ambient + light_contribution;
OUT.a = 1.0f;
return OUT;
}
/************* Techniques *************/
technique10 main10
{
pass p0
{
SetVertexShader(CompileShader(vs_4_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, pixel_shader()));
SetRasterizerState(DisableCulling);
}
}
Normal Mapping Preamble
在该effect中,首先使用了一个ambient light,specular highlight以及一个directional light。新增了一个用于表示normal map的Texture2D对象,VS_INPUT结构体包含一个surface normal和一个tangent vector(处于object space中)。VS_OUPUT中新增了表示tangent和binormal的成员,在传递给下一个管线阶段之间,需要变换到world space中。
Normal Mapping Vertex and Pixel Shader
在vertex shader中,先把normal和tangent向量变换到world space中,然后计算这两个向量的cross product得到binormal向量。在pixel shader,先对normal map进行采样,再把采样得到的normal向量由范围[0, 1]转换到范围[-1, 1]。然后创建TBN矩阵,并用该矩阵把采样的normal向量变换到world space中。当normal变换到world space中之后,接下来的光照计算与之前的完全一样。
注意
如果你发现vertex shader是一个性能瓶颈,可以在输入参数中直接提供bionormal(与surface normal和tangent一起)。这是为了平衡vertex shader性能和图形总线中数据传输成本的一种折衷方法。更普遍的情况是,图形管线成为了瓶颈。
Normal Mapping Output
图9.3显示了在一个带有stone wall纹理的plane上,使用normal mapping effect的输出结果。左图中使用了图9.1中的normal map。而右图中,使用了一个normal map但是没有任何效果。两幅图中,ambient light都是禁用的,direcitonal light是纯白色,full-intensity(强度值为1.0),specular highlight的power值为100,intensity值为0.35。
图9.3 NormalMapping.fx applied to a plane with a stone wall texture using a normal
map (left) and without a normal map (right). (Textures by Nick Zuccarello, Florida Interactive
Entertainment Academy.)