dx12 龙书第十二章学习笔记 -- 几何着色器

24 篇文章 10 订阅

如果不启用曲面细分(tessellation)这一环节,那么几何着色器(geometry shader)这个可选阶段便会位于顶点着色器与像素着色器之间。顶点着色器以顶点作为输入数据,而几何着色器的输入数据则是完整的图元。例如,如果要绘制三角形列表(triangle list),则几何着色器程序实际将对列表中的每个三角形T执行下列操作:

for(UINT i = 0; i < numTriangles; ++i)
    OutputPrimitiveList = GeometryShader( T[i].vertexList );

👆几何着色器以每个三角形的3个顶点作为输入,且输出的是对应的图元列表。

与VS不能销毁或创建顶点不同,几何着色器的亮点便是可以创建或销毁几何图形,这对GPU实现一些有趣的效果成为可能。比如说,借助GS可以将输入的图元扩展为一个或更多其他类型的图元,或者能根据某些条件而选择不输出图元。

注意:几何着色器的输出图元类型不一定与输入图元的类型相同。例如,几何着色器的一个常见拿手好戏即使将一个点扩展为一个四边形(即两个三角形)。

几何着色器所输出的图元由顶点列表定义而成。在退出几何着色器时,必将顶点的位置变换到其次裁剪空间。换言之,经过几何着色器阶段的处理后,我们就得到了位于齐次裁剪空间中由一系列顶点所定义的多个图元。这些顶点会同样历经投影(齐次除法)与光栅化等后续步骤。

1.编写几何着色器

[maxvertexcount(N)]
void ShaderName(
    PrimitiveType InputVertexType InputName[NumElements],
    inout StreamOutputObject<OutputVertexType> OutputName
)
{
    // 几何着色器的具体实现
}

我们必须先指定几何着色器单词调用所输出的顶点数量最大值(每个图元都会调用一次几何着色器,走一遍流程)。对此,可以使用下列属性语法来设置着色器定义之前的最大顶点数量:

[maxvertexcount(N)]

其中,N是几何着色器单词调用所输出的顶点数量最大值。GS每次输出的顶点个数可能各不相同,但这个数量却不能超过我们之前定义的最大值。出于性能方面的考量,我们应当令maxvertexcount的值尽可能地小相关资料显示[NVIDIA08],在GS每次输出地标量数量在1~20时,它将发挥出最佳的性能;而当GS每次输出的标量数量保持在27~40时,它的性能将下降到峰值性能的50%。每次调用GS所输出的标量个数为:maxvertexcount与输出顶点类型结构体中标量个数的乘积

假设顶点结构体中定义了:float3 pos:POSITION; float2 tex:TEXCOORD;这两个成员,则意味着每个顶点元素中含有5个标量。假设将maxvertexcount设置为4,则GS每次输出20个标量,以峰值性能执行。

-- [NVIDIA08]文献公布于2008年,对应的GPU型号为GeForce 8800 GTX

在实际使用中完全满足这些限制是比较困难的,所以我们或采取比最佳性能稍差的解决方案,或干脆选择另一种与GS无关的实现方法(但也许其他方法还不如GS给力)。

几何着色器有输入、输出共两个参数(也可拥有更多的参数)。

输入参数必须是一个定义有特定图元的顶点数组:

不同图元类型对应的顶点数组的成员个数:

点:输入1个顶点

线条:输入2个顶点

三角形:输入3个顶点

线及其邻接图元:输入4个顶点

三角形及其邻接图元:输入6个顶点

GS的输入顶点类型即为顶点着色器输出的顶点类型。输入参数一定要以图元类型作为前缀,用以描述输入到几何着色器的具体图元类型。该前缀可以是下列类型之一:

  • point:输入的图元为点
  • line:输入的图元为线列表或线条带
  • triangle:输入的图元为三角形列表或三角形带
  • lineadj:输入的图元为线列表及其邻接图元,或线条带及其邻接图元
  • triangleadj:输入的图元为三角形列表及其邻接图元,或三角形带及其邻接图元

GS并不会区分输入的图元究竟是列表结构(list)还是带状结构(strip)。举个例子,若绘制的图元实际上是三角形带,但GS仍会把三角形带视作多个三角形并分别进行单独的处理,即将每个三角形的3个顶点作为其输入数据。绘制带状结构的过程中产生额外的开销,因为多个图元所共用的顶点在GS中会被处理多次。

输出参数一定要标有inout修饰符。另外,它必须是一种流类型(stream type,即某种类型的流输出对象)。流类型存有一系列顶点,它们定义了GS输出的几何图形。GS可以通过内置方法Append向输出流列表添加单个顶点:

void StreamOutputObject<OutputVertexType>::Append(OutputVertexType v);

流类型本质上是一种模板类型(template type),其模板参数用以指定输出顶点的具体类型(如GeoOut)。流类型有如下3种:

  • PointStream<OutputVertexType>:一系列顶点所定义的点列表
  • LineStream<OutputVertexType>:一系列顶点所定义的线条带
  • TriangleStream<OutputVertexType>:一系列顶点所定义的三角形带

GS输出的多个顶点会构成图元,图元的输出类型由流类型(👆三种)来指定。对于线条与三角形来说,几何着色器输出的对应图元必定是线条带与三角形带。而线条列表与三角形列表可借助内置函数RestartStrip来实现。RestartStrip函数表示结束当前三角形带的绘制,下面绘制另一个三角形带。特殊情况:如果每3个顶点调用一次RestartStrip表示每3个顶点组成一个三角形带,其实就是一个三角形列表。

void StreamOutputObject<OutputVertexType>::RestartStrip();

比如,如果希望输出三角形列表,则需要在每次向输出流追加3个顶点之后调用RestartStrip。

👇几何着色器签名的具体用例:

示例1:GS最多输出4个顶点。输入的图元一根是线条,输出的是一个三角形带

[maxvertexcount(4)]
void GS(line VertexOut gin[2],
inout TriangleStream<GeoOut> triStream)
{
    // GS的具体实现...
}

示例2:GS最多输出32个顶点。输入的图元是一个三角形,输出的是一个三角形带

[maxvertexcount(32)]
void GS(triangle VertexOut gin[3],
inout TriangleStream<GeoOut> triStream)
{
    // GS的具体实现...
}

示例3:GS最多输出4个顶点。输入的图元是一个点,输出的是一个三角形带

[maxvertexcount(4)]
void GS(point VertexOut gin[1],
inout TriangleStream<GeoOut> triStream)
{
    // GS的具体实现...
}

👇几何着色器展示Append与RestartStrip方法的调用过程:

示例:将输入的三角形进行细分,并输出细分后的4个小三角形(如图):

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])
{
    VertexOut m[3];
    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);
    
    // 把顶点投影到单位球面上
    m[0].PosL = normalize(m[0].PosL);
    m[1].PosL = normalize(m[1].PosL);
    m[2].PosL = normalize(m[2].PosL);

    // 求出法线
    m[0].NormalL = m[0].PosL;
    m[1].NormalL = m[1].PosL;
    m[2].NormalL = m[2].PosL;
    
    // 对纹理坐标进行插值
    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<GeoOut> triStream)
{
    GeoOut gout[6];
    
    [unroll] // [unroll]的意思:展开for循环,代价是产生更多的机器码
    for(int i = 0; i < 6; ++i)
    {
        // 将顶点变换到世界空间
        gout[i].PosW = mul(float4(v[i].PosL, 1.f), gWorld).xyz;
        gout[i].NormalW = mul(v[i].NormalL, (float3x3)gWorldInvTranspose); // ?

        // 将顶点变换到齐次裁剪空间
        gout[i].PosH = mul(float4(v[i].PosL, 1.f), gWorldViewProj);
        gout[i].Tex = v[i].Tex;
    }

    // 我们可以将细分的小三角形绘制到两个三角形带中去:
    // 三角形带1:底端的3个三角形
    // 三角形带2:顶部的三角形
    [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<GeoOut>)
{
    VertexOut v[6];
    Subdivide(gin, v);
    OutputSubdivision(v, triStream);
}

几何着色器的编译过程与VS和PS如出一辙,假设几何着色器名为GS,则可以用下列方法将其编译为字节码:

mShaders["treeSpriteGS"] = d3dUtil::CompileShader(
    L"Shaders\\TreeSprite.hlsl", nullptr, "GS", "gs_5_0"
);

将指定的几何着色器作为PSO的一部分,以此将它绑定到渲染流水线上:

D3D12_GRAPHICS_PIPELINE_STATE_DESC treeSpritePsoDesc = opaquePsoDesc;
...
treeSpritePsoDesc.GS = 
{
    reinterpret_cast<BYTE*>(mShaders["treeSpriteGS"]->GetBufferPointer(), 
        mShaders["treeSpriteGS"]->GetBufferSize())
};

若给出一个输入图元,几何着色器也可以根据某些条件而选择不输出任何数据。通过这种方式,GS便可以轻易地销毁几何图形,这对于一些算法的实现来讲是很有帮助的。

如果没有向几何着色器输入组装完整图元所需的足够顶点,将会导致部分图元的遗失。-- 假设处理三角形中的最后一个三角形时,少了个顶点,那么最后一个三角形就默认失踪了。

2.以公告牌技术实现森林效果

①概述:

当树与树之间的距离较远时,就轮到公告牌技术大显神威了,即以绘有3D树木图片的四边形来代替对整棵3D树的渲染。从远处看,公告牌技术往往不会露出破绽。这里还有一个小秘诀,就是使公告牌总是面向摄像机(否则容易露馅)。

假设y轴指向正上方,且平面xz表示底面。则树木公告牌通常被置于xz平面内并于y轴对齐而面向摄像机。

在世界空间中,若给定一公告牌的中心坐标为C=(C_x,C_y,C_z),而摄像机的位置为E=(E_x,E_y,E_z),那么,这些信息就足以表示出该公告牌局部坐标系(u,v,w)与世界空间(x,y,z)的相对关系:

\\ w=\frac{(E_x-C_x,0,E_z-C_z)}{||E_x-C_x,0,E_z-C_z||} \\ v = (0,1,0) \\ u = v\times w

给出公告牌局部坐标系与世界空间的相对关系以及公告牌在世界空间中的大小,我们就能以下列代码来获取公告牌四边形的四个顶点坐标:

v[0] = float4(gin[0].CenterW + halfWidth.right - halfHeight.up, 1.f);
v[1] = float4(gin[0].CenterW + halfWidth.right + halfHeight.up, 1.f);
v[2] = float4(gin[0].CenterW - halfWidth.right - halfHeight.up, 1.f);
v[3] = float4(gin[0].CenterW - halfWidth.right + halfHeight.up, 1.f);

对每个公告牌的四边形进行计算,因为公告牌彼此之间的局部坐标系并不一致。

 对于实现公告牌树的程序,我们将构造一系列距离陆地表面有着特定距离的点图元(PSO.PrimitiveTopologyType=D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT,并把ID3D12GraphicsCommandList::IASetPrimitiveTopology函数参数指定为D3D_PRIMITIVE_TOPOLOGY_POOINTLIST)。这些点表示的就是我们希望绘制公告牌的中心点,我们再通过几何着色器将这些点扩展为公告牌四边形,又要计算公告牌的世界矩阵。

一种通过CPU来实现公告牌技术的常用方法是,在动态顶点缓冲区中来控制每个公告牌的4个顶点。每当摄像头移动,顶点重新经CPU计算更新,并通过memcpy函数将其复制到GPU端的缓冲区。

若使用此方法,就一定要将每个公告牌的4个顶点都提交到IA(Input Assembler, 输入装配器)阶段,并且还需要更新动态顶点缓冲区,这会产生一定的开销。

若采用几何着色器来实现公告牌技术,我们就能从容运用静态的顶点缓冲区,这是因为GS可以扩展四边形,并令公告牌面对摄像机。另外GS占用内存少,因为创建每个公告牌只需要向IA阶段提交一个顶点即可。

②顶点结构体:

struct TreeSpriteVertex
{
    XMFLOAT3 Pos;
    XMFLOAT2 Size;
};

mTreeSpriteInputLayout = 
{
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
        D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
    {"SIZE", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,
        D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
}

效果(effects)文件,即效果。DirectX 12之前的版本提供了一种已开源的"效果框架",使用户可以在运行时更便捷地管理渲染流水线状态、HLSL着色器以及运行时变量等。但在DirectX 12中,这套框架已不复存在。效果文件在本书中出现若干次,其意思与HLSL文件等价。 

③HLSL文件:

完整TreeSprite.hlsl文件如下:

其中出现了未曾讨论过的新对象:SV_PrimitiveID 与 Texture2DArray

//***************************************************************************************
// TreeSprite.hlsl 
//***************************************************************************************

#ifndef NUM_DIR_LIGHTS
    #define NUM_DIR_LIGHTS 3
#endif

#ifndef NUM_POINT_LIGHTS
    #define NUM_POINT_LIGHTS 0
#endif

#ifndef NUM_SPOT_LIGHTS
    #define NUM_SPOT_LIGHTS 0
#endif

#include "LightingUtil.hlsl"

Texture2DArray gTreeMapArray : register(t0); // Texture2DArray


SamplerState gsamPointWrap        : register(s0);
SamplerState gsamPointClamp       : register(s1);
SamplerState gsamLinearWrap       : register(s2);
SamplerState gsamLinearClamp      : register(s3);
SamplerState gsamAnisotropicWrap  : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);


cbuffer cbPerObject : register(b0)
{
    float4x4 gWorld;
	float4x4 gTexTransform; 
};

cbuffer cbPass : register(b1)
{
    float4x4 gView;
    float4x4 gInvView;
    float4x4 gProj;
    float4x4 gInvProj;
    float4x4 gViewProj;
    float4x4 gInvViewProj;
    float3 gEyePosW;
    float cbPerObjectPad1;
    float2 gRenderTargetSize;
    float2 gInvRenderTargetSize;
    float gNearZ;
    float gFarZ;
    float gTotalTime;
    float gDeltaTime;
    float4 gAmbientLight;

	float4 gFogColor;
	float gFogStart;
	float gFogRange;
	float2 cbPerObjectPad2;

    // Indices [0, NUM_DIR_LIGHTS) are directional lights;
    // indices [NUM_DIR_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights;
    // indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS)
    // are spot lights for a maximum of MaxLights per object.
    Light gLights[MaxLights];
};

cbuffer cbMaterial : register(b2)
{
	float4   gDiffuseAlbedo;
    float3   gFresnelR0;
    float    gRoughness;
	float4x4 gMatTransform;
};
 
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 TexC    : TEXCOORD;
    uint   PrimID  : SV_PrimitiveID; // SV_PrimitiveID
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout;

	// 直接将数据传入GS
	vout.CenterW = vin.PosW; // 顶点坐标作为公告牌中心点坐标
	vout.SizeW   = vin.SizeW; 

	return vout;
}
 
 // 由于我们将每个点扩展为一个四边形,因此每次调用几何着色器最多输出4个顶点
[maxvertexcount(4)]
void GS(point VertexOut gin[1], 
        uint primID : SV_PrimitiveID, 
        inout TriangleStream<GeoOut> triStream)
{	
	// 计算精灵的局部坐标系与世界空间的相对关系,以使公告牌与y轴对齐且面向观察者

	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);

	//
	//  计算世界空间中三角形带的顶点(即四边形)
	//
	float halfWidth  = 0.5f*gin[0].SizeW.x;
	float halfHeight = 0.5f*gin[0].SizeW.y;
	
    // 四边形顶点顺序:
    // 1---2
    // |   |
    // 0---3

	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);

	//
	// 将四边形的顶点变换到世界空间,并将它们以三角形带的形式输出
	//
	
	float2 texC[4] = 
	{
		float2(0.0f, 1.0f),
		float2(0.0f, 0.0f),
		float2(1.0f, 1.0f),
		float2(1.0f, 0.0f)
	};
	
	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.TexC     = texC[i];
		gout.PrimID   = primID; // primID
		
		triStream.Append(gout);
	}
}

float4 PS(GeoOut pin) : SV_Target
{
	float3 uvw = float3(pin.TexC, pin.PrimID%3); // PrimID%3 ???
    float4 diffuseAlbedo = gTreeMapArray.Sample(gsamAnisotropicWrap, uvw) * gDiffuseAlbedo;
	
#ifdef ALPHA_TEST
	clip(diffuseAlbedo.a - 0.1f);
#endif

    pin.NormalW = normalize(pin.NormalW);

	float3 toEyeW = gEyePosW - pin.PosW;
	float distToEye = length(toEyeW);
	toEyeW /= distToEye; // normalize

    float4 ambient = gAmbientLight*diffuseAlbedo;

    const float shininess = 1.0f - gRoughness;
    Material mat = { diffuseAlbedo, gFresnelR0, shininess };
    float3 shadowFactor = 1.0f;
    float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
        pin.NormalW, toEyeW, shadowFactor);

    float4 litColor = ambient + directLight;

#ifdef FOG
	float fogAmount = saturate((distToEye - gFogStart) / gFogRange);
	litColor = lerp(litColor, gFogColor, fogAmount);
#endif

    litColor.a = diffuseAlbedo.a;

    return litColor;
}

我们所提到的精灵(sprite),通常来讲,是一种不经渲染流水线而直接绘制到渲染目标的2D位图。公告牌实为应用于3D环境中的精灵,是具有alpha通道且面向摄像机的图像。DX8~DX10专门提供了一组绘制精灵的相关函数,但从DX11开始,这些函数被取消了。

SV_PrimitiveID语义:

上述示例中,几何着色器内含有一个使用SV_PrimitiveID语义的特殊无符号整数参数

[maxvertexcount(4)]
void GS(point VertexOut gin[1],
    uint primID : SV_PrimitiveID,
    inout TriangleStream<GeoOut> triStream
);

若指定了该语义,则输入装配器阶段会自动为每个图元生成图元ID。在绘制n个图元的调用执行过程中,第一个图元被标记为0,第二个图元被标记为1,以此类推,到最后一个图元被标记为n-1为止。对于单次绘制调用来说,其中的图元ID都是唯一的。在公告牌实例中,几何着色器不会使用到此ID(尽管它可以使用),它仅将图元ID写入到输出的顶点之中,以此将它们传递到像素着色器阶段。而像素着色器则把图元ID作为纹理数组的索引,这是下一小节的内容。

如果不存在几何着色器,我们可以将图元ID参数加入像素着色器的参数列表

float4 PS(VertexOut pin, uint primID : SV_PrimitiveID) : SV_Target
{
    // ...
}

但是,如果代码中具有几何着色器,则图元ID必首先存在于其签名的参数之中。继而几何着色器能使用图元ID,也可以通过几何着色器输出传入像素着色器。

总结:几何着色器和像素着色器谁离输入装配阶段最近且被开启,谁先获得图元ID

另外:输入装配器也能够生成顶点ID。为了实现这一点,我们需要向顶点着色器签名额外添加一个由语义SV_VertexID修饰的uint类型的参数:

VertexOut VS(VertexIn vin, uint vertID : SV_VertexID)
{
    // ...
}

此时,对于绘制方法DrawInstanced来说,其调用过程中的顶点ID将被标记为0,1,...,n-1,这里的n即为本次绘制调用中的顶点数量。而对于DrawIndexedInstanced绘制方法而言,其顶点ID则对应于顶点的索引值

3.纹理数组

①概述:

纹理数组同其他资源一样,是由ID3D12Resource接口来表示。创建ID3D12Resource对象时,DepthOrArraySize属性来指定纹理数组所存储的元素个数(对于3D纹理:此项为深度值;对于深度/模板纹理:此项为1)。查看Common/DDSTextureLoader.cpp中的CreateD3DResource12函数,就可以明白这些代码如何创建纹理数组和体纹理(volumn texture)。

纹理数组的具体使用和其他纹理资源一样,填写D3D12_SHADER_RESOURCE_VIEW_DESC结构,创建SRV。

在HLSL文件中,纹理数组是通过Texture2DArray类型来表示的:

Texture2DArray gTreeMapArray;

现在,我们必须搞清楚为什么使用纹理数组,而不是像👇这样做:

②对纹理数组进行采样:

float3 uvw = float3(pin.TexC, pin.PrimID % 3); 
float4 diffuseAlbedo = gTreeMapArray.Sample(gsamAnisotropicWrap, uvw) * gDiffuseAlbedo;

%3:dds文件中有3种树木图像,所以这里的%3的意思是除以3取余。

使用纹理数组共需要3个坐标值:前两个坐标就是普通的2D纹理坐标,第三个坐标则是纹理数组的索引。(纹理数组的索引从0开始计数)

注意问题:①如果不使用%3运算,ID号会超过纹理数组中纹理个数。但这不会报错,因为超过的数字会被钳制到最大有效索引(这里为3)

②ID号什么时候被重置:1次DrawInstanced操作或1次DrawIndexedInstanced操作,会从0开始重置,不管是SV_VertexID或是SV_PrimitiveID

在完成公告牌树木的程序中,我们准备了3种不同的树木纹理,但是,由于每次所画出的树木要对于3棵,所以图元ID势必要大于3。所以我们对图元ID进行模3运算,将其映射为0、1或2。

⭐纹理数组的优点之一是可以在一次绘制调用过程中,画出一系列具有不同纹理的图元。一般来说,我们必须用不同的纹理来对每一个有着独立渲染项的网格进行处理👇:

// 之前
SetTextureA();
DrawPrimitiveWithTextureA();

SetTextureB();
DrawPrimitiveWithTextureB();

...

SetTextureZ();
DrawPrimitiveWithTextureZ();

每当设置纹理或绘制调用的时候都会相应地产生一些开销。但使用了纹理数组,我们就能将设置纹理于绘制调用的过程都减少到一次:

// 现在
SetTextureArray();
DrawPrimitivesWithTextureArray();

③加载纹理数组:

位于Common/DDSTextureLoader.h/.cpp文件中的代码,可以加载存有纹理数组的DDS文件。因此,重点就落在创建含有纹理数组的DDS文件上。为此,我们使用微软公司提供的texassemble工具。通过下列语法,此工具可以将t0.dds,t1.dds,t2.dds,t3.dds这四个图像合并成一个名为treeArray.dds的纹理数组

texassemble -array -o treeArray.dds t0.dds t1.dds t2.dds t3.dds

注意,在使用texassemble程序构建纹理数组时,输入的多个图像只能各有一种mipmap层级。经texassemble创建的纹理数组,可以再根据需求通过texconv工具生成多种mipmap层级,并改变纹理的格式

texconv -m 10 -f BC3_UNORM treeArray.dds

④纹理子资源:

前面讨论了纹理数组,现在来谈一谈它的子资源(subresource)。👇图展示了一个拥有若干纹理的纹理数组,其中的纹理还各有自己的mipmap链。Direct3D API用术语数组切片(array slice)来表示纹理数组中的某个纹理及其mipmap链,又用术语mip切片(mip slice)来表示纹理数组中特定层级的所有mipmap子资源则是指纹理数组种某纹理的单个mipmap层级。

这就是说,若给出纹理数组的索引以及mipmap层级,我们就能访问纹理数组中的相应子资源。值得注意的是,子资源也是有线性索引来标记的,而Direct3D所用的线性索引规则如👇图:

D3D12提供了👇实用工具函数可根据mip切片索引、数组切片索引、平面切片(plane slice,D3D12新引入的术语,使用户能将某些YUV平面格式的分量以索引方式单独提取出来,不使用该功能设置为0即可)索引、mipmap层级以及纹理数组的大小,直接计算出子资源的线性索引:

UINT inline D3D12CalcSubresource(
   UINT MipSlice,
   UINT ArraySlice,
   UINT PlaneSlice,
   UINT MipLevels,
   UINT ArraySize)
{
    return MipSlice + ArraySlice * MipLevels + PlaneSlice * MipLevels * ArraySize;
}

4.alpha-to-coverage技术

在运行树木公告牌的程序时,若以特定距离观察树木公告牌,便会发现其裁剪的边缘部分呈锯齿状。这个问题。这个问题出在clip函数上,我们用它来遮罩不属于树木纹理的像素。[我们通过alpha通道绘制树木,然后在PS中利用clip扣去不满足的像素。] 此裁剪函数负责像素的生杀大权,也导致了树木边缘的过渡不够平滑。当观察者与树木之间的距离过近时,引起纹理放大的发生,使块状失真更加明显,且会导致低分辨率mipmap层级的启用。

解决此问题的方法之一是以透明混合取代alpha测试。通过线性纹理过滤,使边缘像素稍显模糊,从而促使由白(不透明的像素)至黑(被遮罩的像素)的过渡更为平滑。即透明混合将使公告牌图像边缘的不透明像素到被遮罩像素之间实现平滑的渐变。-- 出现的问题:①使用透明混合技术必须要从后至前绘制物体,物体的排序的代价是不容小觑的②从后至前的排序过程,将引发大量的重复绘制(over draw),也就是我们在第11章练习题8讨论的“深度复杂性” => 这让我们的应用程序置于死地

不妨试一试MSAA(多重采样抗锯齿技术, multisampling antialiasing -- 参考4.1.7节),它可以缓解多边形边沿的锯齿效果,从而使之相对平滑。该项技术确实有效,但也带来了一些问题。MSAA会为每个像素逐一执行一次像素着色器,使像素着色器在像素的中心采样,并基于可视性与覆盖情况[visibility和coverage在第4章提到过],将颜色信息共享给它的子像素(subpixel)。而更为关键的是,覆盖情况是在多边形层级(polygon level)上才确定下来的。因此MSAA并不会检测alpha通道所定义的树木公告牌的裁剪边缘,而只是关注纹理所映射到四边形的边缘。

有没有办法通知Direct3D,使它在计算覆盖情况时考虑alpha通道这个因素呢?答案是肯定的,这便是倍称为"alpha-to-coverage"(或译为:由透明至覆盖..)技术。

在开启MSAA与alpha-to-coverage后(令成员D3D12_BLEND_DESC::AlphaToCoverageEnable=true来实现),硬件就会检测像素着色器所返回的alpha值,并将其用于确定覆盖的情况[NVIDIA05]。例如,在使用4X MSAA时,若像素着色器返回的alpha值为0.5,则我们即可认为此像素里4个子像素中2个位于多边形的范围之外,并据此来创建平滑的图像边缘

一般来说,在以alpha遮罩的方式来裁剪树叶与围栏这类纹理时,建议总是使用alpha-to-coverage技术。当然,前提是需要开启MSAA。在演示程序中,可以通过Set4xMsaaState函数使示例框架创建出支持4X MSAA的后台缓冲区与深度缓冲区。


 ⭐alpha通道图常常是黑白色的,黑白代表什么?

alpha通道可以指出一张图片的透明度和不透明度,alpha值越大,不透明度越强。alpha通道图常常是黑白色的,如👇图,但是注意,黑色部分不代表不透明,白色部分也不代表透明

理解:白色(255,255,255),黑色(0,0,0),所以数值大的应该显示为更偏向于白色,所以这样可以记住白色部分就是alpha值偏大的部分。

其他解释:Alpha通道使用8位二进制数,就可以表示256级灰度,即256级的透明度。白色(值为255)的Alpha像素用以定义不透明的彩色像素,而黑色(值为0)的Alpha通道像素用以定义透明像素,介于黑白之间的灰度(值为30-255)的Alpha像素用以定义不同程度的半透明像素。因而通过一个32位总线的图形卡来显示带Alpha通道的图形,就可能呈现出透明或半透明的视觉效果。

我们往往将需要部分的alpha设置为偏大,让它在alpha图中占据白色部分,再使用clip函数[clip函数的参数如果<0,则扔弃当前像素]


遇到的问题及解决:

①忘记修改maxvertexcount的数字,导致只输出较少的顶点!!! 


课后练习题

1.利用三角形条带画(不带底)圆柱体,利用GS扩展顶点(从点扩展或从圆扩展):

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值