阅读目录 前言 回顾RenderStates类 BasicManager类 默认状态绘制 Alpha透明混合绘制 无重复混合(单次混合) 仅写入模板值 对指定模板值区域进行常规绘制 对指定模板值区域进行Alpha透明混合绘制 2D默认绘制 2D透明混合绘制 更新常量缓冲区 防止GameApp::OnResize在BasicManager未初始化时修改投影矩阵 绘制平面阴影 使用模板缓冲区防止过度混合 着色器代码的变化 场景绘制 第1步: 镜面区域写入模板缓冲区 第2步: 绘制不透明的反射物体和阴影 第3步: 绘制透明镜面 第4步: 绘制不透明的正常物体和阴影 回到顶部 前言 到现在为止,所有的教程项目都没有使用Effects11框架类来绘制场景。因为在D3DCompile API (#47)版本中,如果你尝试编译fx_5_0的效果文件,会收到这样的警告: X4717: Effects deprecated for D3DCompiler_47 在未来的版本中,D3DCompiler可能会停止对FX11的支持,所以我们需要自行去管理各种特效,并改用HLSL编译器去编译每一个着色器。可以参考本系列前面的教程: 教程 01 DirectX11初始化 02 渲染一个三角形 03 渲染一个立方体 这篇教程还会提到用深度/模板状态去实现简单的阴影效果,但不会深入数学公式原理。 DirectX11 With Windows SDK完整目录 Github项目源码 回到顶部 回顾RenderStates类 目前的RenderStates类存放有比较常用的各种状态,原来在Effects11框架下是可以在fx文件初始化各种渲染状态,并设置到Technique11中。但现在我们只能在C++代码层中一次性创建好各种所需的渲染状态: class RenderStates { public: template using ComPtr = Microsoft::WRL::ComPtr; static void InitAll(const ComPtr& device); // 使用ComPtr无需手工释放 public: static ComPtr RSWireframe; // 光栅化器状态:线框模式 static ComPtr RSNoCull; // 光栅化器状态:无背面裁剪模式 static ComPtr RSCullClockWise; // 光栅化器状态:顺时针裁剪模式 static ComPtr SSLinearWrap; // 采样器状态:线性过滤 static ComPtr SSAnistropicWrap; // 采样器状态:各项异性过滤 static ComPtr BSNoColorWrite; // 混合状态:不写入颜色 static ComPtr BSTransparent; // 混合状态:透明混合 static ComPtr BSAlphaToCoverage; // 混合状态:Alpha-To-Coverage static ComPtr DSSWriteStencil; // 深度/模板状态:写入模板值 static ComPtr DSSDrawWithStencil; // 深度/模板状态:对指定模板值的区域进行绘制 static ComPtr DSSNoDoubleBlend; // 深度/模板状态:无二次混合区域 static ComPtr DSSNoDepthTest; // 深度/模板状态:关闭深度测试 static ComPtr DSSNoDepthWrite; // 深度/模板状态:仅深度测试,不写入深度值 }; 具体的设置可以参照源码或者上一章内容。 回到顶部 BasicManager类 现在,为了减轻GameApp类的负担,并且为了能够实现部分类似Effects11的功能,需要根据HLSL的代码去实现一个对应的简易BasicManager类。 在BasicManager.h中还包含了对应HLSL常量缓冲区的结构体: #ifndef BASICMANAGER_H #define BASICMANAGER_H #include #include #include #include #include #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 CBDrawingState { int isReflection; int isShadow; DirectX::XMINT2 pad; }; struct CBChangesOnResize { DirectX::XMMATRIX proj; }; struct CBNeverChange { DirectX::XMMATRIX reflection; DirectX::XMMATRIX shadow; DirectX::XMMATRIX refShadow; DirectionalLight dirLight[10]; PointLight pointLight[10]; SpotLight spotLight[10]; int numDirLight; int numPointLight; int numSpotLight; float pad; // 打包保证16字节对齐 }; // 暂时省略BasicManager类 // ... #endif 现在HLSL中的Basic.fx如下: #include "LightHelper.hlsli" Texture2D tex : register(t0); SamplerState sam : register(s0); cbuffer CBChangesEveryDrawing : register(b0) { row_major matrix gWorld; row_major matrix gWorldInvTranspose; row_major matrix gTexTransform; Material gMaterial; } cbuffer CBDrawingState : register(b1) { int gIsReflection; int gIsShadow; } cbuffer CBChangesEveryFrame : register(b2) { row_major matrix gView; float3 gEyePosW; } cbuffer CBChangesOnResize : register(b3) { row_major matrix gProj; } cbuffer CBNeverChange : register(b4) { row_major matrix gReflection; row_major matrix gShadow; row_major matrix gRefShadow; DirectionalLight gDirLight[10]; PointLight gPointLight[10]; SpotLight gSpotLight[10]; int gNumDirLight; int gNumPointLight; int gNumSpotLight; float gPad; } struct Vertex3DIn { float3 Pos : POSITION; float3 Normal : NORMAL; float2 Tex : TEXCOORD; }; struct Vertex3DOut { float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD; }; struct Vertex2DIn { float3 Pos : POSITION; float2 Tex : TEXCOORD; }; struct Vertex2DOut { float4 PosH : SV_POSITION; float2 Tex : TEXCOORD; }; 各着色器的HLSL代码都分别放入独立的文件内,后面会详细讲述。 一个简易的BasicManager具有如下功能: 修改常量缓冲区的变量(整块更新) 类似Effects11那样,可以自己设置单通道下的各种状态、着色器 但是该管理类并不用于绘制,具体的绘制函数交给了简易GameObject类来操作。所以每次绘制物体时,根据情况可能需要调用BasicManager的设置方法来指定当前要以怎样的形式来渲染,然后才是调用GameObject::Draw方法绘制。 因为该类没有反射功能,所以用户需要决定什么时候才去更新常量缓冲区资源。 class BasicManager { public: // 使用模板别名(C++11)简化类型名 template using ComPtr = Microsoft::WRL::ComPtr; // 初始化Basix.fx所需资源并初始化光栅化状态 bool InitAll(ComPtr device); // 是否已经初始化 bool IsInit() const; template void UpdateConstantBuffer(const T& cbuffer); // 默认状态来绘制 void SetRenderDefault(); // Alpha混合绘制 void SetRenderAlphaBlend(); // 无二次混合 void SetRenderNoDoubleBlend(UINT stencilRef); // 仅写入模板值 void SetWriteStencilOnly(UINT stencilRef); // 对指定模板值的区域进行绘制,采用默认状态 void SetRenderDefaultWithStencil(UINT stencilRef); // 对指定模板值的区域进行绘制,采用Alpha混合 void SetRenderAlphaBlendWithStencil(UINT stencilRef); // 2D默认状态绘制 void Set2DRenderDefault(); // 2D混合绘制 void Set2DRenderAlphaBlend(); private: // 从.fx/.hlsl文件中编译着色器 HRESULT CompileShaderFromFile(const WCHAR* szFileName, LPCSTR szEntryPoint, LPCSTR szShaderModel, ID3DBlob** ppBlobOut); private: ComPtr mVertexShader3D; // 用于3D的顶点着色器 ComPtr mPixelShader3D; // 用于3D的像素着色器 ComPtr mVertexShader2D; // 用于2D的顶点着色器 ComPtr mPixelShader2D; // 用于2D的像素着色器 ComPtr mVertexLayout2D; // 用于2D的顶点输入布局 ComPtr mVertexLayout3D; // 用于3D的顶点输入布局 ComPtr md3dImmediateContext; // 设备上下文 std::vector> mConstantBuffers; // 常量缓冲区 }; 默认状态绘制 该绘制模式和后面的所有绘制模式都使用的是线性Wrap采样器。 BasicManager::SetRenderDefault方法使用了默认的3D像素着色器和顶点着色器,并且其余各状态都保留使用默认状态: void BasicManager::SetRenderDefault() { md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(nullptr); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(nullptr, 0); md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); } Alpha透明混合绘制 该绘制模式关闭了光栅化裁剪,并采用透明混合方式。 void BasicManager::SetRenderAlphaBlend() { md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get()); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(nullptr, 0); md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF); } 无重复混合(单次混合) 该绘制模式用于绘制阴影,防止过度混合。需要指定绘制区域的模板值。 void BasicManager::SetRenderNoDoubleBlend(UINT stencilRef) { md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get()); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSNoDoubleBlend.Get(), stencilRef); md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF); } 仅写入模板值 该模式用于向模板缓冲区写入用户指定的模板值,并且不写入到深度缓冲区和后备缓冲区。 void BasicManager::SetWriteStencilOnly(UINT stencilRef) { md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(nullptr); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), stencilRef); md3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF); } 对指定模板值区域进行常规绘制 该模式下,仅对模板缓冲区的模板值和用户指定的相等的区域进行常规绘制。 void BasicManager::SetRenderDefaultWithStencil(UINT stencilRef) { md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(RenderStates::RSCullClockWise.Get()); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef); md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); } 对指定模板值区域进行Alpha透明混合绘制 该模式下,仅对模板缓冲区的模板值和用户指定的相等的区域进行Alpha透明混合绘制。 void BasicManager::SetRenderAlphaBlendWithStencil(UINT stencilRef) { md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get()); md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get()); md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef); md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF); } 2D默认绘制 该模式使用的是2D顶点着色器和像素着色器,并修改为2D输入布局。 void BasicManager::Set2DRenderDefault() { md3dImmediateContext->IASetInputLayout(mVertexLayout2D.Get()); md3dImmediateContext->VSSetShader(mVertexShader2D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(nullptr); md3dImmediateContext->PSSetShader(mPixelShader2D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(nullptr, 0); md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); } 2D透明混合绘制 相比上面,多了透明混合状态。 void BasicManager::Set2DRenderAlphaBlend() { md3dImmediateContext->IASetInputLayout(mVertexLayout2D.Get()); md3dImmediateContext->VSSetShader(mVertexShader2D.Get(), nullptr, 0); md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get()); md3dImmediateContext->PSSetShader(mPixelShader2D.Get(), nullptr, 0); md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); md3dImmediateContext->OMSetDepthStencilState(nullptr, 0); md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF); } 更新常量缓冲区 这里使用的是成员模板函数,根据传入的结构体类型来更新常量缓冲区: template void BasicManager::UpdateConstantBuffer(const T& cbuffer) { } template<> void BasicManager::UpdateConstantBuffer(const CBChangesEveryDrawing& cbuffer) { md3dImmediateContext->UpdateSubresource(mConstantBuffers[0].Get(), 0, nullptr, &cbuffer, 0, 0); } template<> void BasicManager::UpdateConstantBuffer(const CBDrawingState& cbuffer) { md3dImmediateContext->UpdateSubresource(mConstantBuffers[1].Get(), 0, nullptr, &cbuffer, 0, 0); } template<> void BasicManager::UpdateConstantBuffer(const CBChangesEveryFrame& cbuffer) { md3dImmediateContext->UpdateSubresource(mConstantBuffers[2].Get(), 0, nullptr, &cbuffer, 0, 0); } template<> void BasicManager::UpdateConstantBuffer(const CBChangesOnResize& cbuffer) { md3dImmediateContext->UpdateSubresource(mConstantBuffers[3].Get(), 0, nullptr, &cbuffer, 0, 0); } template<> void BasicManager::UpdateConstantBuffer(const CBNeverChange& cbuffer) { md3dImmediateContext->UpdateSubresource(mConstantBuffers[4].Get(), 0, nullptr, &cbuffer, 0, 0); } 防止GameApp::OnResize在BasicManager未初始化时修改投影矩阵 BasicManager::IsInit方法判断摄像机是否经过了初始化。由于BasicManager的初始化在GameApp::Init之前,但却会调用GameApp::OnResize先更新其中的一个常量缓冲区,所以需要加上一重防护在未初始化的时候不应该操作,并在GameApp::InitResource方法初始化摄像机的投影矩阵。 当然,目前BasicManager能做的事情还是比较有限的,并且还需要随着HLSL代码的变动而随之调整。更多的功能会在后续教程中实现。 回到顶部 绘制平面阴影 使用XMMatrixShadow可以生成阴影矩阵,根据光照类型和位置对几何体投影到平面上的。 XMMATRIX XMMatrixShadow( FXMVECTOR ShadowPlane, // 平面向量(nx, ny, nz, d) FXMVECTOR LightPosition); // w = 0时表示平行光方向, w = 1时表示光源位置 通常指定的平面会稍微比实际平面高那么一点点,以避免深度缓冲区资源争夺导致阴影显示有问题。 使用模板缓冲区防止过度混合 一个物体投影到平面上时,投影区域的某些位置可能位于多个三角形之内,这会导致这些位置会有多个像素通过测试并进行混合操作,渲染的次数越多,显示的颜色会越黑。 我们可以使用模板缓冲区来解决这个问题。 在之前的例子中,我们用模板值为0的区域表示非镜面反射区,模板值为1的区域表示为镜面反射区; 使用RenderS