Chord - Normal to Height
Turn your normal map into a real height map without the usual seams
- normal
- height
A normal map will fake detail on a flat polygon all day, but the moment you want to actually carve something - displacement in a game engine, bump-to-mesh in Blender, a CNC relief - you need a height map. This node is the bridge: drop in a normal map, get a height map back, with none of the drift and seams that usually ruin the naive conversion.
Normal maps and height maps are cousins in PBR land. Normals encode which way each surface faces; heights encode actual offset. In theory you get one from the other by integration. In practice, integrating a normal map naively turns every little noise bump into an accumulating error, so your height field tilts, drifts, and tears at tile edges. That's exactly the failure the paper behind this pack is built to kill.
How it works
The node takes your normal map, rescales it to 1024×1024 internally, then splits it into overlapping subregions (128px chunks with 50% overlap, padded circularly so edges wrap). Each region's gradient field - Nx/Nz and Ny/Nz, the standard normal-to-slope conversion - is solved as a Poisson equation in the Fourier domain, which is a mathematically clean way to reconstruct a surface that fits the given gradients. The overlapping chunks are then cosine-blended back together and the circular padding is cropped off, so the seams between regions just disappear.
A couple of smart fallbacks live in the source. If the resulting height field has almost no variance (a suspiciously flat result), it re-runs without re-normalizing the input normal map. And it loops over the batch dimension, so a stack of normal maps processes in one go.
The inputs that matter
There's exactly one: normal (IMAGE). Feed it any normal map you've got - the normal output of ChordMaterialEstimation in the same pack works perfectly, but so does a normal map from ControlNet preprocessors or your game-asset pile. You get one output, height (IMAGE), a grayscale field normalized to 0–1 where white is high and black is low. Pipe it into a displacement node, save it as a PNG, or export it for Blender's displacement modifier.
Two genuine gotchas. First, it assumes the normal map is stored in the usual 0–1 "RGB" encoding and un-encodes it to −1..1 space - feed it a raw linear normal and the result will be garbage. Second, normal maps come in two conventions: OpenGL-style (green up) and DirectX-style (green down). If your height comes out inverted - bumps become craters - flip the green channel first.
Installation
This node ships inside the ComfyUI-Chord pack, and here's the pleasant part: it's pure math, so it works with zero model downloads. No checkpoint, no Hugging Face login. Install the pack and you're done:
- ComfyUI Manager → search "ComfyUI-Chord" → Install → restart.
- Or manually:
cd ComfyUI/custom_nodes && git clone https://github.com/ubisoft/ComfyUI-Chord.git, thenpip install -r ComfyUI-Chord/requirements.txt(the deps are justdiffusers,omegaconf, andimageio).
Common issues
The most common stumble isn't a bug - it's people assuming they need the Chord model for this node too, so they fight the gated Hugging Face download for a node that never touches the model. You don't. If the height looks inverted, flip the green channel. If it looks flat, that's usually a normal map with a baked-in color profile rather than a real normal map - re-export it as linear. Beyond that, the node is one of the most boring-reliable parts of this pack, which is a compliment.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| normal | IMAGE | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| height | IMAGE | — |