Skip to content

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.

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

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 only

The 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

  1. Drop a sprite PNG with transparency (a sheet is cut into frames in the Sprite workspace first).
  2. Keep the suggested bevel or change its width and profile in the Normal map panel.
  3. 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.
  4. 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

  1. Godot 4: put _n.png into the Normal Map Texture of a CanvasTexture used as the sprite's texture, and give a PointLight2D a Height above 0 (its default is 0). The exported _lit.tscn scene is already wired; details on the Godot 2D normal map page.
  2. Unity 6 URP 2D: attach _n.png to 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.
  3. An engine that expects DirectX-style green: take _n_dx.png from the PNG set. It differs from _n.png only in green (255 − g).
  4. 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.png in 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 pictureStart withThen check
Pixel-art sprite (frames ≤ 128 px)Bevel 1.5 px, round profile, Clamp edges; Pixel kernel; optionally pixel-art normalsMove a light around in Lit view; the lit edge should follow the light
Larger or painted spriteBevel ≈ 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 mapWrap test 0 = seamless; look at Tile 3×3 (T)
An existing normal mapUse it as the source instead of generatingThe Check panel verdict, then "It is OpenGL / It is DirectX"; on sprites, Flip red (X−) if red is reported flipped

Troubleshooting

SymptomLikely causeHow to checkFix
Dark paint (a cloak, an outline) looks like a holeDetail from brightness reads dark as lowHeight view: the area sits in a pitLower 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 edgeA pixel-art rim (1.5 px) on a large painted spriteNormal view: colour only in a 1–2 px ringWiden the bevel towards ≈ 0.9 × the inscribed radius, or choose the round profile
Lit from the wrong side in the engineThe engine reads the other green convention, or another tool wrote red invertedPut a light above the sprite: top edges must brightenUse _n.png for Godot and Unity; for a map from elsewhere read OpenGL or DirectX
No visible effect in the engineThe map is not where the renderer reads it, or the light ignores normal mapsGodot: the CanvasTexture's normal slot; Unity: _NormalMap and the Light 2D qualityUse the exported scene or importer, or follow the engine page
Hand-painted pixel art looks worse lit than unlitStylised shading cannot be derived from the coloursCompare 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

OptionWhen 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 modelSprites rendered from 3D: baked normals are the ground truth that every generator only approximates.
Paint the normal map by handStylised 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.

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

  • 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).