😺NKD Bake Projection
Repaint a generated mesh by inpainting it from eight angles
- mesh
- passes
- model_3d_info
- mesh
- texture
- report
Every image-to-3D model has the same weak spot, and it isn't the silhouette - it's the texture. Hunyuan3D, TRELLIS, all of them: a shape that reads fine in a turntable, and a surface skin that falls apart up close. Retopologising is a lost afternoon. Repainting isn't.
That's the job here. You render a view in 😺NKD Preview 3D, inpaint it with whatever you like, wrap the result in a 😺NKD Projection Pass, and this node merges every pass onto the mesh at once - repainting the UV texture it already has, or falling back to vertex colours if it has no UVs at all.
Why one merge instead of stacking passes
The source comment says it plainly: incremental accumulation makes the result depend on the order you painted in. Merge as you go and a good early view ends up buried under a mediocre later one. This node normalises by total weight - colour = sum(colour_i * w_i) / sum(w_i) - across all passes in a single evaluation, so swapping two cables gives you the same mesh, and changing falloff re-bakes all eight views at once instead of asking you to start over. There's a unit test pinning that.
What you wire
mesh takes a MESH from Trellis2, Pixal3D or any mesh builder, a TRIMESH straight out of the Hunyuan3D wrapper, or a GLB/GLTF file. passes is an autogrow socket: it sprouts a fresh slot each time you wire one, up to eight.
model_3d_info is the one people skip. It's the placement the viewport rendered the model at, from the same Load3D. Without it the bake projects onto the mesh where its own coordinates put it - which is not where the camera saw it. Wire it on the viewport and here. A move you make by hand in the viewport's Object panel never reaches the graph - it lives in the browser - so bake with it at Reset.
The bake settings, in the order you'll actually touch them:
texture_size(default 2048) is the resolution of the repainted atlas, and it's ignored on a mesh with no UVs.angle_threshold(75°) drops surface turned further than that from the camera. Grazing angles smear, so they're left to a view that faces them. It fades over about nine degrees, so no knife edge.falloff(6) is how hard the most head-on view wins. Low values average the views into mud; high ones make the pick clean but can show a visible handover line between views.occlusion(on) skips geometry hidden behind the model from that camera, so a face doesn't print through the back of the head. Off is faster and fine on a convex shape.occlusion_biasis the slack in that test - raise it if surface that should be painted comes out blank.
Three outputs. mesh plugs straight back into Preview 3D or core's 3D nodes; anything the bake doesn't touch - metallic roughness, tangents, the unlit flag - rides along untouched. texture is the atlas as an image, safe to wire even when there's nothing to show (it gives you 1×1 grey). report belongs on a text preview: it says which pass reached how much of the mesh, and when one contributed nothing at all.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/Nekodificador/ComfyUI-NKD-VFX-Tools
Or search NKD VFX Tools in ComfyUI Manager and restart. No model downloads, no compiled dependencies - the project declares numpy and nothing else. GLB and TRIMESH import reach for trimesh lazily - the same library ComfyUI's own 3D loading uses.
The trap that actually bites
Order of operations, on a generated mesh. Trellis2 and Pixal3D emit their raw shape with no UVs, and only Unwrap Mesh UVs and Bake Texture From Voxel give it a layout and a texture. Bake before those and you get vertex colours on a mesh that was about to have a good texture - faceted patches of colour scattered over the model, and a mesh whose actual texture a viewer may still show instead of your paint. The report warns about this explicitly, and it warns when it had to resample your existing texture down to texture_size, which measurably eats small charts.
Two more real ones. On an older ComfyUI core without the UV rasteriser the pack borrows from comfy_extras, the node says so and falls through to vertex colours - the fix is updating ComfyUI, not fiddling with the node. And occlusion uses a point z-buffer built at a resolution matched to how dense your mesh is, so very thin geometry (a cape a few vertices thick) can fail to hide what's behind it; that's what occlusion_bias is for.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| mesh | TRIMESH,MESH,FILE_3D_GLB,FILE_3D_GLTF,FILE_3D | The mesh being painted: a MESH from Trellis2, Pixal3D or any mesh builder, a TRIMESH, or a GLB/GLTF. Bake with it at Reset in the viewport's Object panel, since a transform made there stays in the browser. | |
| texture_size | INT | 204864–8192 | Resolution of the repainted texture. Ignored on a mesh with no UVs, which gets vertex colours. |
| angle_threshold | FLOAT | 750–89 | Surface turned further than this from the camera is not painted by that pass. Grazing angles smear. |
| falloff | FLOAT | 6.01–16 | How sharply the most head-on view wins. Low values average the views into mud; high ones can show the switch between them. |
| occlusion | BOOLEAN | true | Skip surface hidden behind the model from that camera. Off is faster and fine on a convex shape. |
| occlusion_bias | FLOAT | 0.0200–0.5 | Slack in the hidden-surface test, as a fraction of distance. Raise it if surface that should be painted comes out blank. |
| passesopt | COMFY_AUTOGROW_V3 | One 😺NKD Projection Pass per view. A slot fed by a muted group is skipped, so Ctrl+M is the per-view switch. | |
| model_3d_infoopt | LOAD3D_MODEL_INFO | The placement the viewport rendered the model at, straight from the same Load3D. Without it the bake projects onto where the mesh sits untransformed, which is not where the camera saw it. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| mesh | MESH | — |
| texture | IMAGE | — |
| report | STRING | — |