Skip to content

Pixel Art Normal Map Generator — From Sprite Brightness or Alpha

For 2D lights you need a normal map per sprite. The Texture Lab reads height from the sprite's own grey values (or its alpha) and turns the gradient into a tangent-space normal map, with the convention named on screen.

Drop texture maps (PNG keeps exact channel bytes)
Runs in your browser. Files are not uploaded.
Texture Lab: a normal map generated from a CC0 texture, with the OpenGL/DirectX convention shown
The real Texture Lab with a CC0 asset (ambientCG).
Outputs
  • Channel PNGsMeasured· numpy
  • Normal mapMeasured· numpy
  • Packed mask / ORMMeasured· Pillow
  • Engine importUNVERIFIED

What it does

  • Height from luminance or from the alpha channel.
  • Kernels Sobel 3×3, Scharr and Sobel 5×5, normalised so the same strength means the same slope.
  • OpenGL (+Y: Unity, Godot) or DirectX (−Y), with X/Y invert; flat areas come out exactly (128, 128, 255).
  • Converted and generated maps decode to unit-length vectors (checked).

How to do it

  1. Drop the sprite (or a hand-painted height map).
  2. Choose luminance or alpha as height, the kernel and the strength.
  3. Pick the convention your engine expects.
  4. Download the normal map.

Outputs and how each was checked

"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.

TargetFilesCheck
Channel PNGs<name>-r/g/b/a.png (8-bit grey)Measured numpyByte-identical per channel, including RGB under fully transparent texels.
Normal mapPNG · OpenGL (+Y) or DirectX (−Y)Measured numpyDecoded vectors are unit length (worst deviation 0.0002); OpenGL and DirectX differ in green only.
Packed mask / ORMUnity HDRP mask · URP metallic · ORM (glTF, Godot)Measured PillowPacked bytes checked: ORM preset over AO 10 / roughness 80 / metallic 220 → (10, 80, 220, 255).
Engine importUnity · Unreal · GodotUNVERIFIEDNot imported in any engine.

Evidence

Channel unpack byte-identical on all four channels (max difference 0), 2048 transparent texels keep their RGB; edge bleed changed 448 texels, all with alpha 0, and no alpha byte; OpenGL ↔ DirectX differ exactly in green (255 − g); converted normals decode to unit vectors.

Limits

  • Exact channel bytes need PNG; JPEG/WebP are decoded by the browser and marked "not byte-exact".
  • Interlaced PNG and 16-bit output are not supported; up to 33.5 million pixels on the exact path.
  • A normal map's convention cannot be detected from its pixels, only converted.
  • Nothing was imported into Unity, Unreal or Godot: channel orders rest on their documentation and engine source.

Questions

Is this like Laigter or SpriteIlluminator?

No. It computes the gradient of height or brightness; it does not inflate or bevel the silhouette and has no painting brushes. Paint a height map yourself for full control.

Was it tested in Godot or Unity lighting?

No. The conventions come from the engines' documentation; the maps were checked numerically, not rendered in an engine.

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