Skip to content

Godot Pixel Art Blurry? Import Settings That Keep Sprites Sharp

Blurry pixel art in Godot usually comes from linear filtering, lossy or mipmapped import, or non-integer scaling. The Studio's Godot bundle fixes the parts that live in the files; the rest are project settings, listed below.

Pixel art in Godot 4 looks soft for one of four reasons: the node draws with the project’s default Linear filter, the texture was imported with mipmaps or lossy/VRAM compression, the window stretches the game by a fractional factor, or the sprite or Camera2D sits between screen pixels. Check a still frame first, then a moving one. Nerulio’s Godot 4 bundle fixes the file side (a scene with Nearest filtering and a .png.import that stays lossless without mipmaps); stretch, snapping and camera are project settings you change yourself.

Drop frame images, a GIF or an .aseprite file to pack
Runs in your browser. Files are not uploaded.
Studio Pack & Export: the packed atlas page with every frame outlined, the used area and the export targets
The real Studio with a CC0 asset (OpenGameArt).
Exports to
  • Godot 4Verified· Godot 4.7.2
  • Unity 6Verified· Unity 6000.5.3f1
  • Phaser 3 / 4Verified· Phaser 3.90 + 4.2
  • PixiJS 8Verified· PixiJS 8.21
  • Aseprite JSONVerified· Phaser load.aseprite · PixiJS 8.21
  • .asepriteVerified· Aseprite 1.3.18
  • Spine / libGDXVerified· spine-canvas 4.2
  • Starling / Sparrow XMLVerified· Phaser 4.2
  • LÖVEVerified· LÖVE 11.5
  • DefoldBuilt· Defold bob.jar 1.13.1
  • GameMakerUNVERIFIED
  • CSS spritesVerified· Chromium
  • GIF / APNGDecoded· Pillow

Four kinds of blur, four places to fix them

Filtering. Since Godot 4.0 the filter belongs to the node (CanvasItem › Texture › Filter), not to the imported texture. A node left on Inherit takes its parent’s filter, and at the top of the tree that is the project setting rendering/textures/canvas_textures/default_texture_filter, which is Linear by default. Linear blends the four nearest texels, so every pixel edge becomes a soft ramp as soon as the sprite is drawn larger than 1×.

Import. Godot imports a PNG as Lossless without mipmaps, which the docs call the recommended setting for pixel art. That changes when the texture is used on a 3D material (Detect 3D switches it to VRAM Compressed with mipmaps), when someone picks Lossy or VRAM compression, or when Process › Size Limit shrinks it — with cubic interpolation. Mipmaps also soften a sprite drawn below 1×, for example with the camera zoomed out.

Scaling. A 320 × 180 game in a 1920 × 1080 window is scaled by exactly 6, so every art pixel is 6 × 6 screen pixels. In a 1366 × 768 window the factor is 4.27: some art pixels become 4 screen pixels wide and some 5, which reads as lumpy lines. display/window/stretch/mode, aspect and scale_mode decide what happens; scale_mode = integer floors the factor to a whole number.

Movement. A sprite or camera at x = 10.4 is drawn between screen pixels: with Linear filtering that is blur, with Nearest it is shimmer, and the character and the level may round differently, so one jitters against the other. rendering/2d/snap/snap_2d_transforms_to_pixel rounds CanvasItem positions to whole pixels.

texture_filter
CanvasItem property: Inherit (from the parent), Nearest, Linear and the mipmap variants. The project default fills in at the top of the tree.
display/window/stretch/mode
disabled, canvas_items (2D drawn at the window resolution) or viewport (the game is rendered at the base size, then that image is scaled to the window).
display/window/stretch/scale_mode
fractional keeps the exact factor; integer floors it, so the screen is always a whole multiple of the base size.
rendering/2d/snap/snap_2d_transforms_to_pixel
Rounds CanvasItem positions to whole pixels: crisper, but movement is less smooth. Read only when the project starts.

Example: a 320 × 180 game in three window sizes

The base size is display/window/size/viewport_width × viewport_height. The stretch factor is the window size divided by it:

Window        factor              scale_mode = fractional     scale_mode = integer
1920 × 1080   1920/320 = 6.00     6 × 6 px per art pixel      6 × 6, fills the window
2560 × 1440   2560/320 = 8.00     8 × 8                       8 × 8
1366 × 768    768/180  = 4.27     art pixels 4 or 5 px wide   floor → 4: 1280 × 720 + bars

why 4.27 fails: 32 art pixels × 4.27 = 136.5 screen pixels — no equal split exists

With aspect = keep the remaining space becomes black bars; expand shows more of the level instead (the Godot docs suggest either for pixel art). The docs also warn that with integer scaling a window smaller than the base size clips the game, so set Window.min_size to the base size.

Which cause the Nerulio bundle removes, and which stay with you

CauseWhere it livesFixed by the Godot 4 bundle?
Linear filter on the nodeCanvasItem › Texture › Filter, or the project defaultOnly on the bundle’s own AnimatedSprite2D (texture_filter = 1, Nearest); your other nodes still inherit the project default
Mipmaps, lossy or VRAM compression, Size LimitThe PNG’s import settingsYes: the shipped .png.import has compress/mode=0 (Lossless), mipmaps/generate=false, process/size_limit=0
Faint pixels recoloured along alpha edgesImport option Process › Fix Alpha Border (on by default)Yes: process/fix_alpha_border=false
Fractional stretch factorProject Settings › Display › Window › StretchNo — project setting
Sub-pixel sprite or camera positionsrendering/2d/snap/*, your scripts, Camera2D smoothingNo — project setting and code
Camera2D zoom that is not a whole number (1.5)The Camera2D nodeNo — use 1, 2, 3 …
Art that is already soft inside the PNGThe source imageNo: the bundle packs what you drew; rebuild the 1× art with the pixel art downscaler

What it does

  • The .tscn sets texture_filter = Nearest on the AnimatedSprite2D, so the sprite is sharp even when the project default is linear.
  • The .png.import uses lossless compression and no mipmaps.
  • fix_alpha_border is off: Godot's default recolours pixels under alpha 20 (measured on a real 4096² FX sheet); with the bundle's settings that sheet passed.
  • In the verification run Godot drew the shipped scene at 4× and it stayed sharp.

How to do it

  1. Drop your sheet, frames or .aseprite file and apply the import.
  2. Open Pack & Export and choose Godot 4.
  3. Copy the bundle folder (with the .png.import) into your project.
  4. For the whole game: set Rendering › Textures › Canvas Textures › Default Texture Filter to Nearest and scale by whole numbers.

Fix it in Godot 4, in this order

  1. Project Settings › Rendering › Textures › Canvas Textures › Default Texture Filter = Nearest (or set Texture › Filter = Nearest on the pixel-art nodes only, if the rest of the game is high-resolution).
  2. Project Settings › Display › Window › Size: set the viewport width and height to your art resolution, for example 320 × 180.
  3. Project Settings › Display › Window › Stretch: Mode = viewport, Aspect = keep (or expand), Scale Mode = integer — the pixel-art combination the Godot docs recommend. Use canvas_items instead only if you want sprites to move and rotate at sub-pixel positions.
  4. If sprites shimmer while moving, turn on rendering/2d/snap/snap_2d_transforms_to_pixel and restart the project (it is read at startup). The docs advise not to combine it with snap_2d_vertices_to_pixel.
  5. Select each pixel-art PNG and check the Import dock: Compress › Mode Lossless, Mipmaps › Generate off, Fix Alpha Border off for faint pixels; click Reimport after a change. A Nerulio bundle (Aseprite to Godot, sprite sheet to Godot) already ships these values.
  6. Keep Camera2D.zoom and node scales at whole numbers.

Check the result

  • Zoom the 2D editor to 800 % on a still sprite: square pixel edges mean Nearest is in effect; soft ramps mean Linear.
  • Run the game in a 1920 × 1080 window (for a 320 × 180 base), take a screenshot and zoom in: every art pixel should be exactly 6 × 6 screen pixels.
  • Resize the window to 1366 × 768: with scale_mode = integer you get bars and the same crisp pixels at 4 × 4, not a mix of 4 and 5.
  • Move the camera slowly: with transform snapping on, the sprite steps by whole pixels instead of shimmering.

Troubleshooting

SymptomLikely causeHow to checkFix
Soft edges even when nothing movesLinear filtering: the node inherits the project defaultSelect the node: Texture › Filter says Inherit, and Default Texture Filter is LinearDefault Texture Filter = Nearest once, or Filter = Nearest on the node
Blurry only when zoomed out, or after the sprite was used on a 3D materialMipmaps or VRAM compression — Detect 3D changed the importImport dock of the PNG: Compress › Mode, Mipmaps › Generate; the Output panel prints a message when Detect 3D firesCompress › Mode = Lossless, Mipmaps off, Reimport
Some pixels wider than others, lumpy diagonalsFractional stretch factor (4.27×) or a Camera2D zoom such as 1.5Window size ÷ base size; Camera2D › ZoomStretch Mode viewport + Scale Mode integer; whole-number zoom
Sharp at rest, shimmers or jitters while movingSprite or camera between pixels, often with Camera2D position smoothingprint(global_position) shows values like 10.4 while movingsnap_2d_transforms_to_pixel on (restart), or round positions in code
Faint glow or smoke pixels change colourFix Alpha Border (on by default) fills transparent pixels with neighbouring coloursImport dock: Process › Fix Alpha BorderTurn it off and Reimport — the bundle’s .png.import does
Crisp at 1920 × 1080, uneven on a laptop screen or after resizingThe stretch factor depends on the window sizeRun at 1366 × 768 or maximise the windowScale Mode integer, and Window.min_size = base size
The PNG itself has soft edges in an image viewer at 800 %The art was resized with smoothing before it reached GodotOpen the source file outside GodotNo Godot setting helps: rebuild the 1× art first

Other ways to do it

OptionWhen it is the better choice
Set the project up for pixel art once (the steps above) and keep importing plain PNGsYou draw straight into PNGs and use Sprite2D: the default Lossless import is already right, so only filter and stretch need changing.
Nearest on selected nodes onlyA high-resolution game with a few pixel-art elements: keep the project default Linear and set Texture › Filter = Nearest on those nodes.
A Nerulio Godot 4 bundleYou start from a sheet or an .aseprite file and want the animation, the Nearest scene and the import file in one step.

Engine exports and how each was checked

"Verified" means the exported files were loaded by the real engine or tool on real CC0 assets and the result was compared with the source. Anything that was not loaded is labelled as such.

TargetFilesCheck
Godot 4.tres SpriteFrames · .tscn AnimatedSprite2D · .png.importVerified Godot 4.7.2Loaded by Godot; every animation frame drawn and compared with the source file. The scene uses Nearest filtering; rotation is never used.
Unity 6.unity.json · Editor/NerulioSpriteImporter.cs · AnimationClipVerified Unity 6000.5.3f1Batch mode: sprite rects, pivots, pixels, clip keys and durations checked.
Phaser 3 / 4atlas JSON (multiatlas) · .anims.jsonVerified Phaser 3.90 + 4.2Frames and animations drawn by both versions.
PixiJS 8spritesheet JSON · animations · anchor · meta.scaleVerified PixiJS 8.21Rotated and multi-page atlases included.
Aseprite JSONhash / array · frameTags · slicesVerified Phaser load.aseprite · PixiJS 8.21Tags keep their direction and repeat count.
.asepritelayers · tags · durations · slicesVerified Aseprite 1.3.18Opened and rendered by the Aseprite command line.
Spine / libGDX.atlasVerified spine-canvas 4.2Every region drawn by the Spine runtime, rotated and trimmed; libGDX itself was not run.
Starling / Sparrow XMLXML per pageVerified Phaser 4.2Phaser 3.90 draws trimmed XML frames wrongly (its own bug); the "Sparrow XML (Phaser 3)" preset turns trim off and passes in 3.90 and 4.2.
LÖVELua quads · nerulio_atlas.lua · main.luaVerified LÖVE 11.5Every frame drawn and every animation stepped through the shipped helper.
Defold.atlas · .tilesourceBuilt Defold bob.jar 1.13.1Built by Defold's command-line builder; the built texture's UVs were read back.
GameMakerspr_<name>_stripN.png · gamemaker.jsonUNVERIFIEDNot loaded in GameMaker (no headless GameMaker). The strips are cut the way its strip importer cuts them.
CSS sprites.css · preview .htmlVerified ChromiumEvery class drawn from the shipped stylesheet.
GIF / APNGone file per animationDecoded PillowEvery frame and delay decoded again after export; APNG is exact.

Evidence

Exports: 49 runs in the real engines on real CC0 assets, 47 pass — the 2 failures are Phaser 3.90's trimmed-XML bug, which the Phaser 3 preset avoids. Packer head-to-head on 5 real frame sets (CodeAndWeb free web packer, GAPTools, free-tex-packer): smallest sheet on all 5 with rotation allowed, 4 of 5 without; every frame restored pixel-exact.

Limits

  • The bundle never edits project.godot: default filter, stretch, snapping and camera stay as you set them.
  • A sprite that was smoothed before export stays smoothed; no import setting sharpens it again.
  • The stretch and snap settings above come from the Godot docs; our Godot runs checked the bundle, not those settings.
  • Polygon (mesh) packing is not offered: none of the verified engines draws a mesh atlas from exported data.
  • GameMaker strips are UNVERIFIED in GameMaker itself.
  • Scale variants (@0.5x–@4x) use nearest-neighbour only.
  • Trimmed Starling XML is drawn wrongly by Phaser 3.90; use the "Sparrow XML (Phaser 3)" preset there.

Versions and sources

Godot 4.7.2 loaded the Nerulio bundle in our verification runs, drew the shipped scene at 4× and it stayed sharp; a 4096² effects sheet whose faint pixels Godot’s default Fix Alpha Border recoloured passed with the bundle’s import file. Setting names, defaults and the pixel-art recommendation follow the Godot 4.7 documentation (which also notes that projects created in 4.7 start with stretch mode canvas_items).

Official documentation used

Questions

My sprite still shimmers when it moves. Why?

That is sub-pixel movement, not import: enable 2D pixel snapping and keep camera and window scaling at whole numbers. Those are project settings the bundle does not change.

Is anything in my project overwritten?

Only the files in the bundle folder you copy in. Project settings stay as they are.

Is it free, and do I need an account?

Everything on these pages is free and works without an account. There is no watermark and no export that is held back.

Are my sprites uploaded?

No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.

Does it need Aseprite or an engine plugin installed?

No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).