NormalLighting
Relight anything with a normal map — no model, no API, no GPU
- diffuse_map
- normal_map
- specular_map
- IMAGE
NormalLighting (class EulerLightingNode) is the rare ComfyUI node that isn't AI at all. Give it three images - a plain color image, its normal map, and a specular map - and it re-lights the whole thing in real time as you drag a virtual light source around. No checkpoint, no VRAM spike, no prompt. It's just math, and it runs instantly.
That's also its superpower. Diffusion-based relighting like IC-Light is heavy and can quietly hallucinate detail; this node touches nothing but pixel values. If you're doing game/texture art, product shots, or any workflow where you already have normal maps, it's a free lighting rig that fits inside your graph. In fact, the math is the same kind of thing games have done for decades - this is just the first place I've seen it wrapped as a Comfy node.
How it works
The code is short and readable, and the README says exactly what it computes: Lambertian diffuse plus Phong specular reflection.
- The normal map is decoded from RGB (which stores XYZ in [0,1] space) back to real vector space with
*2 - 1, then normalized per pixel. light_yawandlight_pitchare converted to a direction vector. The camera is fixed - you only move the light.- Diffuse is
clamp(dot(normal, light), 0, 1)- the classic Lambert term, brightest where the surface faces the light. - Specular uses the half-vector between light and camera:
clamp(dot(normal, half), 0, 1) ^ specular_power, giving those tight metallic highlights.
Everything combines into the output formula from the README:
output = (diffuse * (ambient + lambert * NormalDiffuseStrength)
+ specular * specular_map * SpecularHighlightsStrength) * TotalGain
The inputs that matter
All three image inputs - diffuse_map, normal_map, specular_map - are required, and they must be the same resolution (there's no auto-resize, mismatched shapes just fail). Of the sliders, you'll actually touch these:
light_yaw/light_pitch- point the light. Yaw wraps ±180°, pitch ±90°. This is the whole fun of the node: drag them to see highlights crawl across the surface.specular_power- gloss. Small values (1–10) are matte and spread; around 32 (the default) looks plastic; 100+ goes shiny/metal.ambient_light- set to 1 and you get the input image back, untouched. It's your "amount of relight" safety knob.TotalGain- overall brightness, up to 2×. Note the output is not clamped to [0,1], which is a feature: people push gain past 1 deliberately to fake an HDR range.
The single output is an IMAGE - wire it into a preview or save node and you're done.
Where the normal map comes from
You need a decent normal map or the whole thing falls apart. The standard moves: ComfyUI ControlNet Auxiliary Preprocessors has a normal-map preprocessor, or Kijai's ComfyUI-Lotus nodes which someone on r/comfyui explicitly recommends for this exact node ("it works super well"). Keep the normal map in RGB-XYZ convention - if your highlights look inverted, the map is flipped and so is your light.
Install
Two ways, both boring (in a good way):
cd ComfyUI/custom_nodes && git clone https://github.com/TJ16th/comfyUI_TJ_NormalLighting
then restart ComfyUI. Or use ComfyUI Manager and search for comfyUI_TJ_NormalLighting. There are no model downloads and no requirements.txt - the node only imports torch, numpy, and PIL, all already in ComfyUI.
Gotchas
- Sizes must match across the three inputs - no rescaling happens.
specular_mapblack means no specular - the community trick is to feed a pure black image when you only want the diffuse relight.- Over-bright output is normal - with
TotalGain> 1 the tensor exceeds white, so don't panic when previews clip; that's the point.
One honest caveat: the author (TJ16th) is a self-described hobbyist and this is his first GitHub project, and it shows in little ways - the node lives in the custom category, some parameter names are camelCase. But it works, it's dependency-free, and for what it does nothing else in ComfyUI is this cheap.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| diffuse_map | IMAGE | — | |
| normal_map | IMAGE | — | |
| specular_map | IMAGE | — | |
| light_yaw | FLOAT | 45-180–180 | — |
| light_pitch | FLOAT | 30-90–90 | — |
| specular_power | FLOAT | 321–200 | — |
| ambient_light | FLOAT | 0.500–1 | — |
| NormalDiffuseStrength | FLOAT | 1.000–5 | — |
| SpecularHighlightsStrength | FLOAT | 1.000–5 | — |
| TotalGain | FLOAT | 1.000–2 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | — |