Thursday, July 2, 2009

Matrices, Rays, and Math - Oh My!

[EDIT 06/09/2011] Note that this is XNA 2.0, and has not been updated for newer versions. The code might not still stand, but the idea should!

After pulling from many resources online, (Remiers, Ziggyware, XNA Creators Club) I finally had heightmap collision working. What was missing, was the player rotating towards the position it was heading. When I first tried implementing the model rotation, I was using the same ray that was cast for my click-to-move code to calculate the direction. What I didn't realize was that because the camera is positioned behind the player, clicking behind the player was still clicking in front of the camera. I will let this illustration explain:



So, my direction vector was still saying "In front", even though I was clicking behind the player. The solution was to use a camera like camera 2 in the illustration for the players rotation, and to use view camera to, well, view. The code is as follows. Credit to Remiers for the majority of the processes.

Calculate the collision point here. Note the second camera used to get the directionCharacter vector:

void CalculateClickToMovePosition()
{
    if (currentMouseState.LeftButton == ButtonState.Pressed)
    {
        Vector3 nearScreenPoint = new Vector3(currentMouseState.X, currentMouseState.Y, 0);
        Vector3 farScreenPoint = new Vector3(currentMouseState.X, currentMouseState.Y, 1);
        Vector3 nearWorldPoint = GraphicsDevice.Viewport.Unproject(nearScreenPoint, viewCamera.cameraProjectionMatrix, viewCamera.cameraViewMatrix, Matrix.Identity);
        Vector3 farWorldPoint = GraphicsDevice.Viewport.Unproject(farScreenPoint, viewCamera.cameraProjectionMatrix, viewCamera.cameraViewMatrix, Matrix.Identity);

        //Direction for the Click to Move process
        direction = farWorldPoint - nearWorldPoint;
        direction.Normalize();

        Vector3 nearScreenPointCharacter = new Vector3(currentMouseState.X, currentMouseState.Y, 0);
        Vector3 farScreenPointCharacter = new Vector3(currentMouseState.X, currentMouseState.Y, 1);
        Vector3 nearWorldPointCharacter = GraphicsDevice.Viewport.Unproject(nearScreenPointCharacter, playerMovementCamera.cameraProjectionMatrix, playerMovementCamera.cameraViewMatrix, Matrix.Identity);
        Vector3 farWorldPointCharacter = GraphicsDevice.Viewport.Unproject(farScreenPointCharacter, playerMovementCamera.cameraProjectionMatrix, playerMovementCamera.cameraViewMatrix, Matrix.Identity);

        //Direction for player model rotation
        directionCharacter = farWorldPointCharacter - nearWorldPointCharacter;
        directionCharacter.Normalize();

        pointerRay = new Ray(nearWorldPoint, direction);
        lastRayPosition = new Vector3();
        height = heightMapInfo.GetHeight(pointerRay.Position);
        
        //Step the ray downwards until it no longer it intersecting
        //with the ground. At that point, we know the lastRayPosition
        //holds the exact collision point.
        while (pointerRay.Position.Y > height)
        {
            lastRayPosition = pointerRay.Position;
            pointerRay.Position += direction;
            height = heightMapInfo.GetHeight(pointerRay.Position);
        }
    }
}

Now we need to draw everything to the screen using those matrices we just calculated:

void DrawPlayer(Player player)
{
    foreach (ModelMesh mesh in player.model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = true;

            effect.World = Matrix.CreateWorld(new Vector3(player.position.X, player.position.Y + 12, player.position.Z), 
                                              new Vector3(directionCharacter.X, -90, directionCharacter.Z), 
                                              Vector3.Up) *Matrix.CreateScale(player.scale);

            effect.Projection = viewCamera.cameraProjectionMatrix;
            effect.View = viewCamera.cameraViewMatrix;
        }
        mesh.Draw();
    }
}

Notice how in Matrix.CreateWorld, we use the directionCharacter vector. That is the vector that we get from the playerMovementCamera.

I am not sure if I went about this the wrong way, but it seems to be working fine for my implementation. Hopefully someone will find this useful, and won't have to spend three days fighting with it.

Matrices! Woo!

1 comment:

  1. Hey! Great post. If I ever start doing movement in a 3D world I will surely come back to this.

    Also, Great use of Nootpad!

    ReplyDelete