Raylib - Jittering camera in the y direction

I wonder if anyone can help to resolve this for me? I am seeing a jittering movement when moving the camera in the y direction.

The code is based off one of the Raylib demos for virtual screen implementation.

If you comment out this line you can see that the x direction is very smooth, but for me the y movement jitters and jumps a little.

cam.y=math.sin(counter)*20.0
package ray_virtual

import rl "vendor:raylib"
import "core:math"

WIN_W :: 720
WIN_H :: 450
VIRT_W :: 200
VIRT_H :: 110
VRATIO :f32: WIN_W/VIRT_W

main :: proc(){
    using rl
    InitWindow(WIN_W,WIN_H,"Raylib virtual screen res")
    worldcam:=Camera2D{}
    worldcam.zoom=1
    screencam:=Camera2D{}
    screencam.zoom=1
    target:=LoadRenderTexture(VIRT_W,VIRT_H)
    rec1:=Rectangle{70,35,20,20}
    rec2:=Rectangle{90,55,30,10}
    rec3:=Rectangle{80,65,15,25}
    srcRec:=Rectangle{0,0,f32(target.texture.width),f32(target.texture.height)}
    dstRec:=Rectangle{-VRATIO,-VRATIO,WIN_W+VRATIO*2,WIN_H+VRATIO*2}
    origin:=Vector2{}
    cam:=Vector2{}
    rot,counter:f32
    SetTargetFPS(60)
    // main loop
    for !WindowShouldClose(){
        rot+=60*GetFrameTime()
        
        counter+=0.02
        cam.x=math.cos(counter)*50.0
        cam.y=math.sin(counter)*20.0
        
        screencam.target=cam
        worldcam.target.x=math.trunc(screencam.target.x)
        screencam.target.x-=worldcam.target.x
        screencam.target.x*=VRATIO

        worldcam.target.y=math.trunc(screencam.target.y)
        screencam.target.y-=worldcam.target.y
        screencam.target.y*=VRATIO
        
        BeginTextureMode(target)
            ClearBackground(RAYWHITE)
            BeginMode2D(worldcam)
                DrawRectanglePro(rec1,origin,rot,BLACK)
                DrawRectanglePro(rec2,origin,-rot,RED)
                DrawRectanglePro(rec3,origin,rot+45,BLUE)
            EndMode2D()
        EndTextureMode()

        BeginDrawing()
            ClearBackground(RED)
            BeginMode2D(screencam)
                DrawTexturePro(target.texture,srcRec,dstRec,origin,0,WHITE)
            EndMode2D()
            DrawText(TextFormat("Screen res: %ix%i",WIN_W,WIN_H),10,10,20,DARKBLUE)
            DrawText(TextFormat("World res: %ix%i", VIRT_W,VIRT_H),10,40,20,DARKGREEN)
            DrawFPS(WIN_W-95,10)
        EndDrawing()
    }
    UnloadRenderTexture(target)
    CloseWindow()
}

At first glance here’s some thoughts. Hopefully they are helpful. I’m also making some assumptions about a few things, so pardon if I’m off.

I’m guessing the cam.x and cam.y factors are different because either the screen or window you are drawing to is a rectangle, and accelaration of the camera needs to be different in each direction depending on that ratio.

So let’s guess it’s a 16:9 ratio display (or window rectangle). My guess is that the x and y factors should also be a similar ratio.

So for 16:9 I’d go with 64:36 for the x and y factors to start. If your display and/or window rectangle is a different ratio, I’d adjust acordingly.

cam.x=math.cos(counter)*64.0
cam.y=math.sin(counter)*36.0

64:36 ratio is roughly 1.77777

The standard deviation of cos(n)64 is roughly 45.25.
The standard deviation of sin(n)36 is roughly 25.5.
standard deviation ratio of roughly 1.78431

If it is desired to have a perceived accelaration the same in all directions, my gut says to make the standard deviation ratio 1:1 for x and y.

edit: after further thought, since both sin and cos oscillate between 1 and -1 and the reverse respectively, the factor ratio (what ever is multiplied to cos(n) or sin(n) ) will be the same as the standard deviation ratio. So that was extra math for no reason. With that said, I’d still make the factor ratio relative to the display screen or window pixil ratio.

Also I’m confused about the constants at the beginning. I see VRATIO :f32: WIN_W/VIRT_W which only reference width it seems. Should there be a consideration for height too? 720/200 (W) is not the same as 450/110 (H). Without knowing the specific intentions of the rest of the code, I could be off, but it seems height is not respected in the same way as width.

I stopped being lazy and copy pasted your code. I think I found the culprit.

worldcam.target.y=math.trunc(screencam.target.y)

The above is causing y to round to a different whole number more frequently than it does for x. You can see the behaviour with this.

cam:=Vector2{}

  for counter:f32; counter < 1; counter+=0.02 {
    cam.x=math.cos(counter)*50.0
    cam.y=math.sin(counter)*20.0
    fmt.printfln("x:  %-12v y:  %-12v", cam.x, cam.y)
    fmt.printfln("tx: %-12v ty: %-12v", math.trunc(cam.x), math.trunc(cam.y))
  }

Taking out the math.trunc() procedures for both now allows both axis to move smoothly.

screencam.target=cam
worldcam.target.x=screencam.target.x
screencam.target.x-=worldcam.target.x
screencam.target.x*=VRATIO

worldcam.target.y=screencam.target.y
screencam.target.y-=worldcam.target.y
screencam.target.y*=VRATIO

Is there a reason this needed to be rounded and not just use the f32 value?

Hi xuul,
Thanks for doing such a deep dive. According to the comment in the Raylib code example:

// Round worldSpace coordinates, keep decimals into screenSpace coordinates

If I comment out both trunc operations then the virtual screen does indeed move around smoothly but then I can then see the red background being revealed behind it. To get around that I can increase the zoom level of the virtual display and offset it accordingly.

The Raylib example code can be found here: raylib - examples
Although I can’t get a direct link to the actual example. It’s called Smooth pixel perfect

Here the the C equivalent code section for reference. The section that deals with updating the camera positions:

        // Set the camera's target to the values computed above
        screenSpaceCamera.target = (Vector2){ cameraX, cameraY };

        // Round worldSpace coordinates, keep decimals into screenSpace coordinates
        worldSpaceCamera.target.x = truncf(screenSpaceCamera.target.x);
        screenSpaceCamera.target.x -= worldSpaceCamera.target.x;
        screenSpaceCamera.target.x *= virtualRatio;

        worldSpaceCamera.target.y = truncf(screenSpaceCamera.target.y);
        screenSpaceCamera.target.y -= worldSpaceCamera.target.y;
        screenSpaceCamera.target.y *= virtualRatio;

I just wondered really why the online Raylib version is smooth but my re-write in Odin/Raylib was jittering so much. I thought it might be a small bug somewhere.

Don’t comment them out. Just change them to not truncate the float value.

If I use them like the below, it is smooth and no red background.

worldcam.target.x=screencam.target.x
worldcam.target.y=screencam.target.y

I also compiled them as 2 separate exe names and ran them simultaneously and it appears they both move the shapes in the same way. They both drift off the edges roughly the same amount in the same sequence.

In powershell:

 Start-Process -WindowStyle hidden .\scratch_no_trunc.exe && Start-Process -WindowStyle hidden .\scratch_trunc.exe

I just wondered really why the online Raylib version is smooth but my re-write in Odin/Raylib was jittering so much. I thought it might be a small bug somewhere.

There may be a bug somewhere else, but I’m not sure how a whole number will be smooth in such a small window. My first guess is that a float is desirable, but may need a check to see if it’s outside the bounds of the screen, so then set it to the highest whole number. Something to that affect.

In graphics I’ve often run into imprecise drawing when I’m rescricted to whole numbers and not allowed to use float (looking at you Widows GDI).

I decided to do a straight 1:1 code conversion from the c example to Odin. Glad I did, because Odin’s vender:raylib library made this very easy. Another reason to love working with Odin.

The 1:1 conversion works without any modification to the code’s logic. Seems the truncation is intended. I’d start with this and then make your changes so that you will know what breaks what. IMHO, it’s much easier to trace your own changes than to chase bugs that you don’t know if they are your own, or the sources.

package smooth_ppc

import "core:math"
import "vendor:raylib"

main :: proc(){
  using raylib

  screenWidth         :: 800
  screenHeight        :: 450
  virtualScreenWidth  :: 160
  virtualScreenHeight :: 90
  virtualRatio        :: f32(screenWidth)/f32(virtualScreenWidth)

  InitWindow(screenWidth, screenHeight, "raylib [core] example - smooth pixel-perfect camera")

  worldSpaceCamera: Camera2D  // Game world camera
  worldSpaceCamera.zoom = 1.0
  screenSpaceCamera: Camera2D
  screenSpaceCamera.zoom = 1.0

  target := LoadRenderTexture(virtualScreenWidth, virtualScreenHeight)

  rec01 := Rectangle{ 70.0, 35.0, 20.0, 20.0 }
  rec02 := Rectangle{ 90.0, 55.0, 30.0, 10.0 }
  rec03 := Rectangle{ 80.0, 65.0, 15.0, 25.0 }

  // The target's height is flipped (in the source Rectangle), due to OpenGL reasons
  sourceRec := Rectangle{ 0.0, 0.0, f32(target.texture.width), -f32(target.texture.height) }
  destRec   := Rectangle{ -virtualRatio, -virtualRatio, screenWidth + (virtualRatio*2), screenHeight + (virtualRatio*2) }

  origin   := Vector2{ 0.0, 0.0 }
  rotation := f32(0)
  cameraX  := f32(0)
  cameraY  := f32(0)

  SetTargetFPS(60)
  for !WindowShouldClose() {    // Main loop - Detect window close button or ESC key
    rotation += 60.0*GetFrameTime()   // Rotate the rectangles, 60 degrees per second

    // Make the camera move to demonstrate the effect
    cameraX = math.sin(f32(GetTime()))*50.0 - 10.0
    cameraY = math.cos(f32(GetTime()))*30.0

    // Set the camera's target to the values computed above
    screenSpaceCamera.target = Vector2{ cameraX, cameraY }

    // Round worldSpace coordinates, keep decimals into screenSpace coordinates
    worldSpaceCamera.target.x = math.trunc(screenSpaceCamera.target.x)
    screenSpaceCamera.target.x -= worldSpaceCamera.target.x
    screenSpaceCamera.target.x *= virtualRatio

    worldSpaceCamera.target.y = math.trunc(screenSpaceCamera.target.y)
    screenSpaceCamera.target.y -= worldSpaceCamera.target.y
    screenSpaceCamera.target.y *= virtualRatio

    // Draw
    BeginTextureMode(target)
    ClearBackground(RAYWHITE)
    BeginMode2D(worldSpaceCamera)
    DrawRectanglePro(rec01, origin, rotation, BLACK)
    DrawRectanglePro(rec02, origin, -rotation, RED)
    DrawRectanglePro(rec03, origin, rotation + 45.0, BLUE)
    EndMode2D()
    EndTextureMode()

    BeginDrawing()
    ClearBackground(RED)
    BeginMode2D(screenSpaceCamera)
    DrawTexturePro(target.texture, sourceRec, destRec, origin, 0.0, WHITE)
    EndMode2D()
    DrawText(TextFormat("Screen resolution: %ix%i", screenWidth, screenHeight), 10, 10, 20, DARKBLUE)
    DrawText(TextFormat("World resolution: %ix%i", virtualScreenWidth, virtualScreenHeight), 10, 40, 20, DARKGREEN)
    DrawFPS(GetScreenWidth() - 95, 10)
    EndDrawing()
  }
  UnloadRenderTexture(target)    // Unload render texture
  CloseWindow()                  // Close window and OpenGL context
}
1 Like

Thank you very much indeed. Your 1:1 version works so perfectly. Thank you for taking the time to delve into this. I can now use what you have provided to try and see where I have gone wrong during the re-write. I tried to keep things simpler/tighter so must have broken something along the way.
I am very much in the early stages of playing around with Odin. I like that it comes with ‘batteries’ and complies fast while also producing small code.

Update - It turns out if you make the source rec height positive then the jittering re-appears.
So, it really needs to be negative as shown in this section of code:

// The target's height is flipped (in the source Rectangle), due to OpenGL reasons
  sourceRec := Rectangle{ 0.0, 0.0, f32(target.texture.width), -f32(target.texture.height) }
1 Like