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.

- 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/modedisabled,canvas_items(2D drawn at the window resolution) orviewport(the game is rendered at the base size, then that image is scaled to the window).display/window/stretch/scale_modefractionalkeeps the exact factor;integerfloors 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 existsWith 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
| Cause | Where it lives | Fixed by the Godot 4 bundle? |
|---|---|---|
| Linear filter on the node | CanvasItem › Texture › Filter, or the project default | Only on the bundle’s own AnimatedSprite2D (texture_filter = 1, Nearest); your other nodes still inherit the project default |
| Mipmaps, lossy or VRAM compression, Size Limit | The PNG’s import settings | Yes: the shipped .png.import has compress/mode=0 (Lossless), mipmaps/generate=false, process/size_limit=0 |
| Faint pixels recoloured along alpha edges | Import option Process › Fix Alpha Border (on by default) | Yes: process/fix_alpha_border=false |
| Fractional stretch factor | Project Settings › Display › Window › Stretch | No — project setting |
| Sub-pixel sprite or camera positions | rendering/2d/snap/*, your scripts, Camera2D smoothing | No — project setting and code |
| Camera2D zoom that is not a whole number (1.5) | The Camera2D node | No — use 1, 2, 3 … |
| Art that is already soft inside the PNG | The source image | No: 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
- Drop your sheet, frames or .aseprite file and apply the import.
- Open Pack & Export and choose Godot 4.
- Copy the bundle folder (with the .png.import) into your project.
- 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
- 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).
- Project Settings › Display › Window › Size: set the viewport width and height to your art resolution, for example 320 × 180.
- Project Settings › Display › Window › Stretch: Mode =
viewport, Aspect =keep(orexpand), Scale Mode =integer— the pixel-art combination the Godot docs recommend. Usecanvas_itemsinstead only if you want sprites to move and rotate at sub-pixel positions. - If sprites shimmer while moving, turn on
rendering/2d/snap/snap_2d_transforms_to_pixeland restart the project (it is read at startup). The docs advise not to combine it withsnap_2d_vertices_to_pixel. - 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.
- Keep
Camera2D.zoomand 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 = integeryou 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
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| Soft edges even when nothing moves | Linear filtering: the node inherits the project default | Select the node: Texture › Filter says Inherit, and Default Texture Filter is Linear | Default Texture Filter = Nearest once, or Filter = Nearest on the node |
| Blurry only when zoomed out, or after the sprite was used on a 3D material | Mipmaps or VRAM compression — Detect 3D changed the import | Import dock of the PNG: Compress › Mode, Mipmaps › Generate; the Output panel prints a message when Detect 3D fires | Compress › Mode = Lossless, Mipmaps off, Reimport |
| Some pixels wider than others, lumpy diagonals | Fractional stretch factor (4.27×) or a Camera2D zoom such as 1.5 | Window size ÷ base size; Camera2D › Zoom | Stretch Mode viewport + Scale Mode integer; whole-number zoom |
| Sharp at rest, shimmers or jitters while moving | Sprite or camera between pixels, often with Camera2D position smoothing | print(global_position) shows values like 10.4 while moving | snap_2d_transforms_to_pixel on (restart), or round positions in code |
| Faint glow or smoke pixels change colour | Fix Alpha Border (on by default) fills transparent pixels with neighbouring colours | Import dock: Process › Fix Alpha Border | Turn it off and Reimport — the bundle’s .png.import does |
| Crisp at 1920 × 1080, uneven on a laptop screen or after resizing | The stretch factor depends on the window size | Run at 1366 × 768 or maximise the window | Scale 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 Godot | Open the source file outside Godot | No Godot setting helps: rebuild the 1× art first |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Set the project up for pixel art once (the steps above) and keep importing plain PNGs | You 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 only | A 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 bundle | You 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.
| Target | Files | Check |
|---|---|---|
| Godot 4 | .tres SpriteFrames · .tscn AnimatedSprite2D · .png.import | Verified 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 · AnimationClip | Verified Unity 6000.5.3f1Batch mode: sprite rects, pivots, pixels, clip keys and durations checked. |
| Phaser 3 / 4 | atlas JSON (multiatlas) · .anims.json | Verified Phaser 3.90 + 4.2Frames and animations drawn by both versions. |
| PixiJS 8 | spritesheet JSON · animations · anchor · meta.scale | Verified PixiJS 8.21Rotated and multi-page atlases included. |
| Aseprite JSON | hash / array · frameTags · slices | Verified Phaser load.aseprite · PixiJS 8.21Tags keep their direction and repeat count. |
| .aseprite | layers · tags · durations · slices | Verified Aseprite 1.3.18Opened and rendered by the Aseprite command line. |
| Spine / libGDX | .atlas | Verified spine-canvas 4.2Every region drawn by the Spine runtime, rotated and trimmed; libGDX itself was not run. |
| Starling / Sparrow XML | XML per page | Verified 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ÖVE | Lua quads · nerulio_atlas.lua · main.lua | Verified LÖVE 11.5Every frame drawn and every animation stepped through the shipped helper. |
| Defold | .atlas · .tilesource | Built Defold bob.jar 1.13.1Built by Defold's command-line builder; the built texture's UVs were read back. |
| GameMaker | spr_<name>_stripN.png · gamemaker.json | UNVERIFIEDNot loaded in GameMaker (no headless GameMaker). The strips are cut the way its strip importer cuts them. |
| CSS sprites | .css · preview .html | Verified ChromiumEvery class drawn from the shipped stylesheet. |
| GIF / APNG | one file per animation | Decoded 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).