From 23511e028c8891c61c7cf4a91ef536e5ed5b7557 Mon Sep 17 00:00:00 2001 From: Bruno Fargnoli Date: Mon, 20 Apr 2026 12:40:42 +0200 Subject: [PATCH] Added the node "Fill Holes Nicely with Meshlib" --- README.md | 1 + nodes.py | 102 ++++++++++++++++++--- pyproject.toml | 2 +- trellis2/pipelines/trellis2_image_to_3d.py | 3 + 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b0a9645..ac10d32 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ | Date | Description | | --- | --- | +| **2026-04-20** | Added node "Fill Holes Nicely with Meshlib" | | **2026-04-06** | Added new "DINO-Lock" functionality
New fill_holes in Sparse Generator
Thanks to Easymode on Discord | | **2026-04-05** | Added node "Extract Images from Video"
Can be used with "Sparse Generator with ReconViaGen" | | **2026-04-04** | Added node "Sparse Generator with ReconViaGen" | diff --git a/nodes.py b/nodes.py index 0ef00f2..217da1c 100644 --- a/nodes.py +++ b/nodes.py @@ -1355,7 +1355,7 @@ class Trellis2MeshWithVoxelAdvancedGenerator: "hole_iterations": ("INT",{"default":1,"min":1,"max":9,"step":1}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "hole_fill_algorithm": (["morphological_closing","flood_fill","remove_small_holes"],{"default":"flood_fill"}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), "keep_only_shell": ("BOOLEAN",{"default":True}), @@ -1496,7 +1496,7 @@ class Trellis2MeshWithVoxelMultiViewGenerator: "hole_iterations": ("INT",{"default":1,"min":1,"max":9,"step":1}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "hole_fill_algorithm": (["morphological_closing","flood_fill","remove_small_holes"],{"default":"flood_fill"}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), "keep_only_shell": ("BOOLEAN",{"default":True}), @@ -2278,7 +2278,7 @@ class Trellis2MeshTexturing: "inpainting": (["telea","ns"],{"default":"telea"}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), }, } @@ -2354,7 +2354,7 @@ class Trellis2MeshTexturingMultiView: "inpainting": (["telea","ns"],{"default":"telea"}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), }, "optional": { @@ -2623,7 +2623,7 @@ class Trellis2MeshRefiner: "sampler": (["euler", "heun", "rk4", "rk5"], {"default": "euler"}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), }, } @@ -3051,7 +3051,7 @@ class Trellis2FillHolesWithMeshlib: last_reported_percent = -1 # Initialize at -1 to ensure 0% triggers an update - for i, e in enumerate(hole_edges): + for i, e in enumerate(hole_edges): params = mrmeshpy.FillHoleParams() params.metric = mrmeshpy.getUniversalMetric(mesh) mrmeshpy.fillHole(mesh, e, params) @@ -3090,6 +3090,82 @@ class Trellis2FillHolesWithMeshlib: return (mesh_copy, holes_filled) +class Trellis2FillHolesNicelyWithMeshlib: + """Fill all holes in a mesh""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mesh": ("MESHWITHVOXEL",), + }, + } + + RETURN_TYPES = ("MESHWITHVOXEL", "INT") + RETURN_NAMES = ("mesh", "holes_filled") + FUNCTION = "process" + CATEGORY = "Trellis2Wrapper" + DESCRIPTION = "Fill all holes in a mesh using optimal triangulation." + + def process(self, mesh): + import meshlib.mrmeshpy as mrmeshpy + + mesh_copy = copy.deepcopy(mesh) + mesh = mrmeshnumpy.meshFromFacesVerts(mesh_copy.faces.detach().clone().cpu().numpy(), mesh_copy.vertices.detach().clone().cpu().numpy()) + + hole_edges = mesh.topology.findHoleRepresentiveEdges() + holes_filled = 0 + + nb_holes = len(hole_edges) + print(f"{nb_holes} holes found") + + if nb_holes > 0: + progress_bar = tqdm(total=nb_holes, desc="Filling holes") + pbar = ProgressBar(nb_holes) + + last_reported_percent = -1 # Initialize at -1 to ensure 0% triggers an update + + for i, e in enumerate(hole_edges): + params = mrmeshpy.FillHoleNicelySettings() + params.triangulateParams.metric = mrmeshpy.getMinAreaMetric(mesh) + params.smoothCurvature = False + # Fill hole represented by `e` + mrmeshpy.fillHoleNicely(mesh, e, params) + + # Calculate current progress + current_step = i + 1 + current_percent = int((current_step / nb_holes) * 100) + + # Only update the UI if the percentage has moved up + if current_percent > last_reported_percent: + # Calculate how many holes have been filled since the last UI update + # This handles cases where 1% might represent multiple holes + if last_reported_percent == -1: + # First update + progress_bar.update(current_step) + pbar.update(current_step) + else: + # Update by the difference since the last check + last_step = int((last_reported_percent * nb_holes) / 100) + diff = current_step - last_step + progress_bar.update(diff) + pbar.update(diff) + + last_reported_percent = current_percent + + progress_bar.close() + + new_vertices = mrmeshnumpy.getNumpyVerts(mesh) + new_faces = mrmeshnumpy.getNumpyFaces(mesh.topology) + + del mesh + gc.collect() + + mesh_copy.vertices = torch.from_numpy(new_vertices).float().to(mesh_copy.device) + mesh_copy.faces = torch.from_numpy(new_faces).int().to(mesh_copy.device) + + return (mesh_copy, holes_filled) + class Trellis2SmoothNormals: @classmethod def INPUT_TYPES(s): @@ -3581,7 +3657,7 @@ class Trellis2MeshWithVoxelCascadeGenerator: "hole_iterations": ("INT",{"default":1,"min":1,"max":9,"step":1}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "hole_fill_algorithm": (["morphological_closing","flood_fill","remove_small_holes"],{"default":"flood_fill"}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), "keep_only_shell": ("BOOLEAN",{"default":True}), @@ -3763,7 +3839,7 @@ class Trellis2SparseGenerator: "hole_iterations": ("INT",{"default":1,"min":1,"max":9,"step":1}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "hole_fill_algorithm": (["morphological_closing","flood_fill","remove_small_holes"],{"default":"flood_fill"}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), "keep_only_shell": ("BOOLEAN",{"default":True}), @@ -3851,7 +3927,7 @@ class Trellis2ShapeGenerator: "shape_guidance_interval_end": ("FLOAT",{"default":1.00,"min":0.00,"max":1.00,"step":0.01}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), }, } @@ -3936,7 +4012,7 @@ class Trellis2ShapeCascadeGenerator: "shape_guidance_interval_end": ("FLOAT",{"default":1.00,"min":0.00,"max":1.00,"step":0.01}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), }, } @@ -4071,7 +4147,7 @@ class Trellis2TexSlatGenerator: "texture_guidance_interval_end": ("FLOAT",{"default":0.90,"min":0.00,"max":1.00,"step":0.01}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), }, } @@ -5022,7 +5098,7 @@ class Trellis2SparseGeneratorWithReconViaGen: "sparse_structure_guidance_interval_end": ("FLOAT",{"default":1.00,"min":0.00,"max":1.00,"step":0.01}), "verbose": ("BOOLEAN",{"default":False}), "dino_lock": ("FLOAT",{"default":0.00,"min":0.00,"max":1.00,"step":0.01}), - "dino_substeps": ("INT",{"default":4,"min":1,"max":9,"step":1}), + "dino_substeps": ("INT",{"default":4,"min":1,"max":99,"step":1}), "dino_foundation_cap": ("FLOAT",{"default":1.00,"min":0.01,"max":1.00,"step":0.01}), "fill_holes":("BOOLEAN",{"default":True}), "hole_iterations": ("INT",{"default":1,"min":1,"max":9,"step":1}), @@ -5594,6 +5670,7 @@ NODE_CLASS_MAPPINGS = { "Trellis2SparseGeneratorWithReconViaGen": Trellis2SparseGeneratorWithReconViaGen, "Trellis2ExtractImagesFromVideo": Trellis2ExtractImagesFromVideo, "Trellis2MaxTokensCalculator": Trellis2MaxTokensCalculator, + "Trellis2FillHolesNicelyWithMeshlib": Trellis2FillHolesNicelyWithMeshlib, } @@ -5656,4 +5733,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Trellis2SparseGeneratorWithReconViaGen": "Trellis2 - Sparse Generator with ReconViaGen", "Trellis2ExtractImagesFromVideo": "Trellis2 - Extract Images from Video", "Trellis2MaxTokensCalculator": "Trellis2 - Max Tokens Calculator", + "Trellis2FillHolesNicelyWithMeshlib": "Trellis2 - Fill Holes Nicely With Meshlib", } diff --git a/pyproject.toml b/pyproject.toml index 843b3fa..a986e5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "trellis2" description = "ComfyUI Wrapper for Microsoft Trellis.2 - Native and Compact Structured Latents for 3D Generation" -version = "1.0.22" +version = "1.0.23" license = {file = "LICENSE"} # classifiers = [ # # For OS-independent nodes (works on all operating systems) diff --git a/trellis2/pipelines/trellis2_image_to_3d.py b/trellis2/pipelines/trellis2_image_to_3d.py index 9eeafb7..c422771 100644 --- a/trellis2/pipelines/trellis2_image_to_3d.py +++ b/trellis2/pipelines/trellis2_image_to_3d.py @@ -3169,13 +3169,16 @@ class Trellis2ImageTo3DPipeline(Pipeline): if num_tokens < max_num_tokens: if hr_resolution != resolution: print(f"Due to the limited number of tokens, the resolution is reduced to {hr_resolution}.") + print(f"Num tokens {num_tokens}") break hr_resolution -= 128 if hr_resolution < 1024 and resolution >= 1024: hr_resolution = 1024 + print(f"Num tokens {num_tokens}") break if hr_resolution < 512: hr_resolution = 512 + print(f"Num tokens {num_tokens}") break coords_dev = coords.to(self.device)