Advertisement

Zoom has jittering effect on the drawed sprites

Started by April 24, 2019 10:42 PM
7 comments, last by FFA702 5 years, 4 months ago

Hi,

I have a small annoying problem with my draw loop where the zoom function results in very noticeable jerking motion of all the rendered sprites. Here is the whole method and the lerp function :


        protected override void Draw(GameTime gameTime)
        {

            int adjustedZoomLevel = 60 + ZoomLevel;


            // DRAW THE DYNAMIC OBJECTS
            Color spriteColor;
            if (UI.DisplayUI.isFocused == false)
            {
                spriteColor = Color.White;
            }
            else
            {
                spriteColor = Color.Gray;
            }
            GraphicsDevice.Clear(Color.Green);
            point vP = new point((graphics.PreferredBackBufferWidth/ adjustedZoomLevel), ((graphics.PreferredBackBufferHeight / adjustedZoomLevel)));
            point vPo2 = new point(((graphics.PreferredBackBufferWidth) / ((adjustedZoomLevel) * 2) ) , (((graphics.PreferredBackBufferHeight) / ((adjustedZoomLevel) * 2)) ));
            

            point lastViewPort =
            GameZod.GameZod.Game.lastCamera - vPo2;
            point viewPort =
            GameZod.GameZod.Game.Camera - vPo2;

            
            spriteBatch.Begin(samplerState: SamplerState.PointClamp);
            for (int x = 0; x < vP.x; x++)
            {
                for (int y = 0; y < vP.y; y++)
                {
                    point pos = new point((x + viewPort.x), (y + viewPort.y));
                    if (pos.x < GameZod.GameZod.Game.map.size & pos.y < GameZod.GameZod.Game.map.size & pos.x > 0 & pos.y > 0)
                        if (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y] != null)
                        {


                            spriteBatch.Draw(
                             GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].texture,
                            new Rectangle(

                                ((int)(
                                lerp(
                                (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].position.x) * adjustedZoomLevel,
                                (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].lastPosition.x) * adjustedZoomLevel,
                                gameTimeElapsed / 500f) - 

                                    lerp(
                                        viewPort.x * adjustedZoomLevel,
                                        lastViewPort.x * adjustedZoomLevel,
                                        gameTimeElapsed / 500f)
                                    )
                                    )
                                    ,


                                ((int)(
                                lerp(
                                (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].position.y) * adjustedZoomLevel,
                                (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].lastPosition.y) * adjustedZoomLevel,
                                gameTimeElapsed / 500f) - 
                                
                                lerp(
                                    viewPort.y * adjustedZoomLevel, 
                                lastViewPort.y * adjustedZoomLevel, 
                                gameTimeElapsed / 500f)
                                )
                                ),



                                (int)adjustedZoomLevel, (int)adjustedZoomLevel),
                            new Rectangle(0, 0, 30, 30),
                            spriteColor);
                        }
                }
            }
            //END DYNAMIC OBJECTS

            //INTERFACE BEGIN

            for (int i = UI.DisplayUI.UIDisplayList.Count - 1; i >= 0; i--)
            {
                if (UI.DisplayUI.isFocused == true & i == 0)
                {
                    UI.DisplayUI.UIDisplayList[i].draw(spriteBatch, Color.White);
                }
                else
                {
                    UI.DisplayUI.UIDisplayList[i].draw(spriteBatch, Color.Gray);
                }
            }



            spriteBatch.Draw(UI.DisplayUI.MouseTexture,
                new Rectangle(mouseState.X, mouseState.Y, 20, 20),
                new Rectangle(0, 0, 30, 30),
                Color.White);


            spriteBatch.End();

            base.Draw(gameTime);
        }
        float lerp(float p1, float po, float t)
        {
            return (1 - t) * po + t * p1;
        }

and the arguments for the Draw function is : (Texture, Destination rectangle, Origin Rectangle, Color); this is done with MonoGame (XNA)

The zoomlevel is changed in increment of +-1.

I really think the problem is contained in this method, everything is perfectly fine, but zooming shifts the position of the screen to the upper right and then clamps back to the lower right, ever shifting up until it clamp back down again.

What I've done :

  1. Ruled out the fact that it's tile based and may shift everytime new tiles enter the frame
  2. Tried to calculate the delta between the last zoom and the current zoom in terms of the rectangle's position, then subtracting it from the position
  3. Isolate the problem to a particular function
  4. 'center' the coordinate system more accurately to the center of the screen; this has no effect on the issue

I really hope it's just something really obvious I missed, spent 6h on this and made 0 progress.

I see so much int and even type casting. I thought  int  is only  for indices into arrays. For loop variables. Pointers are  Indizes into a byte Array.  Positions  are real numbers and always  better  stored as floats.  Let the graphics library index into pixels in the frame buffer!  I think  OpenGL allows one to specify everything using floats (64bit).

You may need to an index into a tilemap and  an index into a list of sprites. Such stuff.

Also time is float. I am scared by 2032 where we need more than 32 bit in the integer part. I would always use time since program start.

Advertisement

Way to much code in the morning (EU)... xD

It's been a little while since I programmed a Worms clone and integrated a zoom function, but maybe I can help you find the problem.

The following lines might contain errors or false assumptions since I am not a regular zoom function programmer: ;)

2d-zooming is basically a coordinate transformation in camera space where the origin of the coordinate system is the center of the screen. Zooming issues occur if the objects are not correctly transformed into the camera space mentioned above. Possible pitfalls are, that your engines native coordinate system is some edge of the screen and not the center of the screen. The result is usually that zooming seems to approach one corner of the screen as in your case. So what should work are the following steps:

 

- calculate the position of the screen center in your worlds coordinate system (the point you want to zoom into)

- subtract that position from every object's position

- multiply by your zoom factor

- optional: If your engine coordinate systems origin is located at a screen corner, you have to add/subtract half the width and height of your screen. Be aware, that you must also apply the zoom factor to the screen width and height.

- draw

Apart from the optional step, I tested it with a fast Matlab script  (inside spoiler) and it worked.

Spoiler


clc;
clear all;
close all;

% screen world position
sx = -2
sy = -2


% rectangle positions
dx1 = 1
dy1 = 1

dx2 = 2
dy2 = 3

% initial zoom
zoom = 1

% rectangle model coordinates
x = [0,1,1,0,0]
y = [0,0,1,1,0]


% place 2 rectangles somewhere in my world
x1 = (x -dx1) * zoom
y1 = (y -dy1) * zoom

x2 = (x -dx2) * zoom
y2 = (y -dy2) * zoom


figure;

% start zooming
for i=1:100
    %increase zoom
    zoom = zoom +0.1
    
    % calculate object coordinates in zoomed system
    x1z = (x1 -sx) * zoom;
    y1z = (y1 -sy) * zoom;

    x2z = (x2 -sx) * zoom;
    y2z = (y2 -sy) * zoom;
    
    % plot everything
    hold off
    plot(x1z,y1z)
    hold on;
    plot(x2z,y2z, 'color','red')
    xlim([-10,10])
    ylim([-10,10])
    pause(0.1)
end

 

 

 

So check your transformations and in which coordinate system they are done.

 

 

Some general remarks regarding your code:

- If you are using C++ 11 or higher: Don't use C-style casts --> (int)3.32 . Use static_casts or dynamic_casts, depending on the situation. Google will tell you why: https://www.google.com/search?client=ubuntu&amp;channel=fs&amp;q=why+not+use+c+style+cast&amp;ie=utf-8&amp;oe=utf-8

 

- Try to split your code into separate functions with meaningful names. Long function implementations are hard to read/understand, even for the guy who wrote it.

 

- Add more comments so that other people understand what you are trying to do in the code section. Reading a comment is way easier than understanding code

 

- I see a lot of new.  I don't see any delete. That scares me. Don't know if you delete the objects anywhere in a function they are passed to but that would be even scarier.  Look at this line:


point vP = new point((graphics.PreferredBackBufferWidth/ adjustedZoomLevel), ((graphics.PreferredBackBufferHeight / adjustedZoomLevel)));

 

Maybe I am suffering on a low caffeine level and miss something but I think what happens is the following:

- You create a variable vP of type point (not pointer to a point ---> point*)

- You create a new point on the heap with new and use it to initialize vP

- Your point class needs a copy constructor that takes a point* as a parameter, otherwise, the compiler will complain since he can't convert a point* (which is created by new) into a point.

Now the last point is problematic. Since you do not store the pointer to the point created on the heap, your constructor needs to delete that pointer. Otherwise, you get a memory leak. But what happens if you use the same constructor on a pointer which is still needed? Welcome to the land of undefined behavior.

Why do you use 'new' there anyways? You can just remove it. The other new calls could probably also be removed. In general, if you are not doing really low-level stuff, there is no reason why your code should ever contain a single new call. Use STL/Boost smart pointers and containers or your program is in danger of randomly crashing after a while due to memory leaks.

 

Greetings

 

 

@DerTroll: It doesn't look like FFA702 is using C++. If not, that may explain some of the things you mentioned (e.g. 'C-style' casts and use of 'new').

@FFA702: I'm not sure if a suggestion of reformatting will be well-received here, but I think your code would be easier to read if it were formatted more consistently. Also, breaking it up further into smaller functions, as DerTroll suggested, could reduce indentation levels and increase clarity.

Maybe someone will be able to help out as is, but for me at least, the code in its current form is a little hard to follow.

18 minutes ago, Zakwayda said:

@DerTroll: It doesn't look like FFA702 is using C++. If not, that may explain some of the things you mentioned (e.g. 'C-style' casts and use of 'new').

 

 

Maybe you are right... I always forget that there is something called C# ... never learned it. So forget the things about new and casts that I wrote. :D 

I've added a Gif of me zooming in and out to illustrate the problem. The memes are placeholder assets.To clarify the working of the draw loop, the 'adjusted zoom level' is the final size (h and w) of a tile. All objects are stored in a 2d array which represents their position. As such, dividing the length of the screen in pixel by the length of a tile gives the number of tiles to be drawn on the screen. This is the value (vP) I used to iterate trough my array. The viewPort is the position of the camera (an actual point in the array, right now stuck to the player) minus the size of the screen divided by two times the size of the tiles; this gives the top left tile to draw. The loop then simply iterates trough each tiles to draw. All variable labelled 'last' (such as lastViewPort and lastCamera) are just used in the lerp function to give the illusion of smooth transition between tiles. As of now, the program performs really well and fits every performance and stability requirement I have. It's just buggy on the zoom functionality.

@arnero I think you might think I'm using cpp or some unmanaged language. Also the gametime is a feature of MonoGame, not my own code.

@DerTroll It's done in C#, hence the style. I'm really sorry I didn't mention it, given the length of your reply dedicated to language specific features. About the problem being the engine's coordinate system; the point(0,0) is indeed the top left corned of the screen. This might have something to do with it, but I can't quite wrap my head around it given my current draw method.

@Zakwayda I'll refractor and comment the code, then I'll post back. The single reason it's written that way is because I can quickly comment and edit chunk in and out. I realize I should have done that before asking help. 

jitter.gif

Alright guys I've refractored and commented the code; this should be much much clearer :
 


 protected override void Draw(GameTime gameTime)
        {
            //The base ZoomLevel is 60
            //The ZoomLevel is the lenght and widht of a tile
            int adjustedZoomLevel = 60 + ZoomLevel;
            Point tileSize = new Point(adjustedZoomLevel, adjustedZoomLevel);


            // DRAW THE DYNAMIC OBJECTS:
            //If the UI is focused, we draw the game grey
            Color spriteColor;
            if (UI.DisplayUI.isFocused == false)
            {
                spriteColor = Color.White;
            }
            else
            {
                spriteColor = Color.Gray;
            }
            GraphicsDevice.Clear(Color.Green);

            //We calculate the number of tiles to draw (screensize/ tilesize)
            point vP = new point((graphics.PreferredBackBufferWidth/ adjustedZoomLevel), ((graphics.PreferredBackBufferHeight / adjustedZoomLevel)));

            //We calculate the actual top left tile, which gives us the first tile to iterate trough
            point lastViewPort =
            GameZod.GameZod.Game.lastCamera - vP/2;
            point viewPort =
            GameZod.GameZod.Game.Camera - vP/2;

            
            spriteBatch.Begin(samplerState: SamplerState.PointClamp);
            for (int x = 0; x < vP.x; x++)
            {
                for (int y = 0; y < vP.y; y++)
                {
                    //We iterate trought everytile present in the screen
                    point pos = new point((x + viewPort.x), (y + viewPort.y));
                    if (pos.x < GameZod.GameZod.Game.map.size & pos.y < GameZod.GameZod.Game.map.size & pos.x > 0 & pos.y > 0)
                        if (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y] != null)
                        {
                            //If the tile is whitin the map and exists

                            //We calculate the position of the tile on the screen (top left)
                            //The lerp function lineraly interpolates betwen t and t-1 to give smooth mouvements
                            int tilePosX = (int)lerp((GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].position.x) * adjustedZoomLevel,
                                                     (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].lastPosition.x) *adjustedZoomLevel,
                                                      gameTimeElapsed / 500f);
                            int tilePosY = (int)lerp((GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].position.y) * adjustedZoomLevel,
                                                     (GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].lastPosition.y) * adjustedZoomLevel,
                                                      gameTimeElapsed / 500f);

                            Point tilePos = new Point(tilePosX, tilePosY);
                            //We linearly interpolate the viewPort mouvement between t and t-1 to give smooth camera movement
                            //as the camera moves trough tiles in a discrete manner
                            int camDifX = (int)lerp(
                                        viewPort.x * adjustedZoomLevel,
                                        lastViewPort.x * adjustedZoomLevel,
                                        gameTimeElapsed / 500f);
                            int camDifY = (int)lerp(
                                        viewPort.y * adjustedZoomLevel,
                                        lastViewPort.y * adjustedZoomLevel,
                                        gameTimeElapsed / 500f);
                            Point camDif = new Point(camDifX, camDifY);

                            //we add the sprite to the spritebatch
                            spriteBatch.Draw(GameZod.GameZod.Game.map.Dobjects[pos.x, pos.y].texture, new Rectangle(tilePos - camDif, tileSize), spriteColor);
                            
                        }
                }
            }
            //END DYNAMIC OBJECTS

            //INTERFACE BEGIN

            for (int i = UI.DisplayUI.UIDisplayList.Count - 1; i >= 0; i--)
            {
                if (UI.DisplayUI.isFocused == true & i == 0)
                {
                    UI.DisplayUI.UIDisplayList[i].draw(spriteBatch, Color.White);
                }
                else
                {
                    UI.DisplayUI.UIDisplayList[i].draw(spriteBatch, Color.Gray);
                }
            }



            spriteBatch.Draw(UI.DisplayUI.MouseTexture,
                new Rectangle(mouseState.X, mouseState.Y, 20, 20),
                new Rectangle(0, 0, 30, 30),
                Color.White);


            spriteBatch.End();


            lastAdjustedZoomLevel = adjustedZoomLevel;
            base.Draw(gameTime);
        }
        float lerp(float p1, float po, float t)
        {
            return (1 - t) * po + t * p1;
        }

 

Advertisement

This is your problem:


//as the camera moves trough tiles in a discrete manner

In the movie one can actually see the tiles clamp to the left. You want a more continuous placement of the camera instead.

Nowadays I'd use linear filtering and floats everywhere, but I don't think this applies to this art style: Linear filtering on pixelart graphics can smear badly.

But you can still use floats and only at the end clamp, but this time to pixel positions.

Sorry for not pinpointing further code-wise, because, yes, one can do this all by hand, so to speak, but I recommend to get more familiar with vectors and transformations, which XNA provides (Matrix and vector classes). Once understood such things are way shorter to write. For the case at hand you need to learn about scaling and translating only. You should learn them anyway ;)

@unbird Thanks a bunch this was indeed the problem.

This topic is closed to new replies.

Advertisement