DirectX11 With Windows SDK--09 纹理映射与采样器状态

前言

在之前的DirectX SDK中,纹理的读取使用的是D3DX11CreateShaderResourceViewFromFile函数,现在在Windows SDK中已经没有这些函数,我们需要找到DDSTextureLoaderWICTextureLoader这两个库来读取DDS位图和WIC位图

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

纹理坐标系

纹理坐标系和屏幕、图片坐标系的有些相似,它们的U轴都是水平朝右,V轴竖直向下。但是纹理的X和Y的取值范围都为[0.0, 1.0],分别映射到[0, Width][0, Height]

对于一个3D的三角形,通过给这三个顶点额外的纹理坐标信息,那么三个纹理坐标就可以映射到纹理指定的某片三角形区域。

这样的话已知三个顶点的坐标p0,p1p2以及三个纹理坐标q0,q1q2,就可以求出顶点坐标映射与纹理坐标的对应关系:

\[(x, y, z) = \mathbf{p_0} + s(\mathbf{p_1} - \mathbf{p_0}) + t(\mathbf{p_2} - \mathbf{p_0}) \]

\[(u, v) = \mathbf{q_0} + s(\mathbf{q_1} - \mathbf{q_0}) + t(\mathbf{q_2} - \mathbf{q_0}) \]

并且还需要满足\(s >= 0, t >= 0, s + t <= 1\)

所以顶点结构体的内容会有所变化:

struct VertexPosNormalTex
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT2 tex;
    static const D3D11_INPUT_ELEMENT_DESC inputLayout[3];
};

对应的每个输入元素的描述为:

const D3D11_INPUT_ELEMENT_DESC VertexPosNormalTex::inputLayout[3] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0}
};

纹理读取

DDS位图和WIC位图

DDS是一种图片格式,是DirectDraw Surface的缩写,它是DirectX纹理压缩(DirectX Texture Compression,简称DXTC)的产物。由NVIDIA公司开发。大部分3D游戏引擎都可以使用DDS格式的图片用作贴图,也可以制作法线贴图。

WIC(Windows Imaging Component)是一个可以扩展的平台,为数字图像提供底层API,它可以支持bmp、dng、ico、jpeg、png、tiff等格式的位图。

DDSTextureLoader和WICTextureLoader库

要使用这两个库,有两种方案。

第一种:在DirectXTex中找到DDSTextureLoader文件夹和WICTextureLoader文件夹中分别找到对应的头文件和源文件(不带12的),并加入到你的项目中

第二种:将DirectXTK库添加到你的项目中,这里不再赘述

这之后就可以包含DDSTextureLoader.hWICTextureLoader.h进项目中了。

CreateDDSTextureFromFile函数--从文件读取DDS纹理

现在读取DDS纹理的操作变得更简单了:

HRESULT CreateDDSTextureFromFile(
    ID3D11Device* d3dDevice,                // [In]D3D设备
    const wchar_t* szFileName,              // [In]dds图片文件名
    ID3D11Resource** texture,               // [Out]输出一个指向资源接口类的指针,也可以填nullptr
    ID3D11ShaderResourceView** textureView, // [Out]输出一个指向着色器资源视图的指针,也可以填nullptr
    size_t maxsize = 0,                     // [In]忽略
    DDS_ALPHA_MODE* alphaMode = nullptr);  // [In]忽略

下面是一个调用的例子:

// 初始化木箱纹理
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));

CreateWICTextureFromFile函数--从文件读取WIC纹理

函数原型如下:

HRESULT CreateWICTextureFromFile(
    ID3D11Device* d3dDevice,                // [In]D3D设备
    const wchar_t* szFileName,              // [In]wic所支持格式的图片文件名
    ID3D11Resource** texture,               // [Out]输出一个指向资源接口类的指针,也可以填nullptr
    ID3D11ShaderResourceView** textureView, // [Out]输出一个指向着色器资源视图的指针,也可以填nullptr
    size_t maxsize = 0);                     // [In]忽略

下面是一个调用的例子:

// 初始化火焰纹理
WCHAR strFile[40];
m_pFireAnims.resize(120);
for (int i = 1; i <= 120; ++i)
{
    wsprintf(strFile, L"Texture\\FireAnim\\Fire%03d.bmp", i);
    HR(CreateWICTextureFromFile(m_pd3dDevice.Get(), strFile, nullptr, m_pFireAnims[i - 1].GetAddressOf()));
}

这里我们只需要创建着色器资源视图,而不是纹理资源。原因在后面会提到。

过滤器

图片的放大

图片在经过放大操作后,除了图片原有的像素被拉伸,还需要对其余空缺的像素位置选用合适的方式来进行填充。比如一个2x2位图被拉伸成8x8的,除了角上4个像素,还需要对其余60个像素进行填充。下面介绍几种方法

常量插值法

对于2x2位图,它的宽高表示范围都为[0,1],而8x8位图的都为[0,7],且只允许取整数。那么对于放大后的像素点(1, 4)就会映射到(1/7, 4/7)上。

常量插值法的做法十分简单粗暴,就是对X和Y值都进行四舍五入操作,然后取邻近像素点的颜色。比如对于映射后的值如果落在[20.5, 21.5)的范围,最终都会取21。根据上面的例子,最终会落入到像素点(0, 1)上,然后取该像素点的颜色。

线性插值法

现在只讨论一维情况,已知第20个像素点的颜色p0和第21个像素点的颜色p1,并且经过拉伸放大后,有一个像素点落在范围(20, 21)之间,我们就可以使用线性插值法求出最终的颜色(t取(0,1)):

\[\mathbf{p} = t\mathbf{p_1} + (1 - t)\mathbf{p_0} \]

对于二维情况,会有三种使用线性插值法的情况:

  1. X方向使用常量插值法,Y方向使用线性插值法
  2. X方向使用线性插值法,Y方向使用常量插值法
  3. X和Y方向均使用线性插值法

下图展示了双线性插值法的过程,已知4个相邻像素点,当前采样的纹理坐标在这四个点内,则首先根据x方向的纹理坐标进行线性插值,然后根据y方向的纹理坐标再进行一遍线性插值:


而下图则演示了两种插值法的效果,其中左边使用了常量插值法,右边使用了二维线性插值法

图片的缩小

图片在经过缩小操作后,需要抛弃掉一些像素。但显然每次绘制都按实际宽高来进行缩小会对性能有很大影响。 在d3d中可以使用mipmapping技术,以额外牺牲一些内存代价的方式来获得高效的拟合效果。

这里估计使用的是金字塔下采样的原理。一张256x256的纹理,通过不断的向下采样,可以获得256x256、128x128、64x64...一直到1x1的一系列位图,这些位图构建了一条mipmap链,并且不同的纹理标注有不同的mipmap等级

其中mipmap等级为0的纹理即为原来的纹理,等级为1的纹理所占内存为等级为0的1/4,等级为2的纹理所占内存为等级为1的1/4...以此类推我们可以知道包含完整mipmap的纹理占用的内存空间不超过原来纹理的

\[\lim_{n \to +\infty} \frac{1(1-(\frac{1}{4})^n)}{1-\frac{1}{4}} = \frac{1}{1-\frac{1}{4}} = \frac{4}{3} \]

接下来会有两种情况:

  1. 选取mipmap等级对应图片和缩小后的图片大小最接近的一张,然后进行线性插值法或者常量插值法,这种方式叫做点过滤(point filtering)
  2. 选取两张mipmap等级相邻的图片,使得缩小后的图片大小在那两张位图之间,然后对这两张位图进行常量插值法或者线性插值法分别取得颜色结果,最后对两个颜色结果进行线性插值,这种方式叫做线性过滤(linear filtering)。

各向异性过滤

Anisotropic Filtering可以帮助我们处理那些不与屏幕平行的平面,需要额外使用平面的法向量和摄像机的观察方向向量。虽然使用该种过滤器会有比较大的性能损耗,但是能诞生出比较理想的效果。

下面左图使用了线性过滤法,右边使用的是各向异性过滤,可以看到顶面纹理比左边的更加清晰

对纹理进行采样

所谓采样,就是根据纹理坐标取出纹理对应位置最为接近的像素,在HLSL的写法如下:

g_Tex.Sample(g_SamLinear, pIn.Tex);

但大多数时候绘制出的纹理会比所用的纹理大或小,这样就还涉及到了采样器使用什么方式(如常量插值法、线性插值法、各向异性过滤)来处理图片放大、缩小的情况。

HLSL代码的变动

Basic.hlsli代码如下:

#include "LightHelper.hlsli"

Texture2D gTex : register(t0);
SamplerState gSamLinear : register(s0);


cbuffer VSConstantBuffer : register(b0)
{
    matrix g_World; 
    matrix g_View;  
    matrix g_Proj;  
    matrix g_WorldInvTranspose;
}

cbuffer PSConstantBuffer : register(b1)
{
    DirectionalLight g_DirLight[10];
    PointLight g_PointLight[10];
    SpotLight g_SpotLight[10];
    Material g_Material;
    int g_NumDirLight;
    int g_NumPointLight;
    int g_NumSpotLight;
    float g_Pad1;

    float3 g_EyePosW;
    float g_Pad2;
}


struct VertexPosNormalTex
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 Tex : TEXCOORD;
};

struct VertexPosTex
{
    float3 PosL : POSITION;
    float2 Tex : TEXCOORD;
};

struct VertexPosHWNormalTex
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION;     // 在世界中的位置
    float3 NormalW : NORMAL;    // 法向量在世界中的方向
    float2 Tex : TEXCOORD;
};

struct VertexPosHTex
{
    float4 PosH : SV_POSITION;
    float2 Tex : TEXCOORD;
};

Basic_VS_2D.hlsl的代码:

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

// 顶点着色器(2D)
VertexPosHTex VS_2D(VertexPosTex vIn)
{
    VertexPosHTex vOut;
    vOut.PosH = float4(vIn.PosL, 1.0f);
    vOut.Tex = vIn.Tex;
    return vOut;
}

Basic_PS_2D.hlsl的代码:

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

// 像素着色器(2D)
float4 PS_2D(VertexPosHTex pIn) : SV_Target
{
    return g_Tex.Sample(g_SamLinear, pIn.Tex);
}

Basic_VS_3D.hlsl的代码:

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

// 顶点着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
    VertexPosHWNormalTex vOut;
    matrix viewProj = mul(g_View, g_Proj);
    float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);

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


Basic_PS_3D.hlsl的代码:

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

// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
    // 标准化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 顶点指向眼睛的向量
    float3 toEyeW = normalize(g_EyePosW - pIn.PosW);

    // 初始化为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;

    for (i = 0; i < g_NumDirLight; ++i)
    {
        ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
    for (i = 0; i < g_NumPointLight; ++i)
    {
        ComputePointLight(g_Material, g_PointLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
    for (i = 0; i < g_NumSpotLight; ++i)
    {
        ComputeSpotLight(g_Material, g_SpotLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    

    float4 texColor = g_Tex.Sample(g_SamLinear, pIn.Tex);
    float4 litColor = texColor * (ambient + diffuse) + spec;
    litColor.a = texColor.a * g_Material.Diffuse.a;
    
    return litColor;
}

其中Texture2D类型保存了2D纹理的信息,在这是全局变量。而register(t0)对应起始槽索引0.

SamplerState类型确定采样器应如何进行采样,同样也是全局变量,register(s0)对应起始槽索引0.

上述两种变量都需要在C++应用层中初始化和绑定后才能使用。

Texture2D类型拥有Sample方法,需要提供采样器状态和2D纹理坐标方可使用,然后返回一个包含RGBA信息的float4向量。

除此之外,上面的HLSL代码允许每种灯光最多10盏,然后还提供了2D和3D版本的顶点/像素着色器供使用。

注意Basic.hlsliLightHelper.hlsli是不参与生成的。其余着色器文件需要按照第2章的方式去设置好。

ID3D11DeviceContext::*SSetShaderResources方法--设置着色器资源

需要注意的是,纹理并不能直接绑定到着色器中,需要为纹理创建对应的着色器资源视图才能够给着色器使用。上面打*意味着渲染管线的所有可编程着色器阶段都有该方法。

此外,着色器资源视图不仅可以绑定纹理资源,还可以绑定缓冲区资源。有关缓冲区资源绑定到着色器资源视图的应用,我们留到后面的章节再讲。

目前在DDSTextureLoaderWICTextureLoader中,我们只需要用到纹理的着色器资源。这里以ID3D11DeviceContext::PSSetShaderResources为例:

void ID3D11DeviceContext::PSSetShaderResources(
    UINT StartSlot,    // [In]起始槽索引,对应HLSL的register(t*)
    UINT NumViews,    // [In]着色器资源视图数目
    ID3D11ShaderResourceView * const *ppShaderResourceViews    // [In]着色器资源视图数组
);

然后调用方法如下:

m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());

这样在HLSL里对应regisgter(t0)g_Tex存放的就是木箱表面的纹理了。

ID3D11Device::CreateSamplerState方法--创建采样器状态

在C++代码层中,我们只能通过D3D设备创建采样器状态,然后绑定到渲染管线中,使得在HLSL中可以根据过滤器、寻址模式等进行采样。

在创建采样器状态之前,需要先填充结构体D3D11_SAMPLER_DESC来描述采样器状态:

typedef struct D3D11_SAMPLER_DESC
{
    D3D11_FILTER Filter;                    // 所选过滤器
    D3D11_TEXTURE_ADDRESS_MODE AddressU;    // U方向寻址模式
    D3D11_TEXTURE_ADDRESS_MODE AddressV;    // V方向寻址模式
    D3D11_TEXTURE_ADDRESS_MODE AddressW;    // W方向寻址模式
    FLOAT MipLODBias;   // mipmap等级偏移值,最终算出的mipmap等级会加上该偏移值
    UINT MaxAnisotropy;                     // 最大各向异性等级(1-16)
    D3D11_COMPARISON_FUNC ComparisonFunc;   // 这节不讨论
    FLOAT BorderColor[ 4 ];     // 边界外的颜色,使用D3D11_TEXTURE_BORDER_COLOR时需要指定
    FLOAT MinLOD;   // 若mipmap等级低于MinLOD,则使用等级MinLOD。最小允许设为0
    FLOAT MaxLOD;   // 若mipmap等级高于MaxLOD,则使用等级MaxLOD。必须比MinLOD大        
}     D3D11_SAMPLER_DESC;

D3D11_FILTER部分枚举含义如下:

枚举值 缩小 放大 mipmap
D3D11_FILTER_MIN_MAG_MIP_POINT 点采样 点采样 点采样
D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR 点采样 点采样 线性采样
D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT 点采样 线性采样 点采样
D3D11_FILTER_MIN_MAG_MIP_LINEAR 线性采样 线性采样 线性采样
D3D11_FILTER_ANISOTROPIC 各向异性 各向异性 各向异性

D3D11_TEXTURE_ADDRESS_MODE是单个方向的寻址模式,有时候纹理坐标会超过1.0或者小于0.0,这时候寻址模式可以解释边界外的情况,含义如下:

D3D11_TEXTURE_ADDRESS_WRAP是将指定纹理坐标分量的值[t, t + 1], t ∈ Z映射到[0.0, 1.0],因此作用到u和v分量时看起来就像是把用一张贴图紧密平铺到其他位置上:

D3D11_TEXTURE_ADDRESS_MIRROR在每个整数点处翻转纹理坐标值。例如u在[0.0, 1.0]按正常纹理坐标寻址,在[1.0, 2.0]内则翻转,在[2.0, 3.0]内又回到正常的寻址,以此类推:

D3D11_TEXTURE_ADDRESS_CLAMP对指定纹理坐标分量,小于0.0的值都取作0.0,大于1.0的值都取作1.0,在[0.0, 1.0]的纹理坐标不变:

D3D11_TEXTURE_BORDER_COLOR对于指定纹理坐标分量的值在[0.0, 1.0]外的区域都使用BorderColor进行填充

D3D11_TEXTURE_ADDRESS_MIRROR_ONCE相当于MIRROR和CLAMP的结合,仅[-1.0,1.0]的范围内镜像有效,若小于-1.0则取-1.0,大于1.0则取1.0,在[-1.0, 0.0]进行翻转。

最后就是ID3D11Device::CreateSamplerState方法:

HRESULT ID3D11Device::CreateSamplerState( 
    const D3D11_SAMPLER_DESC *pSamplerDesc, // [In]采样器状态描述
    ID3D11SamplerState **ppSamplerState);   // [Out]输出的采样器

接下来演示了如何创建采样器状态;

// 初始化采样器状态描述
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));

ID3D11DeviceContext::PSSetSamplers方法--像素着色阶段设置采样器状态

void ID3D11DeviceContext::PSSetSamplers(
    UINT StartSlot,     // [In]起始槽索引
    UINT NumSamplers,   // [In]采样器状态数目
    ID3D11SamplerState * const * ppSamplers);   // [In]采样器数组  

根据前面的HLSL代码,samLinear使用了索引为0起始槽,所以需要这样调用:

// 像素着色阶段设置好采样器
m_pd3dImmediateContext->PSSetSamplers(0, 1, m_pSamplerState.GetAddressOf());

这样HLSL中对应的采样器状态就可以使用了。

GameApp类的变动

GameApp::InitEffect的变动

现在我们需要编译出4个着色器,2个顶点布局,以区分2D和3D的部分。

bool GameApp::InitEffect()
{
    ComPtr<ID3DBlob> blob;

    // 创建顶点着色器(2D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_VS_2D.cso", L"HLSL\\Basic_VS_2D.hlsl", "VS_2D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader2D.GetAddressOf()));
    // 创建顶点布局(2D)
    HR(m_pd3dDevice->CreateInputLayout(VertexPosTex::inputLayout, ARRAYSIZE(VertexPosTex::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout2D.GetAddressOf()));

    // 创建像素着色器(2D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_PS_2D.cso", L"HLSL\\Basic_PS_2D.hlsl", "PS_2D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader2D.GetAddressOf()));

    // 创建顶点着色器(3D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader3D.GetAddressOf()));
    // 创建顶点布局(3D)
    HR(m_pd3dDevice->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout3D.GetAddressOf()));

    // 创建像素着色器(3D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_PS_3D.cso", L"HLSL\\Basic_PS_3D.hlsl", "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader3D.GetAddressOf()));

    return true;
}

GameApp::InitResource的变化

虽然现在允许同时放入多盏灯光了,但在该项目我们只使用一盏点光灯,并且仅用于3D木盒的显示。对于2D火焰动画,实质上是由120张bmp位图构成,我们需要按顺序在每一帧切换下一张位图来达到火焰在动的效果。

bool GameApp::InitResource()
{
    // 初始化网格模型并设置到输入装配阶段
    auto meshData = Geometry::CreateBox();
    ResetMesh(meshData);

    // ******************
    // 设置常量缓冲区描述
    D3D11_BUFFER_DESC cbd;
    ZeroMemory(&cbd, sizeof(cbd));
    cbd.Usage = D3D11_USAGE_DYNAMIC;
    cbd.ByteWidth = sizeof(VSConstantBuffer);
    cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    // 新建用于VS和PS的常量缓冲区
    HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[0].GetAddressOf()));
    cbd.ByteWidth = sizeof(PSConstantBuffer);
    HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[1].GetAddressOf()));

    // ******************
    // 初始化纹理和采样器状态
    
    // 初始化木箱纹理
    HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));
    // 初始化火焰纹理
    WCHAR strFile[40];
    m_pFireAnims.resize(120);
    for (int i = 1; i <= 120; ++i)
    {
        wsprintf(strFile, L"Texture\\FireAnim\\Fire%03d.bmp", i);
        HR(CreateWICTextureFromFile(m_pd3dDevice.Get(), strFile, nullptr, m_pFireAnims[i - 1].GetAddressOf()));
    }
        
    // 初始化采样器状态
    D3D11_SAMPLER_DESC sampDesc;
    ZeroMemory(&sampDesc, sizeof(sampDesc));
    sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
    sampDesc.MinLOD = 0;
    sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
    HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));

    
    // ******************
    // 初始化常量缓冲区的值

    // 初始化用于VS的常量缓冲区的值
    m_VSConstantBuffer.world = XMMatrixIdentity();            
    m_VSConstantBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
        XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
        XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
        XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
    ));
    m_VSConstantBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));
    m_VSConstantBuffer.worldInvTranspose = XMMatrixIdentity();
    
    // 初始化用于PS的常量缓冲区的值
    // 这里只使用一盏点光来演示
    m_PSConstantBuffer.pointLight[0].Position = XMFLOAT3(0.0f, 0.0f, -10.0f);
    m_PSConstantBuffer.pointLight[0].Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
    m_PSConstantBuffer.pointLight[0].Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
    m_PSConstantBuffer.pointLight[0].Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_PSConstantBuffer.pointLight[0].Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
    m_PSConstantBuffer.pointLight[0].Range = 25.0f;
    m_PSConstantBuffer.numDirLight = 0;
    m_PSConstantBuffer.numPointLight = 1;
    m_PSConstantBuffer.numSpotLight = 0;
    m_PSConstantBuffer.eyePos = XMFLOAT4(0.0f, 0.0f, -5.0f, 0.0f);    // 这里容易遗漏,已补上
    // 初始化材质
    m_PSConstantBuffer.material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_PSConstantBuffer.material.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
    m_PSConstantBuffer.material.Specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 5.0f);
    // 注意不要忘记设置此处的观察位置,否则高亮部分会有问题
    m_PSConstantBuffer.eyePos = XMFLOAT4(0.0f, 0.0f, -5.0f, 0.0f);

    // 更新PS常量缓冲区资源
    D3D11_MAPPED_SUBRESOURCE mappedData;
    HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
    memcpy_s(mappedData.pData, sizeof(PSConstantBuffer), &m_PSConstantBuffer, sizeof(PSConstantBuffer));
    m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);

    // ******************
    // 给渲染管线各个阶段绑定好所需资源
    // 设置图元类型,设定输入布局
    m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
    // 默认绑定3D着色器
    m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
    // VS常量缓冲区对应HLSL寄存于b0的常量缓冲区
    m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffers[0].GetAddressOf());
    // PS常量缓冲区对应HLSL寄存于b1的常量缓冲区
    m_pd3dImmediateContext->PSSetConstantBuffers(1, 1, m_pConstantBuffers[1].GetAddressOf());
    // 像素着色阶段设置好采样器
    m_pd3dImmediateContext->PSSetSamplers(0, 1, m_pSamplerState.GetAddressOf());
    m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
    m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
    
    // 像素着色阶段默认设置木箱纹理
    m_CurrMode = ShowMode::WoodCrate;

    return true;
}

GameApp::UpdateScene的变化

该项目可以选择播放3D木箱或2D火焰动画,则需要有个当前正在播放内容的状态值,并随之进行更新。

其中ShowMode是一个枚举类,可以选择WoodCrate或者FireAnim

void GameApp::UpdateScene(float dt)
{

    Keyboard::State state = m_pKeyboard->GetState();
    m_KeyboardTracker.Update(state);    

    // 键盘切换模式
    if (m_KeyboardTracker.IsKeyPressed(Keyboard::D1))
    {
        // 播放木箱动画
        m_CurrMode = ShowMode::WoodCrate;
        m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
        auto meshData = Geometry::CreateBox();
        ResetMesh(meshData);
        m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
        m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
        m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
    }
    else if (m_KeyboardTracker.IsKeyPressed(Keyboard::D2))
    {
        m_CurrMode = ShowMode::FireAnim;
        m_CurrFrame = 0;
        m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout2D.Get());
        auto meshData = Geometry::Create2DShow();
        ResetMesh(meshData);
        m_pd3dImmediateContext->VSSetShader(m_pVertexShader2D.Get(), nullptr, 0);
        m_pd3dImmediateContext->PSSetShader(m_pPixelShader2D.Get(), nullptr, 0);
        m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pFireAnims[0].GetAddressOf());
    }

    if (m_CurrMode == ShowMode::WoodCrate)
    {
        static float phi = 0.0f, theta = 0.0f;
        phi += 0.00003f, theta += 0.00005f;
        XMMATRIX W = XMMatrixRotationX(phi) * XMMatrixRotationY(theta);
        m_VSConstantBuffer.world = XMMatrixTranspose(W);
        m_VSConstantBuffer.worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));

        // 更新常量缓冲区,让立方体转起来
        D3D11_MAPPED_SUBRESOURCE mappedData;
        HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[0].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
        memcpy_s(mappedData.pData, sizeof(VSConstantBuffer), &m_VSConstantBuffer, sizeof(VSConstantBuffer));
        m_pd3dImmediateContext->Unmap(m_pConstantBuffers[0].Get(), 0);
    }
    else if (m_CurrMode == ShowMode::FireAnim)
    {
        // 用于限制在1秒60帧
        static float totDeltaTime = 0;

        totDeltaTime += dt;
        if (totDeltaTime > 1.0f / 60)
        {
            totDeltaTime -= 1.0f / 60;
            m_CurrFrame = (m_CurrFrame + 1) % 120;
            m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pFireAnims[m_CurrFrame].GetAddressOf());
        }        
    }
}

最终的显示效果如下:

练习题

粗体字为自定义题目

  1. 自己动手将过滤器修改为常量插值法、线性插值法、各向异性过滤,观察立方体盒的效果
  2. 尝试在使用Geometry::MeshData创建的立方体网格数据(不能对其修改)的基础上,让立方体的六个面使用不同的纹理来绘制,可以使用魔方项目里的纹理
  3. 使用教程项目第26章Texture文件夹中的flare.dds和flarealpha.dds,在着色器中通过分量乘法实现

    然后让纹理在立方体的表面旋转(考虑对纹理坐标的变换),纹理采样器的寻址模式使用BORDER_COLOR,边界色为黑色。效果如下:

    着色器需要提供两个纹理,并在一个顶点着色器、一个像素着色器完成任务。
    提示:需通过旋转矩阵(4x4矩阵)对纹理坐标进行变换来实现。
  4. 如果你阅读过"深入理解与使用2D纹理资源"这篇,那么尝试用一个纹理数组存储所有的火焰纹理,在HLSL中使用Texture2DArray

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

posted @ 2018-07-12 09:26  X_Jun  阅读(15569)  评论(9编辑  收藏  举报
levels of contents