NormalCrafter Clean - Generate Normals
Video surface normals that don't flicker, without the VRAM gymnastics
- model
- images
- normal_maps
Normal maps are the quieter cousin of depth maps. Depth says "how far away is this pixel"; a normal map says "which way is this surface facing." Run a clip through this node and you get a per-frame normal map that stays consistent across time - no shimmer, no flicker, no per-frame guessing. That temporal consistency is the entire point of NormalCrafter, and this is the ComfyUI node that runs it without making you babysit VRAM.
So why do you care? Relighting is the headline use: relight a shot after the fact, or take green-screen footage into a game engine like Unreal and let the character react to the scene's lighting. Normal maps also convert into depth maps with a bit of calculus if that's the direction you need, and they're a valid ControlNet condition for material-aware generation. If any of that is your workflow, video normals have historically been the fiddly missing piece.
Why this node, not the other one. There was already a NormalCrafter wrapper, but it mixed node UI, model downloads, device state, inference and cleanup into one glob, and it could hand live CUDA tensors back into ComfyUI's graph cache - a great way to trip memory errors you can't trace. This pack is a ground-up rewrite: one model object owns each component exactly once, and ComfyUI always receives a plain CPU tensor. If you've fought the original wrapper's VRAM weirdness, that's the part you'll feel.
How it runs
NormalCrafter takes a Stable Video Diffusion image encoder, VAE and scheduler, plus NormalCrafter's own fine-tuned UNet and temporal VAE. The generate node pushes frames through four explicit stages: CLIP frame embeddings, VAE encoding to RGB latents, sliding-window UNet inference, then VAE decoding back to normal maps. The clever bit is where the tensors live: full-video intermediates stay on CPU, and only the active chunk or window is moved to CUDA. Peak VRAM stays roughly flat no matter how long the clip is.
The released model uses a deterministic one-step scheduler path, which is why there's deliberately no seed input. Same clip in, same normals out - that's a feature for a geometry task, not a bug.
The knobs that actually matter
offload_mode- the big one.staged(default) moves one heavy component to GPU at a time and is the low-VRAM path.residentkeeps everything on the device and is noticeably faster but wants substantially more VRAM. Start staged, flip to resident if it fits.max_resolution- caps the longest side before inference; input is never upscaled. Lowering this is the cheapest speed/VRAM win.window_size/step_size- defaults 14 and 10 give the 4-frame temporal overlap the model was trained around. Step must never exceed window. Leave them alone until you're chasing artifacts.output_size-original(default) resizes the decoded normals back to your input resolution;processedkeeps the working resolution.renormalize_normals- interpolation after resizing can shorten vectors, so this restores unit length. Keep it on unless you're doing a strict A/B against older glue code.
The three *_chunk_size inputs trade transfer overhead against peak VRAM; only touch them if you're tuning. offload_after drops the CUDA allocator cache once generation finishes.
Installing
Install once for the whole pack, then restart ComfyUI:
cd ComfyUI/custom_nodes
git clone https://github.com/kaski23/BetterNormalCrafterWrapper
cd BetterNormalCrafterWrapper
pip install -r requirements.txt
ComfyUI Manager can do the same if you search for BetterNormalCrafterWrapper. Dependencies are standard huggingface fare - diffusers, transformers, accelerate, safetensors - nothing exotic. Wire it as Load → Generate Normals → your relighting/export nodes; the output normal_maps is a normal IMAGE you can save or feed anywhere.
Gotchas
The first Load run downloads multi-gigabyte weights from Hugging Face, so budget disk and patience. A clip shorter than the window gets padded by repeating its final frame and then trimmed back - no filler frames leak into your output. One honest caveat: at time of writing the author had syntax-checked and unit-tested the scheduler but not run the full pipeline against the real weights on a GPU, so treat your first clip as a smoke test and watch torch.cuda.memory_allocated() across repeated runs. If it's stable, you're set.
Inputs (12)
| Name | Type | Default | Description |
|---|---|---|---|
| model | NORMALCRAFTER_CLEAN_MODEL | — | |
| images | IMAGE | — | |
| max_resolution | INT | 1024256–2048 | — |
| window_size | INT | 142–32 | — |
| step_size | INT | 101–32 | — |
| clip_chunk_size | INT | 161–128 | — |
| vae_encode_chunk_size | INT | 71–32 | — |
| vae_decode_chunk_size | INT | 41–32 | — |
| offload_mode | COMBO | staged | 2 options: staged, resident |
| offload_after | BOOLEAN | true | — |
| output_size | COMBO | original | 2 options: original, processed |
| renormalize_normals | BOOLEAN | true | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| normal_maps | IMAGE | — |