Normal Map Generator for Sprites — Lit Preview, Godot, Unity
Open a sprite and its normal map is already there — a bevel grown inward from the transparent edge, with detail from brightness or from your own height map — lit by a point light you can drag. The lighting uses Godot 4's own 2D formula, so the preview is what the engine shows.
A sprite normal map is a second PNG, the same size as the sprite, whose RGB says which way each pixel's surface faces, so 2D lights can shade a flat drawing as if it had relief. Drop a sprite with transparency and the Studio's Texture workspace builds one at once (a bevel grown in from the transparent edge, plus detail from brightness), lights it with Godot 4's own 2D formula and exports _n.png (OpenGL, Y+), _n_dx.png (DirectX) or a ready Godot 4 or Unity 6 URP 2D bundle, both checked in the engines.

- Godot 4 2D lightsVerified· Godot 4.7.2
- Unity 6 URP 2DVerified· Unity 6000.5.3f1 (URP 17.5)
- PNG set + JSONPlain files
What a sprite normal map stores, and where the relief comes from
A tangent-space normal map stores one unit vector per pixel as a colour: red is X (towards the right of the image), green is Y (towards the top, in the OpenGL convention Godot and Unity use) and blue is Z (out of the screen). Each component is written as (n + 1) ÷ 2 × 255, so a pixel that faces the viewer straight on is (128, 128, 255), the lavender-blue of every normal map, and a face tilted 45° to the right is (218, 128, 218).
A drawing has no depth, so every generator guesses a height field and takes its slope. Nerulio builds that height from a bevel that rises from the transparent edge (distance to the silhouette, shaped by a round, linear, smooth, cove or ledge profile) and, optionally, detail from brightness, which the Studio labels an approximation because paint is not shape. Your own height map of the same size, 16-bit included, can be added.
The normal is the slope of that height: n ∝ (−strength·∂h/∂x, strength·∂h/∂y, 1) with rows counted downward, normalised to length 1. Heights are in pixels, so strength 1 turns a rise of 1 px per px into a 45° face. The kernel (Pixel, Sobel, Scharr or Sobel 5×5) only decides how much the slope is smoothed.
A 2D light then brightens each pixel by N·L, the cosine between the normal and the direction to the light, and the light's height above the sprite plane decides how steeply it arrives. The same map looks almost flat under a light high above and deeply carved under a low, grazing one.
- Tangent space
- Directions measured on the sprite's own surface: X right, Y up, Z towards the viewer.
- Flat normal
- (128, 128, 255): no tilt, so a light shades the pixel as if there were no map.
- Bevel
- Height that grows inward from the transparent edge: a thin rounded rim, or a pillow over the whole shape.
- Strength
- The multiplier on every slope; it steepens faces without moving where the relief is.
Example: the numbers behind one bevelled edge
The left rim of a sprite, where the height climbs 1 px per px to the right into the body, with the Pixel kernel:
slope ∂h/∂x = 1 px/px n ∝ (−s·1, 0, 1)
strength 0.5 → tilt 26.6° RGB ( 70, 128, 242)
strength 1 → tilt 45.0° RGB ( 37, 128, 218)
strength 2 → tilt 63.4° RGB ( 13, 128, 185)
light 64 px to the LEFT, 64 px high → L = (−0.707, 0, 0.707)
strength 1: N·L = 0.707·0.707 + 0.707·0.707 = 1.00 → fully lit
light 64 px to the RIGHT: N·L = max(0, −0.5 + 0.5) = 0 → ambient onlyThe rim that faces the light glows and the opposite rim goes dark; that contrast is the whole effect. Strength steepens every face at once, so to make a body look rounder rather than sharper, widen the bevel instead: the Studio suggests 1.5 px for frames up to 128 px and about 0.9 × the typical inscribed radius for larger sprites.
What it does
- Bevel from the silhouette: width and height in pixels, profiles round, linear, smooth, cove or ledge, and a suggested width — 1.5 px for pixel art up to 128 px, a rounded pillow for larger sprites.
- Detail from brightness (labelled an approximation, with "Ignore shading wider than" so a dark cloak is not read as a pit), or a height map of the same size, 16-bit PNGs included.
- Height brush (B in Texture): raise, lower, smooth, flatten or erase where the guess is wrong; every stroke is one undo step and stays inside its frame.
- Up to 8 point lights with colour, energy, height, radius and falloff, plus ambient and specular. The WebGL2 preview matches the CPU reference within 1/255, and so did Godot 4.7.2.
- Pixel-art normals snap every pixel to 4, 8, 16 or 32 directions with 1 to 4 tilts, for a shaded look that stays in the style of the art.
How to do it
- Drop a sprite PNG with transparency (a sheet is cut into frames in the Sprite workspace first).
- Keep the suggested bevel or change its width and profile in the Normal map panel.
- Drag the light around in the Lit view, press C to compare flat and lit, and paint relief with the Height brush where it reads wrong.
- Export (Ctrl+E) the Godot 4 scene, the Unity 6 URP 2D bundle, or plain PNGs in OpenGL and DirectX.
Use the map in your engine
- Godot 4: put
_n.pnginto the Normal Map Texture of aCanvasTextureused as the sprite's texture, and give aPointLight2Da Height above 0 (its default is 0). The exported_lit.tscnscene is already wired; details on the Godot 2D normal map page. - Unity 6 URP 2D: attach
_n.pngto the sprite as Secondary Texture_NormalMap, keep the Sprite-Lit-Default material, and set the Light 2D's Normal Map Quality to Fast or Accurate (it is Disabled by default). The shipped importer does the texture part; see the Unity 2D normal map page. - An engine that expects DirectX-style green: take
_n_dx.pngfrom the PNG set. It differs from_n.pngonly in green (255 − g). - An animated sheet: cut the frames in the Sprite workspace first so every frame gets its own relief; see normal maps for sprite sheets.
Check the result
- Drag the light in a circle around the sprite in the Lit view: the bright rim must follow the light. A rim that lights up on the far side means the map is being read in the other convention.
- Press C to split flat and lit. Anything darker in the lit half than in the flat half is a face turned away from every light; check that this is where you expect shadowed sides.
- Switch to the Height view: a dark cloak or outline that appears as a pit is brightness read as shape.
- Open
_n.pngin any viewer: transparent areas are (128, 128, 255) with full alpha on purpose, and the edge normals are copied 2 px outward so filtering at the silhouette never mixes in a wrong vector.
Settings to start from, by kind of picture
The Studio suggests Sprite or Tileable texture from the transparency; these are the usual starting points.
| Your picture | Start with | Then check |
|---|---|---|
| Pixel-art sprite (frames ≤ 128 px) | Bevel 1.5 px, round profile, Clamp edges; Pixel kernel; optionally pixel-art normals | Move a light around in Lit view; the lit edge should follow the light |
| Larger or painted sprite | Bevel ≈ 0.9 × the typical inscribed radius (a pillow); detail from brightness with "Ignore shading wider than" | Dark areas should not read as holes; fix with the Height brush |
| Tileable texture (no transparency) | Wrap edges; detail from brightness or your height map | Wrap test 0 = seamless; look at Tile 3×3 (T) |
| An existing normal map | Use it as the source instead of generating | The Check panel verdict, then "It is OpenGL / It is DirectX"; on sprites, Flip red (X−) if red is reported flipped |
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| Dark paint (a cloak, an outline) looks like a hole | Detail from brightness reads dark as low | Height view: the area sits in a pit | Lower the brightness height, set a smaller "Ignore shading wider than", or raise the area with the Height brush (B) |
| The sprite looks like a flat plate with a thin lit edge | A pixel-art rim (1.5 px) on a large painted sprite | Normal view: colour only in a 1–2 px ring | Widen the bevel towards ≈ 0.9 × the inscribed radius, or choose the round profile |
| Lit from the wrong side in the engine | The engine reads the other green convention, or another tool wrote red inverted | Put a light above the sprite: top edges must brighten | Use _n.png for Godot and Unity; for a map from elsewhere read OpenGL or DirectX |
| No visible effect in the engine | The map is not where the renderer reads it, or the light ignores normal maps | Godot: the CanvasTexture's normal slot; Unity: _NormalMap and the Light 2D quality | Use the exported scene or importer, or follow the engine page |
| Hand-painted pixel art looks worse lit than unlit | Stylised shading cannot be derived from the colours | Compare with the flat half (C) | Paint relief with the Height brush, try quantised pixel-art normals, or hand-paint the map |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Laigter (desktop; free GPL-3 build on GitHub) | You want a desktop app with presets, specular, occlusion and parallax maps and a command line. Its default dome came closest on 3D-rendered sprites (18.3° against the Studio's 20.0°); see Laigter compared. |
| Bake from a 3D model | Sprites rendered from 3D: baked normals are the ground truth that every generator only approximates. |
| Paint the normal map by hand | Stylised pixel art: on a hand-painted CC0 torch every generator, Nerulio included, was 37–42° off the artist's map. |
| Height-to-normal generator (Texture Lab) | You already have a height or grey image and want only its gradient, without bevel or lights. |
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
- The relief is a guess from the silhouette and the colours, not the shape the artist had in mind.
- The rim light in the preview is not exported: no engine draws it without a custom shader.
- In Unity the brightness curve is URP's own falloff; the normal-map term (N·L) is what was verified.
- 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
Nerulio: the Godot 4 bundle was rendered by Godot 4.7.2 (Compatibility renderer) on six real CC0 cases, every checked frame within 1/255 of the preview; the Unity 6 bundle passed 12 of 12 runs in Unity 6000.5.3f1 with URP 17.5 (mean N·L error 0.0037–0.0085). The angles against real reference normals come from the head-to-head of 2026-09-24. The byte encoding and the Y+ convention follow the Unity and Godot documentation below.
Official documentation used
Questions
Does it work for pixel art?
Yes. Frames up to 128 px get a 1.5 px rim by default, the Pixel kernel adds no smoothing, and pixel-art normals can snap the directions. Hand-painted pixel light is hard for every generator: on a CC0 torch, Nerulio, Laigter and PBR Forge all came out 37–42° off the artist's own map.
OpenGL or DirectX — which do I need?
Godot and Unity read OpenGL-style green (Y+), which is the default. The PNG set writes both, _n for OpenGL and _n_dx for DirectX. If you import an existing map, the Check panel reads its convention and changes nothing until you press "It is OpenGL" or "It is DirectX".
What does the engine export contain?
Godot 4: a _lit.tscn with a Sprite2D, a CanvasTexture, CanvasModulate for the ambient and one PointLight2D per light, plus an AnimationPlayer for frames. Unity 6: the PNGs, a JSON and an editor script that adds the map as the _NormalMap secondary texture and can create lit preview lights.
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).