假设我们不使用细分阶段,几何着色器阶段是位于顶点和像素着色器阶段之间的可选阶段。顶点着色器输入顶点,几何着色器输入整个图元。例如,如果我们绘制三角形列表,则几何着色器程序将为列表中的每个三角形T所执行:
for(UINT i = 0; i < numTriangles; ++i)
OutputPrimitiveList = GeometryShader(T[i].vertexList);
请注意,每个三角形的三个顶点都输入到几何着色器中,几何着色器输出一个基元列表。不像顶点着色器不能破坏或创建顶点,几何着色器的主要优点在于它可以创建或破坏几何; 这使得能够在GPU上实现一些有趣的效果。例如,输入图元可以扩展成一个或多个其他图元,或者几何着色器可以选择根据某些条件不输出图元。请注意,输出图元不需要与输入图元类型相同; 例如,几何着色器的常见应用是将点扩展成四边形(两个三角形)。
从几何着色器输出的图元由顶点列表定义。几何着色器输出顶点位置必须被转换到齐次空间。在几何着色器阶段之后,我们有一个在齐次空间中定义图元的顶点列表。这些顶点被投影(均匀分割),然后照常进行光栅化。
目标:
1.学习如何编写几何着色器。
2.了解billboard是如何在几何着色器中高效地实现。
3.识别自动生成的图元ID及其一些应用程序。
4.了解如何创建和使用纹理数组,并了解为什么它们是有用的。
5.了解alpha-to-coverage如何提高alpha裁剪的走样问题。
11.1 几何着色器编程
编程几何着色器很像编程顶点或像素着色器,但有一些差异。以下代码显示了一般形式:
[maxvertexcount(N)]
void ShaderName (
PrimitiveType InputVertexType InputName [NumElements],
inout StreamOutputObject OutputName)
{
// Geometry shader body…
}
我们必须首先指定单次调用几何着色器输出的顶点的最大数量(每个图元调用几何着色器)。这可以通过使用以下属性语法在着色器定义之前设置最大顶点数:
[maxvertexcount(N)]
其中N是几何着色器为单个调用输出的顶点的最大数量。几何着色器可以在每次调用时输出的顶点数量是可变的,但不能超过定义的最大值。出于性能考虑,最大顶点数应尽可能小; [NVIDIA08]指出,当GS输出在1到20个标量之间时,可以实现GS的性能峰值,如果GS输出在27-40个标量之间,则性能下降50%。每次调用的标量输出数是最大顶点输出数和输出顶点类型结构中的标量数的乘积。在实践中使用这样的限制是困难的,所以我们可以接受低于最高性能的数据,或选择不使用几何着色器的替代实现;然而,我们还必须考虑到替代实现可能有其他缺点,这仍然可能使几何着色器实现成为更好的选择。此外,[NVIDIA08]中的建议来自2008年(第一代几何着色器),因此状况应该有了改善。
几何着色器需要两个参数:输入参数和输出参数。 (实际上,它可能需要更多,但这是一个特殊的主题;请参见§11.2.4。)输入参数一般是一个定义了图元的顶点数组 — 一个顶点定义一个点,两个定义一条线,三个定义三角形,四个定义邻接线段,六个定义邻接三角形。 输入顶点的顶点类型是顶点着色器返回的顶点类型(例如,VertexOut)。 输入参数必须以原始类型为前缀,描述输入到几何着色器中的图元的类型。这可以是以下任何一种:
1、point:输入图元是点。
2、line:输入图元是线段(lists或strips)。
3、triangle:输入图元是三角形(lists或strips)。
4、lineadj:输入图元是具有邻接(lists或strips)的线段。
5、triangleadj:输入图元是具有邻接(lists或strips)的三角形。
11.1 几何着色器编程
几何着色器编程很像顶点或像素着色器编程,但有一些差异。以下代码显示了一般形式:
[maxvertexcount(N)]
void ShaderName (
PrimitiveType InputVertexType InputName [NumElements],
inout StreamOutputObject OutputName)
{
// Geometry shader body…
}
我们必须首先指定几何着色器为单次调用输出的顶点的最大数量(每个图元调用几何着色器)。 这可以通过使用以下属性语法在着色器定义之前设置最大顶点数:
[maxvertexcount(N)]
其中N是几何着色器为单个调用输出的顶点的最大数量。几何着色器可以在每次调用时输出的顶点数量是可变的,但不能超过定义的最大值。出于性能考虑,最大顶点数应尽可能小; [NVIDIA08]指出,当GS输出在1-20个单位之间时,GS可以实现峰值性能,如果GS输出在27-40个单位之间,则性能下降到50%。每个调用的标量输出的数量是最大顶点数量和输出顶点类型结构中的标量数的乘积。在实践中使用这样的限制比较困难,所以我们可以接受低于最高性能的设置,或选择不使用几何着色器的替代方案;然而,我们还必须考虑到替代方案可能有其他缺点,这使几何着色器仍然可成为较好的选择。此外,[NVIDIA08]中的建议来自2008年(第一代几何着色器),因此情况应该有所改善。
几何着色器需要两个参数:输入参数和输出参数。(实际上,它可能需要更多,但这是一个特殊的主题;请参见§11.2.4。)输入参数始终是一个顶点数组,它定义图元 —— 1个顶点为一个顶点,2个为线段,3个为三角形,4个是邻接线,6个为邻接三角形。 输入顶点的顶点类型是顶点着色器返回的顶点类型(例如,VertexOut)。 输入参数必须以原始类型为前缀,描述输入到几何着色器中的图元的类型。这可以是以下任何一种:
1.point:输入图元是点。
2.line:输入图元是线(lists或strips)。
3.triangle:输入图元是三角形(lists或strips)。
4. lineadj:输入图元是带有邻接(lists或strips)的线段。
5.triangleadj:输入图元是具有邻接(lists或strips)的三角形。
NOTE:几何着色器中的输入图元总是一个完整的图元(例如,一线段的两个顶点,三角形的三个顶点)。 因此,几何着色器不需要区分lists和strips。 例如,如果您正在绘制三角形strips,则strips的每个三角形仍将执行几何着色器,并将每个三角形的三个顶点作为输入传递到几何着色器中。 这需要额外的开销,因为多个基元共享的顶点在几何着色器中被多次处理。
输出参数始终具有inout修饰符。 此外,输出参数始终是流类型。 流类型存储定义几何着色器输出的几何的顶点列表。 几何着色器使用内在的附加方法将一个顶点添加到输出流列表中:
void StreamOutputObject::Append(OutputVertexType v);
流类型是模板类型,其中模板参数用于指定输出顶点的顶点类型(例如,GrOup)。有三种可能的流类型:
1.PointStream :定义点列表的顶点列表。
2. LineStream :定义线条的顶点列表。
3. TriangleStream :定义三角形条的顶点列表。
几何着色器输出的顶点形成基元; 输出原语的类型由流类型(PointStream,LineStream,TriangleStream)指示。 对于线和三角形,输出原语总是strips。 然而,线和三角形列表可以使用内在的RestartStrip方法进行模拟:
void StreamOutputObject::RestartStrip();
例如,如果要输出三角形列表,那么每次在输出流附加三个顶点后,您都可以调用RestartStrip。
以下是几何着色器签名的一些具体示例:
// EXAMPLE 1: GS ouputs at most 4 vertices. The input primitive is a line.
// The output is a triangle strip.
//
[maxvertexcount(4)]
void GS(line VertexOut gin[2],
inout TriangleStream triStream)
{
// Geometry shader body…
}
//
// EXAMPLE 2: GS outputs at most 32 vertices. The input primitive is
// a triangle. The output is a triangle strip.
//
[maxvertexcount(32)]
void GS(triangle VertexOut gin[3],
inout TriangleStream triStream)
{
// Geometry shader body…
}
//
// EXAMPLE 3: GS outputs at most 4 vertices. The input primitive
// is a point. The output is a triangle strip.
//
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
inout TriangleStream triStream)
{
// Geometry shader body…
}
以下几何着色器说明了Append和RestartStrip方法; 它输入一个三角形,细分它(图11.1),并输出四个细分三角形:
图11.1 将三角形细分为四个相等大小的三角形。 可见三个新的顶点是原始三角形边缘的中点。
struct VertexOut
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct GeoOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
float FogLerp : FOG;
};
void Subdivide(VertexOut inVerts[3], out VertexOut outVerts[6])
{
// 1
// *
// / \
// / \
// m0*- - - - - -*m1
// / \ / \
// / \ / \
//* - - - - - * - - - - - *
//0 m2 2
VertexOut m[3];
// Compute edge midpoints.
m[0].PosL = 0.5f*(inVerts[0].PosL+inVerts[1].PosL);
m[1].PosL = 0.5f*(inVerts[1].PosL+inVerts[2].PosL);
m[2].PosL = 0.5f*(inVerts[2].PosL+inVerts[0].PosL);
// Project onto unit sphere
m[0].PosL = normalize(m[0].PosL);
m[1].PosL = normalize(m[1].PosL);
m[2].PosL = normalize(m[2].PosL);
// Derive normals.
m[0].NormalL = m[0].PosL;
m[1].NormalL = m[1].PosL;
m[2].NormalL = m[2].PosL;
// Interpolate texture coordinates.
m[0].Tex = 0.5f*(inVerts[0].Tex+inVerts[1].Tex);
m[1].Tex = 0.5f*(inVerts[1].Tex+inVerts[2].Tex);
m[2].Tex = 0.5f*(inVerts[2].Tex+inVerts[0].Tex);
outVerts[0] = inVerts[0];
outVerts[1] = m[0];
outVerts[2] = m[2];
outVerts[3] = m[1];
outVerts[4] = inVerts[2];
outVerts[5] = inVerts[1];
};
void OutputSubdivision(VertexOut v[6],
inout TriangleStream triStream)
{
GeoOut gout[6];
[unroll]
for(int i = 0; i < 6; ++i)
{
// Transform to world space space.
gout[i].PosW = mul(float4(v[i].PosL, 1.0f), gWorld).xyz;
gout[i].NormalW = mul(v[i].NormalL,
(float3x3)gWorldInvTranspose);
// Transform to homogeneous clip space.
gout[i].PosH = mul(float4(v[i].PosL, 1.0f), gWorldViewProj);
gout[i].Tex = v[i].Tex;
}
// 1
// *
// / \
// / \
// m0*- - - - - -*m1
// / \ / \
// / \ / \
//* - - - - - * - - - - - *
//0 m2 2
// We can draw the subdivision in two strips:
// Strip 1: bottom three triangles
// Strip 2: top triangle
[unroll]
for(int j = 0; j < 5; ++j)
{
triStream.Append(gout[j]);
}
triStream.RestartStrip();
triStream.Append(gout[1]);
triStream.Append(gout[5]);
triStream.Append(gout[3]);
}
maxvertexcount(8)]
void GS(triangle VertexOut gin[3], inout TriangleStream)
{
VertexOut v[6];
Subdivide(gin, v);
OutputSubdivision(v, triStream);
}
一旦几何着色器已被实现,如果在渲染时使用它,我们必须将它绑定到一个效果过程:
technique11 Tech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(CompileShader(gs_5_0, GS()));
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}
NOTE: 给定一个输入图元,几何着色器可以选择不根据某些条件输出它。 以这种方式,几何体被几何着色器“破坏”,这对于某些算法来说可能是有用的。
NOTE: 如果您不输出足够的顶点来完成几何着色器中的原始元素,那么部分原语将被丢弃。
11.2 树的billboard实例
11.2.1概述
当树木很远时,使用广告牌技术来提高效率。也就是说,不是渲染完全3D树的几何,而是在其上绘制具有3D树的图片的四边形(参见图11.2)。从远处看,你察觉不出使用了广告牌。然而,诀窍是确保广告牌总是面对相机(否则效果就不对了)。
图11.2 一个树状广告牌纹理与alpha通道。
假设y轴向上,xz平面是接地平面,树状广告牌通常与y轴对齐,并且在xz平面内正对照相机。图11.3显示了广告牌“看”照相机时鸟瞰的几个广告牌的局部坐标系。
图11.3 广告牌面向相机。
因此,考虑到世界空间广告牌的中心位置C =(Cx ,Cy ,Cz)和世界空间中相机的位置E =(Ex ,Ey ,Ez),我们有足够的信息来描述相对于世界空间的广告牌的局部坐标系:
w=((E_x-x_x ,0 ,E_z-C_z ))/‖(E_x-x_x ,0 ,E_z-C_z )‖
V=(0 , 1 , 0)
u=v×w
给定广告牌相对于世界空间的局部坐标系和世界空间中广告牌的大小,可以如下获得广告牌四角顶点(见图114):
v[0] = float4(gin[0].CenterW + halfWidth*right - halfHeight*up, 1.0f);
v[1] = float4(gin[0].CenterW + halfWidth*right + halfHeight*up, 1.0f);
v[2] = float4(gin[0].CenterW - halfWidth*right - halfHeight*up, 1.0f);
v[3] = float4(gin[0].CenterW - halfWidth*right + halfHeight*up, 1.0f);
请注意,广告牌的局部坐标系对于每个广告牌不同,因此必须为每个广告牌计算。
对于此演示,我们将构建稍微高于陆地质量的点基元列表(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST)。 这些点代表了我们想要绘制的广告牌的中心。 在几何着色器中,我们将这些点扩展为广告牌四边形。另外,我们将在几何着色器中计算广告牌的世界矩阵。图11.5显示了演示的截图。
图11.4 从本地坐标系统和世界空间中广告牌的大小计算广告牌四角顶点。
图11.5 “树广告牌”演示的屏幕截图。
如图11.5所示,此示例构建了第9章中的“Blend”演示。
NOTE: 广告牌的常见CPU实现将是在动态顶点缓冲区中每个广告牌使用四个顶点。每次相机移动时,将使用ID3D11DeviceContext :: Map方法在CPU上更新顶点,以便广告牌面向相机。这种方法必须将每个广告牌的四个顶点提交到IA阶段,并且需要更新具有开销的动态顶点缓冲区。使用几何着色器方法,我们可以使用静态顶点缓冲区,因为几何着色器会进行广告牌扩展,并使广告牌面向相机。此外,广告牌的内存占用相当小,因为我们只需要将每个广告牌的一个顶点提交到IA阶段。
11.2.2顶点结构
我们为我们的广告牌点使用以下顶点结构:
struct TreePointSprite
{
XMFLOAT3 Pos;
XMFLOAT2 Size;
};
const D3D11_INPUT_ELEMENT_DESC InputLayoutDesc::TreePointSprite[2] =
{
{“POSITION”, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{“SIZE”, 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,
D3D11_INPUT_PER_VERTEX_DATA, 0}
};
顶点存储表示广告牌在世界空间中的中心位置的点。 它还包括一个尺寸成员,它存储广告牌的宽度/高度(按世界空间单位); 这使几何着色器知道扩展后广告牌的大小(图11.6)。通过使每个顶点的尺寸变化,我们可以轻松地允许不同尺寸的广告牌。
除了纹理数组(§11.2.4)之外,“Tree Billboard”演示中的其他C ++代码现在应该是常规的Direct3D代码(创建顶点缓冲区,效果,调用绘制方法等)。 因此,我们现在将注意力转到TreeSprite.fx文件。
11.2.3效果文件
因为这是我们第一个使用几何着色器的演示,我们将在此显示整个效果文件,以便您可以看到它如何与顶点和像素着色器以及其他效果对象配合使用。 这个效果还引入了一些我们还没有讨论的新对象(SV_PrimitiveID和Texture2DArray); 这些项目将在下面讨论。现在,主要关注几何着色程序GS; 该着色器将一个点扩展到与面向相机的世界y轴对齐的四边形,如第11.2.1节所述。
图11.6 将一个点扩展成一个四边形。
//***********************************************************
// TreeSprite.fx by Frank Luna (C) 2011 All Rights Reserved.
//
// Uses the geometry shader to expand a point sprite into a y-axis aligned
// billboard that faces the camera.
//***********************************************************
include “LightHelper.fx”
cbuffer cbPerFrame
{
DirectionalLight gDirLights[3];
float3 gEyePosW;
float gFogStart;
float gFogRange;
float4 gFogColor;
};
cbuffer cbPerObject
{
float4x4 gViewProj;
Material gMaterial;
};
cbuffer cbFixed
{
//
// Compute texture coordinates to stretch texture over quad.
//
float2 gTexC[4] =
{
float2(0.0f, 1.0f),
float2(0.0f, 0.0f),
float2(1.0f, 1.0f),
float2(1.0f, 0.0f)
};
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2DArray gTreeMapArray;
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
struct VertexIn
{
float3 PosW : POSITION;
float2 SizeW : SIZE;
};
struct VertexOut
{
float3 CenterW : POSITION;
float2 SizeW : SIZE;
};
struct GeoOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
uint PrimID : SV_PrimitiveID;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Just pass data over to geometry shader.
vout.CenterW = vin.PosW;
vout.SizeW = vin.SizeW;
return vout;
}
// We expand each point into a quad (4 vertices), so the maximum number
// of vertices we output per geometry shader invocation is 4.
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
uint primID : SV_PrimitiveID,
inout TriangleStream triStream)
{
//
// Compute the local coordinate system of the sprite relative to the
// world space such that the billboard is aligned with the y-axis
// and faces the eye.
//
float3 up = float3(0.0f, 1.0f, 0.0f);
float3 look = gEyePosW - gin[0].CenterW;
look.y = 0.0f; // y-axis aligned, so project to xz-plane
look = normalize(look);
float3 right = cross(up, look);
//
// Compute triangle strip vertices (quad) in world space.
//
float halfWidth = 0.5f*gin[0].SizeW.x;
float halfHeight = 0.5f*gin[0].SizeW.y;
float4 v[4];
v[0] = float4(gin[0].CenterW + halfWidth*right - halfHeight*up, 1.0f);
v[1] = float4(gin[0].CenterW + halfWidth*right + halfHeight*up, 1.0f);
v[2] = float4(gin[0].CenterW - halfWidth*right - halfHeight*up, 1.0f);
v[3] = float4(gin[0].CenterW - halfWidth*right + halfHeight*up, 1.0f);
//
// Transform quad vertices to world space and output
// them as a triangle strip.
// GeoOut gout;
[unroll]
for(int i = 0; i < 4; ++i)
{
gout.PosH = mul(v[i], gViewProj);
gout.PosW = v[i].xyz;
gout.NormalW = look;
gout.Tex = gTexC[i];
gout.PrimID = primID;
triStream.Append(gout);
}
}
float4 PS(GeoOut pin, uniform int gLightCount, uniform bool gUseTexure, uniform bool gAlphaClip, uniform bool
gFogEnabled) : SV_Target
{
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
// The toEye vector is used in lighting.
float3 toEye = gEyePosW - pin.PosW;
// Cache the distance to the eye from this surface point.
float distToEye = length(toEye);
// Normalize.
toEye /= distToEye;
// Default to multiplicative identity.
float4 texColor = float4(1, 1, 1, 1);
if(gUseTexure)
{
// Sample texture.
float3 uvw = float3(pin.Tex, pin.PrimID%4);
texColor = gTreeMapArray.Sample(samLinear, uvw);
if(gAlphaClip)
{
// Discard pixel if texture alpha < 0.05. Note that
// we do this test as soon as possible so that we can
// potentially exit the shader early, thereby skipping
// the rest of the shader code.
clip(texColor.a - 0.05f);
}
}
//
// Lighting.
//
float4 litColor = texColor;
if(gLightCount > 0)
{
// Start with a sum of zero.
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);
// Sum the light contribution from each light source.
[unroll]
for(int i = 0; i < gLightCount; ++i)
{
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLights[i],pin.NormalW, toEye,A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
// Modulate with late add.
litColor = texColor*(ambient + diffuse) + spec;
}
//
// Fogging
//
if(gFogEnabled)
{
float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
// Blend the fog color and the lit color.
litColor = lerp(litColor, gFogColor, fogLerp);
}
// Common to take alpha from diffuse material and texture.
litColor.a = gMaterial.Diffuse.a * texColor.a;
return litColor;
}
//———————————————————————
// Techniques–just define the ones our demo needs; you can define the other
// variations as needed.
//———————————————————————
technique11 Light3
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(CompileShader(gs_5_0, GS()));
SetPixelShader(CompileShader(ps_5_0, PS(3, false, false, false)));
}
}
technique11 Light3TexAlphaClip
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(CompileShader(gs_5_0, GS()));
SetPixelShader(CompileShader(ps_5_0, PS(3, true, true, false)));
}
}
technique11 Light3TexAlphaClipFog
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(CompileShader(gs_5_0, GS()));
SetPixelShader(CompileShader(ps_5_0, PS(3, true, true, true)));
}
}
11.2.4 SV_PrimitiveID
本示例中的几何着色器使用具有语义SV_PrimitiveID的特殊无符号整数参数。
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
uint primID : SV_PrimitiveID,
inout TriangleStream triStream)
当指定此语义时,它会告知输入汇编阶段为每个基元自动生成一个基元ID。 当执行绘图调用以绘制n个图元时,第一个图元标记为0; 第二个原语被标记为1; 等等,直到绘图调用中的最后一个原语被标记为n-1。 原始ID只对于单个绘图调用是唯一的。 在我们的广告牌示例中,几何着色器不使用此ID(尽管几何着色器可以); 相反,几何着色器将原始ID写入到出口顶点,从而将其传递到像素着色器阶段。 像素着色器使用原始ID索引到纹理数组,这导致我们到下一节。
NOTE: 如果不存在几何着色器,则可以将基元ID参数添加到像素着色器的参数列表中:
float4 PS(VertexOut pin, uint primID : SV_PrimitiveID) : SV_Target
{
// Pixel shader body…
}
但是,如果存在几何着色器,则原始ID参数必须出现在几何着色器签名中。 然后几何着色器可以使用原始ID或将其传递到像素着色器阶段(或两个都传)。
也可以让输入汇编器生成顶点ID。 为此,请将带有语义SV_VertexID的uint类型的附加参数添加到顶点着色器签名。
以下顶点着色器签名显示了如何完成。
VertexOut VS(VertexIn vin, uint vertID : SV_VertexID)
{
// vertex shader body…
}
对于Draw调用,绘图调用中的顶点将被标记为0,1,…,n-1中的ID,其中n是绘制调用中的顶点数。 对于DrawIndexed调用,顶点ID对应于顶点索引值。
11.3纹理阵列
11.3.1 Overview
纹理数组是存储纹理数据。在C ++代码中,纹理数组由ID3D11Texture2D接口(与单个纹理相同的)表示。当创建一个ID3D11Texture2D对象时,实际上有一个名为ArraySize的属性,可以设置为指定纹理存储的纹理元素的数量。然而,因为我们一直依靠D3DX来创建纹理,所以我们没有明确地设置这个数据成员。在一个效果文件中,纹理数组由Texture2DArray类型表示:
Texture2DArray gTreeMapArray;
现在,你想必知道为什么我们需要纹理数组。为什么不这样做:
Texture2D TexArray[4];
…
float4 PS(GeoOut pin) : SV_Target
{
float4 c = TexArray[pin.PrimID%4].Sample(samLinear, pin.Tex);
这将给出一个错误,称“采样器数组索引必须是文字表达式”。换句话说,它不喜欢阵列索引每像素的方式。 如果我们指定了一个文字数组索引,这个代码将会起作用:
float4 c = TexArray[2].Sample(samLinear, pin.Tex);
但这不如第一种情况那么强大。
11.3.2采样纹理数组
在Billboards演示中,我们使用以下代码对纹理数组进行采样:
float3 uvw = float3(pin.Tex, pin.PrimID%4);
texColor = gTreeMapArray.Sample(samLinear, uvw);
使用纹理数组时,需要三个纹理坐标。前两个纹理坐标是通常的2D纹理坐标; 第三个纹理坐标是纹理数组的索引。例如,0是数组中第一个纹理的索引,1是数组中第二个纹理的索引,2是数组中第三个纹理的索引,依此类推。
在“Billboards”演示中,我们使用具有四个纹理元素的纹理数组,每个纹理元素具有不同的树纹理(图11.7)。但是,由于我们每个绘图调用绘制了四个以上的树,原始ID将大于3。因此,我们将原始ID模4(pin.PrimID%4)将原始ID映射到0,1,2或3,这是具有四个元素的数组的有效数组索引。
图11.7 树的广告牌图像。
纹理数组的一个优点是,我们能够在一个绘图调用中绘制具有不同纹理的图元集合。 通常我们必须这样做(伪代码):
SetTextureA();
DrawPrimitivesWithTextureA();
SetTextureB();
DrawPrimitivesWithTextureB();
…
SetTextureZ();
DrawPrimitivesWithTextureZ();
每个设置和绘制调用都有一些与之相关的开销。 使用纹理数组,我们可以将其减少为一组和一个绘图调用:
SetTextureArray();
DrawPrimitivesWithTextureArray();
11.3.3加载纹理数组
在撰写本文时,没有D3DX功能将一组图像从文件加载到纹理数组中。因此,我们必须自己做这个任务。过程总结如下:
1.在系统内存中逐个地从文件中单独创建每个纹理。
2.创建纹理数组。
3.将每个单独的纹理复制到纹理数组的元素中。
4.创建纹理数组的着色器资源视图。
我们已经在d3dUtil.h / .cpp中实现了一个帮助函数,以帮助从文件名列表创建纹理数组。 纹理应该都是相同的大小。
ID3D11ShaderResourceView* d3dHelper::CreateTexture2DArraySRV(
ID3D11Device* device, ID3D11DeviceContext* context,
std::vector& filenames,
DXGI_FORMAT format,
UINT filter,
UINT mipFilter)
{
//
// Load the texture elements individually from file. These textures
// won’t be used by the GPU (0 bind flags), they are just used to
// load the image data from file. We use the STAGING usage so the
// CPU can read the resource.
//
UINT size = filenames.size();
std::vector