SyntaxHighlighter

Friday 21 June 2013

Day 10: Open day and an explanation of GolfBallDemo

Random thoughts on open day

Open day wasn't as active as I expected, and the audience was a lot more mature than I anticipated - definitely a no-go on the war-themed models! Thanks for pointing that out Toby :)
For some reason I had a vision of a younger target group but it all worked out in the end.


What I did yesterday: my golf ball demo

The bouncing golf balls (where gravity could be adjusted) were probably the most popular demo, looking especially nice when the speed decreased to nothing and they just hung in mid-air! Then sloooowly started falling again.. So hopefully the many, many hours I took to code and debug that weren't a complete waste then. Here's a short description of the code for the bouncing golf balls demo.

The basic idea is a white diffuse platform acting as a base which is overlaid where the marker card appears, and upon this are a number of golf balls spaced about. The golfballs start off at random heights, they drop and are constantly bouncing vertically off the platform over time.

The main programming challenges in making this demo:
  1. There can be any number of golfballs on the scene at once, specified by the user
  2. Each golfball starts at its own random height that must be close to an average height specified by the user
  3. The platform can be of any size, X and Y dimensions individually specified by the user
  4. Golfball models must not clash, so must be spaced out over the platform without overlapping.
  5. Golfballs can only be placed on the platform itself and must not bounce on thin air around it.
  6. Golfballs must follow (or approximate) the laws of motion when bouncing.
  7. As a consequence of differing heights, there will likely be balls in all states of falling, rising, bouncing at any one time.
For simplicity's sake we assume that there are not too many golfballs on a platform too small to hold them (adding checks and error catching for this will be fairly simple in future if required)

This is some simple pseudo code describing my thought process:

  • Declare global variables for everything somewhere.
  • Make addGolfDemo which does the following:

Initializes marker and gets scaling info from config.txt for this demo, 
add this demo to the second model set to be enabled when the program switches to that set,
make the floor, rotate it so that it lies on the marker card, translate it so that it is centered on the centre of the marker card, add it to the scene graph
initialize an array of golfballs, calculate a peak height, a random position for each ball that does not overlap any other ball, and add them to the scene graph

  • update global transform nodes for ever single golf ball in the Draw() method: by updating height offset depending on where the ball is, if its falling, and its current height in such a way that the balls seem to rise and fall naturally over time (maximum speed at instant of bounce, speed slows to nothing at point where ball reaches peak height, accurate gradual speed change between those two points)
  • Add check for keypresses in the Update() method, to alter strength of gravity (affects the transform node updates in the Draw() method)

Global Variables


I used the following global variables which are all completely adjustable (and the demo will adapt to it):
  • the X and Y size of the playing field
  • the number of golf balls
  • array of TransformNodes (one node for every golfball - these is modified in the Draw method on every frame to update positions to make the golfballs look like they are moving and bouncing)
  • average height for any golf ball (the balls are randomly given a starting height within a certain margin above and below this average height)
  • array of floats, storing peak height of every golf ball (the bouncing is perfectly elastic, so golf balls reach their starting height on the peak every bounce)
  • array of floats, storing the height offset of every golf ball (where the exact position of that ball is at this point in time, offset from the peak height. Peak height - height offset = Translation vector component in the TransformNode)
  • array of Booleans, whether each golfball is falling or not (when its falling the height offset is increasing, when it is not falling the height offset is decreasing on every frame)
  • the strength of gravity
The strength of gravity can be adjusted in real time via keyboard input.

addGolfDemo() and the maths involved

The code for initializing the demo was placed in the addGolfDemo method:

 
        private void addGolfDemo()
        {
            int[] ids = new int[4];
            for (int i = 0; i < ids.Length; i++)
                ids[i] = i;
            modelConfig _modelConfig = new modelConfig();
            for (int i = 0; i < modelConfigArray.Length; i++)
            {
                if (modelConfigArray[i].name == "golf")
                    _modelConfig = modelConfigArray[i];
            }

            // So we can enable/disable this demo globally at any time
            TransformNode placer = new TransformNode();
            placer.Enabled = false;
            fireMarkerNode.AddChild(placer);
            modelSet2Nodes.Add(placer);

So this part is pretty much universal, lines 1 to 11 are identical in every addxyzDemo method, although the string in the comparison must match what is given in the config.txt file for this demo.

Lines 14 to 17 are common for every demo being added to the second model set, fireMarkerNode is the marker node of the model in the first model set that shares the same marker array.

Thereafter I created and placed the platform:

            // Do the floor and add it to the scene graph
            GeometryNode floor = new GeometryNode("Floor");
            floor.Model = new Box(new Vector3(Xsize, 3, Ysize));
            floor.Model.CastShadows = true;
            floor.Model.ReceiveShadows = true;

            Material floorMaterial = new Material();
            floorMaterial.Diffuse = new Vector4(1, 1, 1, 1);
            floorMaterial.Specular = Color.White.ToVector4();
            floorMaterial.SpecularPower = 10;

            floor.Material = floorMaterial;

            TransformNode floorTransform = new TransformNode();
            floorTransform.Translation = new Vector3(0, 0, 0);
            floorTransform.Rotation = Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), MathHelper.ToRadians(90));

            placer.AddChild(floorTransform);
            floorTransform.AddChild(floor);

The size is set as specifed by the user, Xsize and Ysize (global variables) as seen on line 3. The next step was to initialize the golfballs:

            ModelLoader loader = new ModelLoader();
            Model[] golfModels = new Model[numGolfballs];
            GeometryNode[] golfNodes = new GeometryNode[numGolfballs];
            TransformNode[] golfPositions = new TransformNode[numGolfballs];
            golfTransNodes = new TransformNode[numGolfballs];

            golfHeights = new float[numGolfballs];
            golfHeightOffsets = new float[numGolfballs];

            falling = new Boolean[numGolfballs];

            for (int i = 0; i < golfNodes.Length; i++)
            {
                falling[i] = true;
                golfModels[i] = (Model)loader.Load("", "golfball");
                golfModels[i].UseInternalMaterials = true;
                golfModels[i].CastShadows = true;
                golfModels[i].ReceiveShadows = true;
                golfNodes[i] = new GeometryNode();
                golfPositions[i] = new TransformNode();
                golfTransNodes[i] = new TransformNode();
                golfNodes[i].Model = golfModels[i];
                golfPositions[i].Translation = new Vector3(0, 0, 3);
                //golfPositions[i].Scale = new Vector3(3f * _modelConfig.scale, 3f * _modelConfig.scale, 3f * _modelConfig.scale);

                // Randomly calculate height for each ball within +-10 of the average height
                golfHeights[i] = (((((float)random.NextDouble() * 2) - 1) * 10) + averageGolfHeight);
                golfHeightOffsets[i] = 1;

                // Randomly places each golfball onto the floor, without overlap
                float randX = ((float)random.NextDouble() * (Xsize / (numGolfballs + 2)));
                
                float a = ((i - (numGolfballs / 2)) * (Xsize / (numGolfballs + 2)));
                randX = a + randX;

                float randY = ((float)random.NextDouble() * Ysize);

                golfTransNodes[i].Translation = new Vector3(randX,  randY, golfHeights[i]);

                placer.AddChild(golfPositions[i]);
                golfPositions[i].AddChild(golfTransNodes[i]);
                golfTransNodes[i].AddChild(golfNodes[i]);
            }

Beautiful (ahem), as visible I went for efficiency over readability and basically did absolutely everything in one for loop.

The ModelLoader on line 1 loads the golfball model, then I create an array of Models for the number of balls I need. An array of GeometryNodes holds the golfball models, array golfPositions to move the golfballs to their initial starting X and Y position on the platform, array golfTransNodes which will be updated later on to make the golfballs move - basically updating the height of every golf ball.

golfHeights stores the peak and initial height, golfHeightOffsets stores the current offset of the golf balls from the peak height. At the moment that will be 0 (all balls start at their peak height).

The way I create a pseudo random distribution of golfballs that doesn't overlap is as follows, divide the length (X direction) of the entire platform into (numGolfballs+2) number of strips, and then limit the X placement of every golfball to one of those strips (except the first and last strip as  a buffer).

In this way, you will have the X component of every golfball spaced over the platform, so the Y component can be completely random. The reason we have two extra strips is so that there is some buffer region, we don't want golfballs bouncing right on the edge of the platform so there is a small area on both sides of the platform that that will be empty.

To place the golfballs on the X direction, we will use base + offset positioning. The base (variable a) is the X co ordinate of the start of that ball's strip. The offset (variable randX) is a random number that is of size minimum 0 to maximum the length of a strip.

Now the interesting maths that's going on (I love maths):

  • Line 27: height of each ball: (((((float)random.NextDouble() * 2) - 1) * 10) + averageGolfHeight) : this just calculates a random height for every golfball that is within 10 units above and below the average height (specified by user), not too complicated
  • Line 31: offset of each ball from its X strip: Xrand = ((float)random.NextDouble() * (Xsize / (numGolfballs + 2))) : The important bit is Xsize/ (numballs+2). As described above, the platform is divided into numballs+2 strips, and every ball is placed in a random X position in its strip. This number randX is the random X position in a strip. It is just a random number that is between 0 and the strip length.
  • Line 33: base position of the current ball's X strip: a = ((i - (numGolfballs / 2)) * (Xsize / (numGolfballs + 2))) : The first part (i - (numGolfballs / 2)) -> remember i is the loop index so will be 0, then 1, then 2, all the way upto numGolfBalls.  Therefore the first position this base points at will be what we want, it will be i - (half the number of allowed strips -> allowed = not the first and last strip that we want empty), times by the length of a strip. This correctly takes us through every single strip in turn.
  • Line 36: ((float)random.NextDouble() * Ysize); this simply gives a random Y position.
Well, next of course is the code in Draw() to make them bounce:

            // Make the golfballs bounce by updating their heightoffsets
            for (int i = 0; i < golfTransNodes.Length; i++)
            {
                if (falling[i])
                    if ((golfHeightOffsets[i] * (1+gravity)) > golfHeights[i])
                        falling[i] = false;
                    else golfHeightOffsets[i] = golfHeightOffsets[i] * (1+gravity);

                if (!falling[i])
                    if ((golfHeightOffsets[i] * (1-gravity)) < 1)
                        falling[i] = true;
                    else golfHeightOffsets[i] = golfHeightOffsets[i] * (1-gravity);

                golfTransNodes[i].Translation = new Vector3(golfTransNodes[i].Translation.X,
                                                            golfTransNodes[i].Translation.Y,
                                                            (golfHeights[i] - golfHeightOffsets[i]));
            }
In short: loop through the balls, if falling & if will not hit ground by next movement, perform next movement.
Else if will hit ground, set falling to false (the second for loop will move the ball upwards in this frame).

If not falling & if will overshoot max height by next movement set falling to true and do nothing this frame. Else if will not overshoot max height then decrease height offset.

I think this is an approximation rather than an exact rendition of the laws of motion but I didn't bother to confirm. For one thing it certainly looks realistic which is the effect I was going for in the end.

Full Documentation of the AR program

The basic structure of the AR demo (aka LowryDemo) program, just the names of the methods. This overview shows "where" the code to perform diffferent tasks exists.

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        // Declare global variables

        public Game1() // Initialise graphics and content

        protected override void Initialize()

        private void CreateLights()
        private void SetupMarkerTracking()  
        private void SetupModelSets()
        private void CreateObjects() // and add to scene graph

        protected override void LoadContent()
        protected override void UnloadContent() // these two don't do much

        /// Update the fire effect on the ship and torus model       
        private void UpdateShipFire(Matrix worldTransform, List particleEffects)
        private void UpdateRingOfFire(Matrix worldTransform, List particleEffects)
        private Vector3 RandomPointOnCircle(Vector3 pos)

        private void toggleGraph() // enable/disable models
        private void setModels1() // switch model sets
        private void setModels2()
        protected override void Update(GameTime gameTime) // run on every tick
        protected override void Draw(GameTime gameTime) // run on every frame
    }

Next step is to explain the different methods and code sections in detail, and if Toby feels it is a good idea then I will manufacture a number of tutorials on how to perform different tasks (again, in detail) such as adding static objects, adding dynamic effects to static objects, implementing keypress detection and making functions for that, manipulating the scene graph, adding new model sets and so on. I presume basic documentation will be enough though, then we can focus on developing GUI's to do all these things and so on.

Tasks at the moment on the XNA code include investigating sound, finishing documentation, if possible testing object inbuilt animations, and developing a GUI to perform various activities visually and easily.

After that it'll be time to move the program to a more future proof programming environment. Exciting!

No comments:

Post a Comment

Note: only a member of this blog may post a comment.