从这一部分开始,感受就像是踏入了无人深空同样,在以前初学DX11的时候,这部份内容都是基本上跳过的,如今打算从新认真地把它给拾回来。html
DirectX11 With Windows SDK完整目录git
Github项目源码github
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。编程
首先用一张图来回顾一下渲染管线的各个阶段,目前为止咱们接触的着色器有顶点着色器和像素着色器,而接触到的渲染管线阶段有:输入装配阶段、顶点着色阶段、光栅化阶段、像素着色阶段、输出合并阶段。数组
能够看到,几何着色器是咱们在将顶点送入光栅化阶段以前,能够操做顶点的最后一个阶段。它一样也容许咱们编写本身的着色器代码。几何着色器能够作以下事情:app
但它也有缺点,几何着色器输出的顶点数据极可能是有较多重复的,从流输出拿回到顶点缓冲区的话会占用较多的内存空间。它自己没法输出索引数组。ide
几何着色阶段会收到一系列表明输入几何体类型的顶点,而后咱们能够自由选择其中的这些顶点信息,而后交给流输出对象从新解释成新的图元类型(或者不变),传递给流输出阶段或者是光栅化阶段。而几何着色器仅可以接受来自输入装配阶段提供的顶点信息,对每一个顶点进行处理,没法自行决定增减顶点。函数
注意:离开几何着色器的顶点若是要传递给光栅化阶段,须要包含有转换到齐次裁剪坐标系的坐标信息(语义为SV_POSITION
的float4
向量)性能
若咱们直接从VS项目新建一个几何着色器文件,则能够看到下面的代码:字体
struct GSOutput { float4 pos : SV_POSITION; }; [maxvertexcount(3)] void main( triangle float4 input[3] : SV_POSITION, inout TriangleStream< GSOutput > output ) { for (uint i = 0; i < 3; i++) { GSOutput element; element.pos = input[i]; output.Append(element); } }
ps. 可能有些人会对void main的写法表示不爽,好比说我。不过这不是C语言的主函数......
若在输入装配阶段指定使用TriangleList图元的话,初步观察该代码,实际上你能够发现其实该着色器只是把输入的顶点按原样输出给流输出对象,即跟什么都没作(咸鱼)有什么区别。。不过从这份代码里面就已经包含了几何着色器所特有的绝大部分语法了。
首先,几何着色器是根据图元类型来进行调用的,若使用的是TriangleList,则每个三角形的三个顶点都会做为输入,触发几何着色器的调用。这样一个TriangleList解释的30个顶点会触发10次调用。
对于几何着色器,咱们必需要指定它每次调用所容许输出的最大顶点数目。咱们可使用属性语法来强行修改着色器行为:
[maxvertexcount(N)]
这里N
就是每次调用容许产出的最大顶点数目,而后最终输出的顶点数目不会超过N
的值。maxvertexcount
的值应当尽量的小。
关于性能上的表现,我根据龙书提供的引用找到了对应的说明文档:
虽然是10年前的文档,这里说到:在GeForce 8800 GTX,一个几何着色器的调用在输出1到20个标量的时候能够达到最大运行性能表现,可是当咱们指定最大容许输出标量的数目在27到40个时,性能仅达到峰值的50%。好比说,若是顶点的声明以下:
struct V0 { float3 pos : POSITION; float2 tex : TEXCOORD; };
这里每一个顶点就已经包含了5个标量了,若是以它做为输出类型,则maxvertexcount
为4的时候就能够达到理论上的峰值性能(20个标量)。
但若是顶点类型中还包含有float3
类型的法向量,每一个顶点就额外包含了3个标量,这样在maxvertexcount
为4的时候就输出了32个标量,只有50%的峰值性能表现。
这份文档已经将近10年了,对于那时候的显卡来讲使用几何着色器可能不是一个很好的选择,不过当初的显卡也早已不能和如今的显卡相提并论了。
注意:
maxvertexcount
的值应当设置到尽量小的值,由于它将直接决定几何着色器的运行效率。- 几何着色器的每次调用最多只能处理1024个标量,对于只包含4D位置向量的顶点来讲也只能处理256个顶点。
- 几何着色器输入的结构体类型不容许超过128个标量,对于只包含4D位置向量的顶点来讲也只能包含32个顶点。
在HLSL编译器里,若是设置的maxvertexcount
过大,会直接收到编译错误:
而后代码中的triangle
是用于指定输入的图元类型,具体支持的关键字以下:
图元类型 | 描述 |
---|---|
point | Point list |
line | Line list or line strip |
triangle | Triangle list or triangle strip |
lineadj | Line list with adjacency or line strip with adjacency |
triangleadj | Triangle list with adjacency or triangle strip with adjacency |
具体的图元类型能够到第2章回顾:点击此处
而参数类型能够是用户自定义的结构体类型,或者是向量(float4)类型。从顶点着色器传过来的顶点至少会包含一个表示齐次裁剪坐标的向量。
参数名inupt
实际上用户是能够任意指定的。
对于该输入参数的元素数目,取决于前面声明的图元类型:
图元类型 | 元素数目 |
---|---|
point | [1] 每次只能处理1个顶点 |
line | [2] 一个线段必须包含2个顶点 |
triangle | [3] 一个三角形须要3个顶点 |
lineadj | [4] 一个邻接线段须要4个顶点 |
triangleadj | [6] 一个邻接三角形须要6个顶点 |
而第二个参数必须是一个流输出对象,并且须要被指定为inout
可读写类型。能够看到,它是一个类模板,模板的形参指定要输出的类型。流输出对象有以下三种:
流输出对象类型 | 描述 |
---|---|
PointStream | 一系列点的图元 |
LineStream | 一系列线段的图元 |
TriangleStream | 一系列三角形的图元 |
流输出对象都具备下面两种方法:
方法 | 描述 |
---|---|
Append | 向指定的流输出对象添加一个输出的数据 |
RestartStrip | 在以线段或者三角形做为图元的时候,默认是以strip的形式输出的, 若是咱们不但愿下一个输出的顶点与以前的顶点构成新图元,则须要 调用此方法来从新开始新的strip。若但愿输出的图元类型也保持和原 来同样的TriangleList,则须要每调用3次Append方法后就调用一次 RestartStrip。 |
注意:
- 所谓的删除顶点,实际上就是不将该顶点传递给流输出对象
- 若传入的顶点中多余的部分没法构成对应的图元,则抛弃掉这些多余的顶点
在开始前,先放出Basic.hlsli
文件的内容:
#include "LightHelper.hlsli" cbuffer CBChangesEveryFrame : register(b0) { matrix g_World; matrix g_WorldInvTranspose; } cbuffer CBChangesOnResize : register(b1) { matrix g_Proj; } cbuffer CBChangesRarely : register(b2) { DirectionalLight g_DirLight[5]; PointLight g_PointLight[5]; SpotLight g_SpotLight[5]; Material g_Material; matrix g_View; float3 g_EyePosW; float g_CylinderHeight; } struct VertexPosColor { float3 PosL : POSITION; float4 Color : COLOR; }; struct VertexPosHColor { float4 PosH : SV_POSITION; float4 Color : COLOR; };
如今咱们的目标是把一个三角形分裂成三个三角形:
这也为之后实现分形作为基础。建议读者能够先自行尝试编写着色器代码再来对比。在编写好着色器代码后,
要给渲染管线绑定好一切所需的资源才可以看到效果。
Triangle_VS.hlsl
, Triangle_GS.hlsl
和Triangle_PS.hlsl
的实现以下:
// Triangle_VS.hlsl #include "Basic.hlsli" VertexPosHColor VS(VertexPosColor vIn) { matrix worldViewProj = mul(mul(g_World, g_View), g_Proj); VertexPosHColor vOut; vOut.Color = vIn.Color; vOut.PosH = mul(float4(vIn.PosL, 1.0f), worldViewProj); return vOut; }
// Triangle_GS.hlsl #include "Basic.hlsli" [maxvertexcount(9)] void GS(triangle VertexPosHColor input[3], inout TriangleStream<VertexPosHColor> output) { // // 将一个三角形分裂成三个三角形,即没有v3v4v5的三角形 // v1 // /\ // / \ // v3/____\v4 // /\xxxx/\ // / \xx/ \ // /____\/____\ // v0 v5 v2 VertexPosHColor vertexes[6]; int i; [unroll] for (i = 0; i < 3; ++i) { vertexes[i] = input[i]; vertexes[i + 3].Color = (input[i].Color + input[(i + 1) % 3].Color) / 2.0f; vertexes[i + 3].PosH = (input[i].PosH + input[(i + 1) % 3].PosH) / 2.0f; } [unroll] for (i = 0; i < 3; ++i) { output.Append(vertexes[i]); output.Append(vertexes[3 + i]); output.Append(vertexes[(i + 2) % 3 + 3]); output.RestartStrip(); } }
// Triangle_PS.hlsl #include "Basic.hlsli" float4 PS(VertexPosHColor pIn) : SV_Target { return pIn.Color; }
这里输入和输出的图元类型都是一致的,但不管什么状况都必定要注意设置好maxvertexcount
的值,这里固定一个三角形的三个顶点输出9个顶点(构成三个三角形),而且每3次Append
就须要调用1次RestartStrip
。
已知图元类型为LineStrip,如今有一系列连续的顶点构成圆线(近似圆弧的连续折线),构造出圆柱体的侧面。即输入图元类型为线段,输出一个矩形(两个三角形)。
思路: 光有顶点位置还不足以构造出圆柱体侧面,由于没法肯定圆柱往哪一个方向延伸。因此咱们还须要对每一个顶点引入所在圆柱侧面的法向量,经过叉乘就能够肯定上方向/下方向并进行延伸了。
Cylinder_VS.hlsl
, Cylinder_GS.hlsl
和Cylinder_PS.hlsl
的实现以下:
// Cylinder_VS.hlsl #include "Basic.hlsli" VertexPosHWNormalColor VS(VertexPosNormalColor vIn) { VertexPosHWNormalColor vOut; matrix viewProj = mul(g_View, g_Proj); float4 posW = mul(float4(vIn.PosL, 1.0f), g_World); vOut.PosH = mul(posW, viewProj); vOut.PosW = posW.xyz; vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose); vOut.Color = vIn.Color; return vOut; }
// Cylinder_GS.hlsl #include "Basic.hlsli" // 一个v0v1线段输出6个三角形顶点 [maxvertexcount(6)] void GS(line VertexPosHWNormalColor input[2], inout TriangleStream<VertexPosHWNormalColor> output) { // ***************************** // 要求圆线是顺时针的,而后自底向上构造圆柱侧面 // --> v2____v3 // ______ |\ | // / \ | \ | // \______/ | \ | // <-- |___\| // v1(i1) v0(i0) float3 upDir = normalize(cross(input[0].NormalW, (input[1].PosW - input[0].PosW))); VertexPosHWNormalColor v2, v3; matrix viewProj = mul(g_View, g_Proj); v2.PosW = input[1].PosW + upDir * g_CylinderHeight; v2.PosH = mul(float4(v2.PosW, 1.0f), viewProj); v2.NormalW = input[1].NormalW; v2.Color = input[1].Color; v3.PosW = input[0].PosW + upDir * g_CylinderHeight; v3.PosH = mul(float4(v3.PosW, 1.0f), viewProj); v3.NormalW = input[0].NormalW; v3.Color = input[0].Color; output.Append(input[0]); output.Append(input[1]); output.Append(v2); output.RestartStrip(); output.Append(v2); output.Append(v3); output.Append(input[0]); }
// Cylinder_PS.hlsl #include "Basic.hlsli" float4 PS(VertexPosHWNormalColor pIn) : SV_Target { // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 顶点指向眼睛的向量 float3 toEyeW = normalize(g_EyePosW - 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); // 只计算方向光 ComputeDirectionalLight(g_Material, g_DirLight[0], pIn.NormalW, toEyeW, ambient, diffuse, spec); return pIn.Color * (ambient + diffuse) + spec; }
画出顶点的法向量能够帮助你进行调试,排查法向量是否出现了问题。这时候图元的类型为PointList,须要经过几何着色器输出一个线段(两个顶点)。因为顶点中包含法向量,剩下的就是要自行决定法向量的长度。
下图的法向量长度为0.5
Normal_VS.hlsl
, Normal_GS.hlsl
和Normal_PS.hlsl
的实现以下:
// Normal_VS.hlsl #include "Basic.hlsli" VertexPosHWNormalColor VS(VertexPosNormalColor vIn) { VertexPosHWNormalColor vOut; matrix viewProj = mul(g_View, g_Proj); float4 posW = mul(float4(vIn.PosL, 1.0f), g_World); vOut.PosH = mul(posW, viewProj); vOut.PosW = posW.xyz; vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose); vOut.Color = vIn.Color; return vOut; }
// Normal_GS.hlsl #include "Basic.hlsli" [maxvertexcount(2)] void GS(point VertexPosHWNormalColor input[1], inout LineStream<VertexPosHWNormalColor> output) { matrix viewProj = mul(g_View, g_Proj); VertexPosHWNormalColor v; // 防止资源争夺 v.PosW = input[0].PosW + input[0].NormalW * 0.01f; v.NormalW = input[0].NormalW; v.PosH = mul(float4(v.PosW, 1.0f), viewProj); v.Color = input[0].Color; output.Append(v); v.PosW = v.PosW + input[0].NormalW * 0.5f; v.PosH = mul(float4(v.PosW, 1.0f), viewProj); output.Append(v); }
// Normal_PS.hlsl #include "Basic.hlsli" float4 PS(VertexPosHWNormalColor pIn) : SV_TARGET { return pIn.Color; }
变化以下:
class BasicEffect : public IEffect { public: BasicEffect(); virtual ~BasicEffect() override; BasicEffect(BasicEffect&& moveFrom) noexcept; BasicEffect& operator=(BasicEffect&& moveFrom) noexcept; // 获取单例 static BasicEffect& Get(); // 初始化Basic.hlsli所需资源并初始化渲染状态 bool InitAll(ID3D11Device * device); // // 渲染模式的变动 // // 绘制三角形分裂 void SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext); // 绘制无上下盖的圆柱体 void SetRenderCylinderNoCap(ID3D11DeviceContext * deviceContext); // 绘制全部顶点的法向量 void SetRenderNormal(ID3D11DeviceContext * deviceContext); // // 矩阵设置 // void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W); void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V); void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P); // // 光照、材质和纹理相关设置 // // 各类类型灯光容许的最大数目 static const int maxLights = 5; void SetDirLight(size_t pos, const DirectionalLight& dirLight); void SetPointLight(size_t pos, const PointLight& pointLight); void SetSpotLight(size_t pos, const SpotLight& spotLight); void SetMaterial(const Material& material); void XM_CALLCONV SetEyePos(DirectX::FXMVECTOR eyePos); // 设置圆柱体侧面高度 void SetCylinderHeight(float height); // 应用常量缓冲区和纹理资源的变动 void Apply(ID3D11DeviceContext * deviceContext); private: class Impl; std::unique_ptr<Impl> pImpl; };
该方法处理的是图元TriangleList。由于后续的方法处理的图元不一样,在调用开始就得设置回正确的图元。也请确保输入装配阶段提供好须要分裂的三角形顶点。
void BasicEffect::SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext) { deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get()); deviceContext->VSSetShader(pImpl->m_pTriangleVS.Get(), nullptr, 0); deviceContext->GSSetShader(pImpl->m_pTriangleGS.Get(), nullptr, 0); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(pImpl->m_pTrianglePS.Get(), nullptr, 0); }
该方法处理的是图元LineStrip,确保输入的一系列顶点和法向量可以在同一平面上。若提供的顶点集合按顺时针排布,则会自底向上构建出圆柱体,反之则是自顶向下构建。
这里须要关闭背面裁剪,由于咱们也能够看到圆柱侧面的内部(没有盖子)。
void BasicEffect::SetRenderCylinderNoCap(ID3D11DeviceContext * deviceContext) { deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP); deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get()); deviceContext->VSSetShader(pImpl->m_pCylinderVS.Get(), nullptr, 0); deviceContext->GSSetShader(pImpl->m_pCylinderGS.Get(), nullptr, 0); deviceContext->RSSetState(RenderStates::RSNoCull.Get()); deviceContext->PSSetShader(pImpl->m_pCylinderPS.Get(), nullptr, 0); }
该方法处理的图元是PointList,确保输入的顶点要包含法向量。
void BasicEffect::SetRenderNormal(ID3D11DeviceContext * deviceContext) { deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get()); deviceContext->VSSetShader(pImpl->m_pNormalVS.Get(), nullptr, 0); deviceContext->GSSetShader(pImpl->m_pNormalGS.Get(), nullptr, 0); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(pImpl->m_pNormalPS.Get(), nullptr, 0); }
该项目包含上面三种实战内容,须要用户去指定当前播放的模式。
首先声明部分变化以下:
class GameApp : public D3DApp { public: enum class Mode { SplitedTriangle, CylinderNoCap, CylinderNoCapWithNormal }; public: GameApp(HINSTANCE hInstance); ~GameApp(); bool Init(); void OnResize(); void UpdateScene(float dt); void DrawScene(); private: bool InitResource(); void ResetTriangle(); void ResetRoundWire(); private: ComPtr<ID2D1SolidColorBrush> m_pColorBrush; // 单色笔刷 ComPtr<IDWriteFont> m_pFont; // 字体 ComPtr<IDWriteTextFormat> m_pTextFormat; // 文本格式 ComPtr<ID3D11Buffer> m_pVertexBuffer; // 顶点集合 int m_VertexCount; // 顶点数目 Mode m_ShowMode; // 当前显示模式 BasicEffect m_BasicEffect; // 对象渲染特效管理 };
void GameApp::ResetTriangle() { // ****************** // 初始化三角形 // // 设置三角形顶点 VertexPosColor vertices[] = { { XMFLOAT3(-1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }, { XMFLOAT3(0.0f * 3, 0.866f * 3, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }, { XMFLOAT3(1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) } }; // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices; HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.ReleaseAndGetAddressOf())); // 三角形顶点数 m_VertexCount = 3; }
void GameApp::ResetRoundWire() { // ****************** // 初始化圆线 // 设置圆边上各顶点 // 必需要按顺时针设置 // 因为要造成闭环,起始点须要使用2次 // ______ // / \ // \______/ // VertexPosNormalColor vertices[41]; for (int i = 0; i < 40; ++i) { vertices[i].pos = XMFLOAT3(cosf(XM_PI / 20 * i), -1.0f, -sinf(XM_PI / 20 * i)); vertices[i].normal = XMFLOAT3(cosf(XM_PI / 20 * i), 0.0f, -sinf(XM_PI / 20 * i)); vertices[i].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f); } vertices[40] = vertices[0]; // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices; HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.ReleaseAndGetAddressOf())); // 线框顶点数 m_VertexCount = 41; }
GameApp
类剩余部分能够在项目源码中查看。
最终效果以下:
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。