DirectX11 With Windows SDK--13 抛弃FX11并初步实现BasicManager类
阅读目录
前言
回顾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