NORMAL BLEND (JOV_GL)
Blend two normal maps without the lighting going weird
- imageA
- imageB
- RGBA
- RGB
- MASK
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.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| imageAopt | IMAGE | Input image A to blend with image B | |
| imageBopt | IMAGE | Input image B to blend with image A | |
| blendopt | FLOAT | 0.500–1 | Intensity of blend |
| FRAGMENTopt | STRING | // 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)
| Name | Type | Description |
|---|---|---|
| RGBA | IMAGE | Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output. |
| RGB | IMAGE | Three channel [RGB] image. There will be no alpha. |
| MASK | MASK | Single channel mask output. |