Normal Map Relighter
Relight Any Texture With a Normal Map and Zero Diffusion Models
- image
- normal_map
- relit_image
- lighting_mask
Everyone who discovers IC-Light and its cousins wants to relight their images with a model. But sometimes you don't need a diffusion model at all - you need the deterministic version: point a virtual light at a normal map and let the math do the shading. That's exactly what Normal Map Relighter does, and it's instant, free, and perfectly reproducible.
The trick it exploits is that a normal map already encodes the surface direction of every pixel. If you multiply a flat albedo by how "facing" each pixel is toward your light, you get a convincingly lit surface with zero sampling. It's the same trick Blender's Normal node shader uses - the docstring straight up says so.
How it works
The node decodes your normal map from color space (0-1) into vector space (-1/1), flips the green channel if you tell it the map is DirectX format, normalizes the vectors, then computes the dot product between each surface normal and your light direction. Front-facing pixels get lit, side-facing pixels fall off, back-facing pixels clamp to your ambient floor.
Two details worth knowing:
- The
light_x/y/zinputs form a 3D direction vector that gets normalized internally, so you don't have to keep it unit length -(0, 0, 1)means "light coming straight at the camera," which is the default and a fine starting point. ambient_light(0-1) is the minimum brightness so nothing goes fully black. Bump it to 0.3-0.4 if you want a softer, less contrasty look.
The output relit_image is your albedo multiplied by that lighting, and lighting_mask is the grayscale shading itself - which is genuinely useful to grab on its own, since it's a ready-made light mask you can reuse elsewhere in the graph.
Inputs worth your time
- normal_format - OpenGL or DirectX. This is the one that bites people. Pick the wrong one and everything looks inverted, like a reversed bump.
- light_x / light_y / light_z - the light direction. Start from
(0,0,1)and nudge X for left/right and Y for up/down. - light_intensity (0-10) and ambient_light (0-1) - brightness and floor.
Why you'd actually use this
This node shines when you want to preview what a normal map will do before you commit to a render, or when you want to bake a lighting pass into an asset without leaving ComfyUI. It's also a great debug tool: flat-looking output usually means your normal map is weak or wrong, and this node will tell you immediately. If your normal map decodes cleanly, the lighting looks like a proper shaded surface; if it looks like noise, the map is broken.
Installing it
It's part of TextureAlchemy. Either ComfyUI Manager (search "TextureAlchemy") or:
cd ComfyUI/custom_nodes
git clone https://github.com/amtarr/ComfyUI-TextureAlchemy
Restart, and you'll find the node under Texture Alchemist → Lighting. No extra dependencies, no model files - pure PyTorch math on the two images you plug in.
Gotchas
The big one is the DirectX/OpenGL flip mentioned above - if the shading looks "sunk in" rather than raised, flip normal_format. Also note this is a directional light only: no distance falloff, no specular. If you want highlights and light color, the Advanced sibling node adds those; if you want true HDRI-style environment lighting, the IBL version is the one. Think of this as the cheap and cheerful starter in the relight family.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| normal_map | IMAGE | — | |
| light_x | FLOAT | 0.00-1–1 | Light direction X component |
| light_y | FLOAT | 0.00-1–1 | Light direction Y component |
| light_z | FLOAT | 1.00-1–1 | Light direction Z component (pointing toward camera) |
| light_intensity | FLOAT | 1.00–10 | Light intensity multiplier |
| ambient_light | FLOAT | 0.200–1 | Ambient light level (minimum brightness) |
| normal_format | COMBO | OpenGL | Normal map format (affects Y channel) |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| relit_image | IMAGE | — |
| lighting_mask | IMAGE | — |