Nodes/Jovi_GLSL/NORMAL BLEND (JOV_GL)
ComfyUI Node

NORMAL BLEND (JOV_GL)

Blend two normal maps without the lighting going weird

By Amorano·Created 2 years ago·Updated about a year ago· 20
NORMAL BLEND (JOV_GL)
  • imageA
  • imageB
  • RGBA
  • RGB
  • MASK
blend0.50
FRAGMENT// name: NORMAL BLEND // desc: Blend two Normal maps // category: COMPOSE uniform sampler2D imageA; // | Input image A to blend with image B uniform sampler2D imageB; // | Input image B to blend with image A uniform float blend; // 0.5; 0; 1; 0.01 | Intensity of blend void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord / iResolution.xy; vec3 normalA = texture(imageA, uv).rgb * 2.0 - 1.0; normalA = normalize(normalA); vec3 normalB = texture(imageB, uv).rgb * 2.0 - 1.0; normalB = normalize(normalB); vec3 blendedNormal = normalize(mix(normalA, normalB, blend)); fragColor = vec4((blendedNormal * 0.5) + 0.5, 1.0); }

Here's the thing about normal maps that nobody warns you about: you can't just average two of them. A normal map stores direction as packed RGB values, and a naive crossfade produces normals that no longer point where they should - your lighting breaks, surfaces look wrong, and you blame the texture when the math was the problem. This node does it properly: it unpacks both maps into actual vectors, normalizes them, blends, and re-packs. If you combine normal maps at all - a base surface plus a detail overlay is the classic case - this is the node you want doing it.

How it works

Each input is read as a normal map where RGB (0–1) encodes a direction (−1 to 1 per channel). The shader unpacks both, normalizes them so they're true unit vectors, interpolates between them by the blend amount, normalizes the result, and packs it back into RGB. That normalize step is the whole game - it keeps the blended normal on the unit sphere, which is what naive blending skips. The output alpha is set to 1.

The inputs

  • imageA - first normal map.
  • imageB - second normal map.
  • blend - 0 to 1, default 0.5. At 0 you get A, at 1 you get B, in between is a proper spherical interpolation.

Outputs are the pack's standard RGBA, RGB, and MASK. For normal maps you want the RGB or RGBA output; the MASK is there because every node in this pack emits it, not because you'll use it here.

Install

One-time pack install. Via ComfyUI Manager, search Jovi_GLSL; or:

cd ComfyUI/custom_nodes
git clone https://github.com/Amorano/Jovi_GLSL.git
cd Jovi_GLSL
pip install -r requirements.txt

Restart ComfyUI. Standard deps (PyOpenGL, glfw, opencv-contrib-python, cozy_comfyui from git), needs an OpenGL context. No models, no keys. Under JOVI_GLSL 🌈 → COMPOSE.

Where people get tripped up

This node assumes both inputs are already normal maps. Feed it two flat images and it'll cheerfully "blend" them into garbage - there's no autodetection. Also, the ordering convention matters: this pack stores normals in the standard RGB-encodes-direction format, but if your other tools use a flipped Y convention (OpenGL-style vs DirectX-style normals is the classic mismatch), a blend will look subtly wrong no matter what. Match your conventions first. Pair it with the pack's NORMAL node for generating the inputs and you have a complete normal-map workflow.

CategoryJOV_GL 🌈/COMPOSE

Inputs (4)

NameTypeDefaultDescription
imageAoptIMAGEInput image A to blend with image B
imageBoptIMAGEInput image B to blend with image A
blendoptFLOAT0.500–1Intensity of blend
FRAGMENToptSTRING// name: NORMAL BLEND // desc: Blend two Normal maps // category: COMPOSE uniform sampler2D imageA; // | Input image A to blend with image B uniform sampler2D imageB; // | Input image B to blend with image A uniform float blend; // 0.5; 0; 1; 0.01 | Intensity of blend void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord / iResolution.xy; vec3 normalA = texture(imageA, uv).rgb * 2.0 - 1.0; normalA = normalize(normalA); vec3 normalB = texture(imageB, uv).rgb * 2.0 - 1.0; normalB = normalize(normalB); vec3 blendedNormal = normalize(mix(normalA, normalB, blend)); fragColor = vec4((blendedNormal * 0.5) + 0.5, 1.0); }

Outputs (3)

NameTypeDescription
RGBAIMAGEFull channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output.
RGBIMAGEThree channel [RGB] image. There will be no alpha.
MASKMASKSingle channel mask output.