Solving Resolution Independent Rendering And 2D Camera Using Monogame

Solving Resolution Independent Rendering And 2D Camera Using Monogame

As i promised in my previous post where i announced my Windows 8 game Marbles, i will try to tackle some of the common problems that beginner game developers encounter.

In this post i will jump ahead a little and talk about Resolution Independent Rendering using Monogame.

What the heck is Resolution Independent Rendering?

Well its fancy name for finding ways to not care about resolution during your game development.
Idea is that you render always using fixed (internal) virtual resolution you choose and then simply stretch or shrink all that for the real resolution of the current device you are using to display the game.
Sounds simple right?

Why solving Resolution Independent Rendering?

Because of multiple reasons:

  • First of all its very important and difficult subject. You don’t want to care about resolution on which you are displaying your game. Game developer has enough problems to solve even without this.
  • Every game developer that is targeting multiple devices/platforms has to solve this in one way or another
  • I struggled until i found a solution for this and want to share it with the community
  • many people lately asked me to share code for this and i kept sending them same email with zipped source code for this so it seems like a good idea for blog post 🙂

How are we going to solve it?

Firstly, we will decide on the internal resolution for our game, lets say 1366×768.

Then we will determine what is the real screen resolution of the device/monitor where we are rendering our game.

Then we will determine what is the best fit rectangle that we can get on this real screen that matches our virtual resolution (but always maintaining aspect ratio of the Virtual Resolution we decided so we don’t have stretching/shrinking artefacts).

For example if real screen resolution is bigger then our internal virtual resolution, we will create a best fit ‘viewport’ inside of that larger resolution by maintaining aspect ratio but stretching our viewport – therefore our game screens will be stretched and rendered inside this bigger viewport on the screen.

For smaller screen we will do the opposite – shrink the viewport and maintain aspect ratio of our virtual resolution – therefore our game gfx will first shrink to fit into the smaller viewport and then be rendered on the screen.

Since the viewport we decide on will often not fill the real screen around it we will fill it with some color – effect known in film industry as Letterboxing.

Once we know that centered viewport rectangle where we will render our stretched/shrinked screen we then create transformation matrix that we will use in all the rendering calls (by passing it to the SpriteBatch.Begin method).

Also we need to set the Mogame/XNA ViewPort property to that same viewport to limit rendering only to this region on the real screen.

To be honest, I’m not the one who invented this technique, first time i saw someone using GraphicDevice Viewport for this is David Amadors blog post on same subject where he shares example on how to achieve resolution independence with XNA.

But this was not enough for me i wanted to have also a Camera on top of that so that i can rotate zoom etc.

So i included camera that builds up on this resolution transformation matrix of the Resolution Renderer and creates its own view matrix (rotated, zoomed, translated etc).

Where is the code???

In the sample visual studio 2012 solution  you can see this in action where we are rendering a fixed image of 1366×768 to any screen you try it on, stretching and shrinking it accordingly without any changes in the code. Also you can use keyboard + and – to control camera zoom and Shift + and Shift – to rotate it.

And the screenshots?

Here are the screenshots of the sample code running on 3 different screens/devices:

1680×1050 monitor: image is stretched with little orange bar above

1680x1050

1280×1024: image is shrinked and we have strong pillarboxing effect

1280x1024

1024×768 – image is shrinked with some pillarboxing

1024x768

Note: this orange color is just for the demo, code allows you to easily set what color will the Pillarbox be.
In the sample Visual Studio 2012 solution for Monogame 3.0.1 you will want to check out two important classes:

1. ResolutionIndependentRenderer – this class handles everything about resolutions and creates viewport transformation matrix for resolution independence

2. Camera2D – this class usess ResolutionIndependentRenderer and its matrix and creates final camera view matrix you should use for SpriteBatch and allows you to set camera position, zoom, rotate etc

Here is the code for ResolutionIndependentRenderer:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Roboblob.XNA.WinRT.ResolutionIndependence
{
    public class ResolutionIndependentRenderer
    {
        private readonly Game _game;
        private Viewport _viewport;
        private float _ratioX;
        private float _ratioY;
        private Vector2 _virtualMousePosition = new Vector2();

        public Color BackgroundColor = Color.Orange;

        public ResolutionIndependentRenderer(Game game)
        {
            _game = game;
            VirtualWidth = 1366;
            VirtualHeight = 768;

            ScreenWidth = 1024;
            ScreenHeight = 768;
        }

        public int VirtualHeight;

        public int VirtualWidth;

        public int ScreenWidth;
        public int ScreenHeight;

        public void Initialize()
        {
            SetupVirtualScreenViewport();

            _ratioX = (float)_viewport.Width / VirtualWidth;
            _ratioY = (float)_viewport.Height / VirtualHeight;

            _dirtyMatrix = true;
        }

        public void SetupFullViewport()
        {
            var vp = new Viewport();
            vp.X = vp.Y = 0;
            vp.Width = ScreenWidth;
            vp.Height = ScreenHeight;
            _game.GraphicsDevice.Viewport = vp;
            _dirtyMatrix = true;
        }

        public void BeginDraw()
        {
            // Start by reseting viewport to (0,0,1,1)
            SetupFullViewport();
            // Clear to Black
            _game.GraphicsDevice.Clear(BackgroundColor);
            // Calculate Proper Viewport according to Aspect Ratio
            SetupVirtualScreenViewport();
            // and clear that
            // This way we are gonna have black bars if aspect ratio requires it and
            // the clear color on the rest
        }

        public bool RenderingToScreenIsFinished;
        private static Matrix _scaleMatrix;
        private bool _dirtyMatrix = true;

        public Matrix GetTransformationMatrix()
        {
            if (_dirtyMatrix)
                RecreateScaleMatrix();

            return _scaleMatrix;
        }

        private void RecreateScaleMatrix()
        {
            Matrix.CreateScale((float)ScreenWidth / VirtualWidth, (float)ScreenWidth / VirtualWidth, 1f, out _scaleMatrix);
            _dirtyMatrix = false;
        }

        public Vector2 ScaleMouseToScreenCoordinates(Vector2 screenPosition)
        {
            var realX = screenPosition.X - _viewport.X;
            var realY = screenPosition.Y - _viewport.Y;

            _virtualMousePosition.X = realX / _ratioX;
            _virtualMousePosition.Y = realY / _ratioY;

            return _virtualMousePosition;
        }

        public void SetupVirtualScreenViewport()
        {
            var targetAspectRatio = VirtualWidth / (float) VirtualHeight;
            // figure out the largest area that fits in this resolution at the desired aspect ratio
            var width = ScreenWidth;
            var height = (int)(width / targetAspectRatio + .5f);

            if (height > ScreenHeight)
            {
                height = ScreenHeight;
                // PillarBox
                width = (int)(height * targetAspectRatio + .5f);
            }

            // set up the new viewport centered in the backbuffer
            _viewport = new Viewport
                            {
                                X = (ScreenWidth / 2) - (width / 2),
                                Y = (ScreenHeight / 2) - (height / 2),
                                Width = width,
                                Height = height
                            };

            _game.GraphicsDevice.Viewport = _viewport;
        }
    }
}

And then there is Camera2D that has all the usual camera properties that you can use to move it around and zoom rotate etc.

Here is the code for camera:

using Microsoft.Xna.Framework;
using Roboblob.XNA.WinRT.ResolutionIndependence;

namespace Roboblob.XNA.WinRT.Camera
{
    public class Camera2D
    {
        private float _zoom;
        private float _rotation;
        private Vector2 _position;
        private Matrix _transform = Matrix.Identity;
        private bool _isViewTransformationDirty = true;
        private Matrix _camTranslationMatrix = Matrix.Identity;
        private Matrix _camRotationMatrix = Matrix.Identity;
        private Matrix _camScaleMatrix = Matrix.Identity;
        private Matrix _resTranslationMatrix = Matrix.Identity;

        protected ResolutionIndependentRenderer ResolutionIndependentRenderer;
        private Vector3 _camTranslationVector = Vector3.Zero;
        private Vector3 _camScaleVector = Vector3.Zero;
        private Vector3 _resTranslationVector = Vector3.Zero;

        public Camera2D(ResolutionIndependentRenderer resolutionIndependence)
        {
            ResolutionIndependentRenderer = resolutionIndependence;

            _zoom = 0.1f;
            _rotation = 0.0f;
            _position = Vector2.Zero;
        }

        public Vector2 Position
        {
            get { return _position; }
            set
            {
                _position = value;
                _isViewTransformationDirty = true;
            }
        }

        public void Move(Vector2 amount)
        {
            Position += amount;
        }

        public void SetPosition(Vector2 position)
        {
            Position = position;
        }

        public float Zoom
        {
            get { return _zoom; }
            set
            {
                _zoom = value;
                if (_zoom < 0.1f)
                {
                    _zoom = 0.1f;
                }
                _isViewTransformationDirty = true;
            }
        }

        public float Rotation
        {
            get
            {
                return _rotation;
            }
            set
            {
                _rotation = value;
                _isViewTransformationDirty = true;
            }
        }

        public Matrix GetViewTransformationMatrix()
        {
            if (_isViewTransformationDirty)
            {
                _camTranslationVector.X = -_position.X;
                _camTranslationVector.Y = -_position.Y;

                Matrix.CreateTranslation(ref _camTranslationVector, out _camTranslationMatrix);
                Matrix.CreateRotationZ(_rotation, out _camRotationMatrix);

                _camScaleVector.X = _zoom;
                _camScaleVector.Y = _zoom;
                _camScaleVector.Z = 1;

                Matrix.CreateScale(ref _camScaleVector, out _camScaleMatrix);

                _resTranslationVector.X = ResolutionIndependentRenderer.VirtualWidth*0.5f;
                _resTranslationVector.Y = ResolutionIndependentRenderer.VirtualHeight * 0.5f;
                _resTranslationVector.Z = 0;

                Matrix.CreateTranslation(ref _resTranslationVector, out _resTranslationMatrix);

                _transform = _camTranslationMatrix *
                             _camRotationMatrix *
                             _camScaleMatrix *
                             _resTranslationMatrix *
                             ResolutionIndependentRenderer.GetTransformationMatrix();

                _isViewTransformationDirty = false;
            }

            return _transform;
        }

        public void RecalculateTransformationMatrices()
        {
            _isViewTransformationDirty = true;
        }
    }
}

How to use all this???

Its simple 🙂

In your Game create instance of ResolutionIndependentRenderer and SimpleCamera2D.

Camera accepts the instance of ResolutionIndependentRenderer in the constructor cause it uses its resolution Matrix to calculate its own view Matrix.

On start of your game (or whenever your real screen resolution changes) – user rotates the device, or moves the game to another monitor on desktop PC etc. – you must initialize the

ResolutionIndependentRenderer like this and also invalidate the camera matrix:

        private void InitializeResolutionIndependence(int realScreenWidth, int realScreenHeight)
        {
            _resolutionIndependence.VirtualWidth = 1366;
            _resolutionIndependence.VirtualHeight = 768;
            _resolutionIndependence.ScreenWidth = realScreenWidth;
            _resolutionIndependence.ScreenHeight = realScreenHeight;
            _resolutionIndependence.Initialize();

            _camera.RecalculateTransformationMatrices();
        }

As you see above we are passing to the ResolutionIndependentRenderer virtual resolution we want to use internally and the real resolution of the screen, and then we recalculate matrices in the camera. From that point we can use the same cached camera matrix until we change position or zoom etc (this is handled internally in the camera code).

Here exceptionally we manually have to notify the Camera class that we changed the ResolutionIndependentRenderer matrix so it can recalculate its own matrix.

Next stop is our Draw method of the game:

        protected override void Draw(GameTime gameTime)
        {
            _resolutionIndependence.BeginDraw();
            _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearWrap, DepthStencilState.None, RasterizerState.CullNone, null, _camera.GetViewTransformationMatrix());
            _spriteBatch.Draw(_bkg, _bkgPos, Color.White);
            _spriteBatch.DrawString(_debugFont, string.Format("Translated Mouse Pos: x:{0:0}  y:{1:0}",_screenMousePos.X,_screenMousePos.Y), _mouseDrawPos, Color.Yellow);
            _spriteBatch.DrawString(_debugFont, _instructions, _instructionsDrawPos, Color.Yellow);
            _spriteBatch.End();

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }

Calling the BeginDraw method on ResolutionIndependentRenderer internally first sets the viewport to full screen, clear it with color that is set in its Background property and then sets the virtual resolution viewport to our virtual resolution so whatever we draw its drawn in the correct centered rectangle.

That way we get that color fill around our game.

Another thing to notice there is that we are passing a camera view matrix to the SpriteBatch.Begin call:


            _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearWrap, DepthStencilState.None, RasterizerState.CullNone,
                null, _camera.GetViewTransformationMatrix());

This is very important cause if you don’t do this, you will still be drawing to the full screen and we don’t want that.
By passing the Matrix from our camera to the SpriteBatch we tell it to rotate, zoom and translate all our draws to the correct place

Benefit of all this is that we can setup camera and zoom and rotate our screen and move camera around.
At start of the sample code i center the camera to the middle of the virtual screen and set zoom to 1.0 but you can change/animate those values and have some nice effects.

Here is how the camera initialization code looks like:


            _camera = new Camera2D(_resolutionIndependence);
            _camera.Zoom = 1f;
            _camera.Position = new Vector2(_resolutionIndependence.VirtualWidth / 2, _resolutionIndependence.VirtualHeight / 2);

Download the sample visual studio 2012 solution and play with it.

Try to run it on multiple screen resolutions, it should always adapt as best possible and maintain aspect ratio of virtual resolution when stretching/shrinking it to the real screen.

Another cool feature of the ResolutionIndependentRenderer is the ability to convert our real screen mouse position to the virtual mouse position:

            _screenMousePos = _resolutionIndependence.ScaleMouseToScreenCoordinates(_inputHelper.MousePosition);

This is needed because our mouse events are happening in the real screen resolution and we need to convert (translate) them to the correct position in our virtual resolution space which is usually shifted (unless your screen is same size as your virtual resolution) so this method allows you to do that easily.

I hope this code will be of help to all the aspiring XNA/Monogame developers out there!

Check out the sample and let me know if it helps you!

31 thoughts on “Solving Resolution Independent Rendering And 2D Camera Using Monogame

  1. THAT is realy awesome! Thanks for such a nice piece of code! I’ve found many implementations on the net but none of them was such complete! 🙂 It saved me a whole bunch of time!

  2. Do you have a solution for the new Windows 8.1 windowing modes? The new windowing modes have the same three states (snapped, filled, and fullscreen), but snapped and filled modes can now be pretty much any size. I ran your sample project, and filled worked well at any size, but snapped remained at 320. Probably something in the monogame framework, but I’m trying to find a workaround right now so that I can get my update out for the 8.1 launch.

    1. Unfortunately i don’t have Win 8.1 so i cannot check, but im sure that my solution for resolution independence works for any resolution.
      Just make sure to set the parameters (ScreenSize and VirtualScreenSize) correctly.

      1. Your solution, with respect to the snapped view on windows 8.1 currently demonstrates an unexpected behavior. The application continues treating this window as if it’s width is 320, and the content is scaled likewise.

      2. OK, so it turns out this was pretty much my fault, and I must apologize. Building an application that works with the new Windows 8.1 RC features requires first installing Visual Studio 2013 RC. Opening your solution in 2013 RC and retargetting it for 8.1 RTM solved the issue I was running into. Your patience and hard work on this tutorial/project are much appreciated.

  3. This is a great solution, because the first one did not handle for a 2D camera, but unfortunately I wasn’t able to get the result I was looking for. I’m developing for 1366×768 and running my game on a 24 inch 1920×1080 resolution monitor and finding problems with my 2D textures. My sprites scale nicely, which saves me from operating on them all of the time, but my fullscreen textures don’t fill the screen, leaving me with an off-centered effect.(drawing at vector.zero, leaving the letterbox to the right and bottom of the screen!) I’m sure things are supposed to be centered, so do you know what might be causing this?

  4. Oops! Nevermind….As soon as I posted my problem, I turned around and solved the problem. I was drawing my background 2D textures with a fullscreen rectangle variable.

    Thanks so much for this great solution!

    1. Hi Rico,

      yes its very common, once you share the problem with someone it solves it self in your mind 🙂

      I’m glad you have it working finally!

      If you build something interesting share with us!

      Have a great day!
      Slobo

  5. Hi Slobo!

    Actually, I already have several games out on Windows Phone, and thanks to skillful coders such as yourself; I can now translate to Windows 8 and other devices. I am getting ready to start using Unity Pro, but have several XNA games that I am going to translate using Monogame.

    I loved making games for Windows phone, because I could target one resolution. After coding about 50 percent of a game for Windows desktop using Vector calculations for each sprites/textures scale, I found the Resolution Independent code, but it didn’t account for the 2D camera like yours does!

    Thank you very much for identifying the needed upgrade to the first solution and sharing! I can now scrap the archaic way I was going about handling multiple resolutions!

    I will definitely post back to your site when the first game is done using XNA/Monogame/IndependentResolution2Dcamera code!

  6. So how do I set a resolution exactly? Setting the preferred back buffer width and height works, but in fullscreen, it takes my native resolution. I would check out the sample project, but I need Windows 8 to view it.

    1. To clarify, this works perfectly when running windowed. It letterboxes and scales nicely. But if I want to make it fullscreen, my 1280×720 background test texture is being drawn at the top left, leaving huge black bars at the bottom and the right. Perhaps you could clarify a bit on how to initialize this renderer correctly?

      1. Hi,

        yes probably some problem with initialization. You need to call method InitializeResolutionIndependence with proper values (real resolution of screen) whenever your screen size changes, so i presume you are not doing that properly when you run it in full screen mode.
        Send me some sample code that demonstrates your problem maybe i could help you.
        I will send you my private email address directly…

  7. I really like this I have been working on implementing it in my code. One thing I changed that solved my Background placement issues was to move setting the VirtualWidth and VirtualHeight to LoadContent before Camera initialization. I did this because I was having issues with my background being pushed down and to the right. I figured out that the position is only set on Camera initialization so it is of no use to set it after it is needed.

  8. I was having issues with the scaling being off when it would switch to pillarbox so I did a little retooling to the ResolutionIndependentRenderer.cs to get it to work.

    Added a _scale field.

    private double _scale;

    changed SetupVirtualScreenViewport to calculate and use _scale

    int height, width;
    double widthScale = 0, heightScale = 0;
    widthScale = (double)ScreenWidth / (double)VirtualWidth;
    heightScale = (double)ScreenHeight / (double)VirtualHeight;

    _scale = Math.Min(widthScale, heightScale);

    width = (int)(VirtualWidth * _scale);
    height = (int)(VirtualHeight * _scale);

    // set up the new viewport centered in the backbuffer
    _viewport = new Viewport
    {
    X = (ScreenWidth - width) / 2,
    Y = (ScreenHeight - height) / 2,
    Width = width,
    Height = height
    };

    _game.GraphicsDevice.Viewport = _viewport;

    Changed RecreateScaleMatrix to use _scale

    Matrix.CreateScale((float)_scale, (float)_scale, 1f, out _scaleMatrix);
    _dirtyMatrix = false;

    1. That corrected the screen pos problem I was having, great stuff many thanks to both of you!

  9. I know this is an old post but I just recently found it and have integrated it into my game ( I’m porting a PC game to iOS). I must say that this has saved me countless hours and I can’t thank you enough!

    I am running into an issue however, when targeting retina ipads (2048×1536). My virtual resolution is set to 1280×720 and when I run the game, my textures are all scaled perfectly, but the viewport is getting clipped to the top left. I am at a loss as to why this is happening, it works perfectly on my regular ipad(1024×768).

    Any thoughts or comments would be greatly appreciated.

    Thanks again!

    1. Hi Jon, im glad my post helped in some way.
      Regarding clipped viewport can you please send me some screenshot of this problem and part of code where you initialize the resolution
      of my class.
      I suspect that after screen is initalized you just need to reinitialize the class with proper size of the screen.

      You can send me more details on spavkov [a t] gmail dot com

        1. Hello,

          I am actually experiencing this same problem in 2017 and haven’t been able to fix it on my own. I was wondering is this is something to do with having a more recent version of MonoGame. Could you share the solution, if you found it? Thank you.

  10. Hi,
    First of all, great post!
    I’m having sort of same problems as Jon. roboblob, could you perhaps post the solution for this issue?

    resolution the app was built for:
    Width = 768;
    HeightHeight = 1280;

    Real resolution:
    Width = 720;
    Height= 1280;

    So what happens is:
    – Orange spacing on top
    – Orange spacing on the bottom
    – Orange spacing at the right side

    But I’d expect that it would resize it to full width (720) with smaller spacing on top and bottom.
    Any suggestions?

  11. Ok,

    Managed to fix the one that i mentioned above, seemed to be an issue in my code.
    But I got another issue at the moment:
    resolution the app was built for in windows phone:
    Width = 768;
    HeightHeight = 1280;

    on android i am testing with
    Width = 225;
    HeightHeight = 301;

    For some reason the content does not fit into the window?
    picture of the issue can be found here:
    http://i61.tinypic.com/szv66v.png

  12. Is there a way to translate the mouse position on the real screen to the virtual screen based on the zoom of the camera?
    So that 0,0 on the real screen isn’t 0,0 on the virtual screen if the zoom != 1.

    I hope I exlpained it enough.
    Thanks for the great solution.

    1. I think this sould work.


      Vector2 CameraWindowDimensions = new Vector2(ResolutionIndependentRenderer.VirtualWidth, ResolutionIndependentRenderer.VirtualHeight) / Camera2D.Zoom;
      Vector2 CameraWindowTopLeftCorner = Camera2D.Position - (CameraWindowDimensions / 2);

      // This is the scaled mouse position based on the camera position and zoom
      Vector2 CameraScaledMousePosition = CameraWindowTopLeftCorner + (ResolutionIndependentRenderer.ScaleMouseToScreenCoordinates(MousePosition) / Camera2D.Zoom);

  13. Hello everyone first I want to say thank you. This is working for my project. Lumia 1520 game scaled down to 800 x 480. I did run into a problem. I see you supported converted mouse positions. Any chance to support touch positions?

  14. Hi,

    I’ve been using this solution for a while but I have a problem. When I zoom with the camera the mouse position is off, the more I move the mouse from the center the offset increases. It works perfectly well as long as I don’t zoom though. Any idea?

    Thank you,
    Simon

  15. Was wondering if there is a way to draw to the letterbox orange areas? basically I need a way to tell if someone has touched outside the play area and wanted to represent a graphic of some kind so users know to press there?

Leave a Reply