3dxnadirectxmonogamexna-4.0

XNA/Monogame 3D Stretching and Aspect Ratios


I'm trying to properly draw a few 3D models to the screen using a movable camera, but I'm encountering two problems. The first problem is immediately visible: the models are supposed to be cubes, but are drawing as rectangular prisms. It's like they're being squashed/compressed, but I can't find a reason for it.

Sides of cubes

The second problem shows up when looking down on the "cubes." Although they should be just rotating, they seem to be stretching in an odd way when they rotate.

Looking down

Still looking down, but rotated CW 90 degrees

Here's my camera code:

public class Camera
{
    private Driver Driver;
    private int ScreenWidth { get { return Driver.GraphicsDevice.Viewport.Width; } }
    private int ScreenHeight { get { return Driver.GraphicsDevice.Viewport.Height; } } 

    public Matrix ViewMatrix { get; private set; }
    public Matrix ProjectionMatrix { get; private set; }

    private const float NearClippingPlane = 0.1f;
    private const float FarClippingPlane = 200.0f;

    public Vector3 Position { get; private set; }

    private Vector3 _direction; // Do not use _direction!
    private Vector3 Direction
    {
        get { return _direction; }
        set
        {
            if (value == Vector3.Zero) { }
            else
            {
                var normalizedTemp = value;
                normalizedTemp.Normalize();

                if (normalizedTemp.Y > -.9999f) _direction = normalizedTemp;
            }
        }
    }

    private Vector3 Up;

    private const float MovementSpeed = 10f;
    private const float PanSpeed = 10f;

    private MouseState previousMouseState;

    private Vector3 HorizontalPlaneDirection
    {
        get
        {
            var direction = new Vector3(Direction.X, 0, Direction.Z);
            if (direction != Vector3.Zero)
                direction.Normalize();
            return direction;
        }
    }

    public Camera(Driver game, Vector3 position, Vector3 target, Vector3 up)
    {
        Driver = game;

        // Build camera view matrix
        Position = position;
        Direction = target - position;
        Up = up;
        CreateLookAt();

        ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(60), ScreenWidth / ScreenHeight, NearClippingPlane, FarClippingPlane);
    }

    public void Initialize()
    {
        // Set mouse position and do initial get state
        Mouse.SetPosition(ScreenWidth / 2, ScreenHeight / 2);

        previousMouseState = Mouse.GetState();
    }

    public void Update(GameTime gameTime, KeyboardState keyboard, MouseState mouse)
    {
        float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
        float moveSpeed = MovementSpeed * elapsed;
        float panSpeed = PanSpeed * elapsed;

        if (keyboard.IsKeyDown(Keys.W))
            Position += HorizontalPlaneDirection * moveSpeed;
        if (keyboard.IsKeyDown(Keys.S))
            Position -= HorizontalPlaneDirection * moveSpeed;

        if (keyboard.IsKeyDown(Keys.A))
            Position += Vector3.Cross(Up, HorizontalPlaneDirection) * moveSpeed;
        if (keyboard.IsKeyDown(Keys.D))
            Position -= Vector3.Cross(Up, HorizontalPlaneDirection) * moveSpeed;

        if (keyboard.IsKeyDown(Keys.Space))
            Position += Up * moveSpeed;
        if (keyboard.IsKeyDown(Keys.LeftShift))
            Position -= Up * moveSpeed;

        // Rotation in the world
        Direction = Vector3.Transform(Direction, Matrix.CreateFromAxisAngle(Up, -MathHelper.ToRadians(1) * (mouse.X - previousMouseState.X) * panSpeed));

        Direction = Vector3.Transform(Direction, Matrix.CreateFromAxisAngle(Vector3.Cross(Up, HorizontalPlaneDirection), MathHelper.ToRadians(1) * (mouse.Y - previousMouseState.Y) * panSpeed));

        Driver.ResetMouseState();
        // Reset prevMouseState
        previousMouseState = Mouse.GetState();

        CreateLookAt();
    }

    private void CreateLookAt()
    {
        ViewMatrix = Matrix.CreateLookAt(Position, Position + Direction, Up);
    }
}

And the code to draw the model:

public void Draw(Camera Camera)
    {
        foreach (var mesh in Model.Meshes)
        {
            foreach (BasicEffect effect in mesh.Effects)
            {
                //effect.EnableDefaultLighting();
                //effect.PreferPerPixelLighting = true;

                effect.World = GetWorldMatrix();
                effect.View = Camera.ViewMatrix;
                effect.Projection = Camera.ProjectionMatrix;
            }

            mesh.Draw();
        }
    }

I know that the code bits are long and probably hard to slog through, but I greatly appreciate any help. Thanks!


Solution

  • Well, I'm stupid. The solution is simple - it's the projection matrix that's the problem.

    In my code, when I set the ProjectionMatrix, I set its aspect ratio to ScreenWidth / ScreenHeight. However, since both of these are integers, the result is also an integer. So, instead of having the proper aspect ration of 16:9, which is the nonintegral result of ScreenWidth / ScreenHeight, it gets cast to an integer and my aspect ratio ends up as ScreenWidth / ScreenHeight = 1920 / 1080 = (int)1.7778 = 1.

    Obviously, the solution is simple. Stick a float cast to the divisor and you end up with the proper answer. ScreenWidth / (float)ScreenHeight = 1920 / 1080.0f = 1.7778f.

    I hope my day of struggling was able to help anybody else with this issue. Also, if you're experiencing any other issues with creating a 3D camera in XNA, I would advise you to check out this code example. It's designed for a phone (for some reason?) and it comes as a DLL, which means you won't be able to run it (you'll have to add an executable project, reference the DLL, and rebuild the executable project any time you make an edit to the DLL), but it serves as a good starting/reference point nonetheless.

    Glorious correctly-aspected cubes