Home > TutorialsMeshes and I/O
3.5. Picking 
In this tutorial you will learn how to pick an object in the world with face precision using BetaCell.



Click here to go to the forum discussion of this tutorial


 

3.5.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.
  • Tutorial [3.2] is the base of this tutorial, it's preferred that you finish it before starting this one.. 
 


3.5.2. Game Assets 
Game assets [2.3.2] for this tutorial:
To be able to create a collision model from the bcm file, follow these instructions:
  1. right click the evilAnimatedTexturedB.bcm file in the gameAssets folder
  2. click properties
  3. set the "BetaCell_mesh_processor"
  4. In the processor's sub-properties (the ones expanded with the '+' sign)
    1. set "Runtime Vertex Access" to true

 

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

using BetaCell.Environment.Animation;
using BetaCell.Environment.Mesh;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Content;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Dynamic.Skinned;
using BetaCell.Environment.Mesh.Collision;
using BetaCell.Util;
using BetaCell.Util.RenderStates;
using BetaCell.Common.Bounding
;

 

3.5.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 evilMesh;

BCAnimationSetBase animationSet;

BCAnimationBase idle;
BCAnimationBase run;
BCAnimationBase iddleToRun;
BCAnimationBase runToIdle;

BCAnimationController controller;

BCAddSequenceListener addRunSequence;
BCAddSequenceListener addIdleSequence;

//----------------------------
//Animation Control Attributes
//----------------------------

private double timeToChange = 5;
private double lastTime = 0;
private bool isIdle = true;

//---------------------------
//Animated collision model
//---------------------------

BCCollisionModel evilCollisionModel;
bool showFace = false;
Vector3[] currentCollisionFace = new Vector3[3];

BCCursor cursor;


 

Advertisement
 

3.5.5. The Initialize method 

This tutorial starts at the end of [3.2]. The first change is can be seen when the mesh is loaded:

evilMesh =
   BCMeshReader.readMesh("Content/gameAssets/evilAnimatedTexturedB",
   new BCCubeBoundingResolver(), device, content);


Note  that we pass a BCCubeBoundingResolver instead of null as the second parameter, this is because we need the bounding volume to perform the collision test. What the BCCubeBoundingResolver does is to calculate a cube bounding for each part of the mesh.

The next portion of code relevant to this tutorial is where we prepare the boundings of the mesh to be drawn:

//Prepare boundings of the mesh for drawing
BCDrawableBoundingPrepareVisitor boundingPrepare =
    new BCDrawableBoundingPrepareVisitor(device, Color.Red);
    evilMesh.visit(boundingPrepare);

What it does is prepare the boundings of the mesh for drawing. This is done for debugging purposes.

After that  we create a collision model

//Collision model
evilColisionModel = new BCCollisionModel(evilMesh);

To perform per face collision testing on a mesh would be too expensive because of the flexible vertex schema used in it, so we define a collision model, which is a simple copy of the mesh. Note that the constructor of the collision model receives the mesh, this is ok for out test but a production application might require a version of the mesh with a lower detail to perform collision operations.

Finally we create the cursor which is the x that's placed t the center of the screen.

cursor = new BCCursor(
    device,
    content.Load<Texture2D>("Content/gameAssets/cursor")
);

cursor.position = new Vector2(
    device.Viewport.Width / 2, device.Viewport.Height / 2);


Notice that the constructor takes an image of the cursor and that the cursor is placed at the center of the screen.

 

3.5.6. The Update method
Here the collision is performed.

First a ray is made; it's start is the position of the camera and it's direction is the direction of the camera
 
Ray currentRay = new Ray(camera.getPosition(), camera.look);

Then we test the intersection with the meshe's bounding volume:

float? distance = evilMesh.bounding.Intersects(currentRay);

If there was a collition and it was in front of the ray, we proceed:

if (distance != null && distance > 0)

The results of the intersection test are stored in:

A list of the faces intersected by the ray:

List<Vector3> intersectedFaces = new List<Vector3>();

A list of the distances from the start of the ray to each of the intersected faces

List<float> distances = new List<float>();

Then we must animate the collision model; to do that we must first obtain the current poses of the animation:

BCPooledLinkedList<BCBonePose> currentPoses = controller.popBonePoses();

Then we obtain the bone transformations of the combination of these poses:

Matrix[] currentPosesTranforms = controller.popFinalMatrices(currentPoses);

Whith them we animate the collision model:

evilCollisionModel.animate(currentPosesTranforms, evilMesh.getTransform());

We then recycle both the poses and the transforms to save memory:

controller.pushFinalMatrices(currentPosesTranforms);
controller.pushBonePoses(currentPoses);

Then the collision is performed:

evilColisionModel.intersects(
    currentRay, intersectedFaces, distances);


The following method is just a helper function that takes the result of the collision and stores the closest face in the currentCollisionFace reference parameter, it returns true if such face exists and false otherwise.

showFace = BCColisionModel.getClosestFace(currentRay,
    intersectedFaces, distances,     currentColisionFace);



 

3.5.7. The Draw method
The draw method has some interesting code; first we draw the bounding of the mesh:

BCDrawUtil.drawBoundings(evilMesh, device, gameTime);

Then the mesh is drawn

evilMesh.Draw(gameTime);

After that  we create some render strategies [2.9.5] that will help us in our debugging purposes:

BCWireFrameRenderStrategy drawWire = new BCWireFrameRenderStrategy();
BCDrawOverGeometryRenderStrategy drawOver =
    new BCDrawOverGeometryRenderStrategy(15);


The first one causes the drawing to be made in wireframe like the green part of the figure in [3.5.1].

The second prevents the same geometry from causing z-fighting when it's drawn twice; this will be explained with drawing of the the intersected triangle portion of code.

Then the collision model is drawn

drawWire.push(device);
drawOver.push(device);
BCDrawUtil.drawCollisionModel(device, evilMesh.transform,
    Color.LightGreen, evilColisionModel);

drawOver.pop(device);
drawWire.pop(device);

After that the intersected face is drawn if it exists:

default.jpg
This is the code that draws the yellow triangle:

if (showFace)
{
    drawOver.push(device);
    BCDrawUtil.drawTriangle(
        device,
        currentColisionFace[0],
        currentColisionFace[1],
        currentColisionFace[2],
        Color.Yellow
    );
    drawOver.pop(device);
}

The figure is what's known as z-fighting, the same triangle (the yellow one) is drawn twice and the graphics card doesn't know which is the triangle to draw so some pixels are drawn as if the triangle most close to the camera is the yellow one and others as if it's the gray one.

To draw a triangle drawn over a coplanar surface without z-fighting, you can use the BCDrawOverGeometryRenderStrategy.

 

3.5.8. Conclusion
Through encapsulation, you were able to add some collision testing with little effort, the techniques shown in this tutorial can be applied to any application that uses BetaCell, that's why we can make a collision ith an animated model.

Click here to go to the forum discussion of this tutorial

 

Advertisement
 

3.5.9. 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.Environment.Animation;
using BetaCell.Environment.Mesh;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Content;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Dynamic.Skinned;
using BetaCell.Environment.Mesh.Collision;
using BetaCell.Util;
using BetaCell.Util.RenderStates;

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

        BCAnimationSetBase animationSet;

        BCAnimationBase idle;
        BCAnimationBase run;
        BCAnimationBase idleToRun;
        BCAnimationBase runToIdle;

        BCAnimationController controller;

        BCAddSequenceListener animationSequence;

        //----------------------------
        //Animation Control Attributes
        //----------------------------

        double timeToChange = 5;
        double lastTime = 0;
        bool isIdle = true;

        //---------------------------
        //Animated collision model
        //---------------------------

        BCCollisionModel evilCollisionModel;
        bool showFace = false;
        Vector3[] currentCollisionFace = new Vector3[3];

        BCCursor cursor;

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

            //-----
            //MESHES
            //-----

            evilMesh =
                BCMeshReader.readMesh("Content/gameAssets/evilAnimatedTexturedB", null, device, content);

            //-----
            //Light Effect
            //-----

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

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

            evilMesh.visit(lightSetter);

            //---------
            //ANIMATION
            //---------
            animationSet = content.Load<BCAnimationSetBase>("Content/gameAssets/evilAnimatedTexturedBAnim");

            animationSet.animations.TryGetValue("Idle", out idle);
            animationSet.animations.TryGetValue("Run", out run);
            animationSet.animations.TryGetValue("IdleToRun", out idleToRun);
            animationSet.animations.TryGetValue("RunToIdle", out runToIdle);

            controller = new BCAnimationController(evilMesh.skeleton.boneCount);

            evilMesh.controller = controller;

            BCSkinnedMeshFeeder skinnedFeeder = new BCSkinnedMeshFeeder();
            BCDynamicEffectVisitor skinnedSetter = new BCDynamicEffectVisitor(
                skinnedFeeder.getFunction("Actuator.Skinned.SkinnedMesh"),
                skinnedFeeder, 0
            );
            evilMesh.visit(skinnedSetter);

            animationSequence = new BCAddSequenceListener();
            animationSequence.setNextAnimation(null, idle);
            controller.addSingle(idle, animationSequence, 0);

            //Prepare boundings of the mesh for drawing
            BCDrawableBoundingPrepareVisitor boundingPrepare =
                new BCDrawableBoundingPrepareVisitor(device, Color.Red);
            evilMesh.visit(boundingPrepare);

            //Collision model
            evilCollisionModel = new BCCollisionModel(evilMesh);

            //Cursor
            cursor = new BCCursor(
                device,
                content.Load<Texture2D>("Content/gameAssets/cursor")
            );

            cursor.position = new Vector2(device.Viewport.Width / 2, device.Viewport.Height / 2);

            base.Initialize();
        }

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

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(6f, 2, 6f);
            Vector3 look = new Vector3(-7, -3.0f, -7);
            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)
        {
            double time = gameTime.TotalGameTime.TotalSeconds;

            if (time - lastTime > timeToChange)
            {
                lastTime = time;
                if (isIdle)
                {
                    if (animationSequence.setNextAnimation(idleToRun, run))
                    {
                        isIdle = false;
                    }
                }
                else
                {
                    if (animationSequence.setNextAnimation(runToIdle, idle))
                    {
                        isIdle = true;
                    }
                }
            }

            controller.update(time);

            moveCamera(gameTime);

            //Collision
            Ray currentRay = new Ray(camera.getPosition(), camera.look);
            float? distance = evilMesh.bounding.Intersects(currentRay);

            if (distance != null && distance > 0)
            {
                List<Vector3> intersectedFaces = new List<Vector3>();
                List<float> distances = new List<float>();

                BCPooledLinkedList<BCBonePose> currentPoses = controller.popBonePoses();
                Matrix[] currentPosesTranforms = controller.popFinalMatrices(currentPoses);

                evilCollisionModel.animate(currentPosesTranforms, evilMesh.getTransform());
               
                controller.pushFinalMatrices(currentPosesTranforms);
                controller.pushBonePoses(currentPoses);

                evilCollisionModel.intersects(currentRay, intersectedFaces, distances);

                showFace = BCCollisionModel.getClosestFace(
                    currentRay,
                    intersectedFaces,
                    distances,
                    currentCollisionFace
                );
            }
        }

        private void moveCamera(GameTime gameTime)
        {

            float df = 0;
            float ds = 0;
            float dy = 0;
            float pitch = 0;
            float angleY = 0;

            float speed = 5f;
#if (XBOX360)
            GamePadState gamepad = GamePad.GetState(PlayerIndex.One);
          
            df = gamepad.ThumbSticks.Left.Y;
            ds = gamepad.ThumbSticks.Left.X;
            pitch = - gamepad.ThumbSticks.Right.Y * 0.05f;
            angleY = gamepad.ThumbSticks.Right.X * 0.05f;

            if (gamepad.IsButtonDown(Buttons.RightShoulder))
            {
                dy += 0.25f;
            }
            if (gamepad.IsButtonDown(Buttons.LeftShoulder))
            {
                dy -= 0.25f;
            }

            speed = new Vector3(df, ds, dy).Length()*10;
#else

            KeyboardState keys = Keyboard.GetState();
            MouseState mouse = Mouse.GetState();

            if (keys.IsKeyDown(Keys.W))
            {
                df += 1;
            }
            if (keys.IsKeyDown(Keys.S))
            {
                df -= 1;
            }
            if (keys.IsKeyDown(Keys.D))
            {
                ds += 1;
            }
            if (keys.IsKeyDown(Keys.A))
            {
                ds -= 1;
            }
            if (keys.IsKeyDown(Keys.Q))
            {
                dy += 1;
            }
            if (keys.IsKeyDown(Keys.E))
            {
                dy -= 1;
            }
            speed = 5f;
            if (keys.IsKeyDown(Keys.LeftControl))
            {
                speed = 10f;
            }

            angleY = ((float)this.Window.ClientBounds.Width / 2.0f - (float)mouse.X) * -0.05f;
            pitch = ((float)this.Window.ClientBounds.Height / 2.0f - (float)mouse.Y) * -0.05f;

            Mouse.SetPosition(this.Window.ClientBounds.Width / 2, this.Window.ClientBounds.Height / 2);

#endif
            camera.update(df, dy, ds, pitch, angleY, speed, (float)gameTime.ElapsedGameTime.TotalSeconds);

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

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

            BCDrawUtil.drawBoundings(evilMesh, device, gameTime);
            evilMesh.Draw(gameTime);

            BCWireFrameRenderStrategy drawWire = new BCWireFrameRenderStrategy();
            BCDrawOverGeometryRenderStrategy drawOver = new BCDrawOverGeometryRenderStrategy(15);

            drawWire.push(device);
            drawOver.push(device);
            BCDrawUtil.drawCollisionModel(device, evilMesh.transform, Color.LightGreen, evilCollisionModel);
            drawOver.pop(device);
            drawWire.pop(device);

            // Add to Util
            if (showFace)
            {
                drawOver.push(device);
                BCDrawUtil.drawTriangle(
                    device,
                    currentCollisionFace[0],
                    currentCollisionFace[1],
                currentCollisionFace[2],
                    Color.Yellow
                );
                drawOver.pop(device);
            }

            cursor.draw(gameTime);

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

}