ComfyUI Node

NormalLighting

Relight anything with a normal map — no model, no API, no GPU

By TJ16th·Created 2 years ago·Updated 2 years ago· 153
NormalLighting
  • diffuse_map
  • normal_map
  • specular_map
  • IMAGE
light_yaw45
light_pitch30
specular_power32
ambient_light0.50
NormalDiffuseStrength1.00
SpecularHighlightsStrength1.00
TotalGain1.00

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_yaw and light_pitch are 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_map black 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.

Categorycustom

Inputs (10)

NameTypeDefaultDescription
diffuse_mapIMAGE
normal_mapIMAGE
specular_mapIMAGE
light_yawFLOAT45-180–180
light_pitchFLOAT30-90–90
specular_powerFLOAT321–200
ambient_lightFLOAT0.500–1
NormalDiffuseStrengthFLOAT1.000–5
SpecularHighlightsStrengthFLOAT1.000–5
TotalGainFLOAT1.000–2

Outputs (1)

NameTypeDescription
IMAGEIMAGE