前言

在之前的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;
// 强制展开循环以减少指令数
[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是将指定纹理坐标分量的值[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 = 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,边界色为黑色。效果如下:



    着色器需要提供两个纹理,并在一个顶点着色器、一个像素着色器完成任务。

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

DirectX11 With Windows SDK完整目录

Github项目源码

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

DirectX11 With Windows SDK--09 纹理映射与采样器状态的更多相关文章

  1. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  2. DirectX11 With Windows SDK--00 目录

    前言 (更新于 2019/4/10) 从第一次接触DirectX 11到现在已经有将近两年的时间了.还记得前年暑假被要求学习DirectX 11,在用龙书的源码配置项目运行环境的时候都花了好几天的时间 ...

  3. DirectX11 With Windows SDK--13 动手实现一个简易Effects框架、阴影效果绘制

    前言 到现在为止,所有的教程项目都没有使用Effects11框架类来管理资源.因为在D3DCompile API (#47)版本中,如果你尝试编译fx_5_0的效果文件,会收到这样的警告: X4717 ...

  4. DirectX11 With Windows SDK--10 摄像机类

    前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 由于考虑后续的项目需要有一个比较好的演示环境 ...

  5. DirectX11 With Windows SDK--26 计算着色器:入门

    前言 现在开始迎来所谓的高级篇了,目前计划是计算着色器部分的内容视项目情况,大概会分3-5章来讲述. DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群 ...

  6. DirectX11 With Windows SDK--07 添加光照与常用几何模型

    前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的光照模型.除此之外,该项目还用到了多个常量缓冲区,因此还会提及HLSL的常量缓冲区打包规则以及如何设置多 ...

  7. DirectX11 With Windows SDK--11 混合状态与光栅化状态

    前言 虽然这一部分的内容主要偏向于混合(Blending),但这里还需提及一下,关于渲染管线可以绑定的状态主要有如下四种: 光栅化状态(光栅化阶段) 采样器状态(像素着色阶段) 混合状态(输出合并阶段 ...

  8. DirectX11 With Windows SDK--12 深度/模板状态、平面镜反射绘制

    前言 深度/模板测试使用的是与后备缓冲区同等分辨率大小的缓冲区,每个元素的一部分连续位用于深度测试,其余的则用作模板测试.两个测试的目的都是为了能够根据深度/模板状态需求的设置来选择需要绘制的像素. ...

  9. DirectX11 With Windows SDK--14 深度测试

    前言 当使用加法/减法/乘法颜色混合,或者使用透明混合的时候,在经过深度测试时可能会引发一些问题.例如现在我们需要使用加法混合来绘制一系列对象,而这些对象彼此之间不会相互阻挡.若我们仍使用原来的深度测 ...

随机推荐

  1. 本地windows下搭建git的本地服务器

    本地windows下搭建git的本地服务器 准备工作: 本地安装java环境,配置环境变量(略) 下载gitblit文件,百度一大堆 开始第一步: 减压gitblit压缩包到某个目录下,比如我在:H: ...

  2. HP ML150 G6 服务器硬件介绍

    前面板和背面板组件介绍: 1 介质托架                           12 系统运行状况 LED 指示灯 2 USB 2.0 端口 (2)               13 AC ...

  3. Dockerfile Volume指令与docker -v的区别

    在学习Dockerfile的过程中有个VOLUME命令,很多教程或书中说的是用来定义匿名卷的,其作用如下: 容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文 ...

  4. LeetCode算法题-Detect Capital(Java实现)

    这是悦乐书的第251次更新,第264篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第118题(顺位题号是520).给定一个单词,你需要判断其中大写字母的使用是否正确.当下 ...

  5. Jenkins插件之显示构建时间

    1.进入jenkin插件管理器中,安装  Timestamper 插件 2.安装完成后,进入到构建任务里面,在 构建环境 中勾选  Add timestamps to the Console Outp ...

  6. Python简单的多线程demo:装逼写法

    用面向对象来写多线程: import threading class MyThread(threading.Thread): def __init__(self, n): super(MyThread ...

  7. 超哥笔记--shell 基本命令(4)

    一 linux 命令行的组成结构 自定义命令行结构 PS1变量来控制 \u \W 最后一位工作目录 \w 绝对路径工作目录 \t 显示24h制的时间 \h PS1="[\u@\h \w \t ...

  8. 在Linux命令行中以图形化窗口打开文件夹

    Linux 系统中也有类似的命令.Ubuntu 发行版的命令行中,我们可以使用 nautilus 命令来打开指定目录的图形化窗口界面.类似下面命令这样使用: nautilus /home/testPr ...

  9. leetcode 136. Single Number 、 137. Single Number II 、 260. Single Number III(剑指offer40 数组中只出现一次的数字)

    136. Single Number 除了一个数字,其他数字都出现了两遍. 用亦或解决,亦或的特点:1.相同的数结果为0,不同的数结果为1 2.与自己亦或为0,与0亦或为原来的数 class Solu ...

  10. Redis常用数据结构

    Redis常用数据结构包括字符串(strings),列表(lists),哈希(hashes),集合(sets),有序集合(sorted sets). redis的key最大不能超过512M,可通过re ...