Nodes/ComfyUI-Tools/3d - Projection MultiView Texturing
ComfyUI Node

3d - Projection MultiView Texturing

Bake your renders onto a mesh, occlusion and all

By visualbruno·Created 8 months ago·Updated a day ago· 5
3d - Projection MultiView Texturing
  • trimesh
  • front_image
  • back_image
  • left_image
  • right_image
  • top_image
  • bottom_image
  • custom_images
  • camera_config
  • trimesh
  • base_color
  • metallic_roughness
◄texture_size4096►
◄blend_texturetrue►
◄blend_exponent1.0►
◄ortho_scale1.10►
◄norm_size1.15►
◄fill_holestrue►
◄max_hole_size20►
◄use_metallictrue►
◄depth_eps0.0100►
◄mesh_cluster_threshold_cone_half_angle_rad60►
◄add_alpha_channelfalse►
◄front_weight1.000►
◄back_weight1.000►
◄left_weight0.010►
◄right_weight0.010►
◄top_weight0.010►
◄bottom_weight0.010►
◄custom_azimuths►
◄custom_elevations►
◄custom_weights►

Texturing, without the texturing model

Here's the thing about image-to-3D in the open stack: the geometry is the good half, the paint is the bad half. Hunyuan3D's shape model gives you a solid object; the texture stage is where it goes muddy. So skip the texturing model - you already have a 2D model that paints beautifully, so generate a few views of the object and project them onto the mesh. That's this node, and it's the job StableProjectorz became popular for, just inside a graph. Reach for it when you have a mesh and a matching set of view images: a spin render from Blender, six images from Flux/Qwen, or a generator's own multiview output.

How it works

Read from the plugin source, the pipeline is:

  1. Unwrap and normalize. If the mesh already carries UVs, it keeps them; if not, it calls cumesh.uv_unwrap on the GPU to build charts and computes vertex normals. Then it centers and scales the mesh so the projection cameras match the convention of the pack's own Blender render script.
  2. Project per view. For each view it builds an orthographic camera from an azimuth and elevation, renders the mesh's depth with nvdiffrast, then walks every texel: project the texel's world position into the view, sample the image, and check visibility - inside the frame, inside a UV island, matching the rendered depth within depth_eps. Survivors get a weight of (facet normal · view direction) ^ blend_exponent × view weight.
  3. Accumulate and fill. Colour and weight are summed in texture space and divided, so a texel seen head-on by the front camera and at a grazing angle by a side camera comes out mostly front. Leftover texels (UV seams, small islands) are inpainted with OpenCV if fill_holes is on, and any base colour texture the mesh already had is kept where the projection isn't confident.

The camera convention is in the node's own docstring: Y-up, azimuth 0 = front, 90 = left, 180 = back, 270 = right; elevation +90 = top, −90 = bottom. Get it wrong and the texture lands on the wrong side of the model.

Inputs that matter

  • trimesh - the mesh, plus optional front_image / back_image / left_image / right_image / top_image / bottom_image, each with a matching *_weight. One view is enough; six is better.
  • The weights are the trap. Front and back default to 1.0; left, right, top and bottom default to 0.01. So wire up six views, change nothing, and you've effectively textured the model from the front and back only. Raise them to 0.5–1.0 for a real six-view bake, lower for a view you don't trust.
  • custom_images with custom_azimuths, custom_elevations, custom_weights - comma-separated strings, one value per image, matched in order. Use them for a 3/4 view or an eight-view ring instead of the six box faces. camera_config (a HY3DCAMERA from a Hunyuan3D-style camera node) supplies the same angles and weights when those strings are empty.
  • texture_size (4096 by default; drop to 1024/2048 while iterating, you pay per view) and ortho_scale, the full frame width in world units (2.0 means −1 to +1). The mesh is normalized to a diameter of 1.15, so the default 1.1 is marginally tighter than the object - match your source images' framing.
  • blend_exponent sharpens the blend towards the most head-on camera: crisper on flat faces, worse at the seam between two views. depth_eps is the visibility tolerance - small keeps back-facing geometry out of the front texture, larger fixes untextured speckle on curves.
  • use_metallic, add_alpha_channel, fill_holes, max_hole_size, mesh_cluster_threshold_cone_half_angle_rad - material and cleanup dials; the last is the UV unwrapper's chart-angle threshold and only matters when the mesh had no UVs.
  • norm_size - the odd one. The projection code overwrites it with a hard-coded 1.15 that matches its Blender camera setup. The widget does nothing.

Outputs: trimesh (a new mesh carrying the baked PBR material and UVs), base_color (an IMAGE - RGBA in practice), and metallic_roughness (an IMAGE in glTF packing: green = roughness, blue = metallic). Save the textures with a save-image node if you want them on disk, and export the mesh through SaveGLB/Preview3D.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/visualbruno/ComfyUI-Tools

The catch is dependencies, and this is the one node in the pack that has them. requirements.txt installs trimesh and rembg and leaves the projection stack commented out under "Used only for Projection": opencv-python, tqdm, nvdiffrast, and cumesh. Those are on you - nvdiffrast is NVIDIA's differentiable rasterizer and needs a matching CUDA toolchain to build, cumesh needs a custom build (the author ships a Windows fork). CUDA is mandatory, there's no CPU path, and six views at 4096² is slow - drop texture_size while you iterate.

Where people get burned

Unseen geometry stays untextured. The fundamental limitation, not a bug you can tune out: any surface no camera saw has no pixels to project, so it gets inpainted or stays blank. That's why the community's verdict on multiview projection is lukewarm. If your object has deep concavities or a hidden interior, keep the generator's own texture underneath and let this refine it.

pil2tensor is missing from the 3D module. In the pack source as of 16 September 2026, the return line calls pil2tensor, which nodes/threed_nodes.py never defines or imports - the helper lives in image_nodes.py. Practically: the bake runs, the console prints its progress, and then the node dies with a NameError. One line at the top of that file (from .image_nodes import pil2tensor) fixes it. Check whether your clone already has it before assuming you did something wrong.

Perspective renders won't line up. This is an orthographic projection against a normalized, centered mesh. Views rendered through a perspective camera, or with the subject off-center, smear the texture in a way that looks like bad UVs.

CategoryVisualBrunoTools/3d

Inputs (29)

NameTypeDefaultDescription
trimeshTRIMESH—
texture_sizeINT4096512–8192—
blend_textureBOOLEANtrue—
blend_exponentFLOAT1.00.1–99—
ortho_scaleFLOAT1.100.05–10—
norm_sizeFLOAT1.150–9.99—
fill_holesBOOLEANtrue—
max_hole_sizeINT200–99999—
use_metallicBOOLEANtrue—
depth_epsFLOAT0.01000.0001–1—
mesh_cluster_threshold_cone_half_angle_radFLOAT601–179—
add_alpha_channelBOOLEANfalse—
front_imageoptIMAGE—
back_imageoptIMAGE—
left_imageoptIMAGE—
right_imageoptIMAGE—
top_imageoptIMAGE—
bottom_imageoptIMAGE—
front_weightoptFLOAT1.0000.001–1—
back_weightoptFLOAT1.0000.001–1—
left_weightoptFLOAT0.0100.001–1—
right_weightoptFLOAT0.0100.001–1—
top_weightoptFLOAT0.0100.001–1—
bottom_weightoptFLOAT0.0100.001–1—
custom_imagesoptIMAGE—
custom_azimuthsoptSTRING—
custom_elevationsoptSTRING—
custom_weightsoptSTRING—
camera_configoptHY3DCAMERA—

Outputs (3)

NameTypeDescription
trimeshTRIMESH—
base_colorIMAGE—
metallic_roughnessIMAGE—