DirectX11 With Windows SDK--13 抛弃FX11并初步实现BasicManager类
前言
DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html
到现在为止,该项目都没有使用Effects11框架类来绘制场景。因为在D3DCompile API (#47)版本中,如果你尝试编译fx_5_0的效果文件,会收到这样的警告:X4717: Effects deprecated for D3DCompiler_47
在未来的版本中,D3DCompiler可能会停止对FX11的支持,所以我们需要自行去管理各种特效,并改用HLSL编译器去编译每一个着色器。可以参考本系列前面的教程:
教程 |
---|
01 DirectX11初始化 |
02 渲染一个三角形 |
03 渲染一个立方体 |
这篇教程还会提到用深度/模板状态去实现简单的阴影效果,但不会深入数学公式原理。
项目源码点此:https://github.com/MKXJun/DX11-Without-DirectX-SDK
回顾RenderStates类
目前的RenderStates
类存放有比较常用的各种状态,原来在Effects11框架下是可以在fx文件初始化各种渲染状态,并设置到Technique11中。但现在我们只能在C++代码层中一次性创建好各种所需的渲染状态:
class RenderStates
{
public:
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
static void InitAll(const ComPtr<ID3D11Device>& device);
// 使用ComPtr无需手工释放
public:
static ComPtr<ID3D11RasterizerState> RSWireframe; // 光栅化器状态:线框模式
static ComPtr<ID3D11RasterizerState> RSNoCull; // 光栅化器状态:无背面裁剪模式
static ComPtr<ID3D11RasterizerState> RSCullClockWise; // 光栅化器状态:顺时针裁剪模式
static ComPtr<ID3D11SamplerState> SSLinearWrap; // 采样器状态:线性过滤
static ComPtr<ID3D11SamplerState> SSAnistropicWrap; // 采样器状态:各项异性过滤
static ComPtr<ID3D11BlendState> BSNoColorWrite; // 混合状态:不写入颜色
static ComPtr<ID3D11BlendState> BSTransparent; // 混合状态:透明混合
static ComPtr<ID3D11BlendState> BSAlphaToCoverage; // 混合状态:Alpha-To-Coverage
static ComPtr<ID3D11DepthStencilState> DSSWriteStencil; // 深度/模板状态:写入模板值
static ComPtr<ID3D11DepthStencilState> DSSDrawWithStencil; // 深度/模板状态:对指定模板值的区域进行绘制
static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend; // 深度/模板状态:无二次混合区域
static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest; // 深度/模板状态:关闭深度测试
static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite; // 深度/模板状态:仅深度测试,不写入深度值
};
具体的设置可以参照源码或者上一章内容。
BasicManager类
现在,为了减轻GameApp
类的负担,并且为了能够实现部分类似Effects11的功能,需要根据HLSL的代码去实现一个对应的简易BasicManager
类。
在BasicManager.h
中还包含了对应HLSL常量缓冲区的结构体:
#ifndef BASICMANAGER_H
#define BASICMANAGER_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 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 <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();
// 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<ID3D11VertexShader> mVertexShader3D; // 用于3D的顶点着色器
ComPtr<ID3D11PixelShader> mPixelShader3D; // 用于3D的像素着色器
ComPtr<ID3D11VertexShader> mVertexShader2D; // 用于2D的顶点着色器
ComPtr<ID3D11PixelShader> mPixelShader2D; // 用于2D的像素着色器
ComPtr<ID3D11InputLayout> mVertexLayout2D; // 用于2D的顶点输入布局
ComPtr<ID3D11InputLayout> mVertexLayout3D; // 用于3D的顶点输入布局
ComPtr<ID3D11DeviceContext> md3dImmediateContext; // 设备上下文
std::vector<ComPtr<ID3D11Buffer>> 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(), 1);
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(), 1);
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<class T>
void BasicManager::UpdateConstantBuffer(const T& cbuffer)
{
}
template<>
void BasicManager::UpdateConstantBuffer<CBChangesEveryDrawing>(const CBChangesEveryDrawing& cbuffer)
{
md3dImmediateContext->UpdateSubresource(mConstantBuffers[0].Get(), 0, nullptr, &cbuffer, 0, 0);
}
template<>
void BasicManager::UpdateConstantBuffer<CBDrawingState>(const CBDrawingState& cbuffer)
{
md3dImmediateContext->UpdateSubresource(mConstantBuffers[1].Get(), 0, nullptr, &cbuffer, 0, 0);
}
template<>
void BasicManager::UpdateConstantBuffer<CBChangesEveryFrame>(const CBChangesEveryFrame& cbuffer)
{
md3dImmediateContext->UpdateSubresource(mConstantBuffers[2].Get(), 0, nullptr, &cbuffer, 0, 0);
}
template<>
void BasicManager::UpdateConstantBuffer<CBChangesOnResize>(const CBChangesOnResize& cbuffer)
{
md3dImmediateContext->UpdateSubresource(mConstantBuffers[3].Get(), 0, nullptr, &cbuffer, 0, 0);
}
template<>
void BasicManager::UpdateConstantBuffer<CBNeverChange>(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的区域表示为镜面反射区;
- 使用
RenderStates::DSSNoDoubleBlend
的深度模板状态,当给定的模板值和深度/模板缓冲区的模板值一致时,通过模板测试并对模板值加1,绘制该像素的混合,然后下一次由于给定的模板值比深度/模板缓冲区的模板值小1,不会再通过模板测试,也就阻挡了后续像素的绘制; - 应当先绘制镜面的阴影区域,再绘制正常的阴影区域。
场景绘制
现在场景只有墙体、地板、木箱和镜面。
第1步: 镜面区域写入模板缓冲区
// *********************
// 1. 给镜面反射区域写入值1到模板缓冲区
//
mBasicManager.SetWriteStencilOnly(1);
mMirror.Draw(md3dImmediateContext);
第2步,绘制不透明的反射物体和阴影
// ***********************
// 2. 绘制不透明的反射物体
//
// 开启反射绘制
mDrawingState.isReflection = 1; // 反射开启
mBasicManager.UpdateConstantBuffer(mDrawingState);
mBasicManager.SetRenderDefaultWithStencil(1);
mWalls[2].Draw(md3dImmediateContext);
mWalls[3].Draw(md3dImmediateContext);
mWalls[4].Draw(md3dImmediateContext);
mFloor.Draw(md3dImmediateContext);
mWoodCrate.Draw(md3dImmediateContext);
// 绘制阴影
mWoodCrate.SetMaterial(mShadowMat);
mDrawingState.isShadow = 1; // 反射开启,阴影开启
mBasicManager.UpdateConstantBuffer(mDrawingState);
mBasicManager.SetRenderNoDoubleBlend(1);
mWoodCrate.Draw(md3dImmediateContext);
// 恢复到原来的状态
mDrawingState.isShadow = 0;
mBasicManager.UpdateConstantBuffer(mDrawingState);
mWoodCrate.SetMaterial(mWoodCrateMat);
第3步,绘制透明镜面
// ***********************
// 3. 绘制透明镜面
//
mBasicManager.SetRenderAlphaBlendWithStencil(1);
mMirror.Draw(md3dImmediateContext);
// 关闭反射绘制
mDrawingState.isReflection = 0;
mBasicManager.UpdateConstantBuffer(mDrawingState);
第4步,绘制不透明的正常物体和阴影
// ************************
// 4. 绘制不透明的正常物体
//
mBasicManager.SetRenderDefault();
for (auto& wall : mWalls)
wall.Draw(md3dImmediateContext);
mFloor.Draw(md3dImmediateContext);
mWoodCrate.Draw(md3dImmediateContext);
// 绘制阴影
mWoodCrate.SetMaterial(mShadowMat);
mDrawingState.isShadow = 1; // 反射关闭,阴影开启
mBasicManager.UpdateConstantBuffer(mDrawingState);
mBasicManager.SetRenderNoDoubleBlend(0);
mWoodCrate.Draw(md3dImmediateContext);
mDrawingState.isShadow = 0; // 反射关闭
mBasicManager.UpdateConstantBuffer(mDrawingState);
mWoodCrate.SetMaterial(mWoodCrateMat);
绘制效果如下:
注意该样例只生成点光灯到地板的阴影。你可以用各种摄像机模式来进行测试。