Curvature From Normal
Extract wear and dirt masks from a normal map
- image
- mask
- image
- mask
- curvature_info
Every PBR artist learns the same trick eventually: from a normal map you can derive where the surface curves, and curvature is where the wear goes. Edges get scuffed, crevices collect dirt - and both of those masks fall out of the math. x1CurvatureFromNormal computes that curvature signal as a mask, directly from a tangent-space normal map. It's the tech-art workhorse of MKRShift Nodes' Surface/Tech Art branch, alongside slope masks and channel packing.
The pack is young (no impressions, no community footprint I could find), but this node is one of its most standard-feeling pieces - no settings JSON, just a clean set of numeric inputs, which makes it approachable.
How it works
The mechanism, readable straight from the source: decode the tangent-space normal map, compute the Laplacian of the normal field (dnx/dx + dny/dy), and scale by strength. That sum is positive where the surface is convex (outward bumps) and negative where it's concave (creases and pits). The mode enum decides what you keep:
convex- the positive signal: edge wear masks, the scuffed bright edgesconcave- the negative signal: dirt and cavity buildup, the dark crease responsecombined- the unsigned absolute value: general detail/micro-curvature
blur_radius smooths the normal before the solve (kills noise on rough normals), and the standard normalize_mode chain (auto_percentile default) plus gamma and invert_values shape the raw signal into a usable mask.
Outputs: image (grayscale curvature map), mask (the scalar map), curvature_info (settings + the actual normalization range used - useful for matching multiple masks to one range).
Inputs that matter
mode-convexfor wear,concavefor dirt,combinedfor general detailstrength(default 2.0) - the intensity multiplier; crank it when edges are too faintblur_radius(default 1.0) - a little goes a long way on noisy normalsnormalize_mode+percentile_low/high- the remap;auto_percentilehandles most cases
Installing
ComfyUI Manager → search MKRShift Nodes, or:
cd ComfyUI/custom_nodes
git clone https://github.com/criskb/MKRShift_Nodes
Restart ComfyUI. No models, no new dependencies - it's numpy gradient math on the CPU.
Where people get burned
- Feeding it a normal map that isn't normalized. The decode assumes a valid tangent-space normal; a color or albedo texture in the input produces garbage curvature. If the output looks like static, check what you're feeding it.
- Convention mismatch. This is designed for the OpenGL convention (green-up). A DirectX-style normal map (green-down) will flip concavity and convexity, and
invert_valueswon't fix that - re-bake or flip the map first. maskoutput vs.maskinput - the output is the derived scalar, the input (withmask_feather) gates where it's emitted. Pack-wide convention.strengthtoo low on smooth geometry produces a nearly-empty mask; 2.0 default is a fine start, but don't be shy about going higher.
The author's summary is the practical takeaway: convex for edge-wear masks, concave for dirt/cavity buildup. One normal map, two very useful masks, zero painting.
Inputs (14)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| mode | COMBO | 3 options: combined, convex, concave | |
| normalize_mode | COMBO | 3 options: auto_percentile, manual_range, auto_range | |
| value_min | FLOAT | 0.000-4–4 | — |
| value_max | FLOAT | 1.000-4–4 | — |
| percentile_low | FLOAT | 2.00–100 | — |
| percentile_high | FLOAT | 98.00–100 | — |
| blur_radius | FLOAT | 1.00–64 | — |
| strength | FLOAT | 2.000–16 | — |
| gamma | FLOAT | 1.000.1–4 | — |
| invert_values | BOOLEAN | false | — |
| mask_feather | FLOAT | 8.00–256 | — |
| invert_mask | BOOLEAN | false | — |
| maskopt | MASK | — |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| mask | MASK | — |
| curvature_info | STRING | — |