第九章 Normal Mapping and Displacement Mapping

第九章 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。
第九章 Normal Mapping and Displacement Mapping_第1张图片
图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描述了这三个向量。
第九章 Normal Mapping and Displacement Mapping_第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.)

你可能感兴趣的:(第九章 Normal Mapping and Displacement Mapping)