Badge Relief Geometry V1 (DAELab)
Relief you don't have to pay GPT for
- flat_image
- height_map
- foreground_mask
- relief_base
- relief_lightness
- normal_preview
- ambient_occlusion
- beveled_height
- report
What this is for
Here's the thing you notice after a few badge renders: generative models are bad at height. Asked to show a two-level enamel pin, they'll produce something that looks raised, but the shoulder wanders, the bevel width changes along the edge, and two regions at the same nominal height come back at visibly different elevations. It reads fine until you put it next to a photograph of the real product.
Badge Relief Geometry V1 builds the relief deterministically from your height map. No model, no API call, no cost, and - the point - the same input gives you the same pixels every time. Finite-width bevels by distance transform, coherent normals from the height gradient, specular lift from a directional light, and contact occlusion where surfaces meet. The flat artwork's colours are reused as the surface albedo, so nothing gets recoloured.
This is the "geometry is geometry" node of the pack. You can ship its output directly for a preview, or use it as the base that a bounded GPT detail transfer is allowed to nudge - which is the design the pack actually recommends.
How it works
It takes the flat image, the discrete height map and the foreground mask, and validates the height map hard: every value must be one of {0, 0.2, 0.4, 0.6, 0.8, 1.0} within a 1e-4 tolerance. A continuous depth map - the kind a depth estimator gives you - is rejected outright with height_map contains values outside {0,.2,.4,.6,.8,1}. That error is a feature: the physical stack has six levels and the node refuses to pretend otherwise. With enforce_1024 on (the default), flat, height and foreground must all be 1024×1024 and share a canvas.
Then it builds the relief. bevel_radius_px sets the finite width of the transition at each level boundary, so a shoulder is a genuine chamfer of known width rather than a blur. relief_depth scales the Z axis. Normals come from the gradient of the beveled height field, with normal_strength controlling how sharply they tilt - this is the dial that makes surfaces look crisp or soft. Lighting is a single key light defined by light_azimuth_degrees (default −135, i.e. upper-left) and light_elevation_degrees (45), mixed with ambient (0.72) and key_strength (0.34). Specular response is specular_strength (0.07) with specular_power (28) as its tightness. Ambient occlusion comes from ao_strength (0.10) and ao_radius_px (10), darkening where levels meet.
Every one of those is a physical-ish parameter, not a vibe. That's why the defaults barely need touching on a normal badge: ambient around 0.7, a modest key, a small but real specular, Ao just enough to seat the lettering.
The inputs and outputs
Inputs you wire: flat_image, height_map, foreground_mask. Fifteen required inputs total but twelve of them are numbers with sane defaults - the ones worth experimenting with are bevel_radius_px (7 default; up for softer cast-metal feel, down for a crisp machined edge) and normal_strength (11 default, and small changes here do a lot).
Outputs: relief_base (the coloured render - this is the one you'll look at), relief_lightness (the lighting-only field, useful as a mask or as input to a transfer stage), normal_preview (normals visualised, for debugging a weird edge), ambient_occlusion, beveled_height, and report with the parameters actually used.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/TeutonicMepheles/ComfyUI-DAELab-Custom-Nodes-Library.git
# restart ComfyUI
Pure PyTorch - no checkpoints, no OpenCV, nothing to download.
Where people get burned
Feeding it a continuous height map. This is the big one, and it's usually self-inflicted: someone takes the six-level map, resizes it with bilinear interpolation to fix a canvas mismatch, and now every value is illegal. The node errors, which is the good outcome - but the fix is to resize the flat art to match the height map, not the other way round, and to keep nearest-neighbour anywhere a mask or height field is resampled.
Non-1024 canvases. With enforce_1024 on it errors rather than silently resizing, and again that's correct. If you genuinely work at another square size, turn it off and accept that the bevel radius and AO radius are in absolute pixels - 7px on a 512 canvas is twice the visual width it is on 1024.
Light direction fighting the reference. If you're compositing this next to a photographed product lit from the right, the default upper-left key will look wrong no matter how good the relief is. light_azimuth_degrees is the fix, not more specular.
Treating it as a finisher. It's a base. Height, bevels and normals are correct; the surface material is still whatever the flat art says, because this node doesn't do materials at all. Materials are a separate, masked, paid stage - and this node is best used as the deterministic thing that a bounded model transfer is allowed to add nuance to, never the thing it replaces.
Inputs (15)
| Name | Type | Default | Description |
|---|---|---|---|
| flat_image | IMAGE | — | |
| height_map | MASK | — | |
| foreground_mask | MASK | — | |
| bevel_radius_px | INT | 71–24 | — |
| relief_depth | FLOAT | 1.000.1–3 | — |
| normal_strength | FLOAT | 11.01–60 | — |
| ambient | FLOAT | 0.720.3–1 | — |
| key_strength | FLOAT | 0.340–1 | — |
| specular_strength | FLOAT | 0.070–0.5 | — |
| specular_power | FLOAT | 282–96 | — |
| ao_strength | FLOAT | 0.100–0.5 | — |
| ao_radius_px | INT | 101–32 | — |
| light_azimuth_degrees | FLOAT | -135-180–180 | — |
| light_elevation_degrees | FLOAT | 4510–85 | — |
| enforce_1024 | BOOLEAN | true | — |
Outputs (6)
| Name | Type | Description |
|---|---|---|
| relief_base | IMAGE | — |
| relief_lightness | MASK | — |
| normal_preview | IMAGE | — |
| ambient_occlusion | MASK | — |
| beveled_height | MASK | — |
| report | STRING | — |