π Smart Normals Conform
For the mesh that renders inside-out
- mesh
- mesh
- info
The symptom is unmistakable once you've seen it: the mesh looks fine in wireframe, then in a shaded preview it's black, or the lighting is inverted, or one shoulder is lit from inside while the rest is correct. That's inconsistent face winding - the triangles disagree about which way is "out".
Generated meshes have this constantly, and every node in this pack that deletes or adds faces can introduce a bit more of it. This is the node that cleans it up, and it's five lines of actual work with a reasonable amount of housekeeping around it.
What it does
- Copies the mesh and welds vertices with
merge_tex=True, merge_norm=True- so vertex UVs and custom normals are considered when deciding what counts as a duplicate. - Removes duplicate faces: it sorts each face's vertex indices, de-duplicates on the sorted triple, and keeps one of each. Two triangles on the same three vertices with opposite winding are a classic artifact of a bad merge or a boolean, and this is where they die.
remove_infinite_values()- cheap insurance against NaNs from a previous step's maths.fix_normals()- trimesh's standard pass, which makes winding consistent per connected component and then tries to orient each component outward.- If
invert_resultis on, the whole mesh gets inverted. - It then nulls the cached
face_normals/vertex_normalsso nothing stale gets re-used downstream.
The info output is just Success or No mesh input. Not much of a report, but there's nothing here that can silently half-work either - which is refreshing in this pack.
The two inputs
mesh- TRIMESH in.invert_result(default off) - flips every face. Use it whenfix_normalspicked the wrong side: a hollow container, an interior surface, a shell that's genuinely meant to be seen from inside. If your mesh came out fully inverted (whole model black rather than patchy), that's this switch.
Outputs: mesh then info.
When it helps, when it can't
fix_normals() is a well-understood operation: it builds the face-adjacency graph, propagates a consistent winding, and for closed components uses the enclosed volume sign to decide which way is out. So:
- Patchy wrongness (most of the model fine, one region inverted, wrong after a merge or a boolean) - fix it here.
- Everything inverted - fix it here, possibly with
invert_result. - An open, non-watertight surface with no volume to measure - consistency gets fixed, "outward" is a guess. There's nothing smarter available; that's the geometry's fault, not the node's.
- A mesh that's inside-out because it's a double shell (outer surface plus an inverted inner copy) - normals will be consistent and still wrong, because the actual problem is a second shell.
Mesh One SurfaceorMesh Fixer Fastis the node for that.
Where it goes in the chain
Run it after any destructive geometry pass and before you colour, texture, or export. It's cheap and it never changes vertex positions, so it's the safest node in the pack to sprinkle - but it also belongs before Refine Mesh and the colorizers look right, because both of those paint or smooth based on face normals. Smoothing a mesh with inconsistent winding is a good way to get a smooth mess.
One practical note for printing: Composite Mesh Saver writing an STL doesn't care about normals (STL stores its own per-facet normals from the winding, and most slicers re-derive orientation), so a normals pass is mostly about rendering and about what your downstream 3D software sees. For GLB/OBJ it matters much more.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/Antonioilev/ComfyUI_Antonioilev_Lightpack.git
python -m pip install trimesh
Restart ComfyUI. Manager will clone the pack for you (search Antonioilev Light Pack) - but the repo has no requirements.txt and no install.py, so nothing else gets installed, and this node's one dependency (trimesh) has to go into ComfyUI's own Python environment by hand. Portable builds: python_embeded\python.exe -m pip install trimesh. Then check the console banner - [Antonioilev_Light_pack] Loaded N nodes (M failed) - and if the node is missing from the menu entirely, the !!! line under that banner names the missing module.
There's a small amount of ceremony in this pack and most of it is that banner. Read it once per install and you'll skip the "the node doesn't exist" phase entirely.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| mesh | TRIMESH | β | |
| invert_result | BOOLEAN | false | β |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| mesh | TRIMESH | β |
| info | STRING | β |