前言

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

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

在此之前需要额外了解的章节如下:

章节回顾
15 几何着色器初探
Visual Studio图形调试器详细使用教程(编程捕获部分)

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

流输出阶段

现在我们知道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;
m_pd3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

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

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

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

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

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

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

绘制各种酷炫的分形

由于现在的着色器多到令人发指,而且有没有很好的办法归类整合,故在下面用一张表列出所有绘制流程用到的着色器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.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_SphereCenter;
float g_SphereRadius;
float3 g_EyePosW;
float g_Pad;
} 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.hlsli" VertexPosColor VS(VertexPosColor vIn)
{
return vIn;
}

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

// TriangleSO_GS.hlsl
#include "Basic.hlsli" [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.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_PS.hlsl
#include "Basic.hlsli" float4 PS(VertexPosHColor pIn) : SV_Target
{
return pIn.Color;
}

实战2: 绘制分形雪花

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

HLSL代码

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

// SnowSO_VS.hlsl
#include "Basic.hlsli" VertexPosNormalColor VS(VertexPosNormalColor vIn)
{
return vIn;
}

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

// SnowSO_GS.hlsl
#include "Basic.hlsli" [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.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;
}
// Snow_PS.hlsl
#include "Basic.hlsli" float4 PS(VertexPosHColor pIn) : SV_Target
{
return pIn.Color;
}

实战3: 绘制分形圆球

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

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

HLSL代码

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

// SphereSO_VS.hlsl
#include "Basic.hlsli" VertexPosNormalColor VS(VertexPosNormalColor vIn)
{
return vIn;
}
// SphereSO_GS.hlsl

#include "Basic.hlsli"

[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(g_View, g_Proj); [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 = g_SphereCenter + g_SphereRadius * 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.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;
}
// Sphere_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;
}

C++代码的变化

BasicEffect::InitAll方法的变化

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

bool BasicEffect::InitAll(ID3D11Device * device)
{
if (!device)
return false; if (!pImpl->m_pCBuffers.empty())
return true; if (!RenderStates::IsInit())
throw std::exception("RenderStates need to be initialized first!"); 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.cso", L"HLSL\\TriangleSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pTriangleSOVS.GetAddressOf()));
// 创建顶点输入布局
HR(device->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(),
blob->GetBufferSize(), pImpl->m_pVertexPosColorLayout.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\TriangleSO_GS.cso", 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, pImpl->m_pTriangleSOGS.GetAddressOf())); // ******************
// 绘制分形三角形
//
HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pTriangleVS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.cso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pTrianglePS.GetAddressOf())); // ******************
// 流输出分形球体
//
HR(CreateShaderFromFile(L"HLSL\\SphereSO_VS.cso", L"HLSL\\SphereSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSphereSOVS.GetAddressOf()));
// 创建顶点输入布局
HR(device->CreateInputLayout(VertexPosNormalColor::inputLayout, ARRAYSIZE(VertexPosNormalColor::inputLayout), blob->GetBufferPointer(),
blob->GetBufferSize(), pImpl->m_pVertexPosNormalColorLayout.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\SphereSO_GS.cso", 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, pImpl->m_pSphereSOGS.GetAddressOf())); // ******************
// 绘制球体
//
HR(CreateShaderFromFile(L"HLSL\\Sphere_VS.cso", L"HLSL\\Sphere_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSphereVS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\Sphere_PS.cso", L"HLSL\\Sphere_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSpherePS.GetAddressOf())); // ******************
// 流输出分形雪花
//
HR(CreateShaderFromFile(L"HLSL\\SnowSO_VS.cso", L"HLSL\\SnowSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSnowSOVS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\SnowSO_GS.cso", 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, pImpl->m_pSnowSOGS.GetAddressOf())); // ******************
// 绘制雪花
//
HR(CreateShaderFromFile(L"HLSL\\Snow_VS.cso", L"HLSL\\Snow_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSnowVS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\Snow_PS.cso", L"HLSL\\Snow_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSnowPS.GetAddressOf())); // ******************
// 绘制法向量
//
HR(CreateShaderFromFile(L"HLSL\\Normal_VS.cso", L"HLSL\\Normal_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pNormalVS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\Normal_GS.cso", L"HLSL\\Normal_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pNormalGS.GetAddressOf()));
HR(CreateShaderFromFile(L"HLSL\\Normal_PS.cso", L"HLSL\\Normal_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pNormalPS.GetAddressOf())); pImpl->m_pCBuffers.assign({
&pImpl->m_CBFrame,
&pImpl->m_CBOnResize,
&pImpl->m_CBRarely}); // 创建常量缓冲区
for (auto& pBuffer : pImpl->m_pCBuffers)
{
HR(pBuffer->CreateBuffer(device));
} return true;
}

BasicEffect::SetRenderSplitedTriangle方法--绘制分形三角形

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

void BasicEffect::SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext)
{
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer* nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get());
deviceContext->VSSetShader(pImpl->m_pTriangleVS.Get(), nullptr, 0); deviceContext->GSSetShader(nullptr, nullptr, 0); deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(pImpl->m_pTrianglePS.Get(), nullptr, 0);
}

BasicEffect::SetRenderSplitedSnow方法--绘制分形雪花

void BasicEffect::SetRenderSplitedSnow(ID3D11DeviceContext * deviceContext)
{
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer* nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get());
deviceContext->VSSetShader(pImpl->m_pSnowVS.Get(), nullptr, 0); deviceContext->GSSetShader(nullptr, nullptr, 0); deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(pImpl->m_pSnowPS.Get(), nullptr, 0);
}

BasicEffect::SetRenderSplitedSphere方法--绘制分形球体

void BasicEffect::SetRenderSplitedSphere(ID3D11DeviceContext * deviceContext)
{
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer* nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get());
deviceContext->VSSetShader(pImpl->m_pSphereVS.Get(), nullptr, 0); deviceContext->GSSetShader(nullptr, nullptr, 0); deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(pImpl->m_pSpherePS.Get(), nullptr, 0); }

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

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

void BasicEffect::SetStreamOutputSplitedTriangle(ID3D11DeviceContext * deviceContext, ID3D11Buffer * vertexBufferIn, ID3D11Buffer * vertexBufferOut)
{
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer * nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetInputLayout(nullptr);
deviceContext->SOSetTargets(0, nullptr, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get()); deviceContext->IASetVertexBuffers(0, 1, &vertexBufferIn, &stride, &offset); deviceContext->VSSetShader(pImpl->m_pTriangleSOVS.Get(), nullptr, 0);
deviceContext->GSSetShader(pImpl->m_pTriangleSOGS.Get(), nullptr, 0); deviceContext->SOSetTargets(1, &vertexBufferOut, &offset); deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(nullptr, nullptr, 0); }

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

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

void BasicEffect::SetStreamOutputSplitedSnow(ID3D11DeviceContext * deviceContext, ID3D11Buffer * vertexBufferIn, ID3D11Buffer * vertexBufferOut)
{
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer * nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get());
deviceContext->IASetVertexBuffers(0, 1, &vertexBufferIn, &stride, &offset); deviceContext->VSSetShader(pImpl->m_pSnowSOVS.Get(), nullptr, 0);
deviceContext->GSSetShader(pImpl->m_pSnowSOGS.Get(), nullptr, 0); deviceContext->SOSetTargets(1, &vertexBufferOut, &offset); deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(nullptr, nullptr, 0); }

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

void BasicEffect::SetStreamOutputSplitedSphere(ID3D11DeviceContext * deviceContext, ID3D11Buffer * vertexBufferIn, ID3D11Buffer * vertexBufferOut)
{
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段
UINT stride = sizeof(VertexPosNormalColor);
UINT offset = 0;
ID3D11Buffer * nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get()); deviceContext->IASetVertexBuffers(0, 1, &vertexBufferIn, &stride, &offset); deviceContext->VSSetShader(pImpl->m_pSphereSOVS.Get(), nullptr, 0);
deviceContext->GSSetShader(pImpl->m_pSphereSOGS.Get(), nullptr, 0); deviceContext->SOSetTargets(1, &vertexBufferOut, &offset); deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(nullptr, nullptr, 0); }

GameApp类的变化

ID3D11DeviceContext::DrawAuto方法--绘制未知数目的图元

时隔多月,是时候请回该方法了。

这是一个唯一不需要形参就能绘制的API,它可以根据输入装配阶段绑定的缓冲区(内部存有图元数目的记录)自动进行绘制,它会经过顶点着色阶段并一直到流输出阶段。它可能会继续经过光栅化阶段到输出合并阶段,也可以不经过。

但是它的调用要求如下:

  1. 在输入装配阶段下,输入槽0需要绑定顶点缓冲区
  2. 绑定到输入槽0的顶点缓冲区需要设置绑定标签D3D11_BIND_VERTEX_BUFFERD3D11_BIND_STREAM_OUTPUT
  3. 作为流输出的缓冲区也需要设置上述同样的两个绑定标签
  4. 绑定到输入槽0的顶点缓冲区,需要存在图元数目的内部记录

关于最后一点,一般的顶点缓冲区是不会存在内部记录的。通常要求第一次流输出绘制时使用DrawDrawIndexed系列的方法,而不是DrawAuto来绘制,这样流输出缓冲区在产生运行结果的同时,其内部还会产生图元数目的记录。这样在后续的调用中,你就可以将该流输出缓冲区作为输入,然后提供另一个流输出缓冲区作为输出,最后调用DrawAuto来正确绘制了。

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

首先我们只需要给1阶的顶点缓冲区使用指定三角形的三个顶点,然后后续阶数的顶点缓冲区就根据上一阶产出的顶点缓冲区进行"绘制"。经过6次Draw方法的调用后,里面的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; // 这里需要允许流输出阶段通过GPU写入
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(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); // 三角形顶点数
m_InitVertexCounts = 3;
// 初始化所有顶点缓冲区
for (int i = 1; i < 7; ++i)
{
vbd.ByteWidth *= 3;
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[i].ReleaseAndGetAddressOf()));
m_BasicEffect.SetStreamOutputSplitedTriangle(m_pd3dImmediateContext.Get(), m_pVertexBuffers[i - 1].Get(), m_pVertexBuffers[i].Get());
// 第一次绘制需要调用一般绘制指令,之后就可以使用DrawAuto了
if (i == 1)
{
m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0);
}
else
{
m_pd3dImmediateContext->DrawAuto();
} }
}

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 (VertexPosColor& v : vertices)
{
v.pos.x *= 3;
v.pos.y *= 3;
} // 设置顶点缓冲区描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_DEFAULT; // 这里需要允许流输出阶段通过GPU写入
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(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); // 顶点数
m_InitVertexCounts = 6;
// 初始化所有顶点缓冲区
for (int i = 1; i < 7; ++i)
{
vbd.ByteWidth *= 4;
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[i].ReleaseAndGetAddressOf()));
m_BasicEffect.SetStreamOutputSplitedSnow(m_pd3dImmediateContext.Get(), m_pVertexBuffers[i - 1].Get(), m_pVertexBuffers[i].Get());
// 第一次绘制需要调用一般绘制指令,之后就可以使用DrawAuto了
if (i == 1)
{
m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0);
}
else
{
m_pd3dImmediateContext->DrawAuto();
}
}
}

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; // 这里需要允许流输出阶段通过GPU写入
vbd.ByteWidth = (UINT)(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(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); //#if defined(DEBUG) | defined(_DEBUG)
// ComPtr<IDXGraphicsAnalysis> graphicsAnalysis;
// HR(DXGIGetDebugInterface1(0, __uuidof(graphicsAnalysis.Get()), reinterpret_cast<void**>(graphicsAnalysis.GetAddressOf())));
// graphicsAnalysis->BeginCapture();
//#endif // 顶点数
m_InitVertexCounts = 24;
// 初始化所有顶点缓冲区
for (int i = 1; i < 7; ++i)
{
vbd.ByteWidth *= 4;
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[i].ReleaseAndGetAddressOf()));
m_BasicEffect.SetStreamOutputSplitedSphere(m_pd3dImmediateContext.Get(), m_pVertexBuffers[i - 1].Get(), m_pVertexBuffers[i].Get());
// 第一次绘制需要调用一般绘制指令,之后就可以使用DrawAuto了
if (i == 1)
{
m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0);
}
else
{
m_pd3dImmediateContext->DrawAuto();
}
}
}

GameApp::DrawScene方法的变化

同理,在进行正常绘制的时候,由于索引1到6的顶点缓冲区内部都记录了图元数目,也可以直接用DrawAuto方法绘制到屏幕上。

void GameApp::DrawScene()
{
assert(m_pd3dImmediateContext);
assert(m_pSwapChain); m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); // 根据当前绘制模式设置需要用于渲染的各项资源
if (m_ShowMode == Mode::SplitedTriangle)
{
m_BasicEffect.SetRenderSplitedTriangle(m_pd3dImmediateContext.Get());
}
else if (m_ShowMode == Mode::SplitedSnow)
{
m_BasicEffect.SetRenderSplitedSnow(m_pd3dImmediateContext.Get());
}
else if (m_ShowMode == Mode::SplitedSphere)
{
m_BasicEffect.SetRenderSplitedSphere(m_pd3dImmediateContext.Get());
} // 设置线框/面模式
if (m_IsWireFrame)
{
m_pd3dImmediateContext->RSSetState(RenderStates::RSWireframe.Get());
}
else
{
m_pd3dImmediateContext->RSSetState(nullptr);
} // 应用常量缓冲区的变更
m_BasicEffect.Apply(m_pd3dImmediateContext.Get());
// 除了索引为0的缓冲区缺少内部图元数目记录,其余都可以使用DrawAuto方法
if (m_CurrIndex == 0)
{
m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0);
}
else
{
m_pd3dImmediateContext->DrawAuto();
} // 绘制法向量
if (m_ShowNormal)
{
m_BasicEffect.SetRenderNormal(m_pd3dImmediateContext.Get());
m_BasicEffect.Apply(m_pd3dImmediateContext.Get());
// 除了索引为0的缓冲区缺少内部图元数目记录,其余都可以使用DrawAuto方法
if (m_CurrIndex == 0)
{
m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0);
}
else
{
m_pd3dImmediateContext->DrawAuto();
}
} // ******************
// 绘制Direct2D部分
//
if (m_pd2dRenderTarget != nullptr)
{
m_pd2dRenderTarget->BeginDraw();
std::wstring text = L"切换分形:Q-三角形(面/线框) W-雪花(线框) E-球(面/线框)\n"
L"主键盘数字1 - 7:分形阶数,越高越精细\n"
L"M-面/线框切换\n\n"
L"当前阶数: " + std::to_wstring(m_CurrIndex + 1) + L"\n"
"当前分形: ";
if (m_ShowMode == Mode::SplitedTriangle)
text += L"三角形";
else if (m_ShowMode == Mode::SplitedSnow)
text += L"雪花";
else
text += L"球"; if (m_IsWireFrame)
text += L"(线框)";
else
text += L"(面)"; if (m_ShowMode == Mode::SplitedSphere)
{
if (m_ShowNormal)
text += L"(N-关闭法向量)";
else
text += L"(N-开启法向量)";
} m_pd2dRenderTarget->DrawTextW(text.c_str(), (UINT32)text.length(), m_pTextFormat.Get(),
D2D1_RECT_F{ 0.0f, 0.0f, 600.0f, 200.0f }, m_pColorBrush.Get());
HR(m_pd2dRenderTarget->EndDraw());
} HR(m_pSwapChain->Present(0, 0)); }

现在来看一下动图感受一些这些酷炫的效果吧:

分形三角形绘制效果

分形雪花绘制效果

分形圆球绘制效果

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

下面是带法向量的:

遗留问题

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

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--16 流输出阶段的更多相关文章

  1. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  2. DirectX11 With Windows SDK--00 目录

    前言 (更新于 2019/4/10) 从第一次接触DirectX 11到现在已经有将近两年的时间了.还记得前年暑假被要求学习DirectX 11,在用龙书的源码配置项目运行环境的时候都花了好几天的时间 ...

  3. DirectX11 With Windows SDK--35 粒子系统

    前言 在这一章中,我们主要关注的是如何模拟一系列粒子,并控制它们运动.这些粒子的行为都是类似的,但它们也带有一定的随机性.这一堆粒子的几何我们叫它为粒子系统,它可以被用于模拟一些比较现象,如:火焰.雨 ...

  4. DirectX11 With Windows SDK--33 曲面细分阶段(Tessellation)

    前言 曲面细分是Direct3D 11带来的其中一项重要的新功能.它引入了两个可编程着色器阶段以及一个固定的镶嵌处理过程.简单来说,曲面细分技术可以将几何体细分为更小的三角形,并以某种方式把这些新生成 ...

  5. DirectX11 With Windows SDK--26 计算着色器:入门

    前言 现在开始迎来所谓的高级篇了,目前计划是计算着色器部分的内容视项目情况,大概会分3-5章来讲述. DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群 ...

  6. DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果

    前言 上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区.接下来我们继续利用它来实现一些新的效果,在这一章,你将了解: 实现公告板效果 Alpha-To-Coverage 对 ...

  7. DirectX11 With Windows SDK--20 硬件实例化与视锥体裁剪

    前言 这一章将了解如何在DirectX 11利用硬件实例化技术高效地绘制重复的物体,以及使用视锥体裁剪技术提前将位于视锥体外的物体进行排除. 在此之前需要额外了解的章节如下: 章节回顾 18 使用Di ...

  8. DirectX11 With Windows SDK--06 DirectXMath数学库

    前言 xnamath.h原本是位于DirectX SDK的一个数学库,但是现在Windows SDK包含的数学库已经抛弃掉原来的xnamath.h,并演变成了现在的DirectXMath.h.其实本质 ...

  9. DirectX11 With Windows SDK--07 添加光照与常用几何模型

    前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的光照模型.除此之外,该项目还用到了多个常量缓冲区,因此还会提及HLSL的常量缓冲区打包规则以及如何设置多 ...

随机推荐

  1. Python(五)模块

    本章内容: 模块介绍 time & datetime random os sys json & picle hashlib XML requests ConfigParser logg ...

  2. 一篇文章搞定mongodb

    一 安装 1 安装目录下新建文件夹data,etc,logs #在bin文件下启动cmd,指定数据存储的路径mongod --dbpath D:\MongoDB\data\db 2 etc文件夹中新建 ...

  3. Django 路由层(urlconf)

    Django 的路由层(URLconf) URL配置(conf)就像是Django所支撑的网站的目录; 本质就是:URL与调用该URL执行的视图函数的映射表; 通俗的讲:就是用户使用哪个url,URL ...

  4. Django 【orm】或

    方式一: q=Q() q.connection="or" q.children.append(("pk",1)) q.children.append((&quo ...

  5. 3.18 总结 java 基础语法

  6. 百度杯”CTF比赛 九月场 123

    进去后让登录,先看源码有提示 进到user.php 后发现是空的,看了wp才知道,有bak 下载下来直接爆破 但是那个1990是蛮骚的 直接进去登录 登录成功后是空的,走fd看看是怎么过 的 改包然后 ...

  7. 一本通 1223:An Easy Problem

    \[传送门qwq\] [题目描述] 给定一个正整数N,求最小的.比N大的正整数M,使得M与N的二进制表示中有相同数目的1. 举个例子,假如给定的N为78,其二进制表示为1001110,包含4个1,那么 ...

  8. PHP利用MySQL保存session

    实现环境: PHP 5.4.24 MySQL 5.6.19 OS X 10.9.4/Apache 2.2.26 一.代码 CREATE TABLE `session` ( `skey` ) CHARA ...

  9. P1577 切绳子

    P1577 切绳子 题目描述 有N条绳子,它们的长度分别为Li.如果从它们中切割出K条长度相同的 绳子,这K条绳子每条最长能有多长?答案保留到小数点后2位. 输入输出格式 输入格式: 第一行两个整数N ...

  10. 关于JavaScript闭包的粗浅理解

    在JavaScript中,使用var创建变量,会创建全局变量或局部变量. 只有在非函数内创建的变量,才是全局变量,该变量可以在任何地方被读取. 而在函数内创建变量时,只有在函数内部才可读取.在函数外部 ...