ComfyUI Node

Normal Map

A height map in, a renderer-ready normal out

By criskb·Created 7 months ago·Updated 5 months ago· 0
Normal Map
  • image
  • source_mask
  • mask
  • image
  • mask
  • normal_info
settings_json{"source_mode":"luma","normalize_mode":"auto_percentile","value_min":0.0,"value_max":1.0,"percentile_low":2.0,"percentile_high":98.0,"gamma":1.0,"blur_radius":0.0,"strength":4.0,"convention":"opengl","invert_height":false,"invert_x":false,"mask_feather":8.0,"invert_mask":false}

Generating a normal map from an image is the most classic material job in the book, and x1NormalMap does it properly. Give it any grayscale height source - luminance of your image, a channel, a mask - and it computes a tangent-space normal map you can drop straight into a renderer, a shader ball, or the pack's own x1PBRPack. It's the middle of the pack's surfacing pipeline: x1Heightmap to art-direct the height, x1NormalMap to convert it, x1NormalBlend to stack detail on top.

How it works

The mechanism is the standard height-to-normal trick: take your heightfield, compute gradients along X and Y, and encode the surface slope as a tangent-space normal. What separates this from a bare numpy implementation is the control around it:

  • source_mode - pick what becomes the height: luma, individual RGB channels, max_rgb, saturation, value, alpha, or mask (fed via the optional source_mask)
  • normalize_mode - auto_percentile (default, robust to outliers), manual_range, or auto_range
  • gamma and blur_radius - shape the heightfield before gradients are computed. This is the subtle one: a small blur before the gradient step is how you suppress noise-amplified bumps
  • strength - the slope multiplier (default 4.0). This is your "how bumpy" knob
  • convention - opengl or directx, which differ only in green-channel orientation (see the gotcha below)
  • invert_height and invert_x - flip the depth direction or the red axis when your source is inverted

Everything ships in a settings_json string, the pack's house style: powerful, and not always discoverable. The defaults are a solid starting point.

Inputs and outputs

Required: image and settings_json. Optional: source_mask (height source when source_mode is mask) and mask (limits where the normals are emitted).

  • image - the tangent-space normal map. Outside the optional mask, it outputs a flat neutral normal rather than guessing
  • mask - the normalized height scalar as a MASK
  • normal_info - a STRING summary of the resolved settings and normalization range

The convention gotcha (read this)

opengl vs directx flips the green channel, and getting it wrong is the single most common normal-map failure: your surface looks fine but lighting reads inverted, as if the detail is recessed instead of raised. Renderers and engines differ in what they expect. The author's guidance for this pack's x1PreviewMaterial is clear - it expects glTF/OpenGL-space normals, and you set normal_convention to directx on the preview node when feeding it an engine-style external map (because the preview flips it back). If you generate with x1NormalMap in opengl mode, tell the preview node opengl. Match the pair and stop guessing.

Also worth knowing: the author suggests using x1Heightmap first if you want to inspect and art-direct the height before converting it - the two nodes are designed to chain, and it's a much better workflow than blind-generating normals and hoping.

Installing it

MKRShift Nodes via ComfyUI Manager (search "MKRShift Nodes"), or:

cd ComfyUI/custom_nodes
git clone https://github.com/criskb/MKRShift_Nodes

Restart and it's live. No requirements, no model downloads, no ffmpeg - the Surface/Maps family is plain numpy/torch math. The caveat, as with every node in this pack: you're installing a very large repo to reach a small family. If you're building PBR assets, the payoff is that all these maps share one consistent normalization vocabulary and chain cleanly into x1PBRPack.

CategoryMKRShift Nodes/Surface/Maps

Inputs (4)

NameTypeDefaultDescription
imageIMAGE
settings_jsonSTRING{"source_mode":"luma","normalize_mode":"auto_percentile","value_min":0.0,"value_max":1.0,"percentile_low":2.0,"percentile_high":98.0,"gamma":1.0,"blur_radius":0.0,"strength":4.0,"convention":"opengl","invert_height":false,"invert_x":false,"mask_feather":8.0,"invert_mask":false}
source_maskoptMASK
maskoptMASK

Outputs (3)

NameTypeDescription
imageIMAGE
maskMASK
normal_infoSTRING