DirectX11 With Windows SDK--25 法线贴图_玖富娱乐主管发


玖富娱乐是一家为代理招商,直属主管信息发布为主的资讯网站,同时也兼顾玖富娱乐代理注册登录地址。

媒介

在很早之前的纹理映照中,纹理寄存的元素是像素的色彩,经由历程纹理坐标映照到目标像素以猎取其色彩。然则我们的法向量依旧只是界说在极点上,关于三角形面内一点的法向量,也只是经由历程对照简单的插值法盘算出响应的法向量值。这对平坦的外面对照有效,但没法显示出内部粗拙的外面。在这一章,你将相识怎样猎取更高精度的法向量以形貌一个粗拙平面。

DirectX11 With Windows SDK完全目次

Github项目源码

法线贴图

法线贴图是指纹理中现实寄存的元素一般是经由紧缩后的法向量,用于显示一个外面凹凸不平的特征,它是凹凸贴图的一种完成体式格局。

开启法线贴图后的结果

封闭法线贴图后的结果

法线贴图中寄存的法向量((x, y, z))离别对应本来的((r, g, b))。每一个像素都寄存了对应的一个法向量,经由紧缩后运用24 bit便可透露表现。现实情况则是一张法线贴图内里的每一个像素运用了32 bit来透露表现,盈余的8 bit(位于Alpha值)要末可以或许不运用,要末用来透露表现高度值或许镜面系数。而未经紧缩的法线贴图一般为每一个像素寄存4个浮点数,即运用128 bit来透露表现。

下面展现了一张法线贴图,每一个像素点地位寄存了恣意偏向的法向量。可以或许看到这里为法线贴图竖立了一个TBN坐标系(左手坐标系),个中T轴(Tangent Axis)对应本来的x轴,B轴(Binormal Axis)对应本来的y轴,N轴(Normal Axis)对应本来的z轴。竖立坐标系的目标在后面再详细形貌。视察这些法向量,它们都有一个配合的特性,就是都朝着N轴的正偏向散射,如许使得大多数法向量的z重量是最大的。

因为紧缩后的法线贴图一般是以R8G8B8A8的花样存储,我们也可以或许直接把它当作图片来翻开视察。

前面说到大部分法向量的z重量会比x, y重量大,致使全部图看起来会偏蓝。

法线贴图的紧缩与解压

经由开端紧缩后的法线贴图的占用空间为本来的1/4(不斟酌文件头),就算每一个重量只要256种透露表现,也充足透露表现出16777216种分歧的法向量了。若是如今我们已经有未经由紧缩的法线贴图,那要怎样举行开端紧缩呢?

关于一个单元法向量来讲,其恣意一个重量的取值也不过就是落在[-1, 1]的区间上。如今我们要将其映照到[0, 255]的区间上,可以或许用下面的公式来举行紧缩:

[f(x) = (0.5x 0.5) * 255]

而若是如今拿到的是24位法向量,要举行复原,则可以或许用下面的公式:

[ f^-1(x) = frac{2x}{255} - 1]

固然,经由复原后的法向量是有部分的精度丧失了,最少可以或许映照回[-1, 1]的区间上。

一般情况下我们能拿到的都是经由紧缩后的法线贴图,然则复原事情照样须要由本身来完成。

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

经由上面的采样后,normalT的每一个重量会自动从[0, 255]映照到[0, 1],但还不是终究[-1, 1]的区间。因而我们还须要完成下面这一步:

normalT = 2.0f * normalT - 1.0f;

这里的1.0f会扩大成float3(1.0f, 1.0f, 1.0f)以完成减法运算。

注重:若是你想要运用紧缩纹理花样(对本来的R8G8B8A8进一步紧缩)来存储法线贴图,可以或许运用BC7(DXGI_FORMAT_BC7_UNORM)来取得最好机能。在DirectXTex中有大批从BC1到BC7的纹理紧缩/解压函数。

纹理/切线空间

这里最先就会发生一个疑问了,为何须要切线空间?

在只要2D的纹理坐标系仅包罗了U轴和V轴,但如今我们的纹理中寄存的是法向量,这些法向量要怎样变更到部分物体上某一个三角形对应地位呢?这就须要我们对以后法向量做一次矩阵变更(平移和扭转),使它可以或许来到部分坐标系下物体的某处外面。因为矩阵变更涉及到的是坐标系变更,我们须要先在本来的2D纹理坐标系加一条坐标轴(N轴),与T轴(本来的U轴)和B轴(本来的V轴)互相垂直,以此组成切线空间。

一最先法向量处在单元切线空间,而须要变更到目标3D三角形的地位也有一个对应的切线空间。关于一个立方体来讲,一个面的两个三角形可以或许共用一个切线空间。

应用极点地位和纹理坐标求TBN坐标系

如今假定我们的极点只包罗了地位和纹理坐标这两个信息,有如许一个三角形,它们的极点为V0(x0, y0, z0), V1(x1, y1, z1), V2(x2, y2, z2),纹理坐标为(u0, v0), (u1, v1), (u2, v2)。

图片展现了一个三角形与所处的切线空间,我们可以或许如许界说向量E0E1

[mathbf{e_0} = mathbf{V_1} - mathbf{V_0}]
[mathbf{e_1} = mathbf{V_2} - mathbf{V_0}]

如今T轴和B轴都是待求的单元向量,可以或许列出下述干系:

[(Delta u_0, Delta v_0) = (u_1 - u_0, v_1 - v_0)]
[(Delta u_1, Delta v_1) = (u_2 - u_0, v_2 - v_0)]
[mathbf{e_0} = Delta u_0mathbf{T} Delta v_0mathbf{B}]
[mathbf{e_1} = Delta u_1mathbf{T} Delta v_1mathbf{B}]

把它用矩阵来形貌:

[ begin{bmatrix} mathbf{e_0} \ mathbf{e_1} end{bmatrix} = begin{bmatrix} Delta u_0 & Delta v_0 \ Delta u_1 & Delta v_1 end{bmatrix} begin{bmatrix} mathbf{T} \ mathbf{B} end{bmatrix} ]

继承细化:

[ begin{bmatrix} e_{0x} & e_{0y} & e_{0z} \ e_{1x} & e_{1y} & e_{1z} end{bmatrix} = begin{bmatrix} Delta u_0 & Delta v_0 \ Delta u_1 & Delta v_1 end{bmatrix} begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z end{bmatrix} ]

为了盘算TB矩阵,须要在等式双方左乘uv矩阵的逆:

[ {begin{bmatrix} Delta u_0 & Delta v_0 \ Delta u_1 & Delta v_1 end{bmatrix}}^{-1} begin{bmatrix} e_{0x} & e_{0y} & e_{0z} \ e_{1x} & e_{1y} & e_{1z} end{bmatrix} = begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z end{bmatrix} ]

-玖富娱乐是一家为代理招商,直属主管信息发布为主的资讯网站,同时也兼顾玖富娱乐代理注册登录地址。-

关于一个二阶矩阵极点求逆,我们不斟酌历程。已知有矩阵(mathbf{A} = begin{bmatrix} a & b \ c & d end{bmatrix}),那末它的逆矩阵为:

[ mathbf{A}^{-1} = frac{1}{ad-bc}begin{bmatrix} d & -b \ -c & a end{bmatrix} ]

因而上面的方程终究酿成:

[ begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z end{bmatrix} = frac{1}{Delta u_0 Delta v_1 - Delta v_0 Delta u_1} begin{bmatrix} Delta v_1 & - Delta v_0 \ -Delta u_1 & Delta u_0 end{bmatrix} begin{bmatrix} e_{0x} & e_{0y} & e_{0z} \ e_{1x} & e_{1y} & e_{1z} end{bmatrix} ]

这里可以或许找一个例子实验一下:
V0坐标为(0, 0, -0.25), 纹理坐标为(0, 0.5)
V1坐标为(0.15, 0, 0), 纹理坐标为(0.3, 0)
V2坐标为(0.4, 0, 0), 纹理坐标为(0.8, 0)

求解历程以下:
[ mathbf{e_0} = mathbf{V_1} - mathbf{V_0} = (0.15, 0, 0.25) ]
[ mathbf{e_1} = mathbf{V_2} - mathbf{V_0} = (0.4, 0, 0.25) ]
[ (Delta u_0, Delta v_0) = (u_1 - u_0, v_1 - v_0) = (0.3, -0.5) ]
[ (Delta u_1, Delta v_1) = (u_2 - u_0, v_2 - v_0) = (0.8, -0.5) ]
[ begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z end{bmatrix} = frac{1}{0.3 times (-0.5) - (-0.5) times 0.8} begin{bmatrix} -0.5 & 0.5 \ -0.8 & 0.3 end{bmatrix} begin{bmatrix} 0.15 & 0 & 0.25 \ 0.4 & 0 & 0.25 end{bmatrix} = begin{bmatrix} 0.5 & 0 & 0 \ 0 & 0 & -0.5 end{bmatrix} ]

因为地位坐标和纹理坐标的不一致性,致使求出来的T向量和B向量很有能够不是单元向量。仅当地位坐标的转变率与纹理坐标的转变率相同时才会获得单元向量。这里我们将其举行标准化便可。

但若是对纹理坐标举行了变更,有能够致使T轴和B轴不互相垂直。好比实验用球体网格模子某个三角形面内的一点映照到球面上一点。

极点切线空间

上面的运算获得的切线空间是基于单个三角形的,可以或许看到其运算历程照样对照复杂,而且交给着色器来举行运算的话还会发生大批的指令。

我们可以或许为极点添加法向量N和切线向量T用于构建基于极点的切线空间。很早之前提到法向量是与该极点共用的一切三角形的法向量取平均值所获得的。切线向量也一样,它是与该极点共用的一切三角形的切线向量取平均值所获得的。

如今Vertex.h界说了我们的新极点范例:

struct VertexPosNormalTangentTex
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT4 tangent;
    DirectX::XMFLOAT2 tex;
    static const D3D11_INPUT_ELEMENT_DESC inputLayout[4];
};

这里的tangent是一个4D向量,斟酌到要和微软DXTK界说的极点范例保持一致,多出来的w重量可以或许留作他用,这里暂不议论。

施密特向量正交化

一般极点供应的NT一般是互相垂直的,而且都是单元向量,我们可以或许经由历程盘算(mathbf{B} = mathbf{N} times mathbf{T})来获得副法线向量B,使得极点可以或许不须要寄存副法线向量B。然则经由插值盘算后的NT能够会致使不是互相垂直,我们最好照样要经由历程施密特正交化来取得现实的切线空间。

如今已知互不垂直的N向量和T向量,我们愿望求出与N向量垂直的T'向量,须要将T向量投影到N向量上。

从上面的图我们可以或许晓得终究求得的T'

[ mathbf{T'} = lVert mathbf{T} - (mathbf{T} cdot mathbf{N}) mathbf{N} rVert ]

B' 终究也可以或许肯定下来
[ mathbf{B'} = mathbf{N} times mathbf{T'}]

如许T', B', N互相垂直,可以或许组成TBN坐标系。在后面的着色器完成中我们也会用到这部分内容。

切线空间的变更

一最先的切线空间可以或许用一个单元矩阵来透露表现,切线向量恰是处在这个空间中。紧接着就是须要对其举行一次到部分工具(详细到某个三角形)切线空间的变更:

[ mathbf{M}_{object} = begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z \ N_x & N_y & N_z end{bmatrix} ]

然后切线向量伴同天下矩阵一同举行变更来到天下坐标系,因而我们可以或许把它写成:

[ mathbf{n}_{world} = mathbf{n}_{tangent}mathbf{M}_{object}mathbf{M}_{world} ]

注重:

  1. 对切线向量举行矩阵变更,我们只须要运用3x3的矩阵便可。
  2. 法线向量变更到天下矩阵须要用天下矩阵求逆的转置举行校订,而对切线向量只须要用天下矩阵变更便可。下图演示了将宽度拉伸为本来2倍后,法线和切线向量的转变:

HLSL代码

为了运用法线贴图,我们须要完成以下步调:

  1. 猎取该纹理所须要用到的法线贴图,在C 端为其建立一个ID3D11Texture2D。这里不斟酌怎样制造一张法线贴图。
  2. 关于一个网格模子来讲,极点数据须要包罗地位、法向量、切线向量、纹理坐标四个元素。一样这里不议论模子的制造,在本教程运用的是Geometry所天生的网格模子
  3. 在极点着色器中,将极点法向量和切线向量从部分坐标系变更到天下坐标系
  4. 在像素着色器中,运用经由插值的法向量和切线向量来为每一个三角形外面的像素点构建TBN坐标系,然后将切线空间的法向量变更到天下坐标系中,如许终究求得的法向量用于光照盘算。

如今我们的Basic.hlsli相沿的是第23章动态天空盒的部分,转变以下:

Texture2D gDiffuseMap : register(t0);
Texture2D gNormalMap : register(t1);
TextureCube gTexCube : register(t2);
SamplerState gSam : register(s0);

// 运用的是第23章的常量缓冲区,省略...
// 省略和之前一样的构造体...

struct VertexPosNormalTangentTex
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float4 TangentL : TANGENT;
    float2 Tex : TEXCOORD;
};

struct InstancePosNormalTangentTex
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float4 TangentL : TANGENT;
    float2 Tex : TEXCOORD;
    matrix World : World;
    matrix WorldInvTranspose : WorldInvTranspose;
};

struct VertexPosHWNormalTangentTex
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION; // 在天下中的地位
    float3 NormalW : NORMAL; // 法向量在天下中的偏向
    float4 TangentW : TANGENT; // 切线在天下中的偏向
    float2 Tex : TEXCOORD;
};

float3 NormalSampleToWorldSpace(float3 normalMapSample,
    float3 unitNormalW,
    float4 tangentW)
{
    // 将读取到法向量中的每一个重量从[0, 1]复原到[-1, 1]
    float3 normalT = 2.0f * normalMapSample - 1.0f;

    // 构建位于天下坐标系的切线空间
    float3 N = unitNormalW;
    float3 T = normalize(tangentW.xyz - dot(tangentW.xyz, N) * N); // 施密特正交化
    float3 B = cross(N, T);

    float3x3 TBN = float3x3(T, B, N);

    // 将凹凸法向量从切线空间变更到天下坐标系
    float3 bumpedNormalW = mul(normalT, TBN);

    return bumpedNormalW;
}

上面的NormalSampleToWorldSpace函数用于将法向量从切线空间变更到天下空间,位于Basic.hlsli。它接受了3个参数:从法线贴图采样获得的向量,变更到天下坐标系的法向量和切线向量。

然后是极点着色器:

// NormalMapObject_VS.hlsl
#include "Basic.hlsli"

// 极点着色器
VertexPosHWNormalTangentTex VS(VertexPosNormalTangentTex vIn)
{
    VertexPosHWNormalTangentTex vOut;
    
    matrix viewProj = mul(gView, gProj);
    vector posW = mul(float4(vIn.PosL, 1.0f), gWorld);

    vOut.PosW = posW.xyz;
    vOut.PosH = mul(posW, viewProj);
    vOut.NormalW = mul(vIn.NormalL, (float3x3) gWorldInvTranspose);
    vOut.TangentW = mul(vIn.TangentL, gWorld);
    vOut.Tex = vIn.Tex;
    return vOut;
}
// NormalMapInstance_VS.hlsl
#include "Basic.hlsli"

// 极点着色器
VertexPosHWNormalTangentTex VS(InstancePosNormalTangentTex vIn)
{
    VertexPosHWNormalTangentTex vOut;
    
    matrix viewProj = mul(gView, gProj);
    vector posW = mul(float4(vIn.PosL, 1.0f), vIn.World);

    vOut.PosW = posW.xyz;
    vOut.PosH = mul(posW, viewProj);
    vOut.NormalW = mul(vIn.NormalL, (float3x3) vIn.WorldInvTranspose);
    vOut.TangentW = mul(vIn.TangentL, vIn.World);
    vOut.Tex = vIn.Tex;
    return vOut;
}

比拟之前的像素着色器,如今它多了对法线映照的处置惩罚:

// 法线映照
float3 normalMapSample = gNormalMap.Sample(gSam, pIn.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample, pIn.NormalW, pIn.TangentW);

求得的法向量bumpedNormalW将用于光照盘算。

如今完全的像素着色器代码以下:

// NormalMap_PS.hlsl
#include "Basic.hlsli"

// 像素着色器(3D)
float4 PS(VertexPosHWNormalTangentTex pIn) : SV_Target
{
    // 若不运用纹理,则运用默许白色
    float4 texColor = float4(1.0f, 1.0f, 1.0f, 1.0f);

    if (gTextureUsed)
    {
        texColor = gDiffuseMap.Sample(gSam, pIn.Tex);
        // 提早举行裁剪,对不符合请求的像素可以或许制止后续运算
        clip(texColor.a - 0.1f);
    }
    
    // 标准化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 求出极点指向眼睛的向量,和极点与眼睛的间隔
    float3 toEyeW = normalize(gEyePosW - pIn.PosW);
    float distToEye = distance(gEyePosW, pIn.PosW);

    // 法线映照
    float3 normalMapSample = gNormalMap.Sample(gSam, pIn.Tex).rgb;
    float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample, pIn.NormalW, pIn.TangentW);

    // 初始化为0 
    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);
    float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
    int i;

    [unroll]
    for (i = 0; i < 5;   i)
    {
        ComputeDirectionalLight(gMaterial, gDirLight[i], bumpedNormalW, toEyeW, A, D, S);
        ambient  = A;
        diffuse  = D;
        spec  = S;
    }
        
    [unroll]
    for (i = 0; i < 5;   i)
    {
        ComputePointLight(gMaterial, gPointLight[i], pIn.PosW, bumpedNormalW, toEyeW, A, D, S);
        ambient  = A;
        diffuse  = D;
        spec  = S;
    }

    [unroll]
    for (i = 0; i < 5;   i)
    {
        ComputeSpotLight(gMaterial, gSpotLight[i], pIn.PosW, bumpedNormalW, toEyeW, A, D, S);
        ambient  = A;
        diffuse  = D;
        spec  = S;
    }
  
    float4 litColor = texColor * (ambient   diffuse)   spec;

    // 反射
    if (gReflectionEnabled)
    {
        float3 incident = -toEyeW;
        float3 reflectionVector = reflect(incident, pIn.NormalW);
        float4 reflectionColor = gTexCube.Sample(gSam, reflectionVector);

        litColor  = gMaterial.Reflect * reflectionColor;
    }
    // 折射
    if (gRefractionEnabled)
    {
        float3 incident = -toEyeW;
        float3 refractionVector = refract(incident, pIn.NormalW, gEta);
        float4 refractionColor = gTexCube.Sample(gSam, refractionVector);

        litColor  = gMaterial.Reflect * refractionColor;
    }

    litColor.a = texColor.a * gMaterial.Diffuse.a;
    return litColor;
}

一切的着色器将共用Basic.hlsli。而对BasicEffect的转变(和C 的交互)这里我们不议论。

下面的动画演示了法线贴图的对照结果(GIF画质有点渣):

至此进阶篇就告一段落了。

DirectX11 With Windows SDK完全目次

Github项目源码

-玖富娱乐是一家为代理招商,直属主管信息发布为主的资讯网站,同时也兼顾玖富娱乐代理注册登录地址。