Home > TutorialsComputer Graphics Essentials
2.11. Billboards 
In this tutorial you will create a set of billboards and load a tree texture over them to make them look like trees. This trick can be useful for trees that are far away from the camera.



Click here to go to the forum discussion of this tutorial


 

2.11.1. Prerequisites
default.jpg
Before you start this tutorial make sure that you have:
  • Installed XNA Game Studio 3.1
  • Downloaded the starter project from the downloads section
  • Successfully run the starter project
  • For a better understanding, is preferred to have finished the previous tutorials.
 

2.11.2. Game Assets 
Game assets [2.3.2] for this tutorial:

 

2.11.3. The using statements
Besides the using statements from the starter project, this tutorial needs the following using statements:

using BetaCell.Util.RenderStates;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Environment;
using BetaCell.Environment.Light;
using BetaCell.Effects;
using BetaCell.Util.Procedural;
using BetaCell.Common.Vertex;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Texture;
using BetaCell.Dynamic.Billboard
;
using BetaCell.Effects.Composer;
using BetaCell.Environment.Mesh;

 

2.11.4. Game Attributes 
The attributes that come with the starter project are explained in tutorial section 1.1.4.

For this tutorial we are going to be needing the following attributes:

BCMesh plane;
BCMesh trees;

BCAlphaGreaterRenderStrategy alphaBlock
;

 

Advertisement
 

2.11.5. The Initialize method 

The only difference between last tutorial and this one is the creation of a mesh we haven't use before; this mesh is a billboard. A billboard is just a set of planes that is constructed as follows.

First, we create an array of positions, one for each billboard:

Vector3[] billboardPositions = new Vector3[5];
billboardPositions[0] = new Vector3(10,5,0);
billboardPositions[1] = new Vector3(10,5,10);
billboardPositions[2] = new Vector3(20,5,-10);
billboardPositions[3] = new Vector3(30,5,-30);
billboardPositions[4] = new Vector3(10,5,20);

Then we create the billboard set by passing the positions, the height , the width and the vertex container for the billboards.

trees =
    ProceduralModelers.BILLBOARD_MODELER.createStaticBillboard(
    billboardPositions,
    10, 7,
    new BCVertexPosNorTexContainer());
BCMeshGraphicUtil.init(trees, device);

default.jpg
Right now what we have is a set of planes, if we apply the tree texture and draw them, they will only look good when they are facing the camera; the trees won't look good when seen from the side because they are really just planes.

So what we do is force the planes to face the camera at all times as the figure shows. Notice that the three billboards (green red and blue) are always in the same position, but they rotate to face the camera at all times (the figure shows four scenarios with the same 3 billboards at the same position but rotated depending on the position of the camera to face it).

To achieve that effect, we must apply the billboard effect part to the effect of the billboards. First we get the billboard function from the billboard feeder:


BCBillboardFeeder billboard = new BCBillboardFeeder();
BCFunction billboardFunction =
    billboard.getFunction("Actuator.Billboard.Billboard");


Then we create the visitor that will set the billboard effect part:

BCDynamicEffectVisitor billboardSetter =
    new BCDynamicEffectVisitor(billboardFunction, billboard, 0
);

Now this is extremely important. The billboard part MUST BE THE FIRST PART because it will affect the normal of the planes, so it must be done before any color calculation. What this mean is that the final parameter of the visitor must be 0 because this states that the billboard effect part should be the first one.

The rest is the same as the previous tutorials except for the part where the billboard visitor visits the billboard mesh.

trees.visit(materialSetter);
trees.visit(textureSetter);
trees.visit(billboardSetter);
trees.visit(colorSetter);
trees.visit(lightSetter);
trees.visit(textureEffectSetter); 

 

2.11.6. The Draw method
Now we draw the plane

plane.Draw(gameTime);

Then the billboards are drawn between a push and pop calls of the alpha greater render state set strategy.

alphaBlock.push(device);
trees.Draw(gameTime);
alphaBlock.pop(device);

 

2.11.7. Conclusion
This example is just one of the many uses that billboards can have, if you need to, investigate about billboards and their use. Also, to see that the billboards actually face the camera, create a procedural rotator to rotate the plane and the billboards and see what happens.

Click here to go to the forum discussion of this tutorial

 

Advertisement
 

2.11.8. Complete source code
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using BetaCell.Debug;
using BetaCell.Util.GlobalInfo.Content;
using BetaCell.Environment.Camera;
using BetaCell.Util.GlobalInfo;

using BetaCell.Util.RenderStates;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Environment;
using BetaCell.Environment.Light;
using BetaCell.Effects;
using BetaCell.Util.Procedural;
using BetaCell.Common.Vertex;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Texture;
using BetaCell.Dynamic.Billboard;
using BetaCell.Effects.Composer;
using BetaCell.Environment.Mesh;

namespace Starter
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Game attributes
        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        ContentManager content;

        BCFirstPersonHumanCamera camera;
        //end game attributes

        //This tutorial's attributes
        BCMesh plane;
        BCMesh trees;

        BCAlphaGreaterRenderStrategy alphaBlock;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            content = new ContentManager(Services);
#if(XBOX360)
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
#else
            graphics.PreferredBackBufferWidth = 852;
            graphics.PreferredBackBufferHeight = 480;
#endif
        }

        protected override void Initialize()
        {
            //base initialization
            device = graphics.GraphicsDevice;

            BCLogger.instance.init(device, content);
            BCInitializationManager.initialize(content);

            OnActivated(null, null);

            alphaBlock = new BCAlphaGreaterRenderStrategy();
            recreateScene();

            base.Initialize();
        }

        private void recreateScene()
        {
            BCMaterialBase material = new BCMaterialBase();
            material.specRange = 0.8f;
            material.diff = new Color(255, 255, 255, 128);
            material.spec = Color.White;
            material.amb = Color.LightGray;

            BCMaterialVisitor materialSetter = new BCMaterialVisitor(material);

            BCColorSet colorSet = new BCColorSet(Color.Black);
            BCDynamicEffectVisitor colorSetter =
                new BCDynamicEffectVisitor(colorSet.getFunction("Shader.ColorSet"), colorSet, 0);

            BCBasicLight light = new BCBasicLight();
            light.range = 2000;
            light.transform = Matrix.CreateTranslation(-1, -1, -1);
            light.constAtten = 1;
            light.linAtten = 0;
            light.cubAtten = 0;
            light.spotRange = 0.9f;
            light.useShadows = false;

            BCDynamicEffectVisitor lightSetter = new BCDynamicEffectVisitor(
                light.getFunction("Shader.Light.Classic.PixelDirectionLight"),
                light,
                BCDynamicEffect.MAX_FUNCTIONS
            );

            Vector3[] billboardPositions = new Vector3[5];
            billboardPositions[0] = new Vector3(10, 5, 0);
            billboardPositions[1] = new Vector3(10, 5, 10);
            billboardPositions[2] = new Vector3(20, 5, -10);
            billboardPositions[3] = new Vector3(30, 5, -30);
            billboardPositions[4] = new Vector3(10, 5, 20);

            trees =
                ProceduralModelers.BILLBOARD_MODELER.createStaticBillboard(
                billboardPositions,
                10, 7,
                new BCVertexPosNorTexContainer());
            BCMeshGraphicUtil.init(trees, device);

            //Setting texture
            BCTextureVisitor textureSetter = new BCTextureVisitor(
                "tex",
                new BCMemoryTextureStrategy(content.Load<Texture2D>("Content/gameAssets/tree"))
            );

            BCBillboardFeeder billboard = new BCBillboardFeeder();
            BCFunction billboardFunction = billboard.getFunction("Actuator.Billboard.Billboard");
            BCDynamicEffectVisitor billboardSetter =
                new BCDynamicEffectVisitor(billboardFunction, billboard, 0
            );

            BCTextureFeeder texFeeder = new BCTextureFeeder();
            texFeeder.addSampler(
                "tex",
                new BCSampler(
                    "LINEAR", "LINEAR", "LINEAR", "4",
                    "WRAP", "WRAP", "0xffffffff"
                )
            );
            BCDynamicEffectVisitor textureEffectSetter = new BCDynamicEffectVisitor(
                texFeeder.getFunction("Shader.Texture.Texture"),
                texFeeder,
                BCDynamicEffect.MAX_FUNCTIONS
            );

            trees.visit(materialSetter);
            trees.visit(textureSetter);
            trees.visit(billboardSetter);
            trees.visit(colorSetter);
            trees.visit(lightSetter);
            trees.visit(textureEffectSetter);

            //-----
            //plane
            //-----

            textureSetter.strategy = new BCMemoryTextureStrategy("Content/gameAssets/ground0", content);

            plane = ProceduralModelers.PLANE_MODELER.createPlane(
                10, 10, 10, 10, new Vector3(0, 0, 0),
                new BCVertexPosNorTexContainer(),
                4);
            BCMeshGraphicUtil.init(plane, device);

            plane.visit(materialSetter);
            plane.visit(textureSetter);
            plane.visit(colorSetter);
            plane.visit(lightSetter);
            plane.visit(textureEffectSetter);
        }

        protected override void OnActivated(object sender, EventArgs args)
        {
            buildViewMatrix();
            base.OnActivated(sender, args);
        }

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(40, 5, 40);
            Vector3 look = new Vector3(-4, -0.5f, -4);
            look.Normalize();

            camera = new BCFirstPersonHumanCamera(look, pos,
                MathHelper.PiOver4,
                (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height,
                1f, 200);

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.VIEW_INDEX,
                camera.getViewMatrix(-1)
            );

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.PROJECTION_INDEX,
                camera.getProjectionMatrix(-1)
            );
        }

        protected override void Update(GameTime gameTime)
        {
        }

        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);

            plane.Draw(gameTime);

            alphaBlock.push(device);
            trees.Draw(gameTime);
            alphaBlock.pop(device);

            BCLogger.instance.printFPS(gameTime);
            BCLogger.instance.flush();
            base.Draw(gameTime);
        }
    }
}