Skip to content

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.

Drop a sprite, a sprite sheet or a tileable texture (PNG)
Runs in your browser. Files are not uploaded.
Studio Texture workspace: a pixel-art torch cut into 6 frames, lit by a point light with its generated normal map
The real Studio with a CC0 asset (OpenGameArt).
Exports to
  • 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 it

A 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

  1. Drop the sprite or sheet; cut a sheet in the Sprite workspace first so its frames animate.
  2. Check the bevel and detail in the Normal map panel and raise or lower relief with the Height brush (B).
  3. Press L, place the lights, set height, energy and falloff, then press Enter in the Lit view to play it.
  4. Export (Ctrl+E) with Godot 4 ticked.
  5. 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:

FileWhat it is for
torch_lit.tscnNode2D with the Ambient CanvasModulate, the Sprite2D (region on, Nearest for pixel art), one PointLight2D per light and, for frames, an AnimationPlayer.
torch_canvas_texture.tresThe CanvasTexture alone (diffuse, normal, optional specular and shininess) for your own Sprite2D.
torch.png, torch_n.pngThe sprite or sheet, byte for byte, and its OpenGL normal map.
torch_light_smooth.pngThe falloff texture of each falloff type used (smooth, linear, quadratic or constant).
torch_s.pngA specular map, only when specular is above 0.
README.mdThese steps and the verification status.

Set it up in Godot 4

  1. Copy the godot folder anywhere under res:// (the files refer to each other by relative paths) and open torch_lit.tscn to see the lit result; the steps below are for your own nodes.
  2. 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 load torch_canvas_texture.tres.
  3. 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.
  4. Check the masks: the light's Range › Item Cull Mask must share a layer with the sprite's Light Mask (layer 1 by default).
  5. 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.
  6. 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 nodeWhat it holdsWhat to do with it in your game
Sprite2D in <name>_lit.tscntexture = the CanvasTexture, region enabled, Nearest filter for pixel artInstance the scene, or copy its texture onto your own sprite
<name>_canvas_texture.tresdiffuse_texture, normal_texture, optional specular_texture and specular_shininessReuse 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 radiusKeep one as a torch, delete the rest, or copy the numbers to your level lights
CanvasModulate "Ambient"The ambient colourKeep a single one in your level; it tints the whole canvas
AnimationPlayerOne animation per tag, region_rect keys at each frame's start, looping as taggedCall play() with the tag name from your script
PNG files<name>.png, <name>_n.png (OpenGL), specular map, one falloff PNG per falloff usedKeep 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

SymptomLikely causeHow to checkFix
The normal map changes nothingThe sprite uses a plain texture, not a CanvasTexture with a Normal Map TextureInspector: 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 darkPointLight2D Height is 0, its default, so the light lies in the sprite's planeInspector: the light's HeightRaise Height; about the light's distance to the sprite gives 45°
The sprite is not lit at allThe light's Item Cull Mask and the sprite's Light Mask share no layer, or the sprite lies outside the light's textureToggle the light; compare both masks; look at Texture ScalePut both on one layer; raise Texture Scale
Lit from below when the light is aboveA DirectX-style (Y−) normal mapTop edges dark under a light straight aboveUse torch_n.png (OpenGL), or enable Process › Normal Map Invert Y and reimport; see OpenGL or DirectX
Everything looks washed outNo CanvasModulate, so the base colour is already at full brightnessThe scene tree has no CanvasModulateAdd one with a dark colour, like the bundle's Ambient node
Pixel-art relief looks smearedLinear filtering on the node or the CanvasTextureZoom in: soft pixel edgesFilter Nearest; see blurry pixel art in Godot

Other ways to do it

OptionWhen it is the better choice
Build the CanvasTexture and lights yourselfYou 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 ownYou 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 exportYou 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.

TargetFilesCheck
Godot 4 2D lights_lit.tscn · CanvasTexture .tres · normal / specular PNGVerified 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 2DPNG · nerulio-texture.json · Editor/NerulioNormalMapImporter.csVerified 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 + JSONalbedo · _n (OpenGL) · _n_dx (DirectX) · 16-bit height · AO · specular · manifestPlain 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).