第二十一章 Geometry and Tessellation Shaders

第二十一章 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 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 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-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中显示了该示例程序的输出结果。


图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-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所示的结果。


图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,并传递给下一个阶段的输入中。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值