Godot 2D Normal Map — Export a Lit Sprite Scene for Godot 4
In Godot 4 a 2D normal map does nothing on its own: it has to sit in a CanvasTexture, and a Light2D has to reach the sprite from some height. The Texture workspace generates the map, lights it with the formula Godot's canvas renderer uses, and exports the finished scene — sprite, lights, ambient colour and frames — which Godot 4.7.2 rendered within 1/255 of the preview.
In Godot 4 a 2D normal map works only inside a CanvasTexture: set it as the Normal Map Texture next to the sprite's Diffuse Texture, then light the sprite with a PointLight2D or DirectionalLight2D whose Height is above 0 and whose Item Cull Mask matches the sprite's Light Mask. Nerulio's Texture workspace generates the map and exports that scene ready-made (Sprite2D, CanvasTexture, CanvasModulate, one PointLight2D per light and an AnimationPlayer for frames), rendered by Godot 4.7.2 within 1/255 of the preview.

- Godot 4 2D lightsVerified· Godot 4.7.2
- Unity 6 URP 2DVerified· Unity 6000.5.3f1 (URP 17.5)
- PNG set + JSONPlain files
How Godot 4 lights a normal-mapped sprite
Godot draws 2D lights per pixel on top of the sprite's colour. A plain texture has no normals, so a light only brightens it; a CanvasTexture bundles a diffuse texture, a normal map and an optional specular map, and the normal map "only has a visible effect if Light2Ds are affecting this CanvasTexture". Godot expects X+, Y+ and Z+ normals, the OpenGL style.
Per pixel the light adds colour × energy × falloff × N·L, where L points from the pixel to the light raised by its Height. For a PointLight2D Height is in pixels: the docs' example is a height of 100 lighting an object 100 px away at 45°. Its default is 0, which puts the light in the sprite's plane. A DirectionalLight2D has a Height from 0 (parallel to the plane) to 1 (perpendicular), and its direction is the node's rotation.
The light's size and falloff come from its texture: a PointLight2D draws a (usually grey) texture scaled by Texture Scale. A CanvasModulate multiplies the whole canvas by an ambient colour, which is what makes unlit parts dark. Which nodes a light reaches is decided by its Range settings: Item Cull Mask against each CanvasItem's Light Mask (default layer 1), plus layer and z ranges.
Nerulio's preview is this model computed on the CPU and in WebGL2 (lighting.js): N is decoded from red and green like Godot does, with blue rebuilt from them, and each exported light carries the Studio's falloff curve as a 256 px texture, so the brightness in Godot matches the preview, not only the direction.
- CanvasTexture
- A 2D texture resource holding diffuse, normal and specular maps plus specular colour and shininess.
- Height
- How far above the canvas a light sits; only visible on normal-mapped surfaces.
- Item Cull Mask / Light Mask
- Layer bits on the light and on the sprite; they must share one for the light to reach it.
- CanvasModulate
- One node per canvas that tints everything by an ambient colour.
Example: what Height and Texture Scale do to one pixel
A flat pixel (N = 0, 0, 1) and a PointLight2D 64 px to its left:
Height 64 px → elevation atan(64/64) = 45° flat N·L = sin 45° = 0.707
Height 0 (default) → elevation 0° flat N·L = 0: only slopes facing the light are lit
Height 256 px → elevation atan(256/64) = 76° flat N·L = 0.970: the relief almost disappears
Nerulio falloff PNG = 256 px wide, so texture_scale = 2 × radius / 256
radius 128 px → texture_scale 1.0 radius 256 px → texture_scale 2.0
DirectionalLight2D: Height 0 = parallel to the plane, 1 = straight down onto itA low light exaggerates relief and a high one flattens it, so change Height before you change the normal map's strength.
What it does
- <name>_lit.tscn: a Sprite2D whose texture is a CanvasTexture (diffuse + normal), a CanvasModulate for the ambient colour, one PointLight2D per light and, for a cut sheet, an AnimationPlayer that steps region_rect through the frames.
- Each PointLight2D carries the light's own falloff (smooth, linear, quadratic or none) as its texture, plus its colour, energy and height — the z you set in pixels.
- <name>_canvas_texture.tres for your own Sprite2D; when specular is above 0, a specular map and specular_shininess are added to it.
- The Lit view uses Godot's canvas light model — base × ambient + falloff × light × energy × (base × N·L + specular) — and the WebGL2 view equals the CPU reference within 1/255.
- Up to 8 point lights. The rim light stays in the preview, because Godot draws none without a custom shader.
How to do it
- Drop the sprite or sheet; cut a sheet in the Sprite workspace first so its frames animate.
- Check the bevel and detail in the Normal map panel and raise or lower relief with the Height brush (B).
- Press L, place the lights, set height, energy and falloff, then press Enter in the Lit view to play it.
- Export (Ctrl+E) with Godot 4 ticked.
- Copy the godot folder anywhere under res:// and open <name>_lit.tscn.
Files you get
For a sprite called torch.png the godot/ folder holds:
| File | What it is for |
|---|---|
torch_lit.tscn | Node2D with the Ambient CanvasModulate, the Sprite2D (region on, Nearest for pixel art), one PointLight2D per light and, for frames, an AnimationPlayer. |
torch_canvas_texture.tres | The CanvasTexture alone (diffuse, normal, optional specular and shininess) for your own Sprite2D. |
torch.png, torch_n.png | The sprite or sheet, byte for byte, and its OpenGL normal map. |
torch_light_smooth.png | The falloff texture of each falloff type used (smooth, linear, quadratic or constant). |
torch_s.png | A specular map, only when specular is above 0. |
README.md | These steps and the verification status. |
Set it up in Godot 4
- Copy the
godotfolder anywhere underres://(the files refer to each other by relative paths) and opentorch_lit.tscnto see the lit result; the steps below are for your own nodes. - Select your Sprite2D; in its Texture property create a New CanvasTexture, then set Diffuse Texture to the sprite PNG and Normal Map Texture to
torch_n.png, or loadtorch_canvas_texture.tres. - Add a PointLight2D: give it a Texture (the bundle's falloff PNG, or any soft round gradient), a Height above 0 in pixels and an Energy; move it over the sprite.
- Check the masks: the light's Range › Item Cull Mask must share a layer with the sprite's Light Mask (layer 1 by default).
- Add one CanvasModulate with a dark colour so unlit areas get dark; without it the sprite starts at full brightness and lights only add to it.
- For pixel art set the Sprite2D's Texture › Filter to Nearest (the bundle does); if you use a DirectX-style map from elsewhere, enable Process › Normal Map Invert Y in its Import dock and reimport.
Check the result
- Move the light above the sprite: top edges must brighten. If bottom edges do, the map is DirectX-style.
- Drag the light's Height from 0 upwards: the relief must get softer as the light rises. No change at all means the normal map is not being read.
- Compare with the Studio's Lit view: in our runs Godot 4.7.2 matched it within 1/255 on every checked frame.
What the Godot bundle contains
| File or node | What it holds | What to do with it in your game |
|---|---|---|
| Sprite2D in <name>_lit.tscn | texture = the CanvasTexture, region enabled, Nearest filter for pixel art | Instance the scene, or copy its texture onto your own sprite |
| <name>_canvas_texture.tres | diffuse_texture, normal_texture, optional specular_texture and specular_shininess | Reuse it on any Sprite2D that draws the same sheet |
| PointLight2D (one per light) | Position in frame pixels, color, energy, height = z, falloff PNG as texture, texture_scale from the radius | Keep one as a torch, delete the rest, or copy the numbers to your level lights |
| CanvasModulate "Ambient" | The ambient colour | Keep a single one in your level; it tints the whole canvas |
| AnimationPlayer | One animation per tag, region_rect keys at each frame's start, looping as tagged | Call play() with the tag name from your script |
| PNG files | <name>.png, <name>_n.png (OpenGL), specular map, one falloff PNG per falloff used | Keep them next to the scene; the paths are relative |
Not exported: the rim light (preview only). The README in the folder repeats the verification status.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| The normal map changes nothing | The sprite uses a plain texture, not a CanvasTexture with a Normal Map Texture | Inspector: Sprite2D › Texture shows CanvasTexture? | Wrap the texture in a CanvasTexture or load torch_canvas_texture.tres |
| Only the rims facing the light glow; the middle stays dark | PointLight2D Height is 0, its default, so the light lies in the sprite's plane | Inspector: the light's Height | Raise Height; about the light's distance to the sprite gives 45° |
| The sprite is not lit at all | The light's Item Cull Mask and the sprite's Light Mask share no layer, or the sprite lies outside the light's texture | Toggle the light; compare both masks; look at Texture Scale | Put both on one layer; raise Texture Scale |
| Lit from below when the light is above | A DirectX-style (Y−) normal map | Top edges dark under a light straight above | Use torch_n.png (OpenGL), or enable Process › Normal Map Invert Y and reimport; see OpenGL or DirectX |
| Everything looks washed out | No CanvasModulate, so the base colour is already at full brightness | The scene tree has no CanvasModulate | Add one with a dark colour, like the bundle's Ambient node |
| Pixel-art relief looks smeared | Linear filtering on the node or the CanvasTexture | Zoom in: soft pixel edges | Filter Nearest; see blurry pixel art in Godot |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Build the CanvasTexture and lights yourself | You already have normal maps (a 3D bake, Laigter, hand-painted): follow the steps above with your own _n file. |
| A canvas_item shader of your own | You need a light Godot's Light2D does not draw, such as a rim light; that is why the Studio's rim light stays in the preview. |
| An AnimatedSprite2D from the SpriteFrames export | You prefer SpriteFrames for animation; attaching normal maps to its frames was not part of our check. |
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 2D lights | _lit.tscn · CanvasTexture .tres · normal / specular PNG | Verified Godot 4.7.2Six real CC0 cases rendered by Godot (gl_compatibility): every checked frame within 1/255 of the Studio's lit preview. The same render with green flipped is 3.8–37 levels off, so the check can see a wrong convention. |
| Unity 6 URP 2D | PNG · nerulio-texture.json · Editor/NerulioNormalMapImporter.cs | Verified Unity 6000.5.3f1 (URP 17.5)12 of 12 runs (6 cases in Gamma and Linear colour space): the lighting URP applied matches the exported normal map (mean error 0.0037–0.0085). URP draws its own light falloff, so brightness is Unity's, not the preview's. |
| PNG set + JSON | albedo · _n (OpenGL) · _n_dx (DirectX) · 16-bit height · AO · specular · manifest | Plain filesPlain files for any engine or tool; no engine run is claimed for them. |
Evidence
Godot 4.7.2 rendered 6 real CC0 cases (pixel art, an HD sprite, a tileable texture, an imported DirectX map): every checked frame within 1/255 of the Studio's preview. Unity 6000.5 URP 2D: 12 of 12 runs pass (6 cases × Gamma and Linear). OpenGL/DirectX detection on real maps of known convention: 128 full 1K maps 100 % right, and not one wrong "high" verdict in 1,562 samples (a naive "green-high means OpenGL" rule gets 46 %).
Limits
- Checked with the Compatibility renderer (gl_compatibility) of Godot 4.7.2; Forward+ and Mobile were not run.
- Shadows (LightOccluder2D) are not exported.
- The exported scene animates a Sprite2D through an AnimationPlayer, not an AnimatedSprite2D.
- On a picture without transparent edges (a tileable texture) a flipped red channel cannot be told apart from a DirectX green; the Check panel says so and offers Flip red.
- A sheet is lit as one picture until its frames are cut in the Sprite workspace; the Texture workspace does not cut frames.
- The rim light is preview only, and Unity gets no specular map (Godot does).
- Unity draws its own light falloff: the exported normal map is verified in Unity, the brightness curve is Unity's.
Versions and sources
Godot 4.7.2 (gl_compatibility), six real CC0 cases: every checked frame within 1/255 of the Studio's reference, mean 0.14–0.22 levels, also through the exported AnimationPlayer; the same render with green flipped was 3.8–37 levels off, so the check sees a wrong convention. Property names and defaults above follow the Godot 4.7 class reference.
Official documentation used
Questions
My normal map has no visible effect in Godot 2D. What is missing?
Usually one of three things: the map is not set as the CanvasTexture's normal_texture, no Light2D reaches the sprite, or the light's height is left at 0 so it only grazes the relief from the side. The exported scene has all three set, so compare your node with it.
Does the bundle use AnimatedSprite2D?
No. It animates a Sprite2D: an AnimationPlayer moves region_rect over the sheet, and one CanvasTexture holds the normal map for every frame. That is the setup Godot 4.7.2 rendered in the check; an AnimatedSprite2D setup was not part of it.
Which renderer and assets was it checked with?
Godot 4.7.2 on the Compatibility renderer (gl_compatibility), six real CC0 cases: a pixel torch with and without specular, a 60-frame samurai, an 80×110 adventurer, a tileable brick texture and an imported DirectX map. Every checked frame was within 1/255; the same render with green flipped was 3.8–37 levels off.
Will Godot's brightness match the preview?
Yes: each light carries the Studio's falloff as its texture, so the curve is the same, and the check compared the final lit pixels. Unity differs — URP draws its own falloff.
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).