DirectX11 With Windows SDK--09 纹理映射
前言
DirectX11 With Windows SDK完整目录:
在之前的DirectX SDK中,纹理的读取使用的是D3DX11CreateShaderResourceViewFromFile
函数,现在在Windows SDK中已经没有这些函数,我们需要找到DDSTextureLoader
和WICTextureLoader
这两个库来读取DDS位图和WIC位图
项目源码点此:
纹理坐标系
纹理坐标系和屏幕、图片坐标系的有些相似,它们的U轴都是水平朝右,V轴竖直向下。但是纹理的X和Y的取值范围都为[0.0, 1.0]
,分别映射到[0, Width]
和[0, Height]
对于一个3D的三角形,通过给这三个顶点额外的纹理坐标信息,那么三个纹理坐标就可以映射到纹理指定的某片三角形区域。
这样的话已知三个顶点的坐标p0
,p1
和p2
以及三个纹理坐标q0
,q1
和q2
,就可以求出顶点坐标映射与纹理坐标的对应关系:(x,y,z) = p0 + s(p1 - p0) + t(p2 - p0)
(u,v) = q0 + s(q1 - q0) + t(q2 - q0)
其中需要满足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库
要使用这两个库,有两种方案。
第一种:在https://github.com/Microsoft/DirectXTex
中找到DDSTextureLoader
文件夹和WICTextureLoader
文件夹中分别找到对应的头文件和源文件(不带12的),并加入到你的项目中
第二种:将DirectXTK
库添加到你的项目中,这里不再赘述
这之后就可以包含DDSTextureLoader.h
和WICTextureLoader.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(md3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, mWoodCrate.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]; mFireAnim.resize(120); for (int i = 1; i <= 120; ++i) { wsprintf(strFile, L"Texture\\FireAnim\\Fire%03d.bmp", i); HR(CreateWICTextureFromFile(md3dDevice.Get(), strFile, nullptr, mFireAnim[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)
):
p = t*p1 + (1 - t)*p0
对于二维情况,会有三种使用线性插值法的情况:
- X方向使用常量插值法,Y方向使用线性插值法
- X方向使用线性插值法,Y方向使用常量插值法
- X和Y方向均使用线性插值法
左图使用了常量插值法,右图使用了二维线性插值法
图片的缩小
图片在经过缩小操作后,需要抛弃掉一些像素。这里我们可以使用mipmapping技术,以额外牺牲一些内存代价的方式来获得高效的拟合效果,这里估计使用的是金字塔下采样的原理。一张512x512的纹理,通过不断的向下采样,可以获得256x256、128x128、64x64...一直到1x1的一系列位图,这些位图构建了一条mipmap链,并且不同的纹理标注有不同的mipmap等级
接下来会有两种情况:
- 选取mipmap等级对应图片和缩小后的图片大小最接近的一张,然后进行线性插值法或者常量插值法,这种方式叫做点过滤(point filtering)
- 选取两张mipmap等级相邻的图片,使得缩小后的图片大小在那两张位图之间,然后对这两张位图进行常量插值法或者线性插值法分别取得颜色结果,最后对两个颜色结果进行线性插值,这种方式叫做线性过滤(linear filtering)。
各向异性过滤
Anisotropic Filtering可以帮助我们处理那些不与屏幕平行的平面,需要额外使用平面的法向量和摄像机的观察方向向量。虽然使用该种过滤器会有比较大的性能损耗,但是能诞生出比较理想的效果。
下面左图使用了线性过滤法,右边使用的是各向异性过滤,可以看到顶面纹理比左边的更加清晰
对纹理进行采样
HLSL代码的变动
Basic.fx
代码如下:
#include "LightHelper.hlsli" Texture2D tex : register(t0); SamplerState samLinear : register(s0); cbuffer VSConstantBuffer : register(b0) { row_major matrix gWorld; row_major matrix gView; row_major matrix gProj; row_major matrix gWorldInvTranspose; } cbuffer PSConstantBuffer : register(b1) { DirectionalLight gDirLight[10]; PointLight gPointLight[10]; SpotLight gSpotLight[10]; Material gMaterial; int gNumDirLight; int gNumPointLight; int gNumSpotLight; float3 gEyePosW; } struct VertexIn { float3 Pos : POSITION; float3 Normal : NORMAL; float2 Tex : TEXCOORD; }; struct VertexOut { float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD; }; // 顶点着色器(3D) VertexOut VS_3D(VertexIn pIn) { VertexOut pOut; row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj); pOut.PosH = mul(float4(pIn.Pos, 1.0f), worldViewProj); pOut.PosW = mul(float4(pIn.Pos, 1.0f), gWorld).xyz; pOut.NormalW = mul(pIn.Normal, (float3x3)gWorldInvTranspose); pOut.Tex = pIn.Tex; return pOut; } // 像素着色器(3D) float4 PS_3D(VertexOut pIn) : SV_Target { // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 顶点指向眼睛的向量 float3 toEyeW = normalize(gEyePosW - 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 < gNumDirLight; ++i) { ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S); ambient += A; diffuse += D; spec += S; } [unroll] for (i = 0; i < gNumPointLight; ++i) { ComputePointLight(gMaterial, gPointLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S); ambient += A; diffuse += D; spec += S; } [unroll] for (i = 0; i < gNumSpotLight; ++i) { ComputeSpotLight(gMaterial, gSpotLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S); ambient += A; diffuse += D; spec += S; } float4 texColor = tex.Sample(samLinear, pIn.Tex); float4 litColor = texColor * (ambient + diffuse) + spec; litColor.a = texColor.a * gMaterial.Diffuse.a; return litColor; } // 顶点着色器(2D) VertexOut VS_2D(VertexIn pIn) { VertexOut pOut; pOut.PosH = float4(pIn.Pos, 1.0f); pOut.PosW = float3(0.0f, 0.0f, 0.0f); pOut.NormalW = pIn.Normal; pOut.Tex = pIn.Tex; return pOut; } // 像素着色器(2D) float4 PS_2D(VertexOut pIn) : SV_Target { return tex.Sample(samLinear, pIn.Tex); }
其中Texture2D
类型保存了2D纹理的信息,在这是全局变量。而register(t0)
对应起始槽索引0.
SamplerState
类型确定采样器应如何进行采样,同样也是全局变量,register(s0)
对应起始槽索引0.
上述两种变量都需要在C++应用层中初始化和绑定后才能使用。
Texture2D
类型拥有Sample
方法,需要提供采样器状态和2D纹理坐标方可使用,然后返回一个包含RGBA信息的float4
向量。
[unroll]
用于展开循环,避免不必要的跳转,但可能会产生大量的指令
除此之外,上面的HLSL代码允许每种灯光最多10盏,然后还提供了2D和3D版本的顶点/像素着色器供使用。
所以这里还需要有Basic_VS_2D.hlsl
,Basic_PS_2D.hlsl
, Basic_VS_3D.hlsl
, Basic_PS_3D.hlsl
,每个文件都需要包含Basic.fx
,然后设置不同的入口点,选择正确的着色器类型。
注意Basic.fx
和LightHelper.hlsli
是不参与生成的。
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(md3dDevice->CreateSamplerState(&sampDesc, mSamplerState.GetAddressOf()));
ID3D11DeviceContext::PSSetSamplers方法--像素着色阶段设置采样器状态
void ID3D11DeviceContext::PSSetSamplers( UINT StartSlot, // [In]起始槽索引 UINT NumSamplers, // [In]采样器状态数目 ID3D11SamplerState * const * ppSamplers); // [In]采样器数组
根据前面的HLSL代码,samLinear
使用了索引为0起始槽,所以需要这样调用:
// 像素着色阶段设置好采样器 md3dImmediateContext->PSSetSamplers(0, 1, mSamplerState.GetAddressOf());
这样HLSL中对应的采样器状态就可以使用了。
GameApp类的变动
GameApp::InitEffect的变动
现在我们需要编译出4个着色器,2个顶点布局,以区分2D和3D的部分。这里只需要绑定用于3D的2个着色器和1个顶点布局。
注意:若HR宏位于if/else if语句中,必须要用花括号包住该宏,因为HR宏本身是一个花括号体,在HR宏后面使用分号的话后面的else或者else if会报错。
bool GameApp::InitEffect() { ComPtr<ID3DBlob> blob; // 已经编译好的着色器文件名 std::wstring pso2DPath = L"HLSL\\Basic_PS_2D.cso", vso2DPath = L"HLSL\\Basic_VS_2D.cso"; std::wstring pso3DPath = L"HLSL\\Basic_PS_3D.cso", vso3DPath = L"HLSL\\Basic_VS_3D.cso"; // ****************************************************** // 寻找是否有已经编译好的顶点着色器(2D),否则在运行期编译 if (filesystem::exists(vso2DPath)) { HR(D3DReadFileToBlob(vso2DPath.c_str(), blob.GetAddressOf())); } else { HR(CompileShaderFromFile(L"HLSL\\Basic.fx", "VS_2D", "vs_4_0", blob.GetAddressOf())) } // 创建顶点着色器(2D) HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mVertexShader2D.GetAddressOf())); // 创建顶点布局(2D) HR(md3dDevice->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), mVertexLayout2D.GetAddressOf())); blob.Reset(); // ****************************************************** // 寻找是否有已经编译好的顶点着色器(3D),否则在运行期编译 if (filesystem::exists(vso3DPath)) { HR(D3DReadFileToBlob(vso3DPath.c_str(), blob.GetAddressOf())); } else { HR(CompileShaderFromFile(L"HLSL\\Basic.fx", "VS_3D", "vs_4_0", blob.GetAddressOf())) } // 创建顶点着色器(3D) HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mVertexShader3D.GetAddressOf())); // 创建顶点布局(3D) HR(md3dDevice->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), mVertexLayout3D.GetAddressOf())); blob.Reset(); // ****************************************************** // 寻找是否有已经编译好的像素着色器(2D),否则在运行期编译 if (filesystem::exists(pso2DPath)) { HR(D3DReadFileToBlob(pso2DPath.c_str(), blob.GetAddressOf())); } else { HR(CompileShaderFromFile(L"HLSL\\Basic.fx", "PS_2D", "ps_4_0", blob.GetAddressOf())) } // 创建顶点着色器(2D) HR(md3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mPixelShader2D.GetAddressOf())); blob.Reset(); // ****************************************************** // 寻找是否有已经编译好的像素着色器(3D),否则在运行期编译 if (filesystem::exists(pso3DPath)) { HR(D3DReadFileToBlob(pso3DPath.c_str(), blob.GetAddressOf())); } else { HR(CompileShaderFromFile(L"HLSL\\Basic.fx", "PS_3D", "ps_4_0", blob.GetAddressOf())) } // 创建顶点着色器(3D) HR(md3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mPixelShader3D.GetAddressOf())); blob.Reset(); // 默认将3D着色器绑定到渲染管线 md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); // 默认将顶点输入布局3D绑定到渲染管线 md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); return true; }
GameApp::InitResource的变化
虽然现在允许同时放入多盏灯光了,但在该项目我们只使用一盏点光灯,并且仅用于3D木盒的显示。对于2D火焰动画,实质上是由120张bmp位图构成,我们需要按顺序在每一帧切换下一张位图来达到火焰在动的效果。
这里只给出有变化的部分
bool GameApp::InitResource() { // 初始化网格模型 Geometry::MeshData meshData = Geometry::CreateBox(); ResetMesh(meshData); // ****************** // 设置常量缓冲区描述 // ...省略... // ****************** // 初始化纹理和采样器状态 // 初始化木箱纹理 HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, mWoodCrate.GetAddressOf())); // 初始化火焰纹理 WCHAR strFile[40]; mFireAnim.resize(120); for (int i = 1; i <= 120; ++i) { wsprintf(strFile, L"Texture\\FireAnim\\Fire%03d.bmp", i); HR(CreateWICTextureFromFile(md3dDevice.Get(), strFile, nullptr, mFireAnim[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(md3dDevice->CreateSamplerState(&sampDesc, mSamplerState.GetAddressOf())); // ****************** // 初始化常量缓冲区的值 // 初始化用于VS的常量缓冲区的值 // ...省略... // 初始化用于PS的常量缓冲区的值 // 这里只使用一盏点光来演示 mPSConstantBuffer.pointLight[0].Position = XMFLOAT3(0.0f, 0.0f, -10.0f); mPSConstantBuffer.pointLight[0].Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f); mPSConstantBuffer.pointLight[0].Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f); mPSConstantBuffer.pointLight[0].Specular = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f); mPSConstantBuffer.pointLight[0].Att = XMFLOAT3(0.0f, 0.1f, 0.0f); mPSConstantBuffer.pointLight[0].Range = 25.0f; mPSConstantBuffer.numDirLight = 0; mPSConstantBuffer.numPointLight = 1; mPSConstantBuffer.numSpotLight = 0; // 初始化材质 mPSConstantBuffer.material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f); mPSConstantBuffer.material.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f); mPSConstantBuffer.material.Specular = XMFLOAT4(1.0f, 1.0f, 1.0f, 50.0f); // ****************************** // 设置好渲染管线各阶段所需资源 md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); // 输入装配阶段设置图元类型 md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); // 像素着色阶段设置好采样器 md3dImmediateContext->PSSetSamplers(0, 1, mSamplerState.GetAddressOf()); // 像素着色阶段默认设置木箱纹理 mCurrMode = ShowMode::WoodCrate; md3dImmediateContext->PSSetShaderResources(0, 1, mWoodCrate.GetAddressOf()); // 更新PS常量缓冲区资源 md3dImmediateContext->UpdateSubresource(mConstantBuffers[1].Get(), 0, nullptr, &mPSConstantBuffer, 0, 0); return true; }
GameApp::UpdateScene的变化
该项目可以选择播放3D木箱或2D火焰动画,则需要有个当前正在播放内容的状态值,并随之进行更新。
其中ShowMode
是一个枚举类,可以选择WoodCrate
或者FireAnim
。
void GameApp::UpdateScene(float dt) { // 键盘切换灯光类型 Keyboard::State state = mKeyboard->GetState(); mKeyboardTracker.Update(state); // 键盘切换模式 if (mKeyboardTracker.IsKeyPressed(Keyboard::D1)) { // 播放木箱动画 mCurrMode = ShowMode::WoodCrate; md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); Geometry::MeshData meshData = Geometry::CreateBox(); ResetMesh(meshData); // 输入装配阶段的顶点缓冲区设置 UINT stride = sizeof(VertexPosNormalTex); // 跨越字节数 UINT offset = 0; // 起始偏移量 md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset); // 输入装配阶段的索引缓冲区设置 md3dImmediateContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetShaderResources(0, 1, mWoodCrate.GetAddressOf()); } else if (mKeyboardTracker.IsKeyPressed(Keyboard::D2)) { mCurrMode = ShowMode::FireAnim; mCurrFrame = 0; md3dImmediateContext->IASetInputLayout(mVertexLayout2D.Get()); Geometry::MeshData meshData = Geometry::Create2DShow(); ResetMesh(meshData); // 输入装配阶段的顶点缓冲区设置 UINT stride = sizeof(VertexPosNormalTex); // 跨越字节数 UINT offset = 0; // 起始偏移量 md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset); // 输入装配阶段的索引缓冲区设置 md3dImmediateContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0); md3dImmediateContext->VSSetShader(mVertexShader2D.Get(), nullptr, 0); md3dImmediateContext->PSSetShader(mPixelShader2D.Get(), nullptr, 0); md3dImmediateContext->PSSetShaderResources(0, 1, mFireAnim[0].GetAddressOf()); } if (mCurrMode == ShowMode::WoodCrate) { // 更新常量缓冲区,让立方体转起来 static float phi = 0.0f, theta = 0.0f; phi += 0.00003f, theta += 0.00005f; mVSConstantBuffer.world = XMMatrixRotationX(phi) * XMMatrixRotationY(theta); mVSConstantBuffer.worldInvTranspose = XMMatrixTranspose(XMMatrixInverse(nullptr, mVSConstantBuffer.world)); md3dImmediateContext->UpdateSubresource(mConstantBuffers[0].Get(), 0, nullptr, &mVSConstantBuffer, 0, 0); } else if (mCurrMode == ShowMode::FireAnim) { // 用于限制在1秒60帧 static float totDeltaTime = 0; totDeltaTime += dt; if (totDeltaTime > 1.0f / 60) { totDeltaTime -= 1.0f / 60; mCurrFrame = (mCurrFrame + 1) % 120; md3dImmediateContext->PSSetShaderResources(0, 1, mFireAnim[mCurrFrame].GetAddressOf()); } } }
GameApp::DrawScene的变化
在这里只有引导内容的变化:
void GameApp::DrawScene() { assert(md3dImmediateContext); assert(mSwapChain); md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black)); md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); // VS常量缓冲区对应HLSL寄存于b0的常量缓冲区 md3dImmediateContext->VSSetConstantBuffers(0, 1, mConstantBuffers[0].GetAddressOf()); // PS常量缓冲区对应HLSL寄存于b1的常量缓冲区 md3dImmediateContext->PSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf()); // 绘制几何模型 md3dImmediateContext->DrawIndexed(mIndexCount, 0, 0); // 绘制Direct2D部分 md2dRenderTarget->BeginDraw(); static const WCHAR* textStr = L"切换显示: 1-木箱(3D) 2-火焰(2D)\n"; md2dRenderTarget->DrawTextW(textStr, (UINT32)wcslen(textStr), mTextFormat.Get(), D2D1_RECT_F{ 0.0f, 0.0f, 400.0f, 20.0f }, mColorBrush.Get()); HR(md2dRenderTarget->EndDraw()); HR(mSwapChain->Present(0, 0)); }
最终的显示效果如下:
上一篇: 昨天下午跟同事出去喝酒
下一篇: 常用排序算法