第二十一章 Geometry and Tessellation Shaders

来源:互联网 发布:鹏业软件格式 编辑:程序博客网 时间:2024/05/28 03:01

第二十一章 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的第二个参数是一个StreamOutputObject<T>类型的数据,并且总是以input修饰符开头。其中stream-output对象是一个模板数据类型,可以使用三种类型表示:PointStream,LineStream和TriangleStream,对应于要用输出的primitive类型。PointStream类型对应于point list topology(这种拓扑结构的输出是一组points),而LineStream和TriangleStream类型分别对应于line和triangle strips。

一个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 spaceOUT.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 billboardingcylindrical 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-leftfloat2(0.0f, 0.0f), // v1, upper-leftfloat2(1.0f, 0.0f), // v2, upper-rightfloat2(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-leftvertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f); // upper-leftvertices[2] = float4(IN[0].Position.xyz - offsetX + offsetY, 1.0f); // upper-rightvertices[3] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f); // lower-right// tri: 0, 1, 2OUT.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, 3OUT.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-leftvertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f); // upper-leftvertices[2] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f); // lower-rightvertices[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 materialmEffect = 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 pointsstd::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中显示了该示例程序的输出结果。


图21.1 Output of the geometry shader demo.

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-leftvertices[1] = float4(IN[0].Position.xyz + offsetX + offsetY, 1.0f); // upper-leftvertices[2] = float4(IN[0].Position.xyz - offsetX - offsetY, 1.0f); // lower-rightvertices[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所示的结果。


图21.2 Output of the geometry shader demo, with sizes based on the primitive ID.
另外,在vertex shader中也支持使用SV_VertexID semantic创建一个ID,实现类型的功能。在geometry和vertex shader中并不需要强制使用这两个对应的IDs。我们可以从一个shader阶段的输出中包含一个ID,并传递给下一个阶段的输入中。
2 0
原创粉丝点击