diff --git a/example_workflows/_Example_Inputs_Files/meshes/robot.glb b/example_workflows/_Example_Inputs_Files/meshes/robot.glb deleted file mode 100644 index 8ab7f41..0000000 Binary files a/example_workflows/_Example_Inputs_Files/meshes/robot.glb and /dev/null differ diff --git a/loadmesh.py b/loadmesh.py deleted file mode 100644 index b1bb3c2..0000000 --- a/loadmesh.py +++ /dev/null @@ -1,269 +0,0 @@ -class Preview_3DMesh: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mesh_file_path": ("STRING", {"default": '', "multiline": False}), - }, - } - - OUTPUT_NODE = True - RETURN_TYPES = () - FUNCTION = "preview_mesh" - CATEGORY = "Comfy3D/Visualize" - - def preview_mesh(self, mesh_file_path): - - mesh_folder_path, filename = os.path.split(mesh_file_path) - - if not os.path.isabs(mesh_file_path): - mesh_file_path = os.path.join(comfy_paths.output_directory, mesh_folder_path, filename) - - if not filename.lower().endswith(SUPPORTED_3D_EXTENSIONS): - cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print() - mesh_file_path = "" - - print(f"[Preview_3DMesh] Final mesh path: {mesh_file_path}") - print(f"[Preview_3DMesh] File exists: {os.path.exists(mesh_file_path) if mesh_file_path else False}") - - previews = [ - { - "filepath": mesh_file_path, - } - ] - return {"ui": {"previews": previews}, "result": ()} - -class Load_3D_Mesh: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mesh_file_path": ("STRING", {"default": '', "multiline": False}), - "use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Использовать FastMesh для GLB/GLTF файлов"}), - "resize": ("BOOLEAN", {"default": False},), - "renormal": ("BOOLEAN", {"default": True},), - "retex": ("BOOLEAN", {"default": False},), - "optimizable": ("BOOLEAN", {"default": False},), - "clean": ("BOOLEAN", {"default": False},), - "resize_bound": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1000.0, "step": 0.001}), - }, - } - - RETURN_TYPES = ( - "MESH", - ) - RETURN_NAMES = ( - "mesh", - ) - FUNCTION = "load_mesh" - CATEGORY = "Comfy3D/Import|Export" - - def load_mesh(self, mesh_file_path, use_fastmesh, resize, renormal, retex, optimizable, clean, resize_bound): - import time - mesh = None - - if not os.path.isabs(mesh_file_path): - mesh_file_path = os.path.join(comfy_paths.input_directory, mesh_file_path) - - if os.path.exists(mesh_file_path): - folder, filename = os.path.split(mesh_file_path) - if filename.lower().endswith(SUPPORTED_3D_EXTENSIONS): - with torch.inference_mode(not optimizable): - start_time = time.time() - if use_fastmesh and mesh_file_path.endswith((".glb", ".gltf")): - mesh = FastMesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound) - print(f"[FastMesh] Loading completed in {time.time() - start_time:.3f}s") - else: - mesh = Mesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound) - print(f"[Mesh] Loading completed in {time.time() - start_time:.3f}s") - else: - cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print() - else: - cstr(f"[{self.__class__.__name__}] File {mesh_file_path} does not exist").error.print() - return (mesh, ) - -class Save_3D_Mesh: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mesh": ("MESH",), - "save_path": ("STRING", {"default": 'Mesh_%Y-%m-%d-%M-%S-%f.glb', "multiline": False}), - "use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Использовать FastMesh для быстрого сохранения GLB/PLY/OBJ"}), - }, - } - - OUTPUT_NODE = True - RETURN_TYPES = ( - "STRING", - ) - RETURN_NAMES = ( - "save_path", - ) - FUNCTION = "save_mesh" - CATEGORY = "Comfy3D/Import|Export" - - def save_mesh(self, mesh, save_path, use_fastmesh): - import time - save_path = parse_save_filename(save_path, comfy_paths.output_directory, SUPPORTED_3D_EXTENSIONS, self.__class__.__name__) - - if save_path is not None: - start_time = time.time() - - # Используем FastMesh методы для быстрого сохранения - if use_fastmesh and save_path.endswith((".glb", ".ply", ".obj")): - # Если это уже FastMesh - используем напрямую - print(f"[Save_3D_Mesh] FastMesh detected") - if hasattr(mesh, '_write_glb_fast'): - mesh.write(save_path) - save_time = time.time() - start_time - print(f"[FastMesh] Saving completed in {save_time:.3f}s") - else: - # Если обычный Mesh - создаем временный FastMesh для быстрых методов - print(f"[Save_3D_Mesh] REGULAR Mesh detected") - fast_mesh = FastMesh( - v=mesh.v, f=mesh.f, vn=mesh.vn, fn=mesh.fn, - vt=mesh.vt, ft=mesh.ft, vc=mesh.vc, - albedo=mesh.albedo, metallicRoughness=getattr(mesh, 'metallicRoughness', None), - device=mesh.device - ) - fast_mesh.ori_center = getattr(mesh, 'ori_center', 0) - fast_mesh.ori_scale = getattr(mesh, 'ori_scale', 1) - fast_mesh.write(save_path) - save_time = time.time() - start_time - print(f"[FastMesh] Saving completed in {save_time:.3f}s") - else: - # Обычное сохранение через стандартный метод - mesh.write(save_path) - save_time = time.time() - start_time - print(f"[Mesh] Saving completed in {save_time:.3f}s") - - return (save_path, ) - - -class Preview_3D_FastMesh: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mesh_file_path": ("STRING", {"default": '', "multiline": False}), - }, - } - - OUTPUT_NODE = True - RETURN_TYPES = () - FUNCTION = "preview_mesh" - CATEGORY = "Comfy3D/Visualize" - - def preview_mesh(self, mesh_file_path): - - mesh_folder_path, filename = os.path.split(mesh_file_path) - - if not os.path.isabs(mesh_file_path): - mesh_file_path = os.path.join(comfy_paths.output_directory, mesh_folder_path, filename) - - if not filename.lower().endswith(SUPPORTED_3D_EXTENSIONS): - cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print() - mesh_file_path = "" - - print(f"[Preview_3D_FastMesh] Final mesh path: {mesh_file_path}") - print(f"[Preview_3D_FastMesh] File exists: {os.path.exists(mesh_file_path) if mesh_file_path else False}") - - previews = [ - { - "filepath": mesh_file_path, - } - ] - return {"ui": {"previews": previews}, "result": ()} - - -class Load_3D_FastMesh: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mesh_file_path": ("STRING", {"default": '', "multiline": False}), - "use_fastmesh": ("BOOLEAN", {"default": True, "tooltip": "Использовать FastMesh загрузчик"}), - "resize": ("BOOLEAN", {"default": False},), - "renormal": ("BOOLEAN", {"default": True},), - "retex": ("BOOLEAN", {"default": False},), - "optimizable": ("BOOLEAN", {"default": False},), - "clean": ("BOOLEAN", {"default": False},), - "resize_bound": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1000.0, "step": 0.001}), - }, - } - - RETURN_TYPES = ( - "MESH", - ) - RETURN_NAMES = ( - "mesh", - ) - FUNCTION = "load_mesh" - CATEGORY = "Comfy3D/Import|Export" - - def load_mesh(self, mesh_file_path, use_fastmesh, resize, renormal, retex, optimizable, clean, resize_bound): - import time - mesh = None - - if not os.path.isabs(mesh_file_path): - mesh_file_path = os.path.join(comfy_paths.input_directory, mesh_file_path) - - if os.path.exists(mesh_file_path): - folder, filename = os.path.split(mesh_file_path) - if filename.lower().endswith(SUPPORTED_3D_EXTENSIONS): - with torch.inference_mode(not optimizable): - start_time = time.time() - if use_fastmesh and mesh_file_path.endswith((".glb", ".gltf")): - from mesh_processor.mesh import FastMesh - mesh = FastMesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound) - print(f"[FastMesh] Loading completed in {time.time() - start_time:.3f}s") - else: - mesh = Mesh.load(mesh_file_path, resize, renormal, retex, clean, resize_bound) - mesh_type = "FastMesh" if use_fastmesh else "Mesh" - print(f"[{mesh_type}] Loading completed in {time.time() - start_time:.3f}s") - else: - cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print() - else: - cstr(f"[{self.__class__.__name__}] File {mesh_file_path} does not exist").error.print() - return (mesh, ) - - -class Save_3D_FastMesh: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mesh": ("MESH",), - "save_path": ("STRING", {"default": 'FastMesh_%Y-%m-%d-%M-%S-%f.glb', "multiline": False}), - }, - } - - OUTPUT_NODE = True - RETURN_TYPES = ( - "STRING", - ) - RETURN_NAMES = ( - "save_path", - ) - FUNCTION = "save_mesh" - CATEGORY = "Comfy3D/Import|Export" - - def save_mesh(self, mesh, save_path): - import time - save_path = parse_save_filename(save_path, comfy_paths.output_directory, SUPPORTED_3D_EXTENSIONS, self.__class__.__name__) - - if save_path is not None: - start_time = time.time() - mesh.write(save_path) - save_time = time.time() - start_time - mesh_type = "FastMesh" if hasattr(mesh, '_create_empty_albedo') else "Mesh" - print(f"[{mesh_type}] Saving completed in {save_time:.3f}s") - - return (save_path, ) \ No newline at end of file diff --git a/mesh_processor/fastmesh.py b/mesh_processor/fastmesh.py index f9ea58d..a2d915f 100644 --- a/mesh_processor/fastmesh.py +++ b/mesh_processor/fastmesh.py @@ -130,10 +130,7 @@ class FastMesh: if colors is not None: mesh.vc = colors print(f"[FastMesh] Colors: {mesh.vc.shape}") - if not lazy_texture: - cls._extract_gltf_textures(mesh, doc, device) - else: - mesh._lazy_texture_doc = doc + cls._extract_gltf_textures_fast(mesh, doc, device, lazy_empty=lazy_texture) print(f"[FastMesh] Loaded: {len(vertices)} vertices, {len(faces)} faces") return mesh @@ -287,6 +284,50 @@ class FastMesh: except Exception as e: print(f"[FastMesh] Error colors: {e}") + @classmethod + def _extract_gltf_textures_fast(cls, mesh, doc, device, lazy_empty=True): + """Fast texture extraction - only load if exists, don't create empty if lazy_empty=True""" + + if not (doc.materials() and doc.textures() and doc.images()): + if not lazy_empty: + mesh._create_empty_albedo() + print(f"[FastMesh] No textures found - created empty albedo") + else: + print(f"[FastMesh] No textures found - skipping empty albedo creation") + return + + texture_loaded = False + + try: + material = doc.materials()[0] + pbr = material.get("pbrMetallicRoughness", {}) + + # Albedo texture + base_color_texture = pbr.get("baseColorTexture") + if base_color_texture: + albedo_texture = cls._extract_texture_by_index(doc, base_color_texture.get("index", 0)) + if albedo_texture is not None: + albedo_float = albedo_texture.astype(np.float32) / 255.0 + mesh.albedo = torch.tensor(albedo_float, dtype=torch.float32, device=device).contiguous() + texture_loaded = True + print(f"[FastMesh] Albedo texture loaded: {mesh.albedo.shape}") + + # Metallic-Roughness texture + mr_texture_info = pbr.get("metallicRoughnessTexture") + if mr_texture_info and texture_loaded: + mr_texture = cls._extract_texture_by_index(doc, mr_texture_info.get("index", 0)) + if mr_texture is not None: + mr_float = mr_texture.astype(np.float32) / 255.0 + mesh.metallicRoughness = torch.tensor(mr_float, dtype=torch.float32, device=device).contiguous() + print(f"[FastMesh] MetallicRoughness texture loaded: {mesh.metallicRoughness.shape}") + + except Exception as e: + print(f"[FastMesh] Error extracting textures: {e}") + + if not texture_loaded and not lazy_empty: + mesh._create_empty_albedo() + print(f"[FastMesh] Created empty albedo texture") + @classmethod def _extract_gltf_textures(cls, mesh, doc, device): """Extract textures from GLTF""" @@ -528,6 +569,11 @@ class FastMesh: def _write_glb_fast(self, path): """Fast GLB writing using mesh_processor""" try: + print(f"[FastMesh] Starting GLB save to: {path}") + print(f"[FastMesh] Mesh has albedo: {self.albedo is not None}") + print(f"[FastMesh] Mesh has UV: {self.vt is not None}") + print(f"[FastMesh] Mesh has metallicRoughness: {self.metallicRoughness is not None}") + # Prepare data vertices = self.v.detach().cpu().numpy().astype(np.float32) faces = self.f.detach().cpu().numpy().astype(np.uint32) @@ -572,6 +618,23 @@ class FastMesh: primitive["attributes"] = {"POSITION": pos_accessor} primitive["indices"] = idx_accessor + # Add UV coordinates if available + if self.vt is not None: + uv_data = self.vt.detach().cpu().numpy().astype(np.float32) + uv_accessor, _ = append_accessor_and_bufferview( + doc, uv_data, + component_type=5126, # FLOAT + element_type="VEC2", + target=34962 # ARRAY_BUFFER + ) + primitive["attributes"]["TEXCOORD_0"] = uv_accessor + print(f"[FastMesh] Added UV coordinates: {uv_data.shape}") + + # Add texture if available + if self.albedo is not None and self.vt is not None: + self._add_texture_to_gltf(doc, primitive) + print(f"[FastMesh] Added albedo texture: {self.albedo.shape}") + # Save to file with open(path, "wb") as f: save_glb(doc, f) @@ -582,6 +645,129 @@ class FastMesh: print(f"[FastMesh] Fast GLB save failed: {e}, using fallback") self._write_fallback(path) + def _add_texture_to_gltf(self, doc, primitive): + """Add texture data to GLTF document using fast approach""" + try: + # Convert albedo texture to PNG bytes + albedo_np = self.albedo.detach().cpu().numpy() + albedo_uint8 = (albedo_np * 255).astype(np.uint8) + + # Encode as PNG + import cv2 + albedo_bgr = cv2.cvtColor(albedo_uint8, cv2.COLOR_RGB2BGR) + success, png_bytes = cv2.imencode('.png', albedo_bgr) + if not success: + print(f"[FastMesh] Failed to encode texture as PNG") + return + + png_data = png_bytes.tobytes() + + # Add image data to buffer using helper function + image_accessor, _ = append_accessor_and_bufferview( + doc, np.frombuffer(png_data, dtype=np.uint8), + component_type=5121, # UNSIGNED_BYTE + element_type="SCALAR", + target=None # No specific target for image data + ) + + # Create GLTF structures + gltf_json = doc.json() + + # Initialize arrays if they don't exist + if "materials" not in gltf_json: + gltf_json["materials"] = [] + if "textures" not in gltf_json: + gltf_json["textures"] = [] + if "images" not in gltf_json: + gltf_json["images"] = [] + if "samplers" not in gltf_json: + gltf_json["samplers"] = [] + + # Add image + image_idx = len(gltf_json["images"]) + gltf_json["images"].append({ + "bufferView": image_accessor, + "mimeType": "image/png" + }) + + # Add sampler + sampler_idx = len(gltf_json["samplers"]) + gltf_json["samplers"].append({ + "magFilter": 9729, # LINEAR + "minFilter": 9987, # LINEAR_MIPMAP_LINEAR + "wrapS": 10497, # REPEAT + "wrapT": 10497 # REPEAT + }) + + # Add texture + texture_idx = len(gltf_json["textures"]) + gltf_json["textures"].append({ + "sampler": sampler_idx, + "source": image_idx + }) + + # Add material + material_idx = len(gltf_json["materials"]) + material = { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": texture_idx, + "texCoord": 0 + }, + "metallicFactor": 0.0, + "roughnessFactor": 1.0 + }, + "alphaMode": "OPAQUE", + "doubleSided": True + } + + # Add metallic-roughness texture if available + if self.metallicRoughness is not None: + mr_np = self.metallicRoughness.detach().cpu().numpy() + mr_uint8 = (mr_np * 255).astype(np.uint8) + mr_bgr = cv2.cvtColor(mr_uint8, cv2.COLOR_RGB2BGR) + mr_success, mr_png_bytes = cv2.imencode('.png', mr_bgr) + + if mr_success: + mr_png_data = mr_png_bytes.tobytes() + mr_image_accessor, _ = append_accessor_and_bufferview( + doc, np.frombuffer(mr_png_data, dtype=np.uint8), + component_type=5121, # UNSIGNED_BYTE + element_type="SCALAR", + target=None + ) + + # Add MR image and texture + mr_image_idx = len(gltf_json["images"]) + gltf_json["images"].append({ + "bufferView": mr_image_accessor, + "mimeType": "image/png" + }) + + mr_texture_idx = len(gltf_json["textures"]) + gltf_json["textures"].append({ + "sampler": sampler_idx, # Reuse same sampler + "source": mr_image_idx + }) + + # Update material + material["pbrMetallicRoughness"]["metallicRoughnessTexture"] = { + "index": mr_texture_idx, + "texCoord": 0 + } + material["pbrMetallicRoughness"]["metallicFactor"] = 1.0 + material["pbrMetallicRoughness"]["roughnessFactor"] = 1.0 + + print(f"[FastMesh] Added metallic-roughness texture: {mr_np.shape}") + + gltf_json["materials"].append(material) + + # Link material to primitive + primitive["material"] = material_idx + + except Exception as e: + print(f"[FastMesh] Error adding texture to GLTF: {e}") + def _write_ply_fast(self, path): """Fast PLY writing""" vertices = self.v.detach().cpu().numpy()