[SOLVED] Difficulty using RenderTarget2D with a camera

Solution: See reply below. TL;DR: Make sure to understand all 3 spaces – window space, camera space, and world space.


Hey all,

I just recently got into MonoGame (and C#, for the most part), though I have some prior experience with OOP and app development. I have also worked with matrices and vectors in a prior university-level course, but it’s been probably too much time since then – I only seem to have a basic idea of what is going on. :laughing:

In order to support resolution independence, I discovered after some research that using a RenderTarget2D is the way to go. Here is basically what I have working:

  • Camera that supports rotating and scaling. :smile:
  • Transitioning from screen to world coordinates (and vice versa) using my camera class
  • Drawing to a RenderTarget2D

However, I can’t seem to get everything to work together. I’m not sure if what I’m doing wrong is due to a conceptual misunderstanding or just a misunderstanding based on implementation (e.g. MonoGame specifics).

Here is the short version of what I’m doing (with more complete code snippets posted below):

  1. Instantiate Camera with bounds equivalent to the virtual resolution bounds that I want to target.
  2. Create a RenderTarget2D with a virtual resolution that I want to target, e.g. 640x360.
  3. Set GraphicsDevice.RenderTarget to my renderTarget.
  4. Draw to world also using my camera’s Transform; worldSpriteBatch.Begin(transformMatrix: camera.Transform);
  5. End my world’s SpriteBatch (worldSpriteBatch) and set GraphicsDevice.RenderTarget(null).
  6. Draw the renderTarget to my mainSpriteBatch with a destination rectangle equivalent to the camera’s bounds; mainSpriteBatch.Draw(texture: renderTarget, destinationRectangle: camera.Bounds, color: Color.White);

That’s the gist of it. Now, here’s what happens:

Option A: Details as above; setting ```destinationRectangle` to camera’s bounds

Everything “works” here. Game world is drawn correctly, camera works, proper world coordinates are received even after rotating and zooming.
:white_check_mark: Drawing with rotation and zoom support
:white_check_mark: Coordinate system

Option B: Setting destinationRectangle to screen’s bounds

Now I want to expand the RenderTarget2D to fit the whole game window so I can achieve a pixel art look and feel.This is where things begin to break. The game world draws correctly to the screen. However, as we can see with the rectangle drawn to screen, the camera’s bounds (and therefore ScreenToWorld method) only work properly in that rectangle.
:white_check_mark: Drawing
:negative_squared_cross_mark: Coordinates

Option C - attempted solution: Well, maybe I simply just need to set the camera’s bounds to fit the whole window.

The coordinate system works, but the drawing does not. I suspect this is due to, upon changing the camera’s bounds, also impacting the drawing when we feed the camera’s Transform into the worldSpriteBatch.Begin(transformMatrix: camera.Bounds).
:negative_squared_cross_mark: Drawing
:white_check_mark: Coordinates

Possible solution

Upon input-related events (e.g. polling mouse position or mouse clicks) that require ScreenToWorld, I’m thinking I could create a virtual matrix for the camera using a scale and run this new matrix through my ScreenToWorld function. Alternatively, I can keep track of both virtual bounds and actual bounds (and corresponding transforms) in the Camera class. These, however, seem hack-y to me.I’m worried about going about the “right” way to do things. I don’t want a code base full of hacks and workarounds. :fearful:

Anyhow, if you read this far, thanks for taking a look! :smile:


Most of the actual code here:

Camera.cs important snippets

Camera.cs

        /// <summary>
        /// Instantiate Camera given a viewport, namely the Bounds
        /// </summary>
        /// <param name="viewport"></param>
        public Camera(Viewport viewport)
        {
            Bounds = viewport.Bounds;
            //VirtualBounds = Bounds;
            Zoom = 1f;
        }

        #region Space Transforms

        /// <summary>
        /// Get screen coordinates from world coordinates
        /// </summary>
        /// <param name="worldPosition"></param>
        /// <returns></returns>
        public Vector2 WorldToScreen(Vector2 worldPosition)
        {
            return Vector2.Transform(worldPosition, Transform);
        }


        /// <summary>
        /// Get world coordinates from screen coordinates
        /// </summary>
        /// <param name="screenPosition"></param>
        /// <returns></returns>
        public Vector2 ScreenToWorld(Vector2 screenPosition)
        {
            return Vector2.Transform(screenPosition, Matrix.Invert(Transform));
        }
        #endregion Space Transforms


        /// <summary>
        /// Update camera to a specific position
        /// Also updates the camera's Transform
        /// </summary>
        /// <param name="position"></param>
        public void Update(Vector2 position)
        {
            // TODO: Check for camera input
            Position = position;
            
            Transform = Matrix.CreateTranslation(new Vector3(-position.X, -position.Y, 0)) *
                            Matrix.CreateRotationZ(Rotation) *                                                      // Rotation
                            Matrix.CreateScale(new Vector3(Zoom, Zoom, 1f)) *                                       // Scale
                            Matrix.CreateTranslation(new Vector3(Bounds.Width * 0.5f, Bounds.Height * 0.5f, 0));    // Centred
        }

Game1.cs important snippets

Game1.cs

        private int actualWidth = 1152;
        private int actualHeight = 648;
        private int virtualWidth = 640;
        private int virtualHeight = 360;
        RenderTarget2D renderTarget;

        GraphicsDeviceManager graphics;
        SpriteBatch mainSpriteBatch;
        SpriteBatch worldSpriteBatch;
        SpriteBatch textSpriteBatch;

        Camera camera;

        ...

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            // Set a 16:9 aspect ratio for game *window*
            graphics.PreferredBackBufferWidth = actualWidth;
            graphics.PreferredBackBufferHeight = actualHeight;
            
            ...
        }

        protected override void LoadContent()
        {
            ...

            // Load camera
            //camera = new Camera(graphics.GraphicsDevice.Viewport);
            camera = new Camera(new Viewport(0, 0, virtualWidth, virtualHeight));

            // Prepare RenderTarget2D
            renderTarget = new RenderTarget2D(graphics.GraphicsDevice, virtualWidth, virtualHeight); // Resolution to target
 
            ....
         }

protected override void Draw(GameTime gameTime)
        {
            // Draw scene to RenderTarget, essentially a texture
            // Width and height of our *virtual* resolution
            GraphicsDevice.SetRenderTarget(renderTarget);

            GraphicsDevice.Clear(Color.CornflowerBlue); // Blue for "inside" the game world

            // Can now draw our stuff to RenderTarget
            #region worldSpriteBatch

            // Use our camera Transform so that we can specify *world* coordinates
            worldSpriteBatch.Begin(transformMatrix: camera.Transform);

            for (int i = 0; i < 10; i++)
            {
                worldSpriteBatch.DrawString(arialFont, "W(0, " + 15 * i + ")", new Vector2(0, 15 * i), Color.White);
            }

            worldSpriteBatch.DrawString(arialFont, "W(250 250)", new Vector2(250, 250), Color.White);
            worldSpriteBatch.Draw(gargoyle, new Vector2(-300, 0), Color.White);
            worldSpriteBatch.DrawString(arialFont, "W(-300, 0)", new Vector2(-300, 0), Color.White);
            //worldSpriteBatch.Draw(frame32, new Vector2(32, 32), Color.White);

            worldSpriteBatch.End();
            #endregion worldSpriteBatch

            // Revert back to the original (the screen -- the backbuffer) render target
            GraphicsDevice.SetRenderTarget(null);
            GraphicsDevice.Clear(Color.Gray);       // Gray for "outside" the world/camera render

            // Now draw the mainSpriteBatch
            string debugMessage =
                "camera Bounds: " + camera.Bounds + "; " +
                "renderTarget: " + renderTarget.Bounds + "; " +
                "x,y scale: " + xScale + ", " + yScale;
            System.Console.WriteLine(debugMessage);
            mainSpriteBatch.Begin(samplerState: SamplerState.PointClamp);       // Use PointClamp for hard edges

            // Option A: Set destination of renderTarget to *camer's* bounds
            Rectangle destination = camera.Bounds;

            // Option B: Set destination of renderTarget to *window's* bounds
            //Rectangle destination = new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);

            // Option C - attempted fix: Initialise camera with bounds to match actual window (see above)
            // I suspect this isn't working due to something to do with drawing to worldSpriteBatch using camera's Transform

            // Draw to our mainSpriteBatch, responsible for what gets shown to screen in totality
            mainSpriteBatch.Draw(
                texture: renderTarget,
                //position: Vector2.Zero, // Draw RenderTarget to top-left of screen (0, 0)
                destinationRectangle: destination,
                color: Color.White);

            // Debug to draw rectangle's with specified pixel width and color
            DrawBorder(mainSpriteBatch, camera.Bounds, 15, Color.Blue); // Where mainSpriteBatch thinks camera Bounds are
            DrawBorder(mainSpriteBatch, destination, 10, Color.Red);    // Where mainSpriteBatch thinks destination of renderTarget is
            mainSpriteBatch.End();

            // SpriteBatch for drawing text and UI, independent of game world
            #region uiSpriteBatch
            textSpriteBatch.Begin();

            //DrawBorder(textSpriteBatch, dst, 4, Color.Green);
            DrawBorder(textSpriteBatch, camera.Bounds, 6, Color.Black);   // Where actual camera bounds are
            DrawBorder(textSpriteBatch, destination, 3, Color.Green);      // Where actual destination bounds are

            // Debug to show screen and world coordinates of mouse. Uses Camera's space transforms (ScreenToWorld and WorldToScreen)
            InputManager.Debug(textSpriteBatch, arialFont, camera, new Vector2(0, 0));
            //textSpriteBatch.DrawString(arialFont, "Mouse: screen " + mousePositionScreenSpace + ", world " + mousePositionWorldSpace, new Vector2(0, 0), Color.White);

            textSpriteBatch.End();
            #endregion uiSpriteBatch

            base.Draw(gameTime);

Hey Shane, welcome to the forum :slight_smile:

There’s actually 3 different coordinate spaces you’re interested in, but it seems like you might be conflating them a little since you only mention screen and world space.
The three spaces are: World <-> Camera <-> Window

  1. World space: the global coordinates in your game world. If you have a static object in the game it will always have the same coordinates in world space.
  2. Camera space: coordinates inside the camera. Same scale as world space (unless there’s a zoom).
  3. Window space: the coordinates in pixels from the top left of the game windows client bounds. You typically don’t care about this space except for catching mouse coordinates and translating them to camera space. Possibly also for UI if you don’t want to render it at the game resolution (not enough pixels for games with small native resolution).

A camera is usually only used to transform back and forth between world space and camera space. To transform coordinates from window space to camera space and back, you only need to multiply the coordinates by the right scale.
If you want mouse coordinates translated to in-game world space, you should first scale to camera space and then apply the inverted camera transform.

Hope I understood your issue right and this clarifies some things for you!

1 Like

Wow, this was it! Thank you so much! Such a simple explanation, yet so important. I had been somewhat aware of going back and forth between spaces, but I can’t believe I had been missing one of them. Here’s my updated methods with a screenshot of it working. Now I can do cool mini-maps and whatnot. :smile:

public Vector2 WindowToWorldSpace(Vector2 windowPosition)
        {
            // Old (wrong)
            //return Vector2.Transform(windowPosition, Matrix.Invert(Transform * _scaleMatrix));

            // First, must go from window -> camera space
            Vector2 cameraSpace = WindowToCameraSpace(windowPosition);

            // And then, camera -> world space
            return Vector2.Transform(cameraSpace, Matrix.Invert(Transform));
        }

        public Vector2 WindowToCameraSpace(Vector2 windowPosition)
        {
            // Scale for camera bounds that vary from window
            // Also, must adjust for translation if camera isn't at 0, 0 in screen space (such as a mini-map)
            return (Scale * windowPosition) + _translationVector;
        }

:smile:

1 Like

Great! Good luck with the game project :wink:

1 Like

Hey! Very interesting topic. Where can I look the same example?