Skip to content

Unity 2D Normal Map — URP Secondary Texture Set Up for You

A 2D normal map in Unity lights up only when four things line up: URP with a 2D Renderer, a lit sprite material, the map attached to the sprite as a Secondary Texture named _NormalMap, and a Light2D with normal mapping on. The Texture workspace makes the map and ships an editor script that does the texture side for you; Unity 6 drew the lighting we predicted in both Gamma and Linear projects.

In Unity 6 a 2D normal map is read only when four things line up: the project renders with URP and its 2D Renderer, the sprite uses the Sprite-Lit-Default material, the map is attached to the sprite texture as a Secondary Texture named _NormalMap, and the Light 2D has Normal Map Quality set to Fast or Accurate (it is Disabled by default). Nerulio exports the PNGs, a JSON and an editor script that does the texture part and can place matching Light 2D lights; it passed 12 of 12 runs in Unity 6000.5.3f1 with URP 17.5.

Drop a sprite, a sprite sheet or a tileable texture (PNG)
Runs in your browser. Files are not uploaded.
Studio Texture workspace: an imported ambientCG normal map read as DirectX with high confidence; nothing changes until the convention is confirmed
The real Studio with a CC0 asset (ambientCG).
Exports to
  • Unity 6 URP 2DVerified· Unity 6000.5.3f1 (URP 17.5)
  • Godot 4 2D lightsVerified· Godot 4.7.2
  • PNG set + JSONPlain files

How URP's 2D lights read a sprite normal map

URP's lit sprite shader samples the normal map through the sprite's Secondary Textures: extra textures stored with the sprite, found by name. The name _NormalMap is what the shader looks for, and the map must share the sprite's UVs, so it has to have exactly the sprite sheet's size and layout. Unity uses Y+ normal maps (the OpenGL style), with RGB = XYZ and (0.5, 0.5, 1) as the flat normal.

A normal map holds vectors, not colours, so sRGB (Color Texture) must be off or the values are gamma-decoded and every slope tilts. Unity's manual imports it with Texture Type Normal Map; Nerulio's importer keeps it a Default texture with sRGB off, uncompressed, no mipmaps and Non-Power of 2 set to None, which is the setup that passed in Unity 6.

On the light, Normal Map Quality switches normal mapping on (Fast or Accurate) and Normal Map Distance sets how far the light sits from the sprite, in Unity units. It works like a height: a small distance means light arriving at a steep angle from the side, a large one light from almost straight above.

The 2D Renderer computes lights into textures at a fraction of the screen resolution, 0.5 by default (Light Render Texture Scale). For smooth art that is invisible; for pixel art it softens one-pixel lighting bands, so the Nerulio README suggests 1.

Secondary Texture
An extra texture attached to a sprite and found by name; _NormalMap for normals, _MaskTex for masks.
Normal Map Quality
Disabled (default), Fast or Accurate on each Light 2D.
Normal Map Distance
The light's distance from the sprite plane in Unity units; Nerulio sets it to the Studio height ÷ Pixels Per Unit.
Pixels Per Unit
How many texture pixels make one Unity unit; 100 in the export by default.

Example: one Studio light converted to Unity units

A 32 × 32 frame, Pixels Per Unit 100, a Studio light at x 8, y 6 (pixels, y down), height 24 px, radius 96 px:

position x = (8  − 32/2) / 100 = −0.08
position y = (32/2 − 6)  / 100 = +0.10     (Unity y points up)
Normal Map Distance = 24 / 100 = 0.24     Outer Radius = 96 / 100 = 0.96

sprite rect, sheet 192 × 32, frame 3:  x = 96, y = 32 − (0 + 32) = 0

Light Render Texture Scale 0.5 at 1920 × 1080 → lights drawn at 960 × 540
                             1.0             → 1920 × 1080, one light texel per screen pixel

Positions are measured from the sprite's centre pivot, which the importer sets for every frame. Rects are written bottom-up because Unity counts texture rows from the bottom.

What it does

  • Tools › Nerulio › Apply Texture JSON gives the sprite its rects and attaches the normal map as Secondary Texture _NormalMap — sRGB off, uncompressed, no mipmaps, no power-of-two resize.
  • Tools › Nerulio › Create Lit Preview adds the sprite and one point Light2D per Studio light, with Normal Map quality Accurate and Normal Map Distance = z ÷ Pixels Per Unit.
  • Pixels Per Unit is a number in nerulio-texture.json (100 by default); light positions, radius and distance are divided by it.
  • The Unity README warns pixel-art users about Light Render Texture Scale, which is 0.5 on a new 2D Renderer and halves the lighting resolution.
  • Checked in Unity 6000.5.3f1, URP 17.5, 2D Renderer: 12 of 12 runs (6 real cases × Gamma and Linear) with a mean error of 0.0037–0.0085.
  • The map is OpenGL-style (Y+), which Unity expects. A DirectX map from another tool can be tested in the Check panel before you use it.

How to do it

  1. Drop the sprite or sheet (cut a sheet in the Sprite workspace first).
  2. Tune the bevel and place the lights; press C to split the view into flat and lit.
  3. Export (Ctrl+E) with Unity 6 URP 2D ticked.
  4. Copy the unity folder into Assets/ and let Unity compile the Editor script.
  5. Run Tools › Nerulio › Apply Texture JSON, then Create Lit Preview.

Files you get

For a sprite called torch.png the unity/ folder holds:

FileWhat it is for
torch.png, torch_n.pngThe sprite or sheet, byte for byte, and its OpenGL (Y+) normal map.
nerulio-texture.jsonSize, Pixels Per Unit, Point filter flag, frame rects (bottom-up) and the lights with position, height, energy, radius and colour.
Editor/NerulioNormalMapImporter.csAdds Tools › Nerulio › Apply Texture JSON and Create Lit Preview.
README.mdThese steps, the Light Render Texture Scale hint and the verification status.

Set it up in Unity 6 (URP 2D)

  1. Use a project that renders with URP and a 2D Renderer, and keep the sprites on the Sprite-Lit-Default material.
  2. Copy the unity folder into Assets/; Unity compiles Editor/NerulioNormalMapImporter.cs.
  3. Run Tools › Nerulio › Apply Texture JSON and pick nerulio-texture.json: the sprite gets its frame rects (Multiple mode for sheets, Point filter for pixel art) and _NormalMap as Secondary Texture; the normal map gets sRGB off, no compression, no mipmaps and no power-of-two resize.
  4. Run Tools › Nerulio › Create Lit Preview: a NerulioLitPreview object with the first frame and one Light 2D per Studio light, Normal Map Quality Accurate and Normal Map Distance = height ÷ Pixels Per Unit.
  5. For your own lights (GameObject › Light, for example Spot Light 2D), set Normal Map Quality to Fast or Accurate and a Normal Map Distance, and make sure Target Sorting Layers include the sprite's layer.
  6. For pixel art, open your Renderer 2D asset and set Light Render Texture Scale to 1.

Check the result

  • Select the sprite texture › Open Sprite Editor › Secondary Textures: _NormalMap points at torch_n.png.
  • Select torch_n.png: sRGB (Color Texture) off and the size in the inspector equals the PNG's size.
  • Move a Light 2D above the sprite: top edges brighten. Set its Normal Map Quality to Disabled and the relief disappears, which proves the map is read.

Unity settings for a 2D normal map

SettingWhat the importer setsWhat goes wrong otherwise
Secondary Texture name_NormalMap on the albedo spriteUnder any other name the lit sprite shader never reads the map
sRGB (Color Texture)Off — the map holds vectors, not coloursThe vectors are gamma-decoded and the relief tilts wrongly
Non-Power of 2NoneA 96×64 map became 128×64 and the normals blurred (mean error 0.045 instead of 0.005)
Compression and mipmapsUncompressed, mipmaps offCompressed or averaged texels no longer hold the exported vectors
Max SizeRaised to fit the sheet (2048–16384)The 2048 default shrinks a larger sheet
Light2D normal mapQuality Accurate, Distance = z ÷ Pixels Per UnitWith normal mapping off the map is ignored; a wrong distance flattens or exaggerates the relief
Light Render Texture ScaleLeft as it is (project setting); the README says use 1At 0.5, per-pixel lighting of small sprites comes out blocky

The rows are what the shipped importer does in Unity 6000.5.3f1 with URP 17.5; other Unity and URP versions were not run.

Troubleshooting

SymptomLikely causeHow to checkFix
Only a flat glow, no reliefThe Light 2D's Normal Map Quality is Disabled, its defaultLight 2D inspectorSet Fast or Accurate (Create Lit Preview sets Accurate)
Relief tilted the wrong way, lit from belowA DirectX-style (Y−) map, or sRGB left on so the vectors are gamma-decodedImport settings of the normal map; the Studio's Check panel verdictUse Nerulio's torch_n.png (Y+) and turn sRGB off; see OpenGL or DirectX
Normals blurred or shifted against the pixelsThe map was resized to a power of two or compressed (a 96 × 64 map became 128 × 64 in our first run)The inspector shows another size than the PNGNon-Power of 2 None, Uncompressed; the current importer sets both
Soft, blocky light bands on pixel artLight Render Texture Scale is 0.5Renderer 2D assetSet it to 1; see also blurry pixel art in Unity
The sprite is not lit at allSprite-Unlit-Default material, or the light's Target Sorting Layers leave out the sprite's layerSprite Renderer material; the light's Target Sorting LayersUse Sprite-Lit-Default and include the layer
Create Lit Preview placed no lightsURP is not installed (the console says so), or an old copy of the importer searched the pre-Unity 6 assembly for Light2DConsole message after the commandInstall URP; re-export so the current importer is used

Other ways to do it

OptionWhen it is the better choice
Unity's Sprite Editor › Secondary Textures by handOne or two sprites: add _NormalMap yourself. Unity's manual sets the map's Texture Type to Normal Map; the importer keeps Default with sRGB off, which is the variant that was verified.
A custom lit sprite shader (Shader Graph)You need a lighting look URP's 2D lights do not give; the texture settings above still apply.
Unity sprite sheet exportYou only need the frames sliced in Unity, without normal maps.

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

  • Brightness follows URP's own light falloff, not the Studio's; placement, height and the N·L term were verified.
  • No specular map is exported for Unity (Godot only).
  • Only Unity 6000.5.3f1 with URP 17.5 was run; other versions may name the light fields differently.
  • 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

Unity 6000.5.3f1, URP 17.5, 2D Renderer, Direct3D 12: 12 of 12 PASS (6 real cases × Gamma and Linear colour space), mean |N·L error| 0.0037–0.0085 and p95 0.013–0.022 against the exported map; with the map green-flipped inside Unity every run failed (mean error 0.07–0.19). The probe ran the shipped importer. Menu names, defaults and the Secondary Texture name follow the Unity manual.

Official documentation used

Questions

Why does my Unity 2D normal map do nothing?

Go through it in order: the project uses URP with a 2D Renderer; the sprite uses the Sprite-Lit-Default material; the map is a Secondary Texture named exactly _NormalMap; the Light2D has normal mapping switched on. The importer takes care of the texture part; the pipeline, the material and your own lights are yours to set.

Lighting on my pixel art looks blocky and soft. Why?

The 2D Renderer computes 2D lighting at half resolution by default (Light Render Texture Scale 0.5). Set it to 1 on your 2D Renderer asset. It is a project setting, so the export leaves it alone; the verification probe set it to 1.

What went wrong in Unity 6 before the fixes?

Three bugs in our own importer, found by the check and fixed: Unity 6 moved Light2D into the Unity.RenderPipelines.Universal.2D.Runtime assembly, so the preview made no lights; Normal Map Distance and Quality are read-only properties in URP 17 and are now set through SerializedObject; and Unity resized a 96×64 map to 128×64, blurring it — mean error 0.045 before, 0.005 with the power-of-two scale set to None.

Is the brightness the same as in the Studio?

No. Positions, colours, heights and energy come from the Studio, but URP draws its own falloff, so the brightness curve is Unity's. The check compared the normal-map term (N·L), not total brightness. Unity also gets no specular map.

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