30.公告牌BillboardSceneNode类
BillboardSceneNode类的代码主要参考自3.11 Billboarding:在3D世界绘制2D图像使它们总是面向相机,使用的是这篇文章中的第二个方法,即在GPU中实现Billboard,只实现了圆柱形Billboard。代码如下:
namespace StunEngine.SceneNodes { ////// BillboardSceneNode是一个始终朝向相机的带纹理的平面。 /// public class BillboardSceneNode :GenericMaterialSceneNode { ////// BillboardSceneNode旋转轴方向,默认为向上 /// private Vector3 allowedRotDir = Vector3.Up ; ////// 获取或设置BillboardSceneNode旋转轴方向,默认为向上 /// public Vector3 AllowedRotDir { get { return allowedRotDir; } set { allowedRotDir = value; material.EffectInstance.Parameters["gAllowedRotDir"].SetValue(allowedRotDir); } } ////// 创建一个BillboardSceneNode对象。 /// /// 引擎 /// 所属场景 /// 漫反射纹理名称 public BillboardSceneNode(StunXnaGE engine, Scene setScene,string setDiffuseTextureName) : base(engine, setScene, setDiffuseTextureName, Vector2 .One , null, Vector2 .Zero ) { // 设置材质和默认effect参数 material.CurrentTechniqueName ="CylBillboard"; material.EffectInstance.Parameters["gAllowedRotDir"].SetValue(allowedRotDir); } ////// 初始化 /// public override void Initialize() { this.UpdateOrder = SceneNodeOrdering.TransparentNode.GetValue(); base.Initialize(); // 创建Billboard顶点数据 this.mesh =MeshBuilder .CreatBillBoard (engine .GraphicsDevice ); } public override int Draw(GameTime gameTime, bool useReflection) { int tris = base.Draw(gameTime, useReflection); // 绘制完成后需将图形设备的状态重置为默认值 engine.GraphicsDevice.RenderState.AlphaBlendEnable = false; engine.GraphicsDevice.RenderState.DepthBufferWriteEnable = true; engine.GraphicsDevice.RenderState.AlphaTestEnable = false; //返回绘制的三角形数量 return tris; } #region 单元测试 #if DEBUG ////// 测试BillboardSceneNode类 /// public static void TestBillboardSceneNode() { TestGame.Start("测试BillboardSceneNode类", delegate { for (int x = -6; x <= 6;x+=2) for (float y=1f;y<=13;y+=2) for (int z=-6;z<=6;z+=2) { BillboardSceneNode bb = new BillboardSceneNode(TestGame.engine, TestGame.scene, "Textures\\tree"); TestGame.scene.AddNode(bb); Vector3 position = new Vector3(x, y, z); bb.Pose.SetPosition(ref position); } TestGame.scene.IsShowMouse = false; }); } #endif #endregion } }
在MeshBuilder中的静态方法CreatBillBoard中创建billboard的顶点,这个方法与创建平面顶点的方法类似,不同之处在于顶点坐标默认为(0,0,0),实际顶点坐标是根据该顶点的纹理坐标在shader中自动计算得出的,代码如下:
////// 创建BillBoard /// /// 图形设备 ///public static Mesh CreatBillBoard(GraphicsDevice g) { VertexDeclaration bbVertexDeclaration = new VertexDeclaration(g, VertexPositionNormalTexture.VertexElements); //创建地形4个顶点 VertexPositionNormalTexture[] bbVertices = new VertexPositionNormalTexture[4]; Vector3 center = Vector3.Zero ; // 左下顶点 bbVertices[0].Position = center; bbVertices[0].TextureCoordinate = new Vector2(0, 1); bbVertices[0].Normal = Vector3.Backward; // 左上顶点 bbVertices[1].Position = center; bbVertices[1].TextureCoordinate = new Vector2(0, 0); bbVertices[1].Normal = Vector3.Backward; // 右下顶点 bbVertices[2].Position = center; bbVertices[2].TextureCoordinate = new Vector2(1, 1); bbVertices[2].Normal = Vector3.Backward; // 右上顶点 bbVertices[3].Position = center; bbVertices[3].TextureCoordinate = new Vector2(1, 0); bbVertices[3].Normal = Vector3.Backward; // 创建顶点缓冲和索引缓冲 VertexBuffer bbVertexBuffer = new VertexBuffer(g, VertexPositionNormalTexture.SizeInBytes * bbVertices.Length, BufferUsage.WriteOnly); bbVertexBuffer.SetData(bbVertices, 0, 4); return new Mesh(PrimitiveType.TriangleStrip, bbVertexBuffer, bbVertexDeclaration, 4, 2); }
真正有趣的部分在effect文件中,我把它放在了Generic.fx中:
// Billboard的旋转轴 uniform extern float3 gAllowedRotDir; // Billboard使用额顶点着色器输入结构 struct BBVS_INPUT { float3 Position : POSITION0; float2 TexCoord : TEXCOORD0; }; // Billboard使用额顶点着色器输出结构 struct BBVS_OUTPUT { float4 Position : POSITION0; float2 TexCoord : TEXCOORD0; }; //------- Technique: CylBillboard -------- BBVS_OUTPUT CylBillboardVS(BBVS_INPUT Input) { BBVS_OUTPUT Output = (BBVS_OUTPUT)0; // 将billboard的中心点位置根据世界矩阵平移,世界矩阵的第4行前三个数字就是x,y,z方向的平移 float3 center=gWorld._m30_m31_m32; // 计算相机指向中心点的向量 float3 eyeVector = center - gCameraPos; // 根据billboard的向上方向计算向右方向 float3 upVector = gAllowedRotDir; upVector = normalize(upVector); float3 sideVector = cross(eyeVector,upVector); sideVector = normalize(sideVector); // 根据顶点的纹理坐标计算顶点的坐标 // 例如顶点的纹理坐标是(0,0)表示这个顶点位于左上角,由下面的代码可以计算出它的坐标为(-0.5,0.5,0) // 还要施加缩放。缩放的数据位于世界矩阵的对角线上,即第一行第一个数据是x方向的缩放值,第二行第二个数据是y方向的缩放 float3 finalPosition = center; finalPosition += (Input.TexCoord.x-0.5f)*sideVector*gWorld._m00; finalPosition += (0.5f-Input.TexCoord.y)*upVector*gWorld._m11; float4 finalPosition4 = float4(finalPosition, 1); float4x4 preViewProjection = mul (gView, gProjection); Output.Position = mul(finalPosition4, preViewProjection); Output.TexCoord = Input.TexCoord; return Output; } float4 BillboardPS(BBVS_OUTPUT Input) : COLOR0 { return tex2D(textureSampler, Input.TexCoord); } technique CylBillboard { // 首先绘制billboard中不透明的像素,本例中为alpha值大于245的像素 pass RenderOpaquePixels { VertexShader = compile vs_3_0 CylBillboardVS(); PixelShader = compile ps_3_0 BillboardPS(); AlphaBlendEnable = false; AlphaTestEnable = true; AlphaFunc = Greater; AlphaRef = 245; ZEnable = true; ZWriteEnable = true; } // 然后绘制billboard中透明的像素,本例中为alpha值小于245的像素 pass RenderAlphaBlendedFringes { VertexShader = compile vs_3_0 CylBillboardVS(); PixelShader = compile ps_3_0 BillboardPS(); AlphaBlendEnable = true; SrcBlend = SrcAlpha; DestBlend = InvSrcAlpha; AlphaTestEnable = true; AlphaFunc = LessEqual; AlphaRef = 245; ZEnable = true; ZWriteEnable = false; } }
为了简单起见,billboard并没有实现光照,如果想实现并不难,可参考Generic.fx中的TexturedLights的做法。
让billboard始终面向相机的方法位于顶点着色器中,具体解释可参见3.11 Billboarding:在3D世界绘制2D图像使它们总是面向相机,在这篇文章的最后还介绍了如何通过在自定义顶点格式中添加一个Vector4改变billboard的大小,但我的引擎中billboard大小是存储在节点的pose的世界矩阵中的,所以处理方法有所不同,请见代码中的注释。
最后,因为billboard通常包含透明部分,要让它们正确显示需要使用一些技巧,具体解释请参见3D系列4.17 设置Alpha混合的Renderstate,核心思想就是每个billboard都要绘制两次,一次只绘制不透明部分,第二次再绘制透明部分。3D系列4.17 设置Alpha混合的Renderstate这个操作是在Draw方法中进行的,我把这个操作移到了shader的technique CylBillboard中,第一个Pass绘制不透明部分,第二个Pass绘制透明部分,这个方法是参考自XNA官网的billboard示例:http://creators.xna.com/en-US/sample/billboard,源代码BillboardSample.rar下载。以上两种方法结果是相同的,帧数也差不多。
单元测试截图如下:
文件下载(已下载 1547 次)
发布时间:2010/4/28 上午8:05:01 阅读次数:6610