DirectX11 With Windows SDK–16 利用几何着色器可选的流输出阶段帮助绘制多种分形

前言

在上一章,我们知道了如何使用几何着色器来重新组装图元,比如从一个三角形分裂成三个三角形。但是为了实现更高阶的分形,我们必须要从几何着色器拿到输出的顶点。这里我们可以使用可选的流输出阶段来拿到顶点集合。

注意: 本章末尾有大量的GIF动图!

DirectX11 With Windows SDK完整目录

Github项目源码

流输出阶段

现在我们知道GPU可以写入纹理(textures),例如深度/模板缓冲区以及后备缓冲区。当然,我们也可以通过渲染管线的流输出阶段让GPU将几何着色器输出的顶点集合写入到指定的顶点缓冲区(vertex buffer)。除此之外,我们还能够指定不进行光栅化以及后续的所有阶段,仅让顶点数据经过流输出阶段。

在几何着色器中,最多四个流输出对象可以被设置,即几何着色器的入口函数中只允许设置四个流输出对象的参数。当多个流输出对象存在时,它们必须都要为PointStream类模板,但允许模板参数不同。输出的顶点回流到顶点缓冲区后可以再次进行一遍新的渲染管线流程。

上一章也提到,几何着色器的单次调用不能产出超过1024个标量。因此分配给所有流输出对象的标量总和不能超过1024。比如现在我有2个流输出对象,它们的结构体相同,容纳512个标量,那最多仅允许输出2个这样的顶点来分配给这2个流输出对象。

流输出状态的配置

ID3D11DeviceContext::SOSetTargets方法–绑定流输出对应用于接收数据的顶点缓冲区

void ID3D11DeviceContext::SOSetTargets(
  UINT         NumBuffers,              // [In]顶点缓冲区数目
  ID3D11Buffer * const *ppSOTargets,    // [In]顶点缓冲区数组
  const UINT   *pOffsets                // [In]一个数组包含对每个顶点缓冲区的字节偏移量
);

该方法多允许设置4个顶点缓冲区。

每个要绑定到流输出阶段的缓冲区资源必须要在创建的时候额外设置D3D11_BIND_STREAM_OUTPUT绑定标签。

若偏移值设为-1,则会引起流输出缓冲区被追加到最后一个缓冲区的后面

顶点缓冲区绑定到流输出阶段的输出槽0操作如下:

UINT offset = 0;
md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

如果我们需要恢复默认的状态,则可以这样调用:

ID3D11Buffer* nullBuffer = nullptr;
UINT offset = 0;
md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

注意: 如果使用的是当前绑定到输入装配阶段的顶点缓冲区,则绑定会失效。因为顶点缓冲区不可以同时被绑定到输入装配阶段和流输出阶段。

因为后续我们是将每一阶输出的顶点都保存下来,即便不需要交换顶点缓冲区,但也有可能出现同时绑定输入/输出的情况。一种合理的绑定顺序如下:

// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer * nullBuffer = nullptr;
md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);
// ...
md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
// ...
md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

当渲染管线完成一次流输出后,我们就可以用下面的方法来获取绑定在流输出阶段上的顶点缓冲区(当然你本身持有该缓冲区的指针的话就不需要了)

ID3D11DeviceContext::SOGetTargets方法–获取绑定在流输出阶段的顶点缓冲区

void ID3D11DeviceContext::SOGetTargets(
  UINT         NumBuffers,          // [In]缓冲区数目
  ID3D11Buffer **ppSOTargets        // [Out]获取绑定流输出阶段的顶点缓冲区
);

输出的顶点缓冲区引用数会加1,最好是能够使用ComPtr来承接顶点缓冲区,否则就要在结束的时候手工调用Release方法,若忘记调用则会引发内存泄漏。

ID3D11Device::CreateGeometryShaderWithStreamOutput方法–创建带流输出阶段的几何着色器

接下来我们需要指定数据会流向哪个输出槽,首先我们需要填充结构体D3D11_SO_DECLARATION_ENTRY,结构体声明如下:

typedef struct D3D11_SO_DECLARATION_ENTRY {
  UINT   Stream;            // 输出流索引,从0开始
  LPCSTR SemanticName;      // 语义名
  UINT   SemanticIndex;     // 语义索引
  BYTE   StartComponent;    // 从第几个分量(xyzw)开始,只能取0-3
  BYTE   ComponentCount;    // 分量的输出数目,只能取1-4
  BYTE   OutputSlot;        // 输出槽索引,只能取0-3
};

其中,语义名SemanticName用于指定在几何着色器的流输出对象对应的结构体中该语义描述的成员,然后用语义索引SemanticIndex指定存在同名语义下用索引值标记的唯一成员。

然后StartComponentComponentCount用于控制该向量需要输出哪些分量。若StartComponent为1,ComponentCount为2,则输出的分量为(y, z),而要输出全部分量,则指定StartCompnent为0, ComponentCount为4.

输出槽索引OutputSlot用于指定选择绑定流输出的缓冲区数组中的某一元素。

由于这里一个结构体只能指定某个输出流中的某一向量,所以通常我们需要像顶点输入布局那样传递一个数组来取出组合成特定顶点。

比如说现在顶点着色器输入的顶点和流输出的顶点是一致的:

struct VertexPosColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT4 color;
    static const D3D11_INPUT_ELEMENT_DESC inputLayout[2];
};

输入布局描述如下:

const D3D11_INPUT_ELEMENT_DESC VertexPosColor::inputLayout[2] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};

HLSL中的结构体如下:

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

流输出的入口描述如下:

const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = {
    { 0, "POSITION", 0, 0, 3, 0 },
    { 0, "COLOR", 0, 0, 4, 0 }
};

这里对应的是索引为0的流输出对象,输出给绑定在索引为0的输出槽的顶点缓冲区,先输出语义为POSITION的向量中的xyz分量,然后输出COLOR整个向量。这样一个输出的顶点就和原来的顶点一致了。

接下来给出ID3D11Device::CreateGeometryShaderWithStreamOutput方法的原型:

HRESULT ID3D11Device::CreateGeometryShaderWithStreamOutput(
  const void                       *pShaderBytecode,    // [In]编译好的着色器字节码
  SIZE_T                           BytecodeLength,      // [In]字节码长度
  const D3D11_SO_DECLARATION_ENTRY *pSODeclaration,     // [In]D3D11_SO_DECLARATION_ENTRY的数组
  UINT                             NumEntries,          // [In]入口总数
  const UINT                       *pBufferStrides,     // [In]一个数组包含了每个绑定到流输出的缓冲区中顶点字节大小
  UINT                             NumStrides,          // [In]上面数组的元素数目
  UINT                             RasterizedStream,    // [In]按索引指定哪个流输出对象用于传递到光栅化阶段
  ID3D11ClassLinkage               *pClassLinkage,      // [In]忽略
  ID3D11GeometryShader             **ppGeometryShader   // [Out]创建好的几何着色器
);

如果不需要有流输出对象提供数据给光栅化阶段,则RasterizedStream应当指定为D3D11_SO_NO_RASTERIZED_STREAM。即便某一流输出对象传递了数据给光栅化阶段,它仍可以提供数据给某一绑定的缓冲区。

当该着色器被绑定到渲染管线上,流输出阶段就会被激活。然后当渲染管线开始执行的时候,任何传递给几何着色器中的流输出对象的数据,都会基于语义名和语义索引尝试匹配输出布局。一旦发现有匹配的语义,该数据就会流向对应的缓冲区来创建完整的输出顶点集。

下面是一个调用的例子:

const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = {
    { 0, "POSITION", 0, 0, 3, 0 },
    { 0, "COLOR", 0, 0, 4, 0 }
};

HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout),
    &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mTriangleSOGS.GetAddressOf()));

绘制各种酷炫的分形

由于现在的着色器多到令人发指,而且有没有很好的办法归类整合,故在下面用一张表列出所有绘制流程用到的着色器hlsl文件名称:

操作 VS GS PS
通过流输出得到分裂的三角形 TriangleSO_VS TriangleSO_GS X
通过流输出得到分形雪花 SnowSO_VS SnowSO_GS X
通过流输出得到分形球体 SphereSO_VS SphereSO_GS X
绘制分形三角形 Triangle_VS X Triangle_PS
绘制分形雪花 Snow_VS X Snow_PS
绘制分形球体 Sphere_VS X Sphere_PS
绘制法向量 Normal_VS Normal_GS Normal_PS

首先给出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 gSphereCenter;
    float gSphereRadius;
    float3 gEyePosW;
}


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

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

struct VertexPosHLColor
{
    float4 PosH : SV_POSITION;
    float3 PosL : 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: 绘制分形三角形

通过流输出阶段,一个三角形就分裂出了三个三角形,顶点的数目翻了3倍。若规定1阶分形三角形的顶点数为3,则N阶分形三角形的顶点数为\(3^{N}\)

HLSL代码

首先是TriangleSO_VS.hlsl,它负责将顶点直接传递给几何着色器。

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

VertexPosColor VS(VertexPosColor pIn)
{
    return pIn;
}

然后和上一章一样,TriangleSO_GS.hlsl中的几何着色器将一个三角形分裂成三个三角形,并且输出的顶点类型和输入的顶点是一致的。

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

[maxvertexcount(9)]
void GS(triangle VertexPosColor input[3], inout TriangleStream<VertexPosColor> output)
{
    //
    // 将一个三角形分裂成三个三角形,即没有v3v4v5的三角形
    //       v1
    //       /\
    //      /  \
    //   v3/____\v4
    //    /\xxxx/\
    //   /  \xx/  \
    //  /____\/____\
    // v0    v5    v2


    VertexPosColor 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].PosL = (input[i].PosL + input[(i + 1) % 3].PosL) / 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_VS.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_PS.hlsl
#include "Basic.fx"

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

实战2: 绘制分形雪花

现在规定第一张图为一阶分形雪花,第二张为二阶分形雪花。观察二者之间的变化,可以发现前者的每一条直线变成了四条折线。其中每个尖锐角的度数都在60度,并且每条边的长度都应该是一致的。

HLSL代码

和之前一样,SnowSO_VS.hlsl中的顶点着色器阶段只用于顶点直传:

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

VertexPosNormalColor VS(VertexPosNormalColor pIn)
{
    return pIn;
}

然后重点就在于SnowSO_GS.hlsl的几何着色器了。这里先放出代码:

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

[maxvertexcount(5)]
void GS(line VertexPosColor input[2], inout LineStream<VertexPosColor> output)
{
    // 要求分形线段按顺时针排布
    // z分量必须相等,因为顶点没有提供法向量无法判断垂直上方向
    //                       v1
    //                       /\
    // ____________ =>  ____/  \____
    // i0         i1   i0  v0  v2  i1
    
    VertexPosColor v0, v1, v2;
    v0.Color = lerp(input[0].Color, input[1].Color, 0.25f);
    v1.Color = lerp(input[0].Color, input[1].Color, 0.5f);
    v2.Color = lerp(input[0].Color, input[1].Color, 0.75f);

    v0.PosL = lerp(input[0].PosL, input[1].PosL, 1.0f / 3.0f);
    v2.PosL = lerp(input[0].PosL, input[1].PosL, 2.0f / 3.0f);

    // xy平面求出它的垂直单位向量
    //     
    //     |
    // ____|_____
    float2 upDir = normalize(input[1].PosL - input[0].PosL).yx;
    float len = length(input[1].PosL.xy - input[0].PosL.xy);
    upDir.x = -upDir.x;

    v1.PosL = lerp(input[0].PosL, input[1].PosL, 0.5f);
    v1.PosL.xy += sqrt(3) / 6.0f * len * upDir;

    output.Append(input[0]);
    output.Append(v0);
    output.Append(v1);
    output.Append(v2);
    output.Append(input[1]);

}

可以发现分形雪花每升一阶,需要绘制的顶点数就变成了上一阶的4倍。

这里要求了z分量必须相等,因为使用的着色器仍把一切的顶点仍当做3D顶点来对待出来(当然你也可以写成2D的着色器)。

然后开始具体分析从直线变折线的过程,可以看到因为顶点v1所在角的度数在60度,且v0, v1, v2构成等边三角形,故v0v2,
v0v1和v1v2的边长是一致的。而且4条折线要求边长相等,故这里的i0v0和v2i1应当各占线段i0i1的1/3.

其中lerp函数是线性插值函数,数学公式如下:
\[ \mathbf{p} = \mathbf{p}_0 + t(\mathbf{p}_1 – \mathbf{p}_0) \]

其中t的取值范围在[0.0f, 1.0f],并且操作对象p0和p1可以是标量,也可以是矢量,对矢量来说则是对每个分量都进行线性插值。

当t = 0.5f时,描述的就是p0和p1的中值或中点。

该函数很容易描述两点之间某一相对位置。

由于我们规定了连续线段必须按顺时针排布,我们就可以利用向量i0i1逆时针旋转90度得到对应的突出方向向量,然后标准化,乘上相应的高度值即可得到顶点v1的位置。

最后就是用于绘制的着色器代码:

// Snow_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;
}
// Snow_PS.hlsl
#include "Basic.fx"

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

实战3: 绘制分形圆球

以下是一阶和二阶的分形圆球:

仔细观察可以看到,原先的一个三角形分裂出了四个三角形,即每升一阶,需要绘制的顶点数就变成了上一阶的4倍。

HLSL代码

SphereSO_VS.hlsl代码和SphereSO_GS.hlsl代码如下:

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

VertexPosNormalColor VS(VertexPosNormalColor pIn)
{
    return pIn;
}
// SphereSO_GS.hlsl

#include "Basic.fx"

[maxvertexcount(12)]
void GS(triangle VertexPosNormalColor input[3], inout TriangleStream<VertexPosNormalColor> output)
{
    //
    // 将一个三角形分裂成四个三角形,但同时顶点v3, v4, v5也需要在球面上
    //       v1
    //       /\
    //      /  \
    //   v3/____\v4
    //    /\xxxx/\
    //   /  \xx/  \
    //  /____\/____\
    // v0    v5    v2
    
    VertexPosNormalColor vertexes[6];

    matrix viewProj = mul(gView, gProj);

    [unroll]
    for (int i = 0; i < 3; ++i)
    {
        vertexes[i] = input[i];
        vertexes[i + 3].Color = lerp(input[i].Color, input[(i + 1) % 3].Color, 0.5f);
        vertexes[i + 3].NormalL = normalize(input[i].NormalL + input[(i + 1) % 3].NormalL);
        vertexes[i + 3].PosL = gSphereCenter + gSphereRadius * vertexes[i + 3].NormalL;
    }
        
    output.Append(vertexes[0]);
    output.Append(vertexes[3]);
    output.Append(vertexes[5]);
    output.RestartStrip();

    output.Append(vertexes[3]);
    output.Append(vertexes[4]);
    output.Append(vertexes[5]);
    output.RestartStrip();

    output.Append(vertexes[5]);
    output.Append(vertexes[4]);
    output.Append(vertexes[2]);
    output.RestartStrip();

    output.Append(vertexes[3]);
    output.Append(vertexes[1]);
    output.Append(vertexes[4]);
}

由于v3, v4, v5也需要在球面上,我们还需要额外知道球的半径和球心位置。虽然说通过三角形三个顶点位置和法向量可以算出圆心和半径,但直接从常量缓冲区提供这两个信息会更方便一些。

要计算诸如v3顶点所在位置,我们可以先求出它的法向量,将v0和v1的法向量相加取其单位向量即为v3的法向量,然后从圆心开始加上半径长度的法向量即可得到顶点v3的位置。

剩下绘制圆的着色器代码如下:

// Sphere_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;
}
// Sphere_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;
}

C++代码的变化

BasicFX.h的变化

首先是常量缓冲区的变化,新增了存储球心位置和半径信息:

struct CBChangesEveryFrame
{
    DirectX::XMMATRIX world;
    DirectX::XMMATRIX worldInvTranspose;
};

struct CBChangesOnResize
{
    DirectX::XMMATRIX proj;
};

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

然后是BasicFX类的变化,其中法向量的绘制可以用于球体绘制的时候,具体实现可以回顾上一章。

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 SetRenderSplitedSnow();
    // 绘制球体
    void SetRenderSplitedSphere();
    // 通过流输出阶段获取三角形分裂的下一阶分形
    void SetStreamOutputSplitedTriangle(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut);
    // 通过流输出阶段获取雪花的下一阶分形
    void SetStreamOutputSplitedSnow(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut);
    // 通过流输出阶段获取球的下一阶分形
    void SetStreamOutputSplitedSphere(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut);

    // 绘制所有顶点的法向量
    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> mTriangleSOVS;
    ComPtr<ID3D11GeometryShader> mTriangleSOGS;

    ComPtr<ID3D11VertexShader> mTriangleVS;
    ComPtr<ID3D11PixelShader> mTrianglePS;

    ComPtr<ID3D11VertexShader> mSphereSOVS;
    ComPtr<ID3D11GeometryShader> mSphereSOGS;

    ComPtr<ID3D11VertexShader> mSphereVS;
    ComPtr<ID3D11PixelShader> mSpherePS;
    
    ComPtr<ID3D11VertexShader> mSnowSOVS;
    ComPtr<ID3D11GeometryShader> mSnowSOGS;

    ComPtr<ID3D11VertexShader> mSnowVS;
    ComPtr<ID3D11PixelShader> mSnowPS;

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

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

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

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

BasicFX::InitAll方法的变化

现在着色器的创建按绘制类别进行分组:

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

    const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = {
        { 0, "POSITION", 0, 0, 3, 0 },
        { 0, "COLOR", 0, 0, 4, 0 }
    };

    const D3D11_SO_DECLARATION_ENTRY posNormalColorLayout[3] = {
        { 0, "POSITION", 0, 0, 3, 0 },
        { 0, "NORMAL", 0, 0, 3, 0 },
        { 0, "COLOR", 0, 0, 4, 0 }
    };

    UINT stridePosColor = sizeof(VertexPosColor);
    UINT stridePosNormalColor = sizeof(VertexPosNormalColor);

    ComPtr<ID3DBlob> blob;

    //
    // 流输出分裂三角形
    //
    HR(CreateShaderFromFile(L"HLSL\\TriangleSO_VS.vso", L"HLSL\\TriangleSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTriangleSOVS.GetAddressOf()));
    // 创建顶点输入布局
    HR(device->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(),
        blob->GetBufferSize(), mVertexPosColorLayout.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\TriangleSO_GS.gso", L"HLSL\\TriangleSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout),
        &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mTriangleSOGS.GetAddressOf()));
    
    //
    // 绘制分形三角形
    //
    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(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\\SphereSO_VS.vso", L"HLSL\\SphereSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSphereSOVS.GetAddressOf()));
    // 创建顶点输入布局
    HR(device->CreateInputLayout(VertexPosNormalColor::inputLayout, ARRAYSIZE(VertexPosNormalColor::inputLayout), blob->GetBufferPointer(),
        blob->GetBufferSize(), mVertexPosNormalColorLayout.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\SphereSO_GS.gso", L"HLSL\\SphereSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posNormalColorLayout, ARRAYSIZE(posNormalColorLayout),
        &stridePosNormalColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mSphereSOGS.GetAddressOf()));
    
    //
    // 绘制球体
    //
    HR(CreateShaderFromFile(L"HLSL\\Sphere_VS.vso", L"HLSL\\Sphere_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSphereVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Sphere_PS.pso", L"HLSL\\Sphere_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSpherePS.GetAddressOf()));
    

    //
    // 流输出分形雪花
    //
    HR(CreateShaderFromFile(L"HLSL\\SnowSO_VS.vso", L"HLSL\\SnowSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSnowSOVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\SnowSO_GS.gso", L"HLSL\\SnowSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout),
        &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mSnowSOGS.GetAddressOf()));

    //
    // 绘制雪花
    //
    HR(CreateShaderFromFile(L"HLSL\\Snow_VS.vso", L"HLSL\\Snow_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSnowVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Snow_PS.pso", L"HLSL\\Snow_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSnowPS.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\\Normal_GS.gso", L"HLSL\\Normal_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalGS.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()));

    

    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;
}

BasicFX::SetRenderSplitedTriangle方法–绘制分形三角形

由于新增了流输出的阶段,这里开始接下来的每一个用于绘制的方法都需要把流输出绑定的顶点缓冲区都解除绑定。

void BasicFX::SetRenderSplitedTriangle()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->VSSetShader(mTriangleVS.Get(), nullptr, 0);
    // 关闭流输出
    md3dImmediateContext->GSSetShader(nullptr, nullptr, 0);
    ID3D11Buffer* bufferArray[1] = { nullptr };
    UINT offset = 0;
    md3dImmediateContext->SOSetTargets(1, bufferArray, &offset);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mTrianglePS.Get(), nullptr, 0);
}

BasicFX::SetRenderSplitedSnow方法–绘制分形雪花

void BasicFX::SetRenderSplitedSnow()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->VSSetShader(mSnowVS.Get(), nullptr, 0);
    // 关闭流输出
    md3dImmediateContext->GSSetShader(nullptr, nullptr, 0);
    ID3D11Buffer* bufferArray[1] = { nullptr };
    UINT offset = 0;
    md3dImmediateContext->SOSetTargets(1, bufferArray, &offset);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mSnowPS.Get(), nullptr, 0);
}

BasicFX::SetRenderSplitedSphere方法–绘制分形球体

void BasicFX::SetRenderSplitedSphere()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosNormalColorLayout.Get());
    md3dImmediateContext->VSSetShader(mSphereVS.Get(), nullptr, 0);
    // 关闭流输出
    md3dImmediateContext->GSSetShader(nullptr, nullptr, 0);
    ID3D11Buffer* bufferArray[1] = { nullptr };
    UINT offset = 0;
    md3dImmediateContext->SOSetTargets(1, bufferArray, &offset);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mSpherePS.Get(), nullptr, 0);
}

BasicFX::SetStreamOutputSplitedTriangle方法–经过流输出保存下一阶分形三角形的顶点

为了简化设置,这里还需要提供额外的输入缓冲区和输出缓冲区。为了防止出现顶点缓冲区同时被绑定到输入装配和流输出阶段的情况,需要先清空流输出绑定的顶点缓冲区,然后将用于输入的顶点缓冲区绑定到输入装配阶段,最后才是把输出的顶点缓冲区绑定到流输出阶段。

void BasicFX::SetStreamOutputSplitedTriangle(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
    // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
    UINT stride = sizeof(VertexPosColor);
    UINT offset = 0;
    ID3D11Buffer * nullBuffer = nullptr;
    md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());

    md3dImmediateContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);

    md3dImmediateContext->VSSetShader(mTriangleSOVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mTriangleSOGS.Get(), nullptr, 0);

    md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);
;
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(nullptr, nullptr, 0);
}

BasicFX::SetStreamOutputSplitedSnow方法–经过流输出保存下一阶分形雪花的顶点

注意这里是用LineList而不是LineStrip方式。

void BasicFX::SetStreamOutputSplitedSnow(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
    // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
    UINT stride = sizeof(VertexPosColor);
    UINT offset = 0;
    ID3D11Buffer * nullBuffer = nullptr;
    md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);

    md3dImmediateContext->VSSetShader(mSnowSOVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mSnowSOGS.Get(), nullptr, 0);

    md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(nullptr, nullptr, 0);
}

BasicFX::SetStreamOutputSplitedSphere方法–经过流输出保存下一阶分形球体的顶点

void BasicFX::SetStreamOutputSplitedSphere(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
    // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
    UINT stride = sizeof(VertexPosNormalColor);
    UINT offset = 0;
    ID3D11Buffer * nullBuffer = nullptr;
    md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosNormalColorLayout.Get());
    md3dImmediateContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);

    md3dImmediateContext->VSSetShader(mSphereSOVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mSphereSOGS.Get(), nullptr, 0);

    md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(nullptr, nullptr, 0);
}

GameApp类的变化

GameApp类的变化如下:

class GameApp : public D3DApp
{
public:
    enum class Mode { SplitedTriangle, SplitedSnow, SplitedSphere };
    
public:
    GameApp(HINSTANCE hInstance);
    ~GameApp();

    bool Init();
    void OnResize();
    void UpdateScene(float dt);
    void DrawScene();

private:
    bool InitResource();

    void ResetSplitedTriangle();
    void ResetSplitedSnow();
    void ResetSplitedSphere();


private:
    
    ComPtr<ID2D1SolidColorBrush> mColorBrush;               // 单色笔刷
    ComPtr<IDWriteFont> mFont;                              // 字体
    ComPtr<IDWriteTextFormat> mTextFormat;                  // 文本格式

    ComPtr<ID3D11Buffer> mVertexBuffers[7];                 // 顶点缓冲区数组
    int mVertexCounts[7];                                   // 顶点数目
    int mCurrIndex;                                         // 当前索引
    Mode mShowMode;                                         // 当前显示模式
    bool mIsWireFrame;                                      // 是否为线框模式
    bool mShowNormal;                                       // 是否显示法向量
    BasicFX mBasicFX;                                       // Basic特效管理类

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

#endif

GameApp::ResetSplitedTriangle方法–重新建立包含1-7阶的分形三角形顶点的缓冲区

首先我们只需要给1阶的顶点缓冲区使用指定三角形的三个顶点,然后后续阶数的顶点缓冲区就根据上一阶产出的顶点缓冲区进行”绘制”。经过6次调用后,里面的7个顶点缓冲区都应该被初始化完毕,后续绘制的时候只需要直接取用某一个顶点缓冲区即可。

注意顶点缓冲区在创建的时候一定要加上D3D11_BIND_STREAM_OUTPUT标签。

void GameApp::ResetSplitedTriangle()
{
    // ******************
    // 初始化三角形
    // 设置三角形顶点
    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 | D3D11_BIND_STREAM_OUTPUT;    // 需要额外添加流输出标签
    vbd.CPUAccessFlags = 0;
    // 新建顶点缓冲区
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices;
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffers[0].ReleaseAndGetAddressOf()));

    
    // 三角形顶点数
    mVertexCounts[0] = 3;
    // 初始化所有顶点缓冲区
    for (int i = 1; i < ARRAYSIZE(mVertexBuffers); ++i)
    {
        vbd.ByteWidth *= 3;
        mVertexCounts[i] = mVertexCounts[i - 1] * 3;
        HR(md3dDevice->CreateBuffer(&vbd, nullptr, mVertexBuffers[i].ReleaseAndGetAddressOf()));
        mBasicFX.SetStreamOutputSplitedTriangle(mVertexBuffers[i - 1], mVertexBuffers[i]);
        md3dImmediateContext->Draw(mVertexCounts[i - 1], 0);
    }
}

GameApp::ResetSplitedSnow方法–重新建立包含1-7阶的分形雪花顶点的缓冲区

由于绘制方式统一用LineList,初始阶段应当提供3条线段的6个顶点,虽然说每个顶点都被重复使用了2次。

void GameApp::ResetSplitedSnow()
{
    // ******************
    // 雪花分形从初始化三角形开始,需要6个顶点
    // 设置三角形顶点
    float sqrt3 = sqrt(3.0f);
    VertexPosColor vertices[] =
    {
        { XMFLOAT3(-3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, sqrt3 / 2, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, sqrt3 / 2, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(-3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }
    };
    // 将三角形宽度和高度都放大3倍
    for (int i = 0; i < ARRAYSIZE(vertices); ++i)
    {
        vertices[i].pos.x *= 3;
        vertices[i].pos.y *= 3;
    }

    // 设置顶点缓冲区描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_DEFAULT;
    vbd.ByteWidth = sizeof vertices;
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;    // 需要额外添加流输出标签
    vbd.CPUAccessFlags = 0;
    // 新建顶点缓冲区
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices;
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffers[0].ReleaseAndGetAddressOf()));

    // 顶点数
    mVertexCounts[0] = 6;
    // 初始化所有顶点缓冲区
    for (int i = 1; i < ARRAYSIZE(mVertexBuffers); ++i)
    {
        vbd.ByteWidth *= 4;
        mVertexCounts[i] = mVertexCounts[i - 1] * 4;
        HR(md3dDevice->CreateBuffer(&vbd, nullptr, mVertexBuffers[i].ReleaseAndGetAddressOf()));
        mBasicFX.SetStreamOutputSplitedSnow(mVertexBuffers[i - 1], mVertexBuffers[i]);
        md3dImmediateContext->Draw(mVertexCounts[i - 1], 0);
    }
}

GameApp::ResetSplitedSphere方法–重新建立包含1-7阶的分形圆球顶点的缓冲区

这里不使用Geometry类来构造一阶圆球,这里仅提供与外接正方体相交的六个顶点,包含八个三角形对应的24个顶点。

void GameApp::ResetSplitedSphere()
{
    VertexPosNormalColor basePoint[] = {
        { XMFLOAT3(0.0f, 2.0f, 0.0f), XMFLOAT3(0.0f, 1.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(2.0f, 0.0f, 0.0f), XMFLOAT3(1.0f, 0.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, 0.0f, 2.0f), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(-2.0f, 0.0f, 0.0f), XMFLOAT3(-1.0f, 0.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, 0.0f, -2.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, -2.0f, 0.0f), XMFLOAT3(0.0f, -1.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
    };
    int indices[] = {0, 2, 1, 0, 3, 2, 0, 4, 3, 0, 1, 4, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 1, 5};

    std::vector<VertexPosNormalColor> vertices;
    for (int pos : indices)
    {
        vertices.push_back(basePoint[pos]);
    }

    

    // 设置顶点缓冲区描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_DEFAULT;
    vbd.ByteWidth = vertices.size() * sizeof(VertexPosNormalColor);
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;    // 需要额外添加流输出标签
    vbd.CPUAccessFlags = 0;
    // 新建顶点缓冲区
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices.data();
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffers[0].ReleaseAndGetAddressOf()));

    // 顶点数
    mVertexCounts[0] = ARRAYSIZE(indices);
    // 初始化所有顶点缓冲区
    for (int i = 1; i < ARRAYSIZE(mVertexBuffers); ++i)
    {
        vbd.ByteWidth *= 4;
        mVertexCounts[i] = mVertexCounts[i - 1] * 4;
        HR(md3dDevice->CreateBuffer(&vbd, nullptr, mVertexBuffers[i].ReleaseAndGetAddressOf()));
        mBasicFX.SetStreamOutputSplitedSphere(mVertexBuffers[i - 1], mVertexBuffers[i]);
        md3dImmediateContext->Draw(mVertexCounts[i - 1], 0);
    }
}

由于篇幅过大,该类的其它方法就在此省略,具体细节可以查阅该章对应源码。来看一下动图感受一些这些酷炫的效果吧:

分形三角形绘制效果

分形雪花绘制效果

分形圆球绘制效果

由于文件大小限制,这里分成两个部分:

下面是带法向量的:

遗留问题

由于我现在找不到ID3D11DeviceContext::DrawAuto方法无法输出的原因,故在这里使用的依然是原来的Draw方法进行绘制。若有人能够在不使用FX11框架的情况下能够调用DrawAuto方法绘制到流输出,可以在下面评论我,并告诉我解决方案。

除此之外,该项目使用图形调试器并退出的时候,会引发内存泄漏,而具体的泄漏对象估计是ID3D11Query,然而我也没有办法直接拿到该接口对象来释放。

DirectX11 With Windows SDK完整目录

Github项目源码