Pixel-art sprite jitters while moving in a "2D gameplay in a 3D world" (2.5D) setup — Godot 4.7

Godot Version

v4.7.2.stable.official [ed1daf0bf]

Hi everyone,

First of all, I’m sorry in advance if this post is a mess or if my whole concept is misguided — I’ve been stuck on this for a while and I’m genuinely not sure anymore whether I even want the right thing or whether my approach can work at all, so please bear with me.

The concept:
I’m trying to make a game that plays like a 2D game but takes place in a 3D world: a top-down, slightly side-angled camera, pixel-art sprites standing in a real 3D scene (so, 2.5D). In Godot terms: a CharacterBody3D player with a collision shape and a sprite as a child, plus a Camera3D on a rig looking down at the arena from a slight angle.

The problem:
As you can see in the attached video, the character sprite jitters / stutters constantly while walking. The movement logic itself works fine, but the sprite visibly shakes instead of moving smoothly pixel-by-pixel, which completely breaks the pixel-art look.

My setup and the rules I followed:

  • Godot 4.7.2 stable
  • Pixel-art scale: 16 pixels = 1 meter in the 3D world
  • Texture filtering set to Nearest everywhere (project default + import settings), mipmaps off
  • A bunch of other “pixel-perfect” tricks I found (snapping on/off, integer zoom, etc.) — honestly, I tried so many contradictory things, including the exact opposites of each other, that I lost track of what is currently “correct”. Maybe I should have done everything the other way around? I have no idea anymore.

Player movement code (_physics_process + move_and_slide()):

class_name PlayerController
extends CharacterBody3D

@export var move_speed: float = 2.0

func _physics_process(delta: float) → void:
    var input_dir := Input.get_vector(“move_left”, “move_right”, “move_up”, “move_down”)
    var move_direction := Vector3(input_dir.x, 0, input_dir.y).normalized()
    if move_direction:
        velocity.x = move_direction.x * move_speed
        velocity.z = move_direction.z * move_speed
    else:
        velocity.x = move_toward(velocity.x, 0, move_speed)
        velocity.z = move_toward(velocity.z, 0, move_speed)
    move_and_slide()

My questions:

  1. What typically causes this kind of jitter in a 2.5D pixel-art-in-3D setup — sub-pixel camera movement, physics-vs-render desync, or a wrong pixels-per-meter scale?
  2. Is my approach fundamentally wrong? Maybe I should do the opposite: render the 3D scene into a low-resolution viewport and upscale it an integer number of times, instead of trying to keep everything pixel-snapped in a full-res 3D view?
  3. What is the recommended way to get a crisp, jitter-free “2D look in a 3D world”?

Video:

Sorry again if I’m mixing up concepts or asking something obvious — I’m ready to redo the whole rendering approach if that’s what it takes. Any pointers would be hugely appreciated.

Thanks!

The video doesn’t show anything, only the editor.

Pixel-perfect 3D will be very difficult, 3D will scale objects by non-whole numbers for distant objects. Enabling Mipmaps for your imported textures (you can use mipmaps with nearest filtering) should help with some of the looks, but maybe not snapping pixels.

You’re right, sorry, probably only one window was displayed when recording the video from the screen. Here’s the recording from the game itself:

Oh yeah that’s just how pixels move “diagonally” if your sub-pixel step and/or direction vector doesn’t match then one axis will move before the other. For example if your position is (1.23, 1.45) and you are adding a perfectly diagonal (0.7, 0.7) movement to it, then the Y value will jump to the next pixel while the X value stays, next frame the opposite happens X moves, Y stays.

I’m impressed to hear this is 3D behind the scenes.