name: gamedev-2d-art-lighting-art
description: |
Painting 2D art for dynamic lighting: workflow for Unity 2D Lights /
Godot CanvasLight, normal map authoring (Sprite Lamp / hand-painted),
separation in layers (diffuse / normal / emissive / mask), self-
shadowing in pixel art, day/night palette swaps vs realtime light
mixing, glow / emissive layers.
USE WHEN: integrating 2D Lights with hand-painted sprites, deciding
baked vs dynamic lighting, painting normal maps for pixel art.
allowed-tools: Read, Grep, Glob
2D Lighting (Art Side)
Two paradigms for 2D lighting:
- Baked into the sprite (classic). Highlights and shadows painted
directly. Fast, predictable, locks the lighting direction.
- Dynamic (engine 2D Lights). Sprite is "neutral" diffuse, engine
computes lighting from scene lights at runtime. Allows day/night
shifts, point lights, dynamic shadows.
Modern indie pixel art usually combines both: baked rim light + dynamic
fill from scene Lights.
Engine systems
| Engine | 2D lighting system |
|---|
| Unity URP 2D | 2D Renderer Data + Light 2D components (Point/Sprite/Freeform/Parametric/Global). Normal map secondary texture. Shadow Caster 2D for occlusion. |
| Godot 4 | PointLight2D / DirectionalLight2D + LightOccluder2D for shadows. Normal map via shader. |
| Defold | Custom shaders. |
| Phaser / web | Pixi.js light filter or custom shader. |
Sprite layer separation for dynamic lighting
A "lit-aware" sprite needs (at minimum):
- Diffuse / albedo: the base color. Drawn flat, no baked shadows.
- Normal map: surface direction encoded in RGB. (See
seamless-textures/SKILL.md for authoring.)
- (Optional) Emissive / glow mask: pixels that should ignore
lighting and stay bright (street lamps, lava, magic). RGB =
emission color, alpha = mask.
- (Optional) Specular / smoothness: where light reflects.
- (Optional) Self-shadow / occlusion: which surfaces of the sprite
shadow other surfaces of the sprite. Rare in pixel art.
Most pixel-art games stop at diffuse + normal + emissive. Adding more
is diminishing returns vs hand-painting.
Painting flat diffuse for dynamic lighting
If you'll use 2D Lights, the diffuse should be flat-lit — single
ambient lighting condition, no strong shadow side. The engine adds
the directional shading.
Typical workflow:
- Paint with uniform mid-tone shading (no strong light direction).
- Add ambient occlusion (where surfaces meet — under hat brim,
under chin, in joints).
- Don't paint a strong rim/highlight; engine will do it.
If you instead want classic fully-shaded diffuse, the engine's 2D
Light affects it ON TOP of the bake — works but the result is
double-shaded (cool effect for dramatic lighting; bad for naturalistic).
Normal map workflow
See seamless-textures/SKILL.md for normal map authoring options
(Sprite Lamp, Sprite DLight, Materialize, hand-paint).
For pixel art: normals on small sprites can look strange because
neighboring pixels have different normals → light shifts pixel-by-pixel
unevenly. Solutions:
- Smooth normals: blur the normal map slightly so light transitions
smoothly across small sprites. Loses some surface detail but feels
more cohesive.
- Hand-paint flat regions: large flat surfaces of a character (chest,
back) should have uniform normal (#8080FF), not noisy.
- Strong normals only at silhouette: where the form turns away
from camera, use stronger Z-component variation; flat interior keeps
uniform.
Self-shadowing in pixel art
Real-time self-shadow on a pixel sprite is usually not worth it.
Dynamic shadows on a 32×32 character look noisy, low-resolution, jittery
as the light moves.
Common approaches:
Bake self-shadow into diffuse
The cheap way. Paint the sprite with shadow side darker (lit assumed
from upper-left). Limits dynamic lighting (always upper-left feel) but
looks sharp.
Use separate top-light and rim-light layers
Author two diffuse variants: top-lit and rim-lit. Engine blends based
on light direction. Doubles art cost but feels alive.
Skip self-shadow, rely on directional fill
Flat-painted diffuse + 2D Light gives a soft directional fill. Sprite
doesn't really self-shadow, but the rim-light hint feels lit. Works for
most indie pixel art.
Emissive / glow
Emissive pixels ignore lighting and stay full-bright. Use cases:
- Street lamps, candles, fireflies, eyes glowing in dark.
- Magic spells, neon signs, lava, screens.
- HUD elements.
Authoring:
- Paint emissive layer in Aseprite (separate layer named "emissive").
- Export as separate texture OR RGBA where alpha = emissive mask.
- Engine shader:
final_color = lit_color + emissive_color * emissive_mask.
In Unity URP 2D: assign emissive secondary texture in Sprite Renderer.
Day / night palette swap vs realtime mixing
Palette swap approach
- One palette per time-of-day (day, dusk, night, dawn).
- Engine fades between palettes over X seconds.
- Pros: total artist control, retro look.
- Cons: doesn't react to dynamic lights (torches stay torch-bright at
noon and night).
Realtime light mixing
- One neutral / day diffuse.
- Multiple Lights with color tint:
- Global Light 2D = sun (warm noon, cool night).
- Point Lights = torches (warm, flicker).
- Pros: dynamic, torches naturally bloom in night, dynamic shadows.
- Cons: can look "modern" vs intended retro feel.
Hybrid (most modern indie)
- Authored ramp shifts per time of day (day uses warm ramp, night
uses cool ramp).
- Plus Global Light 2D color tint.
- Plus emissive lights for explicit glow sources.
Stardew Valley uses palette swap (retro fit). Hyper Light Drifter uses
realtime. Hollow Knight uses realtime + heavy emissive.
Glow / bloom
Two ways to achieve "glow":
- Pre-baked glow halo: paint a halo around emissive pixels. No
shader needed. Cheaper but static.
- Post-process bloom: extract bright pixels, blur, add back.
Looks dynamic, requires post-process pass (URP volume Bloom override
in Unity, post-process shader in Godot).
For pixel art: option 2 conflicts with the pixel-perfect aesthetic
(blur softens pixels). Common compromise: enable bloom only on
emissive layer, not on the diffuse.
Common bugs
- Double-shaded sprites: fully baked diffuse + dynamic light =
shadow stacks. Solution: paint flat diffuse OR flat rim, not both.
- Normal map green channel inverted: light comes from "wrong"
direction. Toggle Y-flip in Sprite Renderer / shader.
- Emissive layer also gets lit: forgot to mask out lighting on
emissive. Solution: emissive added AFTER lighting, not multiplied.
- Bloom blurs pixels: pixel art aesthetic broken. Limit bloom to
emissive mask only.
- Pixel-by-pixel normal noise: sprite looks "wrong" under light.
Smooth the normal map.
See also