FL Voxel Normal Relief
Not voxels, not 3D, and that's exactly why it looks good
- normals
- depth
- voxel_normals
The name is doing a lot of marketing. There's no voxel grid, no mesh, no renderer - FL Voxel Normal Relief takes a normal map and a depth map and draws cuboids with OpenCV, three filled quads each, top over side over front, into a 2D image. It produces that low-poly extruded-normal look that reads as 3D in motion, without any of the cost or fragility of actual geometry.
Which is the point. Compare it to the real thing: image-to-3D pipelines are the most install-fragile corner of this ecosystem and route geometry you can't use without retopology. This is a styling pass. It takes what you already have - normals and depth - and makes facets.
How it works
The frame is quantized into cube_size-pixel cells. Normals are downsampled with area interpolation to one color per cell; depth becomes one height level per cell. Every cell then gets a height:
height = cube_size * relief * max(0.05, 0.3 + 0.6 * level + animation * wave)
so near stuff (higher depth) stands taller, and a sine wave - with a per-cell phase drawn from the seed, offset by grid position - makes the height breathe. Each cell is drawn as a top face, a right side face and a front face with fixed shading multipliers for sides and fronts, a darker outline and a brighter top-left highlight line, painted back-to-front so nearer cells occlude the ones behind them. The seed also controls the phases, so the same settings give you the same wiggle every run.
Because the "cubes" are painted in order and the shading is baked, the result is a normal-colored relief image. Nothing downstream gets geometry; it gets pixels. Which is precisely what makes it useful as an input to a projection node - the pack's docs put it between Depth-to-Normals and the scan compositor, so the reprojection reveals cube facets instead of smooth surfaces.
Inputs and the output
- normals - RGB normal frames. Must be 3-channel; a grayscale depth map will fail the shape check, so run depth through a depth-to-normals pass first. This is the color of every cube.
- depth - the height driver. Aligned to
normals, same resolution and frame count, or the node raises. - cube_size - cell size in pixels, 4–64, default 12. Bigger means fewer, chunkier cubes; smaller starts to look like noise.
- relief - extrusion height, 0–2, default 0.65. Start where the author left it; crank it and neighbouring cubes overlap into a mess.
- animation - wave amplitude, 0–1, default 0.18.
- speed - wave rate in cycles per second (0–5, default 0.7). Zero freezes the wave mid-phase; it does not disable it.
- fps - ties the animation phase to real time, so set it to the video's FPS or your motion will be at the wrong tempo.
- seed - the per-cell phase pattern. Fix it while you compare settings.
One output: voxel_normals, an IMAGE batch. Wire it into the normals input of FL Street Scan Composite, or into the equivalent slot in the pack's FL Interactive Scan FX pipeline, and the depth projection will push your cube relief around with the camera.
Install
ComfyUI Manager → search ComfyUI_Fill-Nodes (publisher machinedelusions), or by hand:
cd ComfyUI/custom_nodes
git clone https://github.com/filliptm/ComfyUI_Fill-Nodes
cd ComfyUI_Fill-Nodes && pip install -r requirements.txt
Restart ComfyUI. This node needs OpenCV, NumPy and Torch, all already present in any ComfyUI install; the pack's long requirements list is for its cloud and API nodes, and there's no model download, no key and no network access involved here. If you're on Linux and skip pip install -r requirements.txt, the OpenGL nodes in the same pack (FL_InfiniteZoom, FL_PaperDrawn, FL_Shadertoy) will complain - this one won't.
Where people get bitten
- Depth is not normals. The most common mistake. Feed a grayscale depth image and the shape check rejects it; feed a 3-channel duplicate of depth and you get flat blue-grey slabs, because you told the node every surface faces the same way. Use a real normal map.
- Misalignment.
Voxel Normal Relief needs aligned RGB normals and depth frames at the same resolution.Resize one and not the other and this is what you'll see. Do the resize before both branches, or you'll be chasing frame drift too. - It's a per-cell Python loop. Cells are drawn one
fillConvexPolyat a time - roughly 14,000 quads per frame at cube size 12 on 1080p. CPU work with a progress bar; test on short crops first. - Relief over 1.0 is a special effect. Cubes start colliding and the depth ordering reads as noise rather than structure.
- Don't expect it to be a mesh. If the goal is an exportable asset, this isn't the tool - the KB's 3D-generation notes cover what actually produces geometry. This one exists to be styled, not printed.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| normals | IMAGE | — | |
| depth | IMAGE | — | |
| cube_size | INT | 124–64 | — |
| relief | FLOAT | 0.650–2 | — |
| animation | FLOAT | 0.180–1 | — |
| speed | FLOAT | 0.700–5 | — |
| fps | FLOAT | 24.001–120 | — |
| seed | INT | 410–2147483647 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| voxel_normals | IMAGE | — |