第二十一章 Geometry and Tessellation Shaders
本章,我们将会使用Direct3D图形管线中两个新增的阶段:geometry和tessellation shaders。这两个管线阶段支持动态创建vertices,甚至在硬件层改变一个surface的topology(基本几何图元的拓扑结构)。此外,我们还会深入学习这两个管线阶段的工作流程,并通过一些示例程序实现各种有趣的显示效果。Motivation:Geometry Shaders
Geometry shaders支持在图形管线增加和删除geometry。这种功能与我们之前所讨论的管线阶段完全不同,并可以用于实现一些非常有趣的应用程序。例如,把lower-fidelity(低精度,也就是vertices数量较少)的geometry发送到管线中,在geometry shader可以描绘出更多的vertices。与些相反,geometry shader还可以进一步处理geometry,去除geometry中的全部或部分primitives(也就是把通过减少geometry中vertices数量来减少primitives)。Processing Primitives
在vertex shader和pixel shader中分别处理单个vertices和单个pixels,而geometry shader则是处理完整的primitives。回顾一下以前所讨论的三种基本primitive:points,lines和triangles(尽管可以把这三种pimitive组合成lists和strips,还可以包含adjacency数据,详见第1章)。编写一个geometry shader程序时,需要使用以下关键字:point,line,triangle,lineadj和triangleadj指定要处理的primitive类型。这是geometry和vertex或pixel shader之间不同语法中的其中一点。以下列出了一个geometry shader示例的声明代码:
[maxvertexcount(3)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_
OUTPUT> triStream) { /*shader body*/ }
其中,maxvertexcount属性值指定了geometry shader能够输出的vertices数量的最大值(在这个示例,该值为3)。该shader的第一个参数定义了primitive的类型(point),输入数据的结构体(VS_OUTPUT),以及每一次执行geometry shader时处理的vertices数量([1])。VS_OUTPUT数据类型也就是vertex shader output所采用的命名约定,可以用于表示任意的HLSL数据类型。由于在图形管线中geometry shader位于vertex shader的下一个阶段(在不包含tessellation shaders的情况下),所以vertex shader的输出数据就成为了geometry shader输入数据。而geometry sahder位于vertex和pixel shader之间,因此geometry shader的输出数据(命名约定为GS_OUTPUT)又成为pixel shader的输入数据。
对于geometry shaderr的第一个参数中[n] array-style syntax(数组语法格式),可以根据表格21.1指定各种不同的primitive类型。
表格21.1 The Array Size of Geometry Shader Input, According to Primitive Type
一个geometry shader的基础功能是处理输入的pimitives,并把vertices添加到stream-output对象中。调用StreamOutputObject<T>::Append()函数可以添加vertices。在前面列出的geometry shader示例基础上,我们把该shader扩展为接收point primitives,并输出一个triangle stream,该stream最多包含3个vertices(每次执行geometry shader时该stream都只输出一个triangle)。以下代码演示了该geometry shader的基本的框架:
struct VS_OUTPUT
{
float4 ObjectPosition : POSITION;
};
struct GS_OUTPUT
{
float4 Position : SV_Position;
};
[maxvertexcount(3)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_
OUTPUT> triStream)
{
GS_OUTPUT OUT = (GS_OUTPUT)0;
for (int i = 0; i < 3; i++)
{
// Some modification of the input point, followed by transformation into homogeneous - clip space
OUT.Position = triStream.Append(OUT);
}
}
A Point Sprite Shader
Geometry shader的一个常见应用是point sprite expansion(渲染大量点精灵)。一个 point sprite,允许你在仅仅指定一个vertex(或一个point)的情况下渲染一个纹理四边形(或其他形状的纹理)。其中point表示四边形的中心点,geometry shader创建4个围绕该point的vertices,用于生成四边形。四边形可以共用一个固定的尺寸大小,也可以对每一个vertex指定一个自定义的尺寸。此外,这种系统可以用于单个texture(每一个四边形都使用同样的贴图),也可以用于一个texture数组(每一个vertex指定了访问数组的索引值)。列表21.1中列出了一种point sprite shader的代码,在该shader中使用了一个固定的texture,并对每一个vertex对应的四边形指定不同的尺寸。该shader中以 billboard(广告牌)方法展示四边形;这种摆放位置使得四边形总是面向camera。有两种常用的billboarding技术: spherical billboarding和 cylindrical billboarding。其中,spherical表示不管绕哪个轴旋转都要把object朝向camera;cylindrical表示把object限制到只围绕一个轴旋转(该示例中,绕y轴)。Cylindrical billboarding技术在模拟树木模型的情况下非常有用,例如,要让一个四边形树木纹理看起来像是一个3D树木模型,需要把每一个树木“种植”到地面上并围绕y轴旋转,使得树木始终朝向camera。列表21.1中所列出的shader中使用了spherical billboarding技术。列表21.1 A Spherical Billboarding Point Sprite Shader
/************* Resources *************/
static const float2 QuadUVs[4] = { float2(0.0f, 1.0f), // v0, lower-left
float2(0.0f, 0.0f), // v1, upper-left
float2(1.0f, 0.0f), // v2, upper-right
float2(1.0f, 1.0f) // v3, lower-right
};
cbuffer CBufferPerFrame
{
float3 CameraPosition : CAMERAPOSITION;
float3 CameraUp;
}
cbuffer CBufferPerObject
{
float4x4 ViewProjection;
}
Texture2D ColorTexture;
SamplerState ColorSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
/************* Data Structures *************/
struct VS_INPUT
{
float4 Position : POSITION;
float2 Size : SIZE;
};
struct VS_OUTPUT
{
float4 Position : POSITION;
float2 Size : SIZE;
};
struct GS_OUTPUT
{
float4 Position : SV_Position;
float2 TextureCoordinate : TEXCOORD;
};
/************* Vertex Shader *************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
VS_OUTPUT OUT = (VS_OUTPUT)0;
OUT.Position = IN.Position;
OUT.Size = IN.Size;
return OUT;
}
/************* Geometry Shader *************/
[maxvertexcount(6)]
void geometry_shader(point VS_OUTPUT IN[1], inout TriangleStream<GS_OUTPUT> triStream)
{
GS_OUTPUT OUT = (GS_OUTPUT)0;
float2 halfSize = IN[0].Size / 2.0f;
float3 direction = CameraPosition - IN[0].Position.xyz;
float3 right = cross(normalize(direction), CameraUp);
float3 offsetX = halfSize.x * right;
float3 offsetY = halfSize.y * CameraUp;
float4 vertices[4];
vertices[0] = float4(IN[0].Position.xyz + offsetX - offsetY, 1.0f); // lower-left
vertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f); // upper-left
vertices[2] = float4(IN[0].Position.xyz - offsetX + offsetY, 1.0f); // upper-right
vertices[3] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f); // lower-right
// tri: 0, 1, 2
OUT.Position = mul(vertices[0], ViewProjection);
OUT.TextureCoordinate = QuadUVs[0];
triStream.Append(OUT);
OUT.Position = mul(vertices[1], ViewProjection);
OUT.TextureCoordinate = QuadUVs[1];
triStream.Append(OUT);
OUT.Position = mul(vertices[2], ViewProjection);
OUT.TextureCoordinate = QuadUVs[2];
triStream.Append(OUT);
triStream.RestartStrip();
// tri: 0, 2, 3
OUT.Position = mul(vertices[0], ViewProjection);
OUT.TextureCoordinate = QuadUVs[0];
triStream.Append(OUT);
OUT.Position = mul(vertices[2], ViewProjection);
OUT.TextureCoordinate = QuadUVs[2];
triStream.Append(OUT);
OUT.Position = mul(vertices[3], ViewProjection);
OUT.TextureCoordinate = QuadUVs[3];
triStream.Append(OUT);
}
/************* Pixel Shader *************/
float4 pixel_shader(GS_OUTPUT IN) : SV_Target
{
return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
}
/************* Techniques *************/
technique11 main11
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(CompileShader(gs_5_0, geometry_shader()));
SetPixelShader(CompileShader(ps_5_0, pixel_shader()));
}
}
为了重点关注geometry shader,在该示例中没有使用任意光照模型。在pixel shader中只是简单的采样color texture。同样,vertex shader也是极其简单,仅仅是输出vertex的position和四边形的尺寸(quad size)。该示例的主要处理过程都在geometry shader中完成,该shader中接收单个point输入数据,构建4个vertices,然后一共输出6个vertices(triangle stream中包含两个triangles,有两个重复的vertices)。由于输入参数point表示四边形的中心位置,因此使用四边形所在平面的水平和垂直方向尺寸的一半,计算包围该中点的四个vertex坐标位置。要计算用于表示平面角度(以camera为参照物)的向量,需要先计算从中心点到camera的方向向量(该方向向量可以作为该平面的法向量)。在这个示例中,中心坐标点已经位于world space中,因此在该中心点与camera坐标点执行运算之前不需要进行变换操作。正交表面向量(又称为right向量)由方向向量和camera的up向量经过cross product运算得到。四边形平面的4个vertices的坐标位置,是沿着right向量在水平和垂直方向的偏移计算得到。 最后,把这两个triangles的vertices positions变换到homogeneous clip space中,并设置texture coordinates,再添加到stream-output对象中。其中调用了StreamOutputObject<T>::RestartStrip()函数。前面讲过LineStream和TriangleStream类型输出的topology为strips,但是通过d在objects之间restart strip可以模拟一个primitives list。另外,使用列表21.2所示的代码重写该shader,可以输出一个triangle strip。
列表21.2 Updated Point Sprite Geometry Shader Using Triangle Strips
[maxvertexcount(4)]
void geometry_shader_strip(point VS_OUTPUT IN[1], inout TriangleStream<GS_OUTPUT> triStream)
{
GS_OUTPUT OUT = (GS_OUTPUT)0;
float2 halfSize = IN[0].Size / 2.0f;
float3 direction = CameraPosition - IN[0].Position.xyz;
float3 right = cross(normalize(direction), CameraUp);
float3 offsetX = halfSize.x * right;
float3 offsetY = halfSize.y * CameraUp;
float4 vertices[4];
vertices[0] = float4(IN[0].Position.xyz + offsetX - offsetY, 1.0f); // lower-left
vertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f); // upper-left
vertices[2] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f); // lower-right
vertices[3] = float4(IN[0].Position.xyz - offsetX + offsetY, 1.0f); // upper-right
[unroll]
for (int i = 0; i < 4; i++)
{
OUT.Position = mul(vertices[i], ViewProjection);
OUT.TextureCoordinate = QuadStripUVs[i];
triStream.Append(OUT);
}
}
注意一下在两次遍历过程之间vertex的顺序是如何变化以满足triangle strip。 在CPU端的应用程序中使用geometry shader并不需要特别的编译设计。在geometry shader的示例程序中(本书配套网站上提供了完整代码),创建了一组随机的points和quad尺寸,并使用列表21.3所示的代码进行渲染。
列表21.3 Initialization and Rendering for the Geometry Shader Demo
void GeometryShaderDemo::Initialize()
{
SetCurrentDirectory(Utility::ExecutableDirectory().c_str());
// Initialize the material
mEffect = new Effect(*mGame);
mEffect->LoadCompiledEffect(L"Assets\\Effects\\PointSprite.cso");
mMaterial = new PointSpriteMaterial();
mMaterial->Initialize(mEffect);
Technique* technique = mEffect->TechniquesByName().at("main11");
mMaterial->SetCurrentTechnique(technique);
mPass = mMaterial->CurrentTechnique()->Passes().at(0);
mInputLayout = mMaterial->InputLayouts().at(mPass);
UINT maxPoints = 100;
float maxDistance = 10;
float minSize = 2;
float maxSize = 2;
std::random_device randomDevice;
std::default_random_engine randomGenerator(randomDevice());
std::uniform_real_distribution<float> distanceDistribution(-maxDistance, maxDistance);
std::uniform_real_distribution<float> sizeDistribution(minSize, maxSize);
// Randomly generate points
std::vector<VertexPositionSize> vertices;
vertices.reserve(maxPoints);
for (UINT i = 0; i < maxPoints; i++)
{
float x = distanceDistribution(randomGenerator);
float y = distanceDistribution(randomGenerator);
float z = distanceDistribution(randomGenerator);
float size = sizeDistribution(randomGenerator);
vertices.push_back(VertexPositionSize(XMFLOAT4(x, y, z, 1.0f), XMFLOAT2(size, size)));
}
mVertexCount = vertices.size();
ReleaseObject(mVertexBuffer);
mMaterial->CreateVertexBuffer(mGame->Direct3DDevice(), &vertices[0], mVertexCount, &mVertexBuffer);
std::wstring textureName = L"Assets\\Textures\\BookCover.png";
HRESULT hr = DirectX::CreateWICTextureFromFile(mGame->Direct3DDevice(), mGame->Direct3DDeviceContext(), textureName.c_str(), nullptr, &mColorTexture);
if (FAILED(hr))
{
throw GameException("CreateWICTextureFromFile() failed.", hr);
}
mKeyboard = (Keyboard*)mGame->Services().GetService(Keyboard::TypeIdClass());
assert(mKeyboard != nullptr);
mSpriteBatch = new SpriteBatch(mGame->Direct3DDeviceContext());
mSpriteFont = new SpriteFont(mGame->Direct3DDevice(), L"Assets\\Fonts\\Arial_14_Regular.spritefont");
}
void GeometryShaderDemo::Draw(const GameTime& gameTime)
{
ID3D11DeviceContext* direct3DDeviceContext = mGame->Direct3DDeviceContext();
direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
direct3DDeviceContext->IASetInputLayout(mInputLayout);
UINT stride = mMaterial->VertexSize();
UINT offset = 0;
direct3DDeviceContext->IASetVertexBuffers(0, 1, &mVertexBuffer, &stride, &offset);
mMaterial->ViewProjection() << mCamera->ViewMatrix() * mCamera->ProjectionMatrix();
mMaterial->CameraPosition() << mCamera->PositionVector();
mMaterial->CameraUp() << mCamera->UpVector();
mMaterial->ColorTexture() << mColorTexture;
mPass->Apply(0, direct3DDeviceContext);
direct3DDeviceContext->Draw(mVertexCount, 0);
direct3DDeviceContext->GSSetShader(nullptr, nullptr, 0);
}
图21.1中显示了该示例程序的输出结果。
Primitive IDs
经过geometry shader的每一个primitive都可以使用一个唯一标识符进行标记(是指每一次绘制时是唯一的,但在再次绘制之间并不是唯一的)。这种标识方法是通过把SV_PrimitiveID semantic关联到一个无符号整形数实现的。使用这种方法可以产生一些有趣的effects。在列表21.4所示geometry shader中,使用primitive ID生成每一个四边形的尺寸大小,而不是从CPU应用程序中传递每一个vertex的尺寸。列表21.4 Example of SV_PrimitiveID Usage
[maxvertexcount(4)]
void geometry_shader_nosize(point VS_NOSIZE_OUTPUT IN[1], uint primitiveID : SV_PrimitiveID, inout TriangleStream<GS_OUTPUT> triStream)
{
GS_OUTPUT OUT = (GS_OUTPUT)0;
float size = primitiveID + 1.0f;
float2 halfSize = size / 2.0f;
float3 direction = CameraPosition - IN[0].Position.xyz;
float3 right = cross(normalize(direction), CameraUp);
float3 offsetX = halfSize.x * right;
float3 offsetY = halfSize.y * CameraUp;
float4 vertices[4];
vertices[0] = float4(IN[0].Position.xyz + offsetX - offsetY, 1.0f); // lower-left
vertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f); // upper-left
vertices[2] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f); // lower-right
vertices[3] = float4(IN[0].Position.xyz - offsetX + offsetY, 1.0f); // upper-right
[unroll]
for (int i = 0; i < 4; i++)
{
OUT.Position = mul(vertices[i], ViewProjection);
OUT.TextureCoordinate = QuadStripUVs[i];
triStream.Append(OUT);
}
}
如果在应用程序中更新创建vertex buffer的代码(沿着x轴以固定间隔产生points),该shader的就会输出如图21.2所示的结果。
另外,在vertex shader中也支持使用SV_VertexID semantic创建一个ID,实现类型的功能。在geometry和vertex shader中并不需要强制使用这两个对应的IDs。我们可以从一个shader阶段的输出中包含一个ID,并传递给下一个阶段的输入中。