彩色立方体的绘制,利用几何着色器实现公告板效果

树的公告板效果

当一棵树离摄像机太远的话,我们可以使用公告板技术,用一张树的贴图来进行绘制,取代原来绘制3D树模型的方式。首先我们给出树的纹理贴图组成:
图片 1

关注Alpha通道部分,白色区域指代Alpha值为1.0(完全不透明),而黑色区域指代Alpha值0.0(完全透明)。所以在渲染树纹理的时候,我们只需要对Alpha值为0.0的像素区域进行裁剪即可。

实现公告板的关键点在于:公告板要永远正向摄像机(即视线要与公告板表面垂直),使得用户的视线在x0z面上的投影一直与贴图表面垂直。这样做省去了大量顶点的输入和处理,显得更加高效,并且这个小技巧还能够欺骗玩家让人误以为还是原来的3D模型(眼尖的玩家还是有可能认得出来),只要你别一开始就告诉人家这棵树的绘制用了公告板原理就行了(→_→)。

现在不考虑坐标系的Y轴部分(即从上方俯视),从下面的图可以看到,公告板投影的中心部分的法向量是直接指向摄像机的。

图片 2

因此我们可以得到公告板的u轴,
v轴和w轴单位向量以及根据公告板构建的局部坐标系:
(mathbf{w}=frac{(E_x-C_x,0,E_z-C_z)}{E_x-C_x,0,E_z-C_z})
(mathbf{v}=(0,1,0))
(mathbf{u}=mathbf{v}timesmathbf{w})

然后已知中心顶点位置、树宽度和高度,就可以求得2D树矩形的四个顶点了:

// 计算出公告板矩形的四个顶点// up// v1___|___v3// | | |// right__|___| |// |__/____|// v0 / v2// look v[0] = float4(center + halfWidth * right - halfHeight * up, 1.0f);v[1] = float4(center + halfWidth * right + halfHeight * up, 1.0f);v[2] = float4(center - halfWidth * right - halfHeight * up, 1.0f);v[3] = float4(center - halfWidth * right + halfHeight * up, 1.0f);

注意上面的加减运算是针对float3进行的,然后用1.0f填充成4D向量。并且由于每个公告板所处的局部坐标系不一样,我们需要对它们分别计算出对应的坐标轴向量。

若现在我们需要绘制公告板,则在输入的时候仅提供对应的中心顶点,然后图元类型选择D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,在几何着色阶段我们直接将顶点直传到几何着色阶段,这些顶点传递给几何着色器后就会解释成一个个矩形(两个三角形),产生公告板。

 

纹理数组的采样

Texture2DArray同样也具有Sample方法:

// 每4棵树一个循环,尽量保证出现不同的树float4 texColor = texArray.Sample(sam, float3(pIn.Tex, pIn.PrimID % 4));

第一个参数依然是采样器

而第二个参数则是一个3D向量,其中x与y的值对应的还是纹理坐标,而z分量即便是个float,主要是用于作为索引值选取纹理数组中的某一个具体纹理。同理索引值0对应纹理数组的第一张纹理,1对应的是第二张纹理等等...

在我们的这个demo中,纹理数组存放了4张不同样式的树的纹理贴图,然后用SV_Primitive模4的值来决定哪张树纹理贴图将被绘制。

图片 3

使用纹理数组的优势是,我们可以一次性预先创建好所有需要用到的纹理,并绑定到HLSL的纹理数组中,而不需要每次都重新绑定一个纹理。然后我们再使用索引值来访问纹理数组中的某一纹理。

/************************************************************************//* 5.创建输入布局 *//************************************************************************/// 顶点输入布局描述D3D11_INPUT_ELEMENT_DESC vertexDesc[] ={ {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}};// 从technique对象中获取pass信息D3DX11_PASS_DESC passDesc;m_pTech->GetPassByIndex(0)->GetDesc(&passDesc);// 创建顶点输入布局HR(m_pD3DDevice->CreateInputLayout(vertexDesc, 2, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, &m_pInputLayout));

ID3D11DeviceContext::Map函数--获取指向子资源中数据的指针并拒绝GPU对该子资源的访问

HRESULT ID3D11DeviceContext::Map( ID3D11Resource *pResource, // [In]包含ID3D11Resource接口的资源对象 UINT Subresource, // [In]子资源索引 D3D11_MAP MapType, // [In]D3D11_MAP枚举值,指定读写相关操作 UINT MapFlags, // [In]填0,忽略 D3D11_MAPPED_SUBRESOURCE *pMappedResource // [Out]获取到的已经映射到内存的子资源);

D3D11_MAP枚举值类型的成员如下:

D3D11_MAP成员含义
D3D11_MAP_READ映射到内存的资源用于读取。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_READ标签
D3D11_MAP_WRITE映射到内存的资源用于写入。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_WRITE标签
D3D11_MAP_READ_WRITE映射到内存的资源用于读写。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE标签
D3D11_MAP_WRITE_DISCARD映射到内存的资源用于写入,之前的资源数据将会被抛弃。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_WRITE和D3D11_USAGE_DYNAMIC标签
D3D11_MAP_WRITE_NO_OVERWRITE映射到内存的资源用于写入,但不能复写已经存在的资源。该枚举值只能用于顶点/索引缓冲区。该资源在创建的时候需要有D3D11_CPU_ACCESS_WRITE标签,在Direct3D 11不能用于设置了D3D11_BIND_CONSTANT_BUFFER标签的资源,但在11.1后可以。具体可以查阅MSDN文档

获取到的结构体D3D11_MAPPED_SUBRESOURCE成员如下:

typedef struct D3D11_MAPPED_SUBRESOURCE { void *pData; UINT RowPitch; UINT DepthPitch;};

首先pData指向的是映射到内存上的子资源首元素地址,即对应Row和Depth
Slice值都为0的位置

其次RowPitch通常是一行元素占用的字节数,对于512x512的纹理来说,若它使用DXGI_R8G8B8A8_UNORM数据类型,则一行占用了512像素*4字节/像素=2048字节。它的每个子资源的RowPitch都是一致的,这样方便其进行下一行跳转,即便在内存上会有些许的浪费,在mipmap等级越高时也有所体现,但一个完整的mipmap链消耗的内存也就逼近原来单张纹理所占字节数的2倍而已。

DepthPitch在2D纹理的含义则是当前子资源占用的字节数。像刚才说的那样,你在调试器上观察每个mipmap等级对应的DepthPitch值,可以发现mip
slice增加1,子资源占用的字节数是原来的1/2,而不是原来的1/4。这可以说明每行占用的字节数是不会改变的。

 

ID3D11DeviceContext::UpdateSubresource函数[2]--将内存数据拷贝到不可进行映射的子资源中

这个函数在之前我们主要是用来将内存数据拷贝到常量缓冲区中,现在我们也可以用它将内存数据拷贝到纹理的子资源当中:

void ID3D11DeviceContext::UpdateSubresource( ID3D11Resource *pDstResource, // [In]目标资源对象 UINT DstSubresource, // [In]对于2D纹理来说,该参数为指定Mipmap等级的子资源 const D3D11_BOX *pDstBox, // [In]这里填nullptr const void *pSrcData, // [In]用于拷贝的内存数据 UINT SrcRowPitch, // [In]该2D纹理的 宽度*数据格式的位数 UINT SrcDepthPitch // [In]对于2D纹理来说并不需要用到该参数,因此可以任意设置);

先看一个顶点着色器的代码: 

CreateDDSTexture2DArrayShaderResourceView函数--创建用于DDS纹理的数组着色器资源视图

该函数放到了GameApp类中,你也可以单独抽离出来。

ComPtr<ID3D11ShaderResourceView> CreateDDSTexture2DArrayShaderResourceView( ComPtr<ID3D11Device> device, // [In]D3D设备 ComPtr<ID3D11DeviceContext> deviceContext, // [In]D3D设备上下文 const std::vector<std::wstring>& filenames, // [In]文件名数组 int maxMipMapSize); // [In]最大允许mipmap等级,若为0,则使用默认纹理mipmap等级

具体的函数实现就是上面四步的所有代码。

编译在.fx文件中的着色器程序可以创建一个Effect,编译步骤如下:

CreateDDSTextureFromFileEx函数--使用更多的参数,从文件中读取DDS纹理

HRESULT CreateDDSTextureFromFileEx( ID3D11Device* d3dDevice, // [In]D3D设备 ID3D11DeviceContext* d3dContext, // [In]D3D设备上下文(可选) const wchar_t* szFileName, // [In].dds文件名 size_t maxsize, // [In]最大允许mipmap等级,默认0 D3D11_USAGE usage, // [In]D3D11_USAGE枚举值类型,指定CPU/GPU读写权限 unsigned int bindFlags, // [In]绑定标签,指定它可以被绑定到什么对象上 unsigned int cpuAccessFlags, // [In]CPU访问权限标签 unsigned int miscFlags, // [In]杂项标签,忽略 bool forceSRGB, // [In]强制使用SRGB,默认false ID3D11Resource** texture, // [Out]获取创建好的纹理(可选) ID3D11ShaderResourceView** textureView, // [Out]获取创建好的纹理资源视图(可选) DDS_ALPHA_MODE* alphaMode = nullptr); // [Out]忽略(可选)

也就是说,图片的数据格式、宽度、高度等信息都是随文件读取的时候获得的,我们无法在这里指定。所以我们要求提供的所有DDS纹理宽度、高度、数据格式都应当一致。对于数据格式不一致的,我们可以使用DirectX Texture Tool来修改,但是该程序包含在DirectX
SDK中,这里我在Github上尝试提供单独的DxTex.exe程序看能不能直接使用。现在我预先确保Demo中的4张树纹理都设置为同样的数据格式。

第一步,读取一系列纹理的代码如下:

//// 1. 读取所有纹理//size_t size = filenames.size();std::vector<ComPtr<ID3D11Texture2D>> srcTex(size);UINT mipLevel = maxMipMapSize;UINT width, height;DXGI_FORMAT format;for (size_t i = 0; i < size; ++i){ // 由于这些纹理并不会被GPU使用,我们使用D3D11_USAGE_STAGING枚举值 // 使得CPU可以读取资源 HR(CreateDDSTextureFromFileEx(device.Get(), deviceContext.Get(), filenames[i].c_str(), maxMipMapSize, D3D11_USAGE_STAGING, // Usage 0, // BindFlags D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ, // CpuAccessFlags 0, // MiscFlags false, (ID3D11Resource**)srcTex[i].GetAddressOf(), nullptr)); // 读取创建好的纹理Mipmap等级, 宽度和高度 D3D11_TEXTURE2D_DESC texDesc; srcTex[i]->GetDesc(&texDesc); if (i == 0) { mipLevel = texDesc.MipLevels; width = texDesc.Width; height = texDesc.Height; format = texDesc.Format; } // 这里断言所有纹理的MipMap等级,宽度和高度应当一致 assert(mipLevel == texDesc.MipLevels); assert(texDesc.Width == width && texDesc.Height == height); // 这里要求所有提供的图片数据格式应当是一致的,若存在不一致的情况,请 // 使用dxtex.exe(DirectX Texture Tool)将所有的图片转成一致的数据格式 assert(texDesc.Format == format);}

接下来的第二步就是创建纹理数组,我们使用第一个纹理的描述去填充纹理数组的一部分描述:

//// 2.创建纹理数组//D3D11_TEXTURE2D_DESC texDesc, texArrayDesc;srcTex[0]->GetDesc(&texDesc);texArrayDesc.Width = texDesc.Width;texArrayDesc.Height = texDesc.Height;texArrayDesc.MipLevels = texDesc.MipLevels;texArrayDesc.ArraySize = size;texArrayDesc.Format = texDesc.Format;texArrayDesc.SampleDesc.Count = 1;texArrayDesc.SampleDesc.Quality = 0;texArrayDesc.Usage = D3D11_USAGE_DEFAULT;texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;texArrayDesc.CPUAccessFlags = 0;texArrayDesc.MiscFlags = 0;ComPtr<ID3D11Texture2D> texArray;HR(device->CreateTexture2D(&texArrayDesc, nullptr, texArray.GetAddressOf()));

在第三步进行复制之前,我们还需要了解纹理的子资源

2.7 绘制场景

实现效果

可以观察到,在与公告版近距离接触时可以很明显地看到公告板在跟着摄像机旋转。如果距离很远的话转动的幅度就会很小,用户才会比较难以分辨出远处物体是否为公告板或3D模型了。
图片 4

下面演示了白天和黑夜的雾效
图片 5

最后则是Alpha-To-Coverage的开启/关闭效果对比
图片 6

DirectX11 With Windows
SDK完整目录

Github项目源码

绘制场景的步骤为:

纹理数组

之前在C++代码层中,我们的每一张纹理使用ID3D11Texture2D的接口对象去单独存储。但实际上在我们创建ID3D11Texture2D对象的时候,我们可以设置它的ArraySize来指定该对象可以存放的纹理数目。

但是我们创建纹理并不是使用D3DX系列的函数,因为我们根本就不使用DirectX
SDK。在之前我们创建纹理使用的是DDSTextureLoader.hWICTextureLoader.h中的函数。这里再提及一下,这两个头文件对应的库可以在下面两个途径找到:

DirectXTex

DirectXTK

回到HLSL代码,我们之所以不使用下面的这种形式创建纹理数组:

Texture2D TexArray[4];float4 PS(GeoOut pin) : SV_Target{ float4 c = TexArray[pin.PrimID%4].Sample(samLinear, pin.Tex);

是因为这样做的话HLSL编译器会报错:sampler array index must be a literal
experssion,即pin.PrimID的值也必须是个字面值,而不是变量。但我们还是想要能够根据变量取对应纹理的能力。

正确的做法应当是声明一个Texture2DArray的数组:

Texture2DArray texArray : register(t1);

这里使用的是索引为1的纹理寄存器是因为前面还有一个纹理已经绑定了t0.

 

BasicFX.h的变化

常量缓冲区对应的结构体和BasicFX类的变化如下:

#ifndef BASICFX_H#define BASICFX_H#include <wrl/client.h>#include <d3d11_1.h>#include <d3dcompiler.h>#include <directxmath.h>#include <vector>#include "LightHelper.h"#include "RenderStates.h"#include "Vertex.h"// 由于常量缓冲区的创建需要是16字节的倍数,该函数可以返回合适的字节大小inline UINT Align16Bytes(UINT size){ return (size + 15) & (UINT)(-16);}struct CBChangesEveryDrawing{ DirectX::XMMATRIX world; DirectX::XMMATRIX worldInvTranspose; DirectX::XMMATRIX texTransform; Material material;};struct CBChangesEveryFrame{ DirectX::XMMATRIX view; DirectX::XMFLOAT4 eyePos;};struct CBDrawingStates{ DirectX::XMFLOAT4 fogColor; int fogEnabled; float fogStart; float fogRange; float pad;};struct CBChangesOnResize{ DirectX::XMMATRIX proj;};struct CBNeverChange{ DirectionalLight dirLight[4];};class BasicFX{public: // 使用模板别名(C++11)简化类型名 template <class T> using ComPtr = Microsoft::WRL::ComPtr<T>; // 初始化Basix.fx所需资源并初始化光栅化状态 bool InitAll(ComPtr<ID3D11Device> device); // 是否已经初始化 bool IsInit() const; template <class T> void UpdateConstantBuffer(const T& cbuffer); // 默认状态绘制 void SetRenderDefault(); // 公告板绘制 void SetRenderBillboard(bool enableAlphaToCoverage);private: // objFileNameInOut为编译好的着色器二进制文件(.*so),若有指定则优先寻找该文件并读取 // hlslFileName为着色器代码,若未找到着色器二进制文件则编译着色器代码 // 编译成功后,若指定了objFileNameInOut,则保存编译好的着色器二进制信息到该文件 // ppBlobOut输出着色器二进制信息 HRESULT CreateShaderFromFile(const WCHAR* objFileNameInOut, const WCHAR* hlslFileName, LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob** ppBlobOut);private: ComPtr<ID3D11VertexShader> mBasicVS; ComPtr<ID3D11PixelShader> mBasicPS; ComPtr<ID3D11VertexShader> mBillboardVS; ComPtr<ID3D11GeometryShader> mBillboardGS; ComPtr<ID3D11PixelShader> mBillboardPS; ComPtr<ID3D11InputLayout> mVertexPosSizeLayout; // 点精灵输入布局 ComPtr<ID3D11InputLayout> mVertexPosNormalTexLayout; // 3D顶点输入布局 ComPtr<ID3D11DeviceContext> md3dImmediateContext; // 设备上下文 std::vector<ComPtr<ID3D11Buffer>> mConstantBuffers; // 常量缓冲区};#endif

初始化函数和SetRenderDeafult方法这里就不赘述了。

图片 7

BasicFX::SetRenderBillboard方法--公告板绘制

实现如下:

void BasicFX::SetRenderBillboard(bool enableAlphaToCoverage){ md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST); md3dImmediateContext->IASetInputLayout(mVertexPosSizeLayout.Get()); md3dImmediateContext->VSSetShader(mBillboardVS.Get(), nullptr, 0); md3dImmediateContext->GSSetShader(mBillboardGS.Get(), nullptr, 0); md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get()); md3dImmediateContext->PSSetShader(mBillboardPS.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(nullptr, 0); md3dImmediateContext->OMSetBlendState( (enableAlphaToCoverage ? RenderStates::BSAlphaToCoverage.Get() : nullptr), nullptr, 0xFFFFFFFF);}

参数enableAlphaToCoverage决定是否要绑定渲染状态对象RenderStates::BSAlphaToCoverage

  technique11:一个特效可以通过多种不同的方法实现,每个方法可以作为一个technique11。

GameApp::InitPointSpritesBuffer方法--初始化存放点精灵的缓冲区

该方法会生成20个顶点,均匀并略带随机性地环绕在原点周围。这些顶点一经创建就不可以被修改了,它们将会被用于公告板的创建:

void GameApp::InitPointSpritesBuffer(){ srand((unsigned)time(nullptr)); VertexPosSize vertexes[16]; float theta = 0.0f; for (int i = 0; i < 16; ++i) { // 取20-50的半径放置随机的树 float radius = (float)(rand() % 31 + 20); float randomRad = rand() % 256 / 256.0f * XM_2PI / 16; vertexes[i].pos = XMFLOAT3(radius * cosf(theta + randomRad), 8.0f, radius * sinf(theta + randomRad)); vertexes[i].size = XMFLOAT2(30.0f, 30.0f); theta += XM_2PI / 16; } // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; // 数据不可修改 vbd.ByteWidth = sizeof (vertexes); vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertexes; HR(md3dDevice->CreateBuffer(&vbd, &InitData, mPointSpritesBuffer.GetAddressOf()));}

为了让GPU访问顶点数组,我们必须把它放在顶点缓冲(vertex
buffer)中,该容器由ID3D11Buffer接口表示。要创建一个顶点缓冲,我们必须执行以下步骤:

ID3D11DeviceContext::UnMap函数--让指向资源的指针无效并重新启用GPU对该资源的访问权限

void ID3D11DeviceContext::Unmap( ID3D11Resource *pResource, // [In]包含ID3D11Resource接口的资源对象 UINT Subresource // [In]需要取消的子资源索引);

第三步的具体代码如下:

//// 3.将所有的纹理子资源赋值到纹理数组中//// 每个纹理元素for (size_t i = 0; i < size; ++i){ // 纹理中的每个mipmap等级 for (UINT j = 0; j < mipLevel; ++j) { D3D11_MAPPED_SUBRESOURCE mappedTex2D; // 允许映射索引i纹理中,索引j的mipmap等级的2D纹理 HR(deviceContext->Map(srcTex[i].Get(), j, D3D11_MAP_READ, 0, &mappedTex2D)); deviceContext->UpdateSubresource( texArray.Get(), D3D11CalcSubresource(j, i, mipLevel), // i * mipLevel + j nullptr, mappedTex2D.pData, mappedTex2D.RowPitch, mappedTex2D.DepthPitch); // 停止映射 deviceContext->Unmap(srcTex[i].Get(), j); }}

最后一步就是要创建着色器资源视图。

g_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj)); 

图元ID

现在讲述系统值SV_PrimitiveID,我们可以将它作为函数的额外形参进行提供。它告诉我们在输入装配阶段下自动分配的图元ID值。当我们调用了一个draw方法,需要绘制n个图元,那么第一个图元对应的ID值为0,第二个为1,直到最后一个为n-1.当前的所有图元ID仅在当前的单次调用绘制是唯一的。其中该系统值的写入操作允许在几何着色器和像素着色器进行,而读取操作则允许在几何/像素/外壳/域着色器中进行。

在上面的例子中,我们将一个顶点产生的矩形四个顶点都标记为同一个图元ID,是因为到后续的像素着色器中,我们用该图元ID映射到纹理数组的索引值,来对应到要绘制的树的纹理。

注意:
如果几何着色器没有提供图元ID,在像素着色器中也可以将它加进参数列表中以使用:

float4 PS(Vertex3DOut pin, uint primID : SV_PrimitiveID) : SV_Target{// Pixel shader body…}

但如果像素着色器提供了图元ID,渲染管线又绑定了几何着色器,则几何着色器必须提供该参数。在几何着色器中你可以使用或修改图元ID值。

接下来我们就可以创建输入布局了,下面是代码:

前言

上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区。接下来我们继续利用它来实现一些新的效果,在这一章,你将了解:

  1. 实现公告板效果
  2. Alpha-To-Coverage
  3. 对GPU资源进行读/写操作
  4. 纹理数组
  5. 实现雾效

DirectX11 With Windows
SDK完整目录

Github项目源码

 

纹理子资源(Texture Subresources)

WICTextureLoader或者DDSTextureLoader读取出来的纹理数据实际上并不是由单纯的一个二维数组构成,而是多个不同大小的二维数组,不同的mipmap等级对应不同的二维数组,这些二维数组都是该纹理的子资源。比如512x512的纹理加载进来包含的mipmap等级数(Mipmap
Levels)为10,包含了从512x512, 256x256,
128x128...到1x1的10个二维数组颜色数据,而Direct3D API使用Mip切片(Mip
slice)来指定某一mipmap等级的纹理,也有点像索引。比如mip
slice值为0时,对应的是512x512的纹理,而mip
slice值1对应的是256x256,以此类推。

对于纹理数组,每个元素就上面说的单个纹理对应的mipmap链,Direct3D
API使用数组切片(array
slice)来访问不同纹理,也是相当于索引.这样我们就可以把所有的纹理资源用下面的图来表示,假定下图有4个纹理,每个纹理包含3个子资源,则当前指定的是Array
Slice为2,Mip Slice为1的子资源。

图片 8

该函数参顶点着色器输出结构作为参数,输出为float4类型,注意函数名后面的语义说明:SV_TARGET,它是函数返回值的说明,显然这也是一个系统值,不可更改,它作为相应片段的颜色值传递给下一个阶段。 

纹理数组的加载

现在我们手头上仅有的就是DDSTextureLoader.hWICTextureLoader.h中的函数,但这里面的函数每次都只能加载一张纹理。我们还需要修改龙书样例中读取纹理的函数,具体的操作顺序如下:

  1. 一个个读取存有纹理的文件,创建出一系列ID3D11Texture2D对象,这里的每个对象单独包含一张纹理;
  2. 创建一个ID3D11Texture2D对象,它同时也是一个纹理数组;
  3. 将之前读取的所有纹理有条理地复制到刚创建的纹理数组对象中;
  4. 为该纹理数组对象创建创建一个纹理资源视图(Shader Resource View)。

首先我们需要了解增强版的纹理创建函数。

  b.指定输入布局、图元拓扑类型、定点缓冲、索引缓冲、渲染状态等等;

HLSL代码

与雾效相关的值存储在下面的常量缓冲区中,并且绘制3D物体的顶点没有发生变化:

// Basic.fx// ...cbuffer CBDrawingStates : register(b2){ float4 gFogColor; int gFogEnabled; float gFogStart; float gFogRange;}// ...struct Vertex3DIn{ float3 PosL : POSITION; float3 NormalL : NORMAL; float2 Tex : TEXCOORD;};struct Vertex3DOut{ float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD;};

Basic_VS_3D.hlsl也与之前一样,没有什么变动:

// Basic_VS_3D.hlsl#include "Basic.fx"// 顶点着色器(3D)Vertex3DOut VS_3D(Vertex3DIn pIn){ Vertex3DOut pOut; row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj); pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj); pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz; pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose); pOut.Tex = mul(float4(pIn.Tex, 0.0f, 1.0f), gTexTransform).xy; return pOut;}

Basic_PS_3D.hlsl现在使用了4盏方向光以保证4种不同方向的光能够均匀照射,并添加了雾效部分的处理:

// Basic_PS_3D.hlsl#include "Basic.fx"// 像素着色器(3D)float4 PS_3D(Vertex3DOut pIn) : SV_Target{ // 提前进行裁剪,对不符合要求的像素可以避免后续运算 float4 texColor = tex.Sample(sam, pIn.Tex); clip(texColor.a - 0.05f); // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 求出顶点指向眼睛的向量,以及顶点与眼睛的距离 float3 toEyeW = normalize(gEyePosW - pIn.PosW); float distToEye = distance(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); [unroll] for (int i = 0; i < 4; ++i) { ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S); ambient += A; diffuse += D; spec += S; } float4 litColor = texColor * (ambient + diffuse) + spec; // 雾效部分 [flatten] if (gFogEnabled) { // 限定在0.0f到1.0f范围 float fogLerp = saturate((distToEye - gFogStart) / gFogRange); // 根据雾色和光照颜色进行线性插值 litColor = lerp(litColor, gFogColor, fogLerp); } litColor.a = texColor.a * gMaterial.Diffuse.a; return litColor;}

对于白天来说,我们可以使用RGBA=(0.75f, 0.75f, 0.75f, 1.0f)来作为雾的颜色。

而对于黑夜来说,这个雾效更像是战争迷雾的效果,我们使用RGBA=(0.0f, 0.0f, 0.0f, 1.0f)来作为雾的颜色,这样远处的物体我们就让它看不见,而在可视范围内,距离越远的物体能见度越低。

具体的演示效果在最后可以看到。

像素着色器是针对逐个像素进行的。在光栅化阶段,一个三角形在其所覆盖的每个像素处使用插值来计算相应顶点的各个属性,然后把插值后的顶点传递给像素着色器。在简单的没有Geometry
Shader和Tessellator时,顶点着色器的输出就是像素着色器的输入,像素着色器最终的输出是该像素处对应片段的颜色值。如下即是一个最简单的像素着色器函数:

HLSL代码

下面是Basic.fx的完整代码:

// Basic.fx#include "LightHelper.hlsli"Texture2D tex : register(t0);Texture2DArray texArray : register(t1);SamplerState sam : register(s0);cbuffer CBChangesEveryDrawing : register(b0){ row_major matrix gWorld; row_major matrix gWorldInvTranspose; row_major matrix gTexTransform; Material gMaterial;}cbuffer CBChangesEveryFrame : register(b1){ row_major matrix gView; float3 gEyePosW;}cbuffer CBDrawingStates : register(b2){ float4 gFogColor; int gFogEnabled; float gFogStart; float gFogRange;}cbuffer CBChangesOnResize : register(b3){ row_major matrix gProj;}cbuffer CBNeverChange : register(b4){ DirectionalLight gDirLight[4];}struct Vertex3DIn{ float3 PosL : POSITION; float3 NormalL : NORMAL; float2 Tex : TEXCOORD;};struct Vertex3DOut{ float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD;};struct PointSprite{ float3 PosW : POSITION; float2 SizeW : SIZE;};struct BillboardVertex{ float4 PosH : SV_POSITION; float3 PosW : POSITION; float3 NormalW : NORMAL; float2 Tex : TEXCOORD; uint PrimID : SV_PrimitiveID;};

对于顶点着色器,仅负责顶点的直传:

// Billboard_VS.hlsl#include "Basic.fx"PointSprite VS(PointSprite pIn){ return pIn;}

而几何着色器的代码如下:

// Billboard_GS.hlsl#include "Basic.fx"// 节省内存资源,先用float4向量声明。static const float4 gVec[2] = { float4(0.0f, 1.0f, 0.0f, 0.0f), float4(1.0f, 1.0f, 1.0f, 0.0f) };static const float2 gTex[4] = (float2[4])gVec;[maxvertexcount(4)]void GS(point PointSprite input[1], uint primID : SV_PrimitiveID, inout TriangleStream<BillboardVertex> output){ // 计算公告板所处的局部坐标系,其中公告板相当于 // 被投影在了局部坐标系的xy平面,z轴 float3 up = float3(0.0f, 1.0f, 0.0f); float3 look = gEyePosW - input[0].PosW; look.y = 0.0f; // look向量只取投影到xz平面的向量 look = normalize(look); float3 right = cross(up, look); // 计算出公告板矩形的四个顶点 // up // v1 ___|___ v3 // | | | // right__|___| | // | / | // |_/_____| // v0 / v2 // look float4 v[4]; float3 center = input[0].PosW; float halfWidth = 0.5f * input[0].SizeW.x; float halfHeight = 0.5f * input[0].SizeW.y; v[0] = float4(center + halfWidth * right - halfHeight * up, 1.0f); v[1] = float4(center + halfWidth * right + halfHeight * up, 1.0f); v[2] = float4(center - halfWidth * right - halfHeight * up, 1.0f); v[3] = float4(center - halfWidth * right + halfHeight * up, 1.0f); // 对顶点位置进行矩阵变换,并以TriangleStrip形式输出 BillboardVertex gOut; row_major matrix viewProj = mul(gView, gProj); [unroll] for (int i = 0; i < 4; ++i) { gOut.PosW = v[i].xyz; gOut.PosH = mul(v[i], viewProj); gOut.NormalW = look; gOut.Tex = gTex[i]; gOut.PrimID = primID; output.Append(gOut); }}

首先一开始不用float2数组是因为每个float2元素会单独打包,浪费了一半的空间,因此这里采取一种特殊的语法形式使得内存可以得到充分利用。

然后要注意maxvertexcount的值要设为4,尽管Append的次数为4,但实际上输出的三角形顶点数为6。

在D3D中,顶点由空间位置和各种附加属性组成。定义顶点结构体如下,由空间位置和颜色组成,我们这个例子中使用的结构体就是它:

顶点ID

紧接着是系统值SV_VertexID,在输入装配阶段的时候渲染管线就会为这些输入的顶点分配顶点ID值。若使用的是Draw方法,则这些顶点将会按顺序从0到n-1被标记(n为顶点数目);若使用的是DrawIndexed方法,则顶点ID对应到的是该顶点所处的索引值。该参数仅能在顶点着色器的参数列表中提供:

VertexOut VS(VertexIn vin, uint vertID : SV_VertexID){// vertex shader body…}

最后给出像素着色器的代码:

// Billboard_PS.hlsl#include "Basic.fx"float4 PS(BillboardVertex pIn) : SV_Target{ // 每4棵树一个循环,尽量保证出现不同的树 float4 texColor = texArray.Sample(sam, float3(pIn.Tex, pIn.PrimID % 4)); // 提前进行裁剪,对不符合要求的像素可以避免后续运算 clip(texColor.a - 0.05f); // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 求出顶点指向眼睛的向量,以及顶点与眼睛的距离 float3 toEyeW = normalize(gEyePosW - pIn.PosW); float distToEye = distance(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); [unroll] for (int i = 0; i < 4; ++i) { ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S); ambient += A; diffuse += D; spec += S; } float4 litColor = texColor * (ambient + diffuse) + spec; // 雾效部分 [flatten] if (gFogEnabled) { // 限定在0.0f到1.0f范围 float fogLerp = saturate((distToEye - gFogStart) / gFogRange); // 根据雾色和光照颜色进行线性插值 litColor = lerp(litColor, gFogColor, fogLerp); } litColor.a = texColor.a * gMaterial.Diffuse.a; return litColor;}

这里加上了刚才的雾效,并使用了纹理数组。

在Direct3D中,顶点由空间位置和各种附加属性组成,Direct3D可以让我们灵活地建立属于我们自己的顶点格式;换句话说,它允许我们定义顶点的分量。

实现雾效

虽然这部分与几何着色器并没有什么关系,但是雾的效果在该Demo中会用到,并且前面也没有讲过这部分内容,故先在这里提出来。

有时候我们需要在游戏中模拟一些特定的天气条件,比如说大雾。它可以让物体平滑出现而不是突然蹦出来那样(物体的一部分留在视锥体内使得只能看到该部分,然后在逐渐靠近该物体的时候,该物体就像经过了一个无形的扫描门被逐渐构造出来那样)。通过让雾在某一范围内具有一定的层次(让不可见区域比视锥体裁剪区域还近),我们可以避免上面所说的情况。但即便是晴朗的天气,你可能仍希望包含一个较广范围的雾效,即距离达到很远的地方才逐渐看不清物体。

我们可以使用这种方式来实现雾效:指定雾的颜色,以摄像机为原点的雾开始的最小距离,雾效范围值(超过起始距离+雾效范围值的范围外的颜色皆被指定的雾色取代)。在需要绘制的三角形内,某一像素片元的颜色如下:
(begin{align} foggedColor &= litColor +
s(fogColor - litColor)\ &= (1-s) cdot litColor + s cdot
fogColor\ end{align})

该函数对应HLSL中的lerp函数,s取0的时候最终颜色为litColor,然后逐渐增大并逼近1的时候,最终颜色就逐渐趋近于fogColor。然后参数s的值取决于下面的函数:
(s =
saturate(frac{dist(mathbf{p},mathbf{E}) -
fogStart}{fogRange}))
(saturate(x) = begin{cases} x, 0 le x
le 1\ 0, x < 0\ 1, x > 1\ end{cases})

其中dist(p,E)指的是两点之间的距离值。配合下面的图去理解:
图片 9

还有注意一点,在每次清空重新绘制的时候,要用雾的颜色进行清空。

下面的代码创建了一个只读的顶点缓冲区,并以中心在原点上的立方体的8个顶点来初始化该缓冲区。之所以说该缓冲区是只读的,是因为当立方体创建后相关的几何数据从不改变——始终保持为一个立方体。另外,我们为每个顶点指定了不同的颜色,颜色类型为D3D11Util.h文件中的类型。

Alpha-To-Coverage

在Demo运行的时候,仔细观察可以发现树公告板的某些边缘部分有一些比较突出的黑边。

这是因为当前默认使用的是Alpha
Test,即HLSL中使用clip函数将Alpha值为0的像素点给剔除掉,这些像素也不是树的一部分。该函数决定某一像素是留下还是抛弃,这会导致不平滑的过渡现象,在摄像机逐渐靠近该纹理时,图片本身也在不断放大,硬边部分也会被放大,就像下面那张图:

图片 10

当然,你也可以使用透明混合的方式,但是透明混合对绘制的顺序是有要求的,要求透明物体按从后到前的顺序进行绘制,即需要在绘制透明物体前先对物体按到摄像机的距离排个序。当然如果需要绘制大量的草丛的话,这种方法所需要的开销会变得非常大,操作起来也十分麻烦。

当然,我们可以考虑下使用MSAA(多重采样抗锯齿),并配合Alpha
Test进行。MSAA可以用于将多边形的锯齿边缘平滑处理,然后让Direct3D开启alpha-to-coverage技术,标记边缘部分。

首先在创建后备缓冲区、深度/模板缓冲区的时候需要打开4倍多重采样的支持,我们只需要在GameApp的构造函数中这样写即可:

GameApp::GameApp(HINSTANCE hInstance) : D3DApp(hInstance){ // 开启4倍多重采样 mEnable4xMsaa = true;}

然后在之前的例子里,我们已经在RenderStates类中预先创建好了混合状态:

D3D11_BLEND_DESC blendDesc;ZeroMemory(&blendDesc, sizeof(blendDesc));auto& rtDesc = blendDesc.RenderTarget[0];// Alpha-To-Coverage模式blendDesc.AlphaToCoverageEnable = true;blendDesc.IndependentBlendEnable = false;rtDesc.BlendEnable = false;rtDesc.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;HR(device->CreateBlendState(&blendDesc, BSAlphaToCoverage.ReleaseAndGetAddressOf()));

然后只需要在需要的时候绑定该状态即可。

顶点着色器(Vertex Shader)和像素着色器(Pixel
Shader)是Direct3D渲染中必不可少的最基本的Shader。

GameApp::InitResource方法--初始化资源

该方法集成了所有资源的初始化,注意树的纹理数组要提供到输入槽1,对应纹理寄存器t1的Texture2DArray

bool GameApp::InitResource(){ // 默认白天,开启AlphaToCoverage mIsNight = false; mEnableAlphaToCoverage = true; // ****************** // 初始化各种物体 // 初始化树纹理资源 mTreeTexArray = CreateDDSTexture2DArrayShaderResourceView( md3dDevice, md3dImmediateContext, std::vector<std::wstring>{ L"Texture\tree0.dds", L"Texture\tree1.dds", L"Texture\tree2.dds", L"Texture\tree3.dds"}); // 初始化点精灵缓冲区 InitPointSpritesBuffer(); // 初始化树的材质 mTreeMat.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f); mTreeMat.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f); mTreeMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f); ComPtr<ID3D11ShaderResourceView> texture; // 初始化地板 mGround.SetBuffer(md3dDevice, Geometry::CreatePlane(XMFLOAT3(0.0f, -5.0f, 0.0f), XMFLOAT2(100.0f, 100.0f), XMFLOAT2(10.0f, 10.0f))); HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\Grass.dds", nullptr, texture.GetAddressOf())); mGround.SetTexture(texture); Material material; material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f); material.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f); material.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f); mGround.SetMaterial(material); mGround.SetWorldMatrix(XMMatrixIdentity()); mGround.SetTexTransformMatrix(XMMatrixIdentity()); // ****************** // 初始化常量缓冲区的值 mCBChangesEveryDrawing.material = mTreeMat; mCBChangesEveryDrawing.world = mCBChangesEveryDrawing.worldInvTranspose = XMMatrixIdentity(); mCBChangesEveryDrawing.texTransform = XMMatrixIdentity(); // 方向光 mCBNeverChange.dirLight[0].Ambient = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f); mCBNeverChange.dirLight[0].Diffuse = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f); mCBNeverChange.dirLight[0].Specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f); mCBNeverChange.dirLight[0].Direction = XMFLOAT3(-0.577f, -0.577f, 0.577f); mCBNeverChange.dirLight[1] = mCBNeverChange.dirLight[0]; mCBNeverChange.dirLight[1].Direction = XMFLOAT3(0.577f, -0.577f, 0.577f); mCBNeverChange.dirLight[2] = mCBNeverChange.dirLight[0]; mCBNeverChange.dirLight[2].Direction = XMFLOAT3(0.577f, -0.577f, -0.577f); mCBNeverChange.dirLight[3] = mCBNeverChange.dirLight[0]; mCBNeverChange.dirLight[3].Direction = XMFLOAT3(-0.577f, -0.577f, -0.577f); // 摄像机相关 mCameraMode = CameraMode::Free; auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera); mCamera = camera; camera->SetPosition(XMFLOAT3()); camera->SetFrustum(XM_PI / 3, AspectRatio(), 1.0f, 1000.0f); camera->LookTo( XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f), XMVectorSet(0.0f, 0.0f, 1.0f, 1.0f), XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)); camera->UpdateViewMatrix(); mCBChangesEveryFrame.view = camera->GetView(); XMStoreFloat4(&mCBChangesEveryFrame.eyePos, camera->GetPositionXM()); mCBChangesOnReSize.proj = camera->GetProj(); // 雾状态默认开启 mCBDrawingStates.fogEnabled = 1; mCBDrawingStates.fogColor = XMFLOAT4(0.75f, 0.75f, 0.75f, 1.0f); // 银色 mCBDrawingStates.fogRange = 75.0f; mCBDrawingStates.fogStart = 15.0f; // 更新常量缓冲区资源 mBasicFX.UpdateConstantBuffer(mCBChangesEveryDrawing); mBasicFX.UpdateConstantBuffer(mCBChangesEveryFrame); mBasicFX.UpdateConstantBuffer(mCBChangesOnReSize); mBasicFX.UpdateConstantBuffer(mCBDrawingStates); mBasicFX.UpdateConstantBuffer(mCBNeverChange); // 直接绑定树的纹理 md3dImmediateContext->PSSetShaderResources(1, 1, mTreeTexArray.GetAddressOf()); return true;}

其余方法限于篇幅就不放在这里了,读者可以查看源码观察剩余部分的代码实现。现在来看实现效果吧。

2.4 编译着色器,创建Effect

GameApp类的变化

类成员相关声明如下:

class GameApp : public D3DApp{public: // 摄像机模式 enum class CameraMode { FirstPerson, ThirdPerson, Free };public: GameApp(HINSTANCE hInstance); ~GameApp(); bool Init(); void OnResize(); void UpdateScene(float dt); void DrawScene();private: bool InitResource(); void InitPointSpritesBuffer(); // 根据给定的DDS纹理文件集合,创建2D纹理数组 // 要求所有纹理的宽度和高度都一致 // 若maxMipMapSize为0,使用默认mipmap等级 // 否则,mipmap等级将不会超过maxMipMapSize ComPtr<ID3D11ShaderResourceView> CreateDDSTexture2DArrayShaderResourceView( ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext, const std::vector<std::wstring>& filenames, int maxMipMapSize = 0);private: ComPtr<ID2D1SolidColorBrush> mColorBrush; // 单色笔刷 ComPtr<IDWriteFont> mFont; // 字体 ComPtr<IDWriteTextFormat> mTextFormat; // 文本格式 ComPtr<ID3D11Buffer> mPointSpritesBuffer; // 点精灵顶点缓冲区 ComPtr<ID3D11ShaderResourceView> mTreeTexArray; // 树的纹理数组 Material mTreeMat; // 树的材质 GameObject mGround; // 地面 BasicFX mBasicFX; // Basic特效管理类 CameraMode mCameraMode; // 摄像机模式 std::shared_ptr<Camera> mCamera; // 摄像机 bool mIsNight; // 是否黑夜 bool mEnableAlphaToCoverage; // 是否开启Alpha-To-Coverage CBChangesEveryDrawing mCBChangesEveryDrawing; // 该缓冲区存放每次绘制更新的变量 CBChangesEveryFrame mCBChangesEveryFrame; // 该缓冲区存放每帧更新的变量 CBDrawingStates mCBDrawingStates; // 该缓冲区存放绘制状态 CBChangesOnResize mCBChangesOnReSize; // 该缓冲区存放仅在窗口大小变化时更新的变量 CBNeverChange mCBNeverChange; // 该缓冲区存放不会再进行修改的变量};

D3D11_SUBRESOURCE_DATA结构体的定义如下:

D3D11CalcSubresource函数--计算子资源的索引值

对于纹理数组的每一个子资源都可以用一个一维的索引值访问,索引值的增减是以Mip切片值为主递增的。
图片 11

然后给定当前纹理数组的mipmap等级数(Mipmap Levels),数组切片(Array
Slice)和Mip切片(Mip
Slice),我们就可以用下面的函数来求得指定子资源的索引值:

inline UINT D3D11CalcSubresource(UINT MipSlice, UINT ArraySlice, UINT MipLevels ){ return MipSlice + ArraySlice * MipLevels; }

然后是映射相关的两个函数

  a.清屏,清空深度/模版缓冲区;

ID3D11Device::CreateShaderResourceView--创建着色器资源视图

HRESULT ID3D11Device::CreateShaderResourceView( ID3D11Resource *pResource, // [In]待绑定资源 const D3D11_SHADER_RESOURCE_VIEW_DESC *pDesc, // [In]着色器资源视图描述 ID3D11ShaderResourceView **ppSRView // [Out]获取创建的着色器资源视图);

所以还需要填充D3D11_SHADER_RESOURCE_VIEW_DESC结构体:

typedef struct D3D11_SHADER_RESOURCE_VIEW_DESC{ DXGI_FORMAT Format; // 数据格式 D3D11_SRV_DIMENSION ViewDimension; // 视图维度,决定下面需要填充哪个共用体成员 union { D3D11_BUFFER_SRV Buffer; D3D11_TEX1D_SRV Texture1D; D3D11_TEX1D_ARRAY_SRV Texture1DArray; D3D11_TEX2D_SRV Texture2D; D3D11_TEX2D_ARRAY_SRV Texture2DArray; D3D11_TEX2DMS_SRV Texture2DMS; D3D11_TEX2DMS_ARRAY_SRV Texture2DMSArray; D3D11_TEX3D_SRV Texture3D; D3D11_TEXCUBE_SRV TextureCube; D3D11_TEXCUBE_ARRAY_SRV TextureCubeArray; D3D11_BUFFEREX_SRV BufferEx; };} D3D11_SHADER_RESOURCE_VIEW_DESC;

最后一步的代码如下:

//// 4.创建纹理数组的SRV//D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;viewDesc.Format = texArrayDesc.Format;viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;viewDesc.Texture2DArray.MostDetailedMip = 0;viewDesc.Texture2DArray.MipLevels = texArrayDesc.MipLevels;viewDesc.Texture2DArray.FirstArraySlice = 0;viewDesc.Texture2DArray.ArraySize = size;ComPtr<ID3D11ShaderResourceView> texArraySRV;HR(device->CreateShaderResourceView(texArray.Get(), &viewDesc, texArraySRV.GetAddressOf()));// 已经确保所有资源由ComPtr管理,无需手动释放return texArraySRV;

2.3 顶点着色器和像素着色器

具体代码如下:

我们使用上面定义的结构体Vertex。有了顶点结构体之后,我们必须设法描述该顶点结构体的分量结构,使Direct3D知道该如何使用每个分量。这一描述信息是以输入布局(ID3D11InputLayout)的形式提供给Direct3D的。输入布局是一个D3D11_INPUT_ELEMENT_DESC数组。D3D11_INPUT_ELEMENT_DESC数组中的每个元素描述了顶点结构体的一个分量。比如,当顶点结构体包含两个分量时,对应的D3D11_INPUT_ELEMENT_DESC数组会包含两个元素。我们将D3D11_INPUT_ELEMENT_DESC称为输入布局描述(input
layout
description)。D3D11_INPUT_ELEMENT_DESC结构体定义如下:

 

通常的建议是根据变量修改的频繁程度创建不同的常量缓冲,对常量缓冲进行分组是为了提高运行效率。当一个常量缓冲区被更新时,它里面的所有变量都会同时更新;所以,根据它们的更新频率进行分组,可以减少不必要的更新操作,提高运行效率。

在这个例子中,用鼠标点击拖动改变摄像机的视角和远近,在UpdateScene函数中计算变换矩阵,并更新到shader中去。这里就不详细介绍了,有兴趣的朋友请看源码。

上面的代码中使用了上一节介绍的Effect框架,通过它获取相应technique11中相应的pass的描述信息。

本文通过绘制一个彩色立方体来演示Direct3D的渲染过程,这个例子本身很简单,但是清晰的包含了Direct3D的渲染基本步骤。因为绘制过程中涉及到Direct3D的API接口和方法,我们将在学习彩色立方体的绘制过程中详细介绍这些API接口和方法。

一、概述

g_fxWorldViewProj = g_effect->GetVariableByName("g_worldViewProj")->AsMatrix(); 

VertexOut是输出结构,注意PosH成员的语义说明SV_POSITION是固定的,代表系统值,在像素着色器阶段会需要这个值来进行裁剪操作。除了系统值必须固定外,其他的语义值都是可以任意指定的。

 // 编译着色器程序 DWORD shaderFlags = 0;#if defined( DEBUG ) || defined( _DEBUG ) shaderFlags |= D3D10_SHADER_DEBUG; shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION;#endif ID3D10Blob* compiledShader = 0; ID3D10Blob* compilationMsgs = 0; HRESULT hr = D3DX11CompileFromFile(L"FX/color.fx", 0, 0, 0, "fx_5_0", shaderFlags, 0, 0, &compiledShader, &compilationMsgs, 0); // compilationMsgs中包含错误或警告信息 if( compilationMsgs != 0 ) { MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0); ReleaseCOM(compilationMsgs); } // 就算没有compilationMsgs,也需要确保没有其他错误 if(FAILED(hr)) { DXTrace(__FILE__, (DWORD)__LINE__, hr, L"D3DX11CompileFromFile", true); } // 创建Effect HR(D3DX11CreateEffectFromMemory(compiledShader->GetBufferPointer(), compiledShader->GetBufferSize(), 0, m_pD3DDevice, &m_pFX)); // 编译完成释放资源 ReleaseCOM(compiledShader); // 从Effect中获取technique对象 m_pTech = m_pFX->GetTechniqueByName("ColorTech");

D3D11_BUFFER_DESC结构体的定义如下:

需要源码的朋友点此下载,源码为4_D3DBoxDemo。

/************************************************************************//* 1.创建顶点缓冲 *//************************************************************************/Vertex vertices[] ={ { XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White }, { XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black }, { XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red }, { XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green }, { XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue }, { XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow }, { XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan }, { XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta }};// 准备结构体,描述缓冲区D3D11_BUFFER_DESC vbd;vbd.Usage = D3D11_USAGE_IMMUTABLE;vbd.ByteWidth = sizeof(Vertex) * 8;vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;vbd.CPUAccessFlags = 0;vbd.MiscFlags = 0;vbd.StructureByteStride = 0;// 准备结构体,为缓冲区指定初始化数据D3D11_SUBRESOURCE_DATA vinitData;vinitData.pSysMem = vertices;// 创建缓冲区HR(m_pD3DDevice->CreateBuffer(&vbd, &vinitData, &m_pBoxVB));
typedef struct D3D11_INPUT_ELEMENT_DESC { LPCSTR SemanticName; //元素相关的字符串。它可以是任何有效的语义名。语义用于将顶点结构体中的元素映射为顶点着色器参数 UINT SemanticIndex; //附加在语义上的索引值。例如:当顶点结构体包含多组纹理坐标时,我们不是添加一个新的语义名,而是在语义名的后面加上一个索引值。 DXGI_FORMAT Format; // 一个用于指定元素格式的DXGI_FORMAT枚举类型成员 UINT InputSlot; // 指定当前元素来自于哪个输入槽(input slot)。 // Direct3D支持16个输入槽(索引依次为 0到15),通过这些输入槽我们可以向着色器传入顶点数据。 // 如:当一个顶点由位置和颜色元素组成,既可以使用一个输入槽传送两种元素,也可以将两种元素分开。 // Direct3D可以将来自于不同输入槽的元素重新组合为顶点。 UINT AlignedByteOffset // 对于单个输入槽来说,该参数表示从顶点结构体的起始位置到顶点元素的起始位置之间的字节偏移量。 D3D11_INPUT_CLASSIFICATION InputSlotClass; // 目前指定为D3D11_INPUT_PER_VERTEX_DATA;其他选项用于高级实例技术。 UINT InstanceDataStepRate; // 目前指定为0;其他值只用于高级实例技术。} D3D11_INPUT_ELEMENT_DESC;

三、结语

在前面的几篇文章中,我们详细介绍了Direct3D渲染所需要的数学基础和渲染管道理论知识。从这篇文章开始,我们就正式开始Direct3D的绘制学习过程了。这篇文章中,主要讲解Direct3D的绘制基础过程,介绍配置渲染管道,定义顶点和像素着色器以及将几何图形提交到渲染管道进行绘制所需的Direct3DAPI接口和方法。

上面的代码中定义了一个称为cbPerObject的cbuffer对象(constant
buffer,常量缓冲)。常量缓冲只是一个用于存储各种变量的数据块,这些变量可以由着色器来访问。顶点着色器不能修改常量缓冲中的数据,但是C++程序可以通过effect框架在运行时修改常量缓冲中的内容。它为C++应用程序代码和effect代码提供了一种有效的通信方式。例如,因为每个物体的世界矩阵各不相同,所以每个物体的“WVP”组合矩阵也各不相同;所以,当使用上述顶点着色器绘制多个物体时,我们必须在绘制每个物体前修改gWorldViewProj变量。

一个Effect至少包含一个顶点着色器、一个像素着色器及所需要的全局变量。此外,还包含technique11和pass,我们这里简单介绍一下,下篇文章将详细介绍。

float4 PS(VertexOut pin) : SV_Target{ return pin.Color;}

相关文章

Comment ()
评论是一种美德,说点什么吧,否则我会恨你的。。。