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

前言

在以前的DirectX SDK中,纹理的读取使用的是D3DX11CreateShaderResourceViewFromFile函数,如今在Windows SDK中已经没有这些函数,咱们须要找到DDSTextureLoaderWICTextureLoader这两个库来读取DDS位图和WIC位图html

DirectX11 With Windows SDK完整目录git

Github项目源码github

欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。编程

纹理坐标系

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

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

这样的话已知三个顶点的坐标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的纹理,经过不断的向下采样,能够得到256x25六、128x12八、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;
    // 强制展开循环以减小指令数
    [unroll]
    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;
    }
    
    [unroll]
    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;
    }
    
    [unroll]
    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向量。

[unroll]属性用于展开循环,避免没必要要的跳转,但可能会产生大量的指令

除此以外,上面的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使用重复的纹理去覆盖整个实数域,能够当作fmod(X, 1.0f)

D3D11_TEXTURE_ADDRESS_MIRROR用重复的纹理覆盖整个实数域,只不过相邻的两个纹理知足按交线对称

D3D11_TEXTURE_ADDRESS_CLAMP对U轴和V轴,小于0的值都取做0,大于1的值都取做1

D3D11_TEXTURE_BORDER_COLOR对于纹理范围外的区域都使用BorderColor进行填充

D3D11_TEXTURE_ADDRESS_MIRROR_ONCE至关于MIRROR和CLAMP的结合,仅[-1,1]的范围内镜像有效,其他范围都会取到-1或者1

最后就是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 = XMMatrixInverse(nullptr, 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,边界色为黑色。效果以下:

    着色器须要提供两个纹理,并在一个顶点着色器、一个像素着色器完成任务。
  4. 若是你阅读过"深刻理解与使用2D纹理资源"这篇,那么尝试用一个纹理数组存储全部的火焰纹理,在HLSL中使用Texture2DArray

DirectX11 With Windows SDK完整目录

Github项目源码

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

相关文章
相关标签/搜索