处理2D图像和纹理——Billboarding:在3D世界绘制2D图像使它们总是面向相机

问题

要使画面给人留下深刻印象,一个3D世界需要包含许多物体,特别是处理室外场景时。例如,如果没有绘制成百上千棵树,树林看起来就会显得不真实。但是,以3D模型的形式绘制几百颗树是不可能的,因为这样做会极大地拖慢应用程序。

你也可以使用少量的billboarding(译者注:billboard可以翻译为公告板,广告牌),例如,绘制激光束或子弹。这通常和一个粒子引擎组合在一起(见教程3-12)。

解决方案

你可以通过使用一张2D图像代替3D物体解决这个问题。但是当相机在2D图像旁边时,观察者很容易看出这是一张2D图像,如图3-16的左图所示,左图中有5张2D图像放置在3D世界中。

1

图3-16 3D场景中的2D图像。没有bilboarded (左图),有billboarded (右图)。

要解决这个问题,对每个图像你想在3D空间中定义两个三角形显示这个图像,而且你想旋转这些三角形使图像能够朝向相机。如图3-16中的右图所示,有与左图相同的5张2D图像,但经过旋转朝向相机。如果这些图像包含树木,它们的边界会使用透明色,可以实现一个漂亮的效果。

XNA Framework包含计算每个图像两个三角形的六个顶点的旋转位置的功能,这个功能由Matrix. CreateBillboard方法提供,但是在vertex shader中进行这些计算会获得极大的性能提升,这会在本教程的第二部分解释。

工作原理

作为树林或粒子引擎的共同部分,你想只定义2D图像的中心的3D位置和它的大小。所以,你想使用一个集合包含每个2D图像的3D位置和大小:

List<Vector4> billboardList = new List<Vector4>(); 

Texture2D myTexture; 

VertexPositionTexture[] billboardVertices; 

可以看到billboards存储为一个Vector4:三个浮点数存储中心的3D位置,另一个浮点数保存billboard的大小。

对每个billboarded 2D图像,你需要计算图像的两个三角形的六个顶点。这些顶点存储在最后一行代码定义的billboardVertices变量中。myTexture变量保存用来给显卡采样颜色的纹理。

技巧:如教程3-4中所述,绘制多个纹理的最快方法是将它们存储在一张大纹理中,这样显卡就无需切换纹理了。

在LoadContent方法中添加以下代码将纹理加载到myTexture变量中:

myTexture = content.Load<Texture2D>("billboardtexture"); 

然后添加一个简单的方法让你可以容易地将billboard添加到场景中:

private void AddBillboards() 

{

    billboardList.Add(new Vector4(-20, -10, 0, 10));

    billboardList.Add(new Vector4(0, 0, 0, 5)); 

    billboardList.Add(new Vector4(20, 10, 0, 10)); 

} 

现在这个方法只添加了三个billboard。第四个分量表示你想让第二个billboard大小只有另两个的四分之一,因为边长为另两个的二分之一。别忘了在Initialize方法中调用这个方法:

AddBillboards(); 

然后,你需要计算六个顶点的位置。Billboarding有两种主要方式:球面billboarding和圆柱面billboarding。球面billboarding主要用于粒子引擎,圆柱面billboarding主要用于绘制树木,人等。

为球面Billboarding计算六个顶点位置

技巧:如果你想使用HLSL实现billboard,可以略过此节直接看本教程的“性能考虑:第二部分”,因为在GPU上进行billboarding计算可以给程序带来极大的提升。“性能考虑:第一部分”也值得一读。

现在你已经定义了billboard的中心位置,可以计算中心点周围的六个顶点了:

private void CreateBBVertices() 

{

    billboardVertices = new VertexPositionTexture[billboardList.Count * 6]; 

    

    int i = 0; 

    foreach (Vector4 currentV4 in billboardList) 

    {

        Vector3 center = new Vector3(currentV4.X, currentV4.Y, currentV4.Z); 

        float scaling = currentV4.W; 

        

        //add rotated vertices to the array 

    }

} 

上述代码首先创建了一个数组存储每个billboard的两个三角形的六个顶点。然后,对存储在billboardList 中的每个billboard,获取它的中心位置和大小。for循环中的其余部分是计算六个顶点位置的实际代码。

对每张2D图像,你需要定义两个三角形用以在3D空间定义一个矩形。这意味着你需要定义六个顶点,但其中四个是独立的。

因为需要显示一张纹理,所以使用VertexPosit ionTexture。因为billboardList保存每个矩形的中心位置,你想让所有的顶点到中心的距离相同。在本例中,需要到/从中心的X和Y分量中增加/减少0.5f的偏移量获取顶点的位置。

例如,下面的三个位置保存离开一个三角形中心位置的偏移量,DL指左下,UR指右上:

Vector3 posDL = new Vector3(-0.5f, -0.5f, 0); 

Vector3 posUR = new Vector3(0.5f, 0.5f, 0); 

Vector3 posUL = new Vector3(-0.5f, 0.5f, 0); 

如果你将中心位置添加到上述位置中,就获得了一个三角形的顶点位置。但是,无论相机的位置如何,这些位置将保持不变。所以当你移动相机时,就会看到这个三角形的侧面,如图3-16的左图所示。而你想让三角形始终朝向相机,因此需要根据相机的位置对这些偏移量进行某种形式的旋转。这需要数学知识,幸运的是,XNA可以立即生成一个矩阵用来进行这种变换。在for循环的顶部添加以下代码:

Matrix bbMatrix = Matrix.CreateBillboard(center, quatCam.Position, quatCam.UpVector, quatCam.Forward); 

要能创建这个矩阵用来旋转三角形使图像始终朝向相机,XNA需要知道矩形和相机的位置,相机的Forward向量。因为你想让球形billboarding旋转图像使它根据相机朝向上方,你还需要指定Up向量。

注意:当你颠倒相机时,矩形也会随之颠倒。要展示这种情况,本例中使用了一个向上箭头作为纹理。而且,为了让你不受限制地旋转相机,使用了教程2-4中的四元数相机而不是教程2-3中的quake相机。

有了这个矩阵,就可以获取旋转偏移量:

Vector3 posDL = new Vector3(-0.5f, -0.5f, 0); 

Vector3 billboardedPosDL = Vector3.Transform(posDL*scaling, bbMatrix); 

billboardVertices[i++] = new VertexPositionTexture(billboardedPosDL, new Vector2(1, 1)); 

第一行代码定义了一个静态偏移量,并没有考虑相机的位置。你还需指定矩形的大小,将偏移量乘以scaling值,这样矩形会在 AddBillboards 方法中成为你指定的大小。现在知道了顶点相对于中心的偏移量,使用矩阵进行变换。最后,在顶点数组中存储这个变换过的位置和纹理坐标。

如果对三角形的三个顶点都进行这样的操作,那么三角形就会始终朝向相机。

注意:你只对偏移量进行变换:相对于中心的顶点平移会通过矩阵变换自动实现!这也是创建bbMatrix时需要指定矩形中心位置的原因之一。

你需要对每个偏移量都进行这个变换。因为两个三角形共享两个两个点,因此只需进行四次变换。下面是CreateBBVertices方法:

private void CreateBBVertices() 

{

    billboardVertices = new VertexPositionTexture[billboardList.Count * 6]; 

    

    int i = 0; 

    foreach (Vector4 currentV4 in billboardList) 

    {

        Vector3 center = new Vector3(currentV4.X, currentV4.Y, currentV4.Z); 

        float scaling = currentV4.W; 

        Matrix bbMatrix = Matrix.CreateBillboard(center, quatCam.Position, quatCam.UpVector, quatCam.Forward); 

        

        //first triangle 

        Vector3 posDL = new Vector3(-0.5f, -0.5f, 0); 

        Vector3 billboardedPosDL = Vector3.Transform(posDL * scaling, bbMatrix); 

        billboardVertices[i++] = new VertexPositionTexture(billboardedPosDL, new Vector2(1, 1)); 

        Vector3 posUR = new Vector3(0.5f, 0.5f, 0); 

        Vector3 billboardedPosUR = Vector3.Transform(posUR * scaling, bbMatrix); 

        billboardVertices[i++] = new VertexPositionTexture(billboardedPosUR, new Vector2(0, 0)); 

        Vector3 posUL = new Vector3(-0.5f, 0.5f, 0); 

        Vector3 billboardedPosUL = Vector3.Transform(posUL * scaling, bbMatrix); 

        billboardVertices[i++] = new VertexPositionTexture(billboardedPosUL, new Vector2(1, 0)); 

        

        //second triangle: 2 of 3 corner points already calculated! 

        billboardVertices[i++] = new VertexPositionTexture(billboardedPosDL, new Vector2(1, 1)); 

        Vector3 posDR = new Vector3(0.5f, -0.5f, 0); 

        Vector3 billboardedPosDR = Vector3.Transform(posDR * scaling, bbMatrix); 

        billboardVertices[i++] = new VertexPositionTexture(billboardedPosDR, new Vector2(0, 1)); 

        billboardVertices[i++] = new VertexPositionTexture(billboardedPosUR, new Vector2(0, 0)); 

    }

} 

对billboardList中的每个项目,这个方法都会计算四个点,让矩形朝向相机。

确保在相机的位置或旋转发生变化时调用这个方法,因为所有billboard的旋转都会相应地进行调整!为了安全起见,在Update方法的最后调用这个方法。

从顶点数组绘制

有了包含3D位置和纹理坐标的所有顶点,你就可以在Draw方法中绘制它们了,解释请见教程5-1:

//draw billboards 

basicEffect.World = Matrix.Identity; 

basicEffect.View = quatMousCam.ViewMatrix; 

basicEffect.Projection = quatMousCam.ProjectionMatrix; 

basicEffect.TextureEnabled = true; 

basicEffect.Texture = myTexture; 



basicEffect.Begin(); 

foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) 

{

    pass.Begin(); 

    device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); 

    device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleList, billboardVertices, 0, billboardList.Count*2); 

    pass.End(); 

}

basicEffect.End(); 

你想从billboardVertices数组进行绘制,数组中的顶点表示三角形的集合。因为每个2D图像都是一个矩形,需要绘制两个三角形,所有你需要绘制总共billboard List . Count*2 个三角形。因为使用的是TriangleList,这个数量等于billboardVertices. Length/3。

性能考虑:第一部分

当使用billboard时,很容易碰到需要绘制几千个billboard的情况。例如你可以像以下代码一样定义几千个billboards:

private void AddBillboards() 

{

    int CPUpower = 10; 

    for (int x = -CPUpower; x < CPUpower; x++) 

        for (int y = -CPUpower; y <CPUpower; y++)

            for (int z = -CPUpower; z < CPUpower; z++) 

                billboardList.Add(new Vector4(x, y, z, 0.5f)); 

}

上述代码会使用边长为0.5f的billboard填充从(-10, -10, -10)到(9, 9, 9)范围内的立方体,billboard 间的距离为1个单位,总共有8000个billboards!如果在你的PC上运行得不流畅,可以试着减少CPU power值,这样绘制的billboard更少。

你的PC之所以可以绘制这么多的billboard只是因为你使用了一个很大的顶点数组存储了所有biillboard。通过这种方式,显卡可以一次性在单一的过程中处理16000个三角形。这个代码与前面的代码相比还有一个小变化。

在前面的代码中,对每个billboard都会创建一个新的顶点数组并填充了六个顶点,与前面的方式一样。然后,对每个billboard,显卡从六个顶点绘制两个三角形,整个过程周而复始。对CPU来说几乎同样的工作量,显卡需要被调用8000次绘制两个三角形,在这个任务中显卡被打搅了7999次,因此显卡很“生气”。

使用这个方法,我可以将CPUpower设为3减少帧率的下降,这种情况下只绘制了216个!

所以底线是让显卡在一次性在单一过程中做自己的工作,这也是显卡喜欢的工作方式。请总是从尽可能少的顶点缓冲/纹理中绘制场景。

为圆柱形Billboarding计算六个顶点

在某些情况中,你并不想让矩形朝向相机。例如,如果使用一张树的2D图像创建一片树林,你想让每张图像绕着树木的主干旋转。使用球形billboarding会导致绕着三个轴旋转,使它们完全朝向相机。

想象一下这种情况,你有一些树木的图像,相机在它们上面。使用球形billboarding,结果如图3-17的左上所示。在图3-17的左下,你可以看到为了让矩形朝向相机,树木以一种不自然的方式进行了旋转,当相机移动时会产生一些奇怪的效果。

你想要的效果是让矩形只绕着树木的Up向量旋转,如图3-17的右下所示。在3D视角中,会产生如图3-17中右上图所示的效果。

2

图3-17 球形billboarding (左),圆柱形billboarding (右)

注意:这种billboarding之所以称之为圆柱形的,是因为如果你将相机放置在如此多的billboard图像之内,它们会成为一个圆柱体隧道。导致这个现象的原因是它们只被允许沿着一个方向旋转。图3-18显示了这样一个隧道。

计算每个billboard顶点的代码与前面的几乎一样。你只需使用另一个billboarding矩阵,这个矩阵可以通过使用Matrix.CreateConstrainedBillboard方法获取:

Matrix bbMatrix = Matrix.CreateConstrainedBillboard(center,quatCam.Position, new Vector3(0, 1, 0), quatCam.Forward, null);

作为一个有约束的旋转,现在你可以指定billboard 绕着那条边旋转。对一颗树来说,这条边是(0,1,0) Up向量。为使当相机非常靠近物体时旋转更加精确,你也可以指定相机的Up向量。

3

图3-18 大量的圆柱形billboarding

性能考虑:第二部分

本教程的第一部分展示了如何使用XNA代码创建一个billboarding引擎。但是,这个方法会给CPU带来极大地压力,而对显卡利用效率不高。

想象一下在3D场景中绘制1000个billboard图像。只要相机改变位置,就需要重新计算图像四个顶点的位置使它们可以重新朝向相机。若更新频率为60次/秒,需要每秒计算60乘以4000个3D位置,这都会在CPU中进行。而且,每帧都需要创建一个6000个顶点的数组保存所需的三角形用来显示每个billboard。

CPU是通用目的的处理器,并没有对这种计算进行优化。这样的任务会拖慢CPU,限制了CPU做其他任务,例如处理游戏逻辑。

而且,每一帧这个更新过的顶点缓冲都要被发送到显卡中!这会让显卡将这个数据放置在系统内存中,在PCI-express (或 AGP)总线上进行大量的搬运工作。

如果你一次性地将每个billboard的中心位置和大小存储在显存中,让GPU(显卡的计算单元)代替CPU进行所有的billboarding计算,性能会有很大的提升。记住显卡对顶点的操作是经过优化的,这些运算在GPU中要比在CPU中要快得多。有以下几个优点:

  • 在PCI-express (或AGP) 总线中只需进行一次传输
  • 计算时间大大缩短(因为GPU对这种计算进行过优化)
  • CPU不进行billboarding计算,让CPU可以做其他更重要的事情
针对HLSL Billboarding所做的代码准备

你仍可以使用一个集合存储billboard,所以AddBillboards方法无需改动。但是CreateBBVertices方法进行了简化,因为所有的billboarding计算在GPU的vertex shader中进行。

一个billboard仍使用两个三角形绘制,因此每对个billboard仍需传递六个顶点至GPU。 那么,你想让vertex shader做什么呢?你想让vertex shader计算六个顶点的3D旋转坐标使billboard可以朝向相机。要做到这步,vertex shader需要知道每个顶点的如下信息:

  • 顶点所属的billboard的中心的3D位置
  • 当前顶点位于billboard的哪个位置,这样vertex shader才可以计算对应的偏移量
  • 纹理坐标,它们被传递到pixel shader用来从纹理中采样正确的位置

注意:显然在旋转billboard之前vertex shader还需要知道相机在3D空间中的位置。相机位置对每个顶点来说都是一样的,因此应该被设置为一个XNA-to-HLSL变量,而不是在每个顶点中存储这个位置。

这意味着属于一个billboard的六个顶点所携带的部分信息是相同的:billboard中心的3D位置。因为定义顶点的信息是四个顶点之一,你可以,例如,传递一个介于0和3之间的数字,0表示左上角顶点,3表示右下角顶点。

但是,你已经将这个信息通过纹理坐标的方式传递到vertex shader中了。事实上,(0,0)纹理坐标表示当前顶点为左上角顶点,(1,0)纹理坐标表示右上角顶点。

总而言之:对你想绘制的每个billboard,你需要绘制两个三角形,所以需要传递六个顶点到vertex shader中。这六个顶点携带了同样的位置信息:billboard的中心点位置。每个顶点都携带各自的纹理坐标,用来对两个三角形正确地施加纹理,但在这种情况中纹理坐标还被用vertex shader来确定当前顶点是哪个位置的顶点,所以vertex shader可以计算当前顶点离开 billboard中心点的偏移量。

所以在XNA代码中,需要调整CreateBBVertices方法使它生成这些顶点并将这些顶点存储在一个数组中:

private void CreateBBVertices() 

{

    billboardVertices = new VertexPositionTexture[billboardList.Count * 6];

    

    int i = 0;

    foreach (Vector4 currentV4 in billboardList)

    {

        Vector3 center = new Vector3(currentV4.X, currentV4.Y, currentV4.Z);

        

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(0, 0)); 

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(1, 0)); 

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(1, 1));

        billboardVertices[i++] = new VertexPositionTexture(center, new Vector2(0, 0)); 

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(1, 1)); 

        billboardVertices[i++] = new VertexPositionTexture(center, new Vector2(0, 1));

    }

}

对集合中的每个billboard,在数组中添加六个顶点。每个顶点包含billboard中心点的位置信息和纹理坐标。

你只需在程序的开头调用这个方法一次,因为相机位置发生变化时数组的内容并不需要更新!所以在Initialize方法中在AddBillboards方法之后调用这个方法:

AddBillboards(); 

CreateBBVertices();

现在顶点已经做好了传递到GPU的准备,可以开始vertex shader的编写了。让我们从圆柱形billboarding开始,因为这个比球形billboarding容易点。

注意:将这个数据存储在显存中可以让你获益,因为你无需更新它的内容。这可以通过从billboardVertices数组创建一个VertexBuffer实现,可见教程5-4学习如何创建一个 VertexBuffer。

用于圆柱形Billboarding的Vertex Shader

首先定义变量,纹理和vertex/pixel shader输出结构:

//XNA interface 

float4x4 xView;

float4x4 xProjection;

float4x4 xWorld; 

float3 xCamPos;

float3 xAllowedRotDir;



//Texture Samplers 	

Texture xBillboardTexture;

sampler textureSampler = sampler_state

{

    texture = <xBillboardTexture>; 

    magfilter = LINEAR; 

    minfilter = LINEAR; 

    mipfilter=LINEAR; 

    AddressU = CLAMP; 

    AddressV = CLAMP; 

}; 



struct BBVertexToPixel 

{

    float4 Position : POSITION; 

    float2 TexCoord : TEXCOORD0; 

}; 



struct BBPixelToFrame 

{

    float4 Color : COLOR0; 

}; 

和往常一样要将3D世界绘制到2D屏幕中,需要World,View和Projection矩阵。要进行billboarding计算,你还需要知道相机的当前位置。因为你编写的是圆柱形billboarding,你还需要定义一个图像绕着旋转的轴。

你只需要一个纹理,将图像绘制在两个三角形中。对每个顶点,vertex shader会计算对应的2D屏幕坐标和纹理坐标。对每个像素,pixel shader将输出颜色。

在billboarding的情况中,vertex shader用来计算偏离中心位置的偏移量。让我们从编写添加偏移量的代码开始,billboarding是建立在这个代码的基础上的:

// Technique: CylBillboard 

BBVertexToPixel CylBillboardVS(float3 inPos: POSITION0, float2 inTexCoord: TEXCOORD0) 

{ 

    BBVertexToPixel Output = (BBVertexToPixel)0; 

    

    float3 center = mul(inPos, xWorld); 

    float3 upVector = float3(0,1,0); 

    float3 sideVector = float3(1,0,0); 

    float3 finalPosition = center; 

    

    finalPosition += (inTexCoord.x-0.5f)*sideVector; 

    finalPosition += (0.5f-inTexCoord.y)*upVector; 

    

    float4 finalPosition4 = float4(finalPosition, 1); 

    float4x4 preViewProjection = mul (xView, xProjection); 

    

    Output.Position = mul(finalPosition4, preViewProjection); 

    Output.TexCoord = inTexCoord; 

    

    return Output; 

} 

对每个顶点,vertex shader接受定义在XNA项目中的位置和纹理坐标。注意这个位置是 billboard 中心点的3D位置。与World矩阵的乘积让你可以定义一个全局World矩阵旋转/缩放/平移所有billboards。最后的中心位置存储在一个叫做center的变量中,对一个billboard中的六个顶点都是相同的。

然后,定义一个静态的Side向量和Up向量。这些向量用来对顶点相对于billboard 中心点进行偏移。在图3-19中看一下如何偏移六个顶点,图中显示了一个billboard的两个三角形。注意看外部四个纹理坐标和三角形顶角上的六个顶点索引。

4

图3-19 六个顶点相对于中心位置的偏移量

接下来的代码计算指定顶点的3D位置。首先从中心位置开始然后,你想知道当前向量是否需要偏移到(-1,0,0)向左方向或(1,0,0)向右方向。你可以通过观察图3-19中的X纹理坐标进行这个判断:纹理坐标为(0,0)的顶点位于左上方,所以它需要移动到左边。纹理坐标为(1,0)的顶点在右上方,它需要被移动到右方。

以上的操作只需使用一行代码:首先将X纹理坐标减0.5,如果顶点在左侧结果是–0.5f,如果在右侧则为+0.5f。将结果乘以(+1,0,0)向量,左边的顶点结果为(–0.5f,0,0),右边的顶点结果为(+0.5f,0,0)!

同样的方法也用在Y纹理坐标上:纹理坐标(0,0)代表左上角顶点,(0,1)代表左下角顶点。所以如果Y纹理坐标为0,就是顶部顶点;如果为1则为底部顶点。

将正确的Side和Up偏移作用在中心位置上,你就获得了指定顶点的3D位置。

现在你要做的就是像往常一样使用ViewProjection矩阵将这个3D位置转换到2D屏幕空间中。在使用一个4 × 4矩阵将这个float3进行转换前,还需要使它变为一个float4,只需将1作为第四个坐标就可以了。

当使用pixel shader时,所有billboard都是漂亮的矩形,但是它们是互相平行的。这是因为你还没有使用代码中的相机位置计算偏移量,而这个偏移量用来旋转三角形使它们朝向相机。

显然在vertex shader中使用静态的Up和Side向量是无法实现上述功能的。圆柱形 billboarding容易些,因为你已经定义了billboard的Up向量:它是允许的旋转方向,通过XNA程序中的xAllowedRotDir变量被传递到shader中。

图3-20中的左图显示了两个billboard的例子,例如两棵树。因为你在XNA程序中指定了旋转方向作为树的Up向量,所以在vertex shader中已经知道了Up向量。

5

图3-20 两个圆柱形bilboard

Billboard的Side向量预先不知道,但是你可以获取这个向量。你需要知道Eye向量,这个向量从眼睛指向billboard的中心,如图3-20中的虚线所示。你知道Side向量垂直于Eye向量和Up向量,如图3-20右图所示,显示的情况与左图相同,只是观察角度不同。

你可以通过叉乘两个向量获取同时垂直于这两个向量的向量(见教程4-18),所以这就是获取Side向量的方法。

现在的vertex shader如下所示:

float3 center = mul(inPos, xWorld); 

float3 eyeVector = center - xCamPos; 

float3 upVector = xAllowedRotDir; 

upVector = normalize(upVector); 

float3 sideVector = cross(eyeVector, upVector); 

sideVector = normalize(sideVector); 



float3 finalPosition = center; 

finalPosition += (inTexCoord.x-0.5f)*sideVector; 

finalPosition += (0.5f-inTexCoord.y)*upVector;

你可以通过B-A获取任何从点A指向点B的向量,这个方法可以获取Eye向量。前面已经说过,billboard的Up向量是允许旋转的方向,是在XNA代码中指定的。因为Side向量垂直于Eye向量和Up向量,你可以通过叉乘这两个向量获取Side向量。

你需要确保Side和Up向量是单位长度(否则你无法控制billboard的大小),因此需要归一化这两个向量。

知道了billboard的Up和Side向量后,你可以重用前面变换3D位置的代码。现在vertex shader使用相机的当前位置计算顶点的最终位置,只要相机发生移动顶点就会改变它们的位置。

完成Billboarding:Pixel Shader和定义Technique

现在vertex shader可以计算顶点的3D坐标了,你要做的就是在pixel shader中将纹理施加在三角形上:

BBPixelToFrame BillboardPS(BBVertexToPixel PSIn) : COLOR0 

{

    BBPixelToFrame Output = (BBPixelToFrame)0; 

    

    Output.Color = tex2D(textureSampler, PSIn.TexCoord); 

    

    return Output; 

}

当然还要定义technique:

technique CylBillboard 

{

    pass Pass0 

    {

        VertexShader = compile vs_1_1 CylBillboardVS(); 

        PixelShader = compile ps_1_1 BillboardPS(); 

    }

}

现在你要做的就是将这个HLSL文件(即bbEffect. fx)导入到项目中,在LoadContent方法中加载到一个变量中:

bbEffect = content.Load<Effect>("bbEffect"); 

在Draw中,设置所需的XNA-to-HLSL变量并绘制billboards!

bbEffect.CurrentTechnique = bbEffect.Techniques["CylBillboard"]; 

bbEffect.Parameters["xWorld"].SetValue(Matrix.Identity); 

bbEffect.Parameters["xProjection"].SetValue(quatMousCam.ProjectionMatrix); 

bbEffect.Parameters["xView"].SetValue(quatMousCam.ViewMatrix); 

bbEffect.Parameters["xCamPos"].SetValue(quatMousCam.Position); 

bbEffect.Parameters["xAllowedRotDir"].SetValue(new Vector3(0,1,0)); 

bbEffect.Parameters["xBillboardTexture"].SetValue(myTexture); 



bbEffect.Begin(); 

foreach (EffectPass pass in bbEffect.CurrentTechnique.Passes) 

{

    pass.Begin(); 

    device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); 

    device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleList, billboardVertices, 0, billboardList.Count*2); 

    pass.End(); 

}

bbEffect.End(); 

这里将(0,1,0) Up方向作为billboards旋转方向。

如果你的PC有一块独立显卡的话,你可以绘制比XNA-only版本多得多的billboard。

用于球形Billboarding的Vertex Shader

球形billboarding和圆柱形billboarding的区别在于球形billboarding中每个billboard都是完全朝向相机的。对vertex shader代码来说,Up和Side向量都要垂直于Eye向量(在圆柱形billboarding中,只有Side向量垂直于Eye向量)。

球形billboarding的挑战是你还需要获取Up向量,因为这个向量预先是不知道的。要解决这个问题,你首先需要知道相机的Up向量。

因为你想让billboard的整个面都朝向相机,所以需要将billboard的Side向量同时垂直于Eye向量和相机的Up向量。要展示这个情况,我在图3-21的左图中将Eye和CamUp向量所在平面涂成了灰色,这样你就可以看出Side向量是垂直于这个平面的。因为它们是垂直的,你已经知道如何获取Side向量了:叉乘Eye向量和CamUp向量:

float3 sideVector = cross(eyeVector,xCamUp); 

sideVector = normalize(sideVector); 

6

图3-21 为球形bilboarding找到Side和Up向量

知道了Side向量,你还可以获取 billboard 的Up向量:它垂直于Side向量和Eye向量。我在图3-21的右图中将Side和Eye向量所在平面涂成了灰色,让你可以将它形象化。这意味着billboard的Up向量就是Eye和Side向量的叉乘:

float3 upVector = cross(sideVector,eyeVector); 

upVector = normalize(upVector); 

这就是球形billboarding相对于圆柱形billboarding需要改变的地方!别忘了定义xCamUp variable . . .

float3 xCamUp;

. . .和一个新technique:

technique SpheBillboard 

{

    pass Pass0 

    {

        VertexShader = compile vs_1_1 SpheBillboardVS(); 

        PixelShader = compile ps_1_1 BillboardPS(); 

    }

}

pixel shader是相同的。

确保在XNA项目中调用正确的technique并设置xCamUp参数:

bbEffect.CurrentTechnique = bbEffect.Techniques["SpheBillboard"]; 

bbEffect.Parameters["xWorld"].SetValue(Matrix.Identity); 

bbEffect.Parameters["xProjection"].SetValue(quatCam.ProjectionMatrix); 

bbEffect.Parameters["xView"].SetValue(quatCam.ViewMatrix); 

bbEffect.Parameters["xCamPos"].SetValue(quatCam.Position); 

bbEffect.Parameters["xCamUp"].SetValue(quatCam.UpVector); 

bbEffect.Parameters["xBillboardTexture"].SetValue(myTexture); 
代码

注意你可以看到五种不同类型的billboarding的代码:

  • XNA—only代码:球形billboarding
  • XNA—only代码:圆柱形billboarding
  • XNA + HLSL代码:球形billboarding
  • XNA + HLSL代码:圆柱形billboarding
  • XNA + HLSL代码:可变大小的球形billboarding

下面的代码是XNA+HLSL球形billboarding。下面的方法定义在哪放置billboards:

private void AddBillboards() 

{

    int CPUpower = 10; 

    for (int x = -CPUpower; x < CPUpower; x++) 

        for (int y = -CPUpower; y <CPUpower; y++)

            for (int z = -CPUpower; z < CPUpower; z++) 

                billboardList.Add(new Vector4(x, y, z, 0.5f));

}

下面的代码将集合转换为数组:

private void CreateBBVertices() 

{

    billboardVertices = new VertexPositionTexture[billboardList.Count * 6];

    

    int i = 0;

    foreach (Vector4 currentV4 in billboardList)

    {

        Vector3 center = new Vector3(currentV4.X, currentV4.Y, currentV4.Z);

        

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(1, 1)); 

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(0, 0)); 

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(1, 0));

        

        billboardVertices[i++]= new VertexPositionTexture(center, new Vector2(1, 1)); 

        billboardVertices[i++]= new VertexPositionTexture(center, new Vector2(0, 1)); 

        billboardVertices[i++] = new VertexPositionTexture(center,new Vector2(0, 0));

    }

}

下面的代码调用正确的technique,设置它的参数,并从数组进行绘制:

bbEffect.CurrentTechnique = bbEffect.Techniques["SpheBillboard"]; 

bbEffect.Parameters["xWorld"].SetValue(Matrix.Identity); 

bbEffect.Parameters["xProjection"].SetValue(quatMousCam.ProjectionMatrix);

bbEffect.Parameters["xView"].SetValue(quatMousCam.ViewMatrix);



bbEffect.Parameters["xCamPos"].SetValue(quatMousCam.Position); 

bbEffect.Parameters["xAllowedRotDir"].SetValue(new Vector3(0,1,0));

bbEffect.Parameters["xBillboardTexture"].SetValue(myTexture);

bbEffect.Parameters["xCamUp"].SetValue(quatMousCam.UpVector);



bbEffect.Begin();

foreach (EffectPass pass in bbEffect.CurrentTechnique.Passes)

{

    pass.Begin();

    device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); 

    device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleList, billboardVertices, 0, billboardList.Count*2); 

    pass.End(); 

}

bbEffect.End(); 

在HLSL部分,首先是vertex shader:

BBVertexToPixel SpheBillboardVS(float3 inPos: POSITION0, float2 inTexCoord: TEXCOORD0) 

{

    BBVertexToPixel Output = (BBVertexToPixel)0; 

    

    float3 center = mul(inPos, xWorld); 

    float3 eyeVector = center - xCamPos; 

    float3 sideVector = cross(eyeVector,xCamUp); 

    sideVector = normalize(sideVector); 

    

    float3 upVector = cross(sideVector,eyeVector); 

    upVector = normalize(upVector); 

    

    float3 finalPosition = center; 

    finalPosition += (inTexCoord.x-0.5f)*sideVector*0.5f;

    finalPosition += (0.5f-inTexCoord.y)*upVector*0.5f; 

    float4 finalPosition4 = float4(finalPosition, 1); 

    

    float4x4 preViewProjection = mul (xView, xProjection); 

    Output.Position = mul(finalPosition4, preViewProjection); 

    Output.TexCoord = inTexCoord; 

    

    return Output; 

} 

然后是简单的pixel shader:

BBPixelToFrame BillboardPS(BBVertexToPixel PSIn) : COLOR0 

{

    BBPixelToFrame Output = (BBPixelToFrame)0; 

    

    Output.Color = tex2D(textureSampler, PSIn.TexCoord); 

    

    return Output; 

}
传递额外的信息

如果你想设置vertex shader生成的billboard的大小或将额外信息从XNA程序传递到 vertex shader中,你需要将这个信息添加到每个顶点中。你可以通过,例如,VertexPositionNormalTexture和在Normal向量的一个分量中存储大小信息达到以上目的。

但是,这样做虽然可以解决问题,当你需要传递顶点中的真实法线数据时仍会碰到同样的问题。因此为了保持通用性,你可以创建自己的顶点格式,它可以保存纹理和额外数据。关于自定义顶点格式的更多信息可参加教程5-14。

public struct VertexBillboard 

{

    public Vector3 Position; 

    public Vector2 TexCoord; 

    public Vector4 AdditionalInfo; 

    public VertexBillboard(Vector3 Position, Vector2 TexCoord,Vector4 AdditionalInfo) 

    {

        this.Position = Position; 

        this.TexCoord = TexCoord; 

        this.AdditionalInfo = AdditionalInfo; 

    }

    

    public static readonly VertexElement[] VertexElements = new VertexElement[] 

    {

        new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0), 

        new VertexElement(0, 12, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0), 

        new VertexElement(0, 20, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 1), 

    }; 

    

    public static readonly int SizeInBytes = sizeof(float) * (3 + 2 + 4); 

} 

这个格式接受一个额外的叫做Add itionalInfo的Vector4。你将这个Vector4作为另一个TextureCoordinate。要区别这两个TextureCoordinate,请注意最后一个参数索引的区别。这意味着真实的纹理坐标在vertex shader中作为TEXCOORD0,而额外的Vector4作为TEXCOORD1 (见教程5-14)。

然后,使用新定义的VertexBillboarding替换VertexPositionTexture(你可以在XNA Game Studi中使用Ctrl+H进行这个操作)。最后,调整CreateBBVertices方法将四个额外的float值添加到顶点中:

private void CreateBBVertices() 

{

    billboardVertices = new VertexBillboard[billboardList.Count * 6]; 

    

    int i = 0; 

    foreach (Vector4 currentV4 in billboardList) 

    {

        Vector3 center = new Vector3(currentV4.X, currentV4.Y, currentV4.Z); 

        billboardVertices[i++] = new VertexBillboard(center, new Vector2(1, 1), new Vector4(0.8f, 0.4f, 0, 0)); 

        billboardVertices[i++] = new VertexBillboard(center, new Vector2(0, 0), new Vector4(0.8f, 0.4f, 0, 0));

        billboardVertices[i++] = new VertexBillboard(center, new Vector2(1, 0), new Vector4(0.8f, 0.4f, 0, 0)); 

        billboardVertices[i++] = new VertexBillboard(center, new Vector2(1, 1), new Vector4(0.8f, 0.4f, 0, 0)); 

        billboardVertices[i++] = new VertexBillboard(center, new Vector2(0, 1), new Vector4(0.8f, 0.4f, 0, 0)); 

        billboardVertices[i++] = new VertexBillboard(center, new Vector2(0, 0), new Vector4(0.8f, 0.4f, 0, 0)); 

    }

}

在本例中,你可以改变让billboard的大小。Vector4中的第一个参数,这里是0.8f,用来缩放billboard的宽度,第二个参数,这里是0.4f,缩放高度。这会导致billboard的宽度是高度的两倍。

以上是XNA代码;你要做的就是在vertex shader中将这个Vector4作为TEXCOORD1:

BBVertexToPixel SpheBillboardVS(float3 inPos: POSITION0, float2 inTexCoord: TEXCOORD0, float4 inExtra: TEXCOORD1)

现在你可以以inExtra 变量的形式访问Vector4,使用它缩放billboard的宽和高:

finalPosition += (inTexCoord.x-0.5f)*sideVector*inExtra.x; 

finalPosition += (0.5f-inTexCoord.y)*upVector*inExtra.y; 

这个例子只使用了float4的x和y值,如果需要你也可以访问z和w值。

译者注:在http://creators.xna.com/en-US/sample/billboard上也有一个示例可供研究,比较复杂,使用内容管道生成billboard图像,在HLSL中还实现了风吹动的效果。

你可能感兴趣的:(DI)