DirectX11 With Windows SDK--15 几何着色器初探

时间:2024-01-21 07:18:03

前言

从这一部分开始,感觉就像是踏入了无人深空一样,在之前初学DX11的时候,这部分内容都是基本上跳过的,现在打算重新认真地把它给拾回来。

DirectX11 With Windows SDK完整目录

Github项目源码

几何着色器

首先用一张图来回顾一下渲染管线的各个阶段,目前为止我们接触的着色器有顶点着色器和像素着色器,而渲染管线阶段有:输入装配阶段、顶点着色阶段、光栅化阶段、像素着色阶段、输出合并阶段。

可以看到,几何着色器是我们在将顶点送入光栅化阶段之前,可以操作顶点的最后一个阶段。它同样也允许我们编写自己的着色器代码。几何着色器可以做如下事情:

  1. 让程序自动决定如何在渲染管线中插入/移除几何体;
  2. 通过流输出阶段将顶点信息再次传递到顶点缓冲区;
  3. 改变图元类型(如输入点图元,输出三角形图元);

几何着色阶段会收到一系列代表输入几何体类型的顶点,然后我们可以*选择其中的这些顶点信息,然后交给流输出对象重新解释成新的原始类型(或者不变),传递给流输出阶段或者是光栅化阶段。而几何着色器仅能够接受来自输入装配阶段提供的顶点信息,对每个顶点进行处理,无法自行决定增减顶点。

注意:离开几何着色器的顶点如果要传递给光栅化阶段,需要包含有转换到齐次裁剪坐标系的坐标信息(语义为SV_POSITIONfloat4向量)

可编程的几何着色器

从一个看似没什么用的几何着色器代码入手

若我们直接从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的值应当尽可能的小。

关于性能上的表现,我根据龙书提供的引用找到了对应的说明文档:

NVIDIA08

虽然是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年了,对于那时候的显卡来说使用几何着色器可能不是一个很好的选择,不过当初的显卡也早已不能和现在的显卡相提并论了。

注意:

  1. maxvertexcount的值应当设置到尽可能小的值,因为它将直接决定几何着色器的运行效率。
  2. 几何着色器的每次调用最多只能处理1024个标量,对于只包含4D位置向量的顶点来说也只能处理256个顶点。

在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。

注意:

  1. 所谓的删除顶点,实际上就是不将该顶点传递给流输出对象
  2. 若传入的顶点中多余的部分无法构成对应的图元,则抛弃掉这些多余的顶点

在开始前,先放出Basic.fx文件的内容:

#include "LightHelper.hlsli"

cbuffer CBChangesEveryFrame : register(b0)
{
    row_major matrix gWorld;
    row_major matrix gWorldInvTranspose;
}

cbuffer CBChangesOnResize : register(b1)
{
    row_major matrix gProj;
}

cbuffer CBNeverChange : register(b2)
{
    DirectionalLight gDirLight;
    Material gMaterial;
    row_major matrix gView;
    float3 gEyePosW;
    float gCylinderHeight;
}


struct VertexPosColor
{
    float3 PosL : POSITION;
    float4 Color : COLOR;
};

struct VertexPosHColor
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};


struct VertexPosNormalColor
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float4 Color : COLOR;
};

struct VertexPosHWNormalColor
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION;
    float3 NormalW : NORMAL;
    float4 Color : COLOR;
};

实战1: 将一个三角形分割成三个三角形

现在我们的目标是把一个三角形分裂成三个三角形:

这也为以后实现分形做为基础。建议读者可以先自行尝试编写着色器代码再来对比。在编写好着色器代码后,
要给渲染管线绑定好一切所需的资源才能够看到效果。

HLSL代码

Triangle_VS.hlsl, Triangle_GS.hlslTriangle_PS.hlsl的实现如下:

// Triangle_VS.hlsl
#include "Basic.fx"

VertexPosHColor VS(VertexPosColor pIn)
{
    row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj);
    VertexPosHColor pOut;
    pOut.Color = pIn.Color;
    pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj);
    return pOut;
}
Triangle_GS.hlsl
#include "Basic.fx"

[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.fx"

float4 PS(VertexPosHColor pIn) : SV_Target
{
    return pIn.Color;
}

这里输入和输出的图元类型都是一致的,但无论什么情况都一定要注意设置好maxvertexcount的值,这里固定一个三角形的三个顶点输出9个顶点(构成三个三角形),并且每3次Append就需要调用1次RestartStrip

实战2: 通过圆线构造圆柱体侧面

已知图元类型为LineStrip,现在有一系列连续的顶点构成圆线(近似圆弧的连续折线),构造出圆柱体的侧面。即输入图元类型为线段,输出一个矩形(两个三角形)。

思路: 光有顶点位置还不足以构造出圆柱体侧面,因为无法确定圆柱往哪个方向延伸。所以我们还需要对每个顶点引入所在圆柱侧面的法向量,通过叉乘就可以确定上方向/下方向并进行延伸了。

HLSL代码

Cylinder_VS.hlsl, Cylinder_GS.hlslCylinder_PS.hlsl的实现如下:

// Cylinder_VS.hlsl
#include "Basic.fx"

VertexPosHWNormalColor VS(VertexPosNormalColor pIn)
{
    VertexPosHWNormalColor pOut;
    row_major matrix viewProj = mul(gView, gProj);
    pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
    pOut.PosH = mul(float4(pOut.PosW, 1.0f), viewProj);
    pOut.NormalW = mul(pIn.NormalL, (float3x3)gWorldInvTranspose);
    pOut.Color = pIn.Color;
    return pOut;
}

// Cylinder_GS.hlsl
#include "Basic.fx"

// 一个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(gView, gProj);


    v2.PosW = input[1].PosW + upDir * gCylinderHeight;
    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 * gCylinderHeight;
    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.fx"

float4 PS(VertexPosHWNormalColor pIn) : SV_Target
{
    // 标准化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 顶点指向眼睛的向量
    float3 toEyeW = normalize(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);

    // 只计算方向光
    ComputeDirectionalLight(gMaterial, gDirLight, pIn.NormalW, toEyeW, ambient, diffuse, spec);

    return pIn.Color * (ambient + diffuse) + spec;
}

实战3: 画出顶点的法向量

画出顶点的法向量可以帮助你进行调试,排查法向量是否出现了问题。这时候图元的类型为PointList,需要通过几何着色器输出一个线段(两个顶点)。由于顶点中包含法向量,剩下的就是要自行决定法向量的长度。

下图的法向量长度为0.5

HLSL代码

Normal_VS.hlsl, Normal_GS.hlslNormal_PS.hlsl的实现如下:

// Normal_VS.hlsl
#include "Basic.fx"

VertexPosHWNormalColor VS(VertexPosNormalColor pIn)
{
    VertexPosHWNormalColor pOut;
    row_major matrix viewProj = mul(gView, gProj);
    pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
    pOut.PosH = mul(float4(pOut.PosW, 1.0f), viewProj);
    pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
    pOut.Color = pIn.Color;
    return pOut;
}
// Normal_GS.hlsl
#include "Basic.fx"

[maxvertexcount(2)]
void GS(point VertexPosHWNormalColor input[1], inout LineStream<VertexPosHWNormalColor> output)
{
    matrix viewProj = mul(gView, gProj);
    

    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.fx"

float4 PS(VertexPosHWNormalColor pIn) : SV_TARGET
{
    return pIn.Color;
}

C++代码的部分变化

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 CBChangesEveryFrame
{
    DirectX::XMMATRIX world;
    DirectX::XMMATRIX worldInvTranspose;
};

struct CBChangesOnResize
{
    DirectX::XMMATRIX proj;
};

struct CBNeverChange
{
    DirectionalLight dirLight;
    Material material;
    DirectX::XMMATRIX view;
    DirectX::XMFLOAT3 eyePos;
    float cylinderHeight;
};

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 SetRenderSplitedTriangle();
    // 绘制无上下盖的圆柱体
    void SetRenderCylinderNoCap();
    // 绘制所有顶点的法向量
    void SetRenderNormal();


private:
    // objFileNameInOut为编译好的着色器二进制文件(.*so),若有指定则优先寻找该文件并读取
    // hlslFileName为着色器代码,若未找到着色器二进制文件则编译着色器代码
    // 编译成功后,若指定了objFileNameInOut,则保存编译好的着色器二进制信息到该文件
    // ppBlobOut输出着色器二进制信息
    HRESULT CreateShaderFromFile(const WCHAR* objFileNameInOut, const WCHAR* hlslFileName, LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob** ppBlobOut);

private:
    ComPtr<ID3D11VertexShader> mTriangleVS;                 
    ComPtr<ID3D11PixelShader> mTrianglePS;                  
    ComPtr<ID3D11GeometryShader> mTriangleGS;               
    
    ComPtr<ID3D11VertexShader> mCylinderVS;                 
    ComPtr<ID3D11PixelShader> mCylinderPS;
    ComPtr<ID3D11GeometryShader> mCylinderGS;

    ComPtr<ID3D11VertexShader> mNormalVS;
    ComPtr<ID3D11PixelShader> mNormalPS;
    ComPtr<ID3D11GeometryShader> mNormalGS;

    ComPtr<ID3D11InputLayout> mVertexPosColorLayout;        // VertexPosColor输入布局
    ComPtr<ID3D11InputLayout> mVertexPosNormalColorLayout;  // VertexPosNormalColor输入布局

    ComPtr<ID3D11DeviceContext> md3dImmediateContext;       // 设备上下文

    std::vector<ComPtr<ID3D11Buffer>> mConstantBuffers;     // 常量缓冲区
};

#endif

Basic::InitAll方法

现在需要初始化一堆着色器、输入布局和常量缓冲区,并绑定常量缓冲区到默认渲染管线:

bool BasicFX::InitAll(ComPtr<ID3D11Device> device)
{
    if (!device)
        return false;

    ComPtr<ID3DBlob> blob;

    // 创建顶点着色器和顶点布局
    HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.vso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTriangleVS.GetAddressOf()));
    HR(device->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), mVertexPosColorLayout.GetAddressOf()));

    HR(CreateShaderFromFile(L"HLSL\\Cylinder_VS.vso", L"HLSL\\Cylinder_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mCylinderVS.GetAddressOf()));
    HR(device->CreateInputLayout(VertexPosNormalColor::inputLayout, ARRAYSIZE(VertexPosNormalColor::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), mVertexPosNormalColorLayout.GetAddressOf()));

    HR(CreateShaderFromFile(L"HLSL\\Normal_VS.vso", L"HLSL\\Normal_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalVS.GetAddressOf()));

    // 创建像素着色器
    HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.pso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTrianglePS.GetAddressOf()));

    HR(CreateShaderFromFile(L"HLSL\\Cylinder_PS.pso", L"HLSL\\Cylinder_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mCylinderPS.GetAddressOf()));

    HR(CreateShaderFromFile(L"HLSL\\Normal_PS.pso", L"HLSL\\Normal_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalPS.GetAddressOf()));

    // 创建几何着色器
    HR(CreateShaderFromFile(L"HLSL\\Triangle_GS.gso", L"HLSL\\Triangle_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTriangleGS.GetAddressOf()));

    HR(CreateShaderFromFile(L"HLSL\\Cylinder_GS.gso", L"HLSL\\Cylinder_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mCylinderGS.GetAddressOf()));

    HR(CreateShaderFromFile(L"HLSL\\Normal_GS.gso", L"HLSL\\Normal_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalGS.GetAddressOf()));


    RenderStates::InitAll(device);
    device->GetImmediateContext(md3dImmediateContext.GetAddressOf());

    // ******************
    // 设置常量缓冲区描述
    mConstantBuffers.assign(3, nullptr);
    D3D11_BUFFER_DESC cbd;
    ZeroMemory(&cbd, sizeof(cbd));
    cbd.Usage = D3D11_USAGE_DEFAULT;
    cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    cbd.CPUAccessFlags = 0;

    cbd.ByteWidth = Align16Bytes(sizeof(CBChangesEveryFrame));
    HR(device->CreateBuffer(&cbd, nullptr, mConstantBuffers[0].GetAddressOf()));
    cbd.ByteWidth = Align16Bytes(sizeof(CBChangesOnResize));
    HR(device->CreateBuffer(&cbd, nullptr, mConstantBuffers[1].GetAddressOf()));
    cbd.ByteWidth = Align16Bytes(sizeof(CBNeverChange));
    HR(device->CreateBuffer(&cbd, nullptr, mConstantBuffers[2].GetAddressOf()));

    // 预先绑定各自所需的缓冲区
    md3dImmediateContext->VSSetConstantBuffers(0, 1, mConstantBuffers[0].GetAddressOf());
    md3dImmediateContext->VSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf());
    md3dImmediateContext->VSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());

    md3dImmediateContext->GSSetConstantBuffers(0, 1, mConstantBuffers[0].GetAddressOf());
    md3dImmediateContext->GSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf());
    md3dImmediateContext->GSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());

    md3dImmediateContext->PSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());
    return true;
}

Basic::SetRenderSplitedTriangle方法--渲染分裂的三角形

该方法处理的是图元TriangleList。因为后续的方法处理的图元不同,在调用开始就得设置回正确的图元。也请确保输入装配阶段提供好需要分裂的三角形顶点。

void BasicFX::SetRenderSplitedTriangle()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->VSSetShader(mTriangleVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mTriangleGS.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mTrianglePS.Get(), nullptr, 0);
}

Basic::SetRenderCylinderNoCap方法--渲染圆柱侧面

该方法处理的是图元LineStrip,确保输入的一系列顶点和法向量能够在同一平面上。若提供的顶点集合按顺时针排布,则会自底向上构建出圆柱体,反之则是自顶向下构建。

这里需要关闭背面裁剪,因为我们也可以看到圆柱体的内部。

void BasicFX::SetRenderCylinderNoCap()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP);
    md3dImmediateContext->IASetInputLayout(mVertexPosNormalColorLayout.Get());
    md3dImmediateContext->VSSetShader(mCylinderVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mCylinderGS.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mCylinderPS.Get(), nullptr, 0);
}

Basic::SetRenderNormal方法--渲染法向量

该方法处理的图元是PointList,确保输入的顶点要包含法向量。

void BasicFX::SetRenderNormal()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosNormalColorLayout.Get());
    md3dImmediateContext->VSSetShader(mNormalVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mNormalGS.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mNormalPS.Get(), nullptr, 0);
}

GameApp类的变化

该项目包含上面三种实战内容,需要用户去指定当前播放的模式。

首先声明部分变化如下:

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> mColorBrush;               // 单色笔刷
    ComPtr<IDWriteFont> mFont;                              // 字体
    ComPtr<IDWriteTextFormat> mTextFormat;                  // 文本格式

    ComPtr<ID3D11Buffer> mVertexBuffer;                     // 顶点集合
    int mVertexCount;                                       // 顶点数目
    Mode mShowMode;                                         // 当前显示模式

    BasicFX mBasicFX;                                       // Basic特效管理类

    CBChangesEveryFrame mCBChangeEveryFrame;                // 该缓冲区存放每帧更新的变量
    CBChangesOnResize mCBOnReSize;                          // 该缓冲区存放仅在窗口大小变化时更新的变量
    CBNeverChange mCBNeverChange;                           // 该缓冲区存放不会再进行修改的变量
};

GameApp::ResetTriangle方法--重设为三角形顶点

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_DEFAULT;
    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(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffer.ReleaseAndGetAddressOf()));
    // 三角形顶点数
    mVertexCount = 3;
}

GameApp::ResetRoundWire方法--重设圆线


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_DEFAULT;
    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(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffer.ReleaseAndGetAddressOf()));
    // 线框顶点数
    mVertexCount = 41;
}

GameApp::UpdateScene方法变化

每次切换需要记得重新设置顶点缓冲区,重新将顶点集绑定到输入,并重设渲染类型。

void GameApp::UpdateScene(float dt)
{

    // 更新鼠标事件,获取相对偏移量
    Mouse::State mouseState = mMouse->GetState();
    Mouse::State lastMouseState = mMouseTracker.GetLastState();
    mMouseTracker.Update(mouseState);

    Keyboard::State keyState = mKeyboard->GetState();
    mKeyboardTracker.Update(keyState);

    // 更新每帧变化的值
    if (mShowMode == Mode::SplitedTriangle)
    {
        mCBChangeEveryFrame.worldInvTranspose = mCBChangeEveryFrame.world = XMMatrixIdentity();
    }
    else
    {
        static float phi = 0.0f, theta = 0.0f;
        phi += 0.0001f, theta += 0.00015f;
        mCBChangeEveryFrame.world = XMMatrixRotationX(phi) * XMMatrixRotationY(theta);
        mCBChangeEveryFrame.worldInvTranspose = XMMatrixTranspose(XMMatrixInverse(nullptr, mCBChangeEveryFrame.world));
    }
    mBasicFX.UpdateConstantBuffer(mCBChangeEveryFrame);


    // 切换显示模式
    if (mKeyboardTracker.IsKeyPressed(Keyboard::D1))
    {
        mShowMode = Mode::SplitedTriangle;
        ResetTriangle();
        // 输入装配阶段的顶点缓冲区设置
        UINT stride = sizeof(VertexPosColor);       // 跨越字节数
        UINT offset = 0;                            // 起始偏移量
        md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset);
        mBasicFX.SetRenderSplitedTriangle();
    }
    else if (mKeyboardTracker.IsKeyPressed(Keyboard::D2))
    {
        mShowMode = Mode::CylinderNoCap;
        ResetRoundWire();
        // 输入装配阶段的顶点缓冲区设置
        UINT stride = sizeof(VertexPosNormalColor);     // 跨越字节数
        UINT offset = 0;                                // 起始偏移量
        md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset);
        mBasicFX.SetRenderCylinderNoCap();
    }

    // 显示法向量
    if (mKeyboardTracker.IsKeyPressed(Keyboard::Q))
    {
        if (mShowMode == Mode::CylinderNoCap)
            mShowMode = Mode::CylinderNoCapWithNormal;
        else if (mShowMode == Mode::CylinderNoCapWithNormal)
            mShowMode = Mode::CylinderNoCap;
    }

}

GameApp::DrawScene的变化

void GameApp::DrawScene()
{
    assert(md3dImmediateContext);
    assert(mSwapChain);

    md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
    md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    md3dImmediateContext->Draw(mVertexCount, 0);
    // 绘制法向量,绘制完后记得归位
    if (mShowMode == Mode::CylinderNoCapWithNormal)
    {
        mBasicFX.SetRenderNormal();
        md3dImmediateContext->Draw(mVertexCount, 0);
        mBasicFX.SetRenderCylinderNoCap();
    }


    //
    // 绘制Direct2D部分
    //
    md2dRenderTarget->BeginDraw();
    std::wstring text = L"切换类型:1-分裂的三角形 2-圆线构造柱面\n"
        "当前模式: ";
    if (mShowMode == Mode::SplitedTriangle)
        text += L"分裂的三角形";
    else if (mShowMode == Mode::CylinderNoCap)
        text += L"圆线构造柱面(Q-显示圆线的法向量)";
    else
        text += L"圆线构造柱面(Q-隐藏圆线的法向量)";
    md2dRenderTarget->DrawTextW(text.c_str(), (UINT32)text.length(), mTextFormat.Get(),
        D2D1_RECT_F{ 0.0f, 0.0f, 500.0f, 60.0f }, mColorBrush.Get());
    HR(md2dRenderTarget->EndDraw());

    HR(mSwapChain->Present(0, 0));
}

最终效果如下:

DirectX11 With Windows SDK完整目录

Github项目源码