From 76531be650c80cf3cbd5be445ad0b3f136ca8507 Mon Sep 17 00:00:00 2001 From: Bruno Fargnoli Date: Wed, 4 Mar 2026 15:24:37 +0100 Subject: [PATCH] Experimental resolutions for sparse structure up to 128 (only for cascade pipelines) Increased DinoV3 Image resolution to 2048 --- README.md | 1 + nodes.py | 73 +++++++++++++++++++-- pyproject.toml | 2 +- trellis2/models/sparse_structure_vae.py | 24 ------- trellis2/modules/image_feature_extractor.py | 10 ++- trellis2/pipelines/trellis2_image_to_3d.py | 49 ++++++++++---- 6 files changed, 114 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 7aec3da..57e8eea 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ | Date | Description | | --- | --- | +| **2026-03-04** | Sparse Structure Resolution supported up to 128
Experimental for "cascade" pipelines only
Can increase the details | | **2026-02-27** | Added the Wheels for Windows Python 3.13, Torch 2.10.0, CUDA 13.1 | | **2026-02-26** | Added FP8 models
Added "sdpa" and "flash_attn_3" for the backend | | **2026-02-21** | Fixed "Vertical lines" bug | diff --git a/nodes.py b/nodes.py index a5ac339..20df00f 100644 --- a/nodes.py +++ b/nodes.py @@ -422,7 +422,7 @@ class Trellis2MeshWithVoxelGenerator: "texture_steps": ("INT",{"default":12, "min":1, "max":100},), "max_num_tokens": ("INT",{"default":49152,"min":0,"max":999999}), "max_views": ("INT", {"default": 4, "min": 1, "max": 16}), - "sparse_structure_resolution": ("INT", {"default":32,"min":8,"max":128,"step":8}), + "sparse_structure_resolution": ("INT", {"default":32,"min":32,"max":128,"step":4}), "generate_texture_slat": ("BOOLEAN", {"default":True}), "use_tiled_decoder": ("BOOLEAN", {"default":True}), }, @@ -1224,7 +1224,7 @@ class Trellis2MeshWithVoxelAdvancedGenerator: "texture_rescale_t": ("FLOAT",{"default":3.00,"min":0.00,"max":9.99,"step":0.01}), "max_num_tokens": ("INT",{"default":999999,"min":0,"max":999999}), "max_views": ("INT", {"default": 4, "min": 1, "max": 16}), - "sparse_structure_resolution": ("INT", {"default":32,"min":8,"max":128,"step":8}), + "sparse_structure_resolution": ("INT", {"default":32,"min":32,"max":128,"step":4}), "generate_texture_slat": ("BOOLEAN", {"default":True}), "sparse_structure_guidance_interval_start": ("FLOAT",{"default":0.10,"min":0.00,"max":1.00,"step":0.01}), "sparse_structure_guidance_interval_end": ("FLOAT",{"default":1.00,"min":0.00,"max":1.00,"step":0.01}), @@ -1325,7 +1325,7 @@ class Trellis2MeshWithVoxelMultiViewGenerator: "texture_guidance_rescale": ("FLOAT",{"default":0.20,"min":0.00,"max":1.00,"step":0.01}), "texture_rescale_t": ("FLOAT",{"default":3.00,"min":0.00,"max":9.99,"step":0.01}), "max_num_tokens": ("INT",{"default":999999,"min":0,"max":999999}), - "sparse_structure_resolution": ("INT", {"default":32,"min":8,"max":128,"step":8}), + "sparse_structure_resolution": ("INT", {"default":32,"min":32,"max":128,"step":4}), "generate_texture_slat": ("BOOLEAN", {"default":True}), "sparse_structure_guidance_interval_start": ("FLOAT",{"default":0.10,"min":0.00,"max":1.00,"step":0.01}), "sparse_structure_guidance_interval_end": ("FLOAT",{"default":1.00,"min":0.00,"max":1.00,"step":0.01}), @@ -2029,7 +2029,7 @@ class Trellis2ReconstructMeshWithQuad: "remesh_band": ("FLOAT",{"default":1.0}), "resolution": ([128,256,512,1024,2048],{"default":512}), "remove_floaters": ("BOOLEAN",{"default":True}), - "remove_inner_faces": ("BOOLEAN",{"default":False}), + "remove_inner_faces": ("BOOLEAN",{"default":False}), } } @@ -2429,6 +2429,10 @@ class Trellis2PostProcess2: "remove_duplicate_faces": ("BOOLEAN",{"default":True}), "weld_vertices": ("BOOLEAN",{"default":True}), "weld_vertices_digits": ("INT",{"default":4,"min":1,"max":8}), + "smooth": ("BOOLEAN",{"default":False}), + "smooth_iterations": ("INT",{"default":10,"min":1,"max":99,"step":1}), + "subdivide": ("BOOLEAN",{"default":False}), + "subdivide_iterations": ("INT",{"default":1,"min":1,"max":10}), }, } @@ -2438,7 +2442,7 @@ class Trellis2PostProcess2: CATEGORY = "Trellis2Wrapper" OUTPUT_NODE = True - def process(self, mesh, fill_holes, fix_normals, fix_face_orientation, remove_duplicate_faces, weld_vertices, weld_vertices_digits,): + def process(self, mesh, fill_holes, fix_normals, fix_face_orientation, remove_duplicate_faces, weld_vertices, weld_vertices_digits,smooth,smooth_iterations,subdivide,subdivide_iterations): mesh_copy = copy.deepcopy(mesh) vertices_np = mesh_copy.vertices.cpu().numpy() @@ -2480,8 +2484,18 @@ class Trellis2PostProcess2: nb_faces_removed = faces_count - new_faces_count print(f"Weld Vertices: Removed {nb_vertices_removed} vertices / {nb_faces_removed} faces") + if smooth: + print('Smoothing ...') + Trimesh.smoothing.filter_taubin(trimesh, lamb=0.5, nu=-0.53, iterations=smooth_iterations) + + if subdivide: + print('Subdividing ...') + trimesh = trimesh.subdivide_loop(iterations=subdivide_iterations) + new_vertices = torch.from_numpy(trimesh.vertices).float() - new_faces = torch.from_numpy(trimesh.faces).int() + new_faces = torch.from_numpy(trimesh.faces).int() + + print(f"After postprocessing: {len(new_faces)} faces") mesh_copy.vertices = new_vertices.to(mesh_copy.device) mesh_copy.faces = new_faces.to(mesh_copy.device) @@ -2736,7 +2750,7 @@ class Trellis2RemeshWithQuad: "remesh_project": ("FLOAT",{"default":0.0}), "dual_contouring_resolution": (["Auto","128","256","512","1024","2048"],{"default":"Auto"}), "remove_floaters": ("BOOLEAN",{"default":True}), - "remove_inner_faces": ("BOOLEAN",{"default":True}), + "remove_inner_faces": ("BOOLEAN",{"default":True}) } } @@ -3055,6 +3069,49 @@ class Trellis2FillHolesWithCuMesh: mesh_copy.fill_holes(max_hole_perimeter = max_permieters) return (mesh_copy,) + +class Trellis2LaplacianSmoothingWithOpen3d: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mesh": ("MESHWITHVOXEL",), + "iterations": ("INT",{"default":10, "min":1, "max":100}), + "method": (["Laplacian", "Taubin"],{"default":"Laplacian"}), + }, + } + + RETURN_TYPES = ("MESHWITHVOXEL", ) + RETURN_NAMES = ("mesh", ) + FUNCTION = "process" + CATEGORY = "Trellis2Wrapper" + OUTPUT_NODE = True + + def process(self, mesh, iterations, method): + import open3d + mesh_copy = copy.deepcopy(mesh) + vertices = mesh_copy.vertices.cpu().numpy() + faces = mesh_copy.faces.cpu().numpy().astype(np.int32) + + open3d_mesh = open3d.geometry.TriangleMesh() + open3d_mesh.vertices = open3d.utility.Vector3dVector(vertices) + open3d_mesh.triangles = open3d.utility.Vector3iVector(faces) + + if method == "Laplacian": + open3d_mesh = open3d_mesh.filter_smooth_laplacian(number_of_iterations=iterations) + elif method == "Taubin": + open3d_mesh = open3d_mesh.filter_smooth_taubin(number_of_iterations=iterations) + + open3d_mesh.compute_vertex_normals() + + new_vertices = np.asarray(open3d_mesh.vertices) + new_faces = np.asarray(open3d_mesh.triangles) + + 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,) + NODE_CLASS_MAPPINGS = { "Trellis2LoadModel": Trellis2LoadModel, @@ -3090,6 +3147,7 @@ NODE_CLASS_MAPPINGS = { "Trellis2ReconstructMeshWithQuad": Trellis2ReconstructMeshWithQuad, "Trellis2StringSelector": Trellis2StringSelector, "Trellis2FillHolesWithCuMesh": Trellis2FillHolesWithCuMesh, + "Trellis2LaplacianSmoothingWithOpen3d": Trellis2LaplacianSmoothingWithOpen3d, } @@ -3127,4 +3185,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Trellis2ReconstructMeshWithQuad": "Trellis2 - Reconstruct Mesh With Quad", "Trellis2StringSelector": "Trellis2 - String Selector", "Trellis2FillHolesWithCuMesh": "Trellis2 - Fill Holes with CuMesh", + "Trellis2LaplacianSmoothingWithOpen3d": "Trellis2 - Laplacian Smoothing (using open3d)", } diff --git a/pyproject.toml b/pyproject.toml index 6a8f6cd..a7fa8d0 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.12" +version = "1.0.13" license = {file = "LICENSE"} # classifiers = [ # # For OS-independent nodes (works on all operating systems) diff --git a/trellis2/models/sparse_structure_vae.py b/trellis2/models/sparse_structure_vae.py index 36714a9..5157882 100644 --- a/trellis2/models/sparse_structure_vae.py +++ b/trellis2/models/sparse_structure_vae.py @@ -99,18 +99,6 @@ class UpsampleBlock3d(nn.Module): class SparseStructureEncoder(nn.Module): - """ - Encoder for Sparse Structure (\mathcal{E}_S in the paper Sec. 3.3). - - Args: - in_channels (int): Channels of the input. - latent_channels (int): Channels of the latent representation. - num_res_blocks (int): Number of residual blocks at each resolution. - channels (List[int]): Channels of the encoder blocks. - num_res_blocks_middle (int): Number of residual blocks in the middle. - norm_type (Literal["group", "layer"]): Type of normalization layer. - use_fp16 (bool): Whether to use FP16. - """ def __init__( self, in_channels: int, @@ -222,18 +210,6 @@ class SparseStructureEncoder(nn.Module): class SparseStructureDecoder(nn.Module): - """ - Decoder for Sparse Structure (\mathcal{D}_S in the paper Sec. 3.3). - - Args: - out_channels (int): Channels of the output. - latent_channels (int): Channels of the latent representation. - num_res_blocks (int): Number of residual blocks at each resolution. - channels (List[int]): Channels of the decoder blocks. - num_res_blocks_middle (int): Number of residual blocks in the middle. - norm_type (Literal["group", "layer"]): Type of normalization layer. - use_fp16 (bool): Whether to use FP16. - """ def __init__( self, out_channels: int, diff --git a/trellis2/modules/image_feature_extractor.py b/trellis2/modules/image_feature_extractor.py index c3cb515..bf152c8 100644 --- a/trellis2/modules/image_feature_extractor.py +++ b/trellis2/modules/image_feature_extractor.py @@ -60,7 +60,7 @@ class DinoV3FeatureExtractor: """ Feature extractor for DINOv3 models. """ - def __init__(self, model_name: str, image_size=512): + def __init__(self, model_name: str, image_size=2048): self.model_name = model_name self.model = DINOv3ViTModel.from_pretrained(model_name) self.model.eval() @@ -106,7 +106,13 @@ class DinoV3FeatureExtractor: assert image.ndim == 4, "Image tensor should be batched (B, C, H, W)" elif isinstance(image, list): assert all(isinstance(i, Image.Image) for i in image), "Image list should be list of PIL images" - image = [i.resize((self.image_size, self.image_size), Image.LANCZOS) for i in image] + # We resize the images only if they are bigger than self.image_size + image = [ + i.resize((self.image_size, self.image_size), Image.LANCZOS) + if max(i.size) > self.image_size else i + for i in image + ] + #image = [i.resize((self.image_size, self.image_size), Image.LANCZOS) for i in image] image = [np.array(i.convert('RGB')).astype(np.float32) / 255 for i in image] image = [torch.from_numpy(i).permute(2, 0, 1).float() for i in image] image = torch.stack(image).cuda() diff --git a/trellis2/pipelines/trellis2_image_to_3d.py b/trellis2/pipelines/trellis2_image_to_3d.py index 0a36b84..1586108 100644 --- a/trellis2/pipelines/trellis2_image_to_3d.py +++ b/trellis2/pipelines/trellis2_image_to_3d.py @@ -497,9 +497,15 @@ class Trellis2ImageTo3DPipeline(Pipeline): decoded = decoder(z_s)>0 if self.low_vram: decoder.cpu() + # if resolution != decoded.shape[2]: + # ratio = decoded.shape[2] // resolution + # decoded = torch.nn.functional.max_pool3d(decoded.float(), ratio, ratio, 0) > 0.5 if resolution != decoded.shape[2]: - ratio = decoded.shape[2] // resolution - decoded = torch.nn.functional.max_pool3d(decoded.float(), ratio, ratio, 0) > 0.5 + if resolution < decoded.shape[2]: + ratio = decoded.shape[2] // resolution + decoded = torch.nn.functional.max_pool3d(decoded.float(), ratio, ratio, 0) > 0.5 + else: + decoded = torch.nn.functional.interpolate(decoded.float(), size=(resolution, resolution, resolution), mode='nearest') > 0.5 coords = torch.argwhere(decoded)[:, [0, 2, 3, 4]].int() coords = coords.cpu() @@ -571,6 +577,7 @@ class Trellis2ImageTo3DPipeline(Pipeline): coords: torch.Tensor, sampler_params: dict = {}, max_num_tokens: int = 49152, + sparse_structure_resolution: int = 32, ) -> SparseTensor: """ Sample structured latent with the given conditioning. @@ -628,23 +635,28 @@ class Trellis2ImageTo3DPipeline(Pipeline): if not self.keep_models_loaded: self.unload_shape_slat_decoder() + + ratio = (sparse_structure_resolution / 32) while True: quant_coords = torch.cat([ hr_coords[:, :1], - ((hr_coords[:, 1:] + 0.5) / lr_resolution * (hr_resolution // 16)).int(), + ((hr_coords[:, 1:] + 0.5) / (lr_resolution * ratio) * (hr_resolution // 16)).int(), ], dim=1) coords = quant_coords.unique(dim=0) num_tokens = coords.shape[0] 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: + print(f"Num Tokens: {num_tokens}") hr_resolution = 1024 break if hr_resolution < 512: + print(f"Num Tokens: {num_tokens}") hr_resolution = 512 break @@ -1015,7 +1027,8 @@ class Trellis2ImageTo3DPipeline(Pipeline): self.models['shape_slat_flow_model_512'], self.models['shape_slat_flow_model_1024'], 512, 1024, coords, shape_slat_sampler_params, - max_num_tokens + max_num_tokens, + sparse_structure_resolution ) if pbar is not None: @@ -1046,7 +1059,8 @@ class Trellis2ImageTo3DPipeline(Pipeline): self.models['shape_slat_flow_model_512'], self.models['shape_slat_flow_model_1024'], 512, 2048, coords, shape_slat_sampler_params, - max_num_tokens + max_num_tokens, + sparse_structure_resolution ) if pbar is not None: @@ -1077,7 +1091,8 @@ class Trellis2ImageTo3DPipeline(Pipeline): self.models['shape_slat_flow_model_512'], self.models['shape_slat_flow_model_1024'], 512, 4096, coords, shape_slat_sampler_params, - max_num_tokens + max_num_tokens, + sparse_structure_resolution ) if pbar is not None: @@ -1108,7 +1123,8 @@ class Trellis2ImageTo3DPipeline(Pipeline): self.models['shape_slat_flow_model_512'], self.models['shape_slat_flow_model_1024'], 512, 1536, coords, shape_slat_sampler_params, - max_num_tokens + max_num_tokens, + sparse_structure_resolution ) if pbar is not None: @@ -1255,6 +1271,7 @@ class Trellis2ImageTo3DPipeline(Pipeline): max_num_tokens, front_axis=front_axis, blend_temperature=blend_temperature, + sparse_structure_resolution = sparse_structure_resolution ) res = 1024 @@ -1273,6 +1290,7 @@ class Trellis2ImageTo3DPipeline(Pipeline): max_num_tokens, front_axis=front_axis, blend_temperature=blend_temperature, + sparse_structure_resolution = sparse_structure_resolution ) res = 1536 @@ -1417,9 +1435,15 @@ class Trellis2ImageTo3DPipeline(Pipeline): decoder.cpu() self._cleanup_cuda() + # if resolution != decoded.shape[2]: + # ratio = decoded.shape[2] // resolution + # decoded = torch.nn.functional.max_pool3d(decoded.float(), ratio, ratio, 0) > 0.5 if resolution != decoded.shape[2]: - ratio = decoded.shape[2] // resolution - decoded = torch.nn.functional.max_pool3d(decoded.float(), ratio, ratio, 0) > 0.5 + if resolution < decoded.shape[2]: + ratio = decoded.shape[2] // resolution + decoded = torch.nn.functional.max_pool3d(decoded.float(), ratio, ratio, 0) > 0.5 + else: + decoded = torch.nn.functional.interpolate(decoded.float(), size=(resolution, resolution, resolution), mode='nearest') > 0.5 # Extract coordinates (N, 4) -> (b, d, h, w) # argwhere returns (b, c, d, h, w), so we want [0, 2, 3, 4] @@ -1507,6 +1531,7 @@ class Trellis2ImageTo3DPipeline(Pipeline): max_num_tokens: int = 49152, front_axis: str = 'z', blend_temperature: float = 2.0, + sparse_structure_resolution: int = 32, ) -> SparseTensor: # LR if self.low_vram: @@ -1561,12 +1586,14 @@ class Trellis2ImageTo3DPipeline(Pipeline): if self.low_vram: self.models['shape_slat_decoder'].cpu() self.models['shape_slat_decoder'].low_vram = False - + + ratio = sparse_structure_resolution / 32 + hr_resolution = resolution while True: quant_coords = torch.cat([ hr_coords[:, :1], - ((hr_coords[:, 1:] + 0.5) / lr_resolution * (hr_resolution // 16)).int(), + ((hr_coords[:, 1:] + 0.5) / (lr_resolution * ratio) * (hr_resolution // 16)).int(), ], dim=1) coords = quant_coords.unique(dim=0) num_tokens = coords.shape[0]