import base64 import io import os import struct import tempfile import numpy as np import torch from PIL import Image from server import BinaryEventTypes, PromptServer class Hy3DtoTD: #https://github.com/kijai/ComfyUI-Hunyuan3DWrapper @classmethod def INPUT_TYPES(cls): return { "required": { "broadcast": ("BOOLEAN", {"default": False}), }, "optional": { "trimesh": ("TRIMESH",), "mesh": ("MESH",), } } RETURN_TYPES = () FUNCTION = "send_mesh" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_mesh(self, broadcast, trimesh=None, mesh=None): try: if trimesh is not None and mesh is not None: raise ValueError("Please connect only one input: trimesh or mesh, not both.") if trimesh is not None: vertices, colors = self.process_trimesh(trimesh) elif mesh is not None: vertices, colors = self.process_comfy_mesh(mesh) else: raise ValueError("Please connect either a trimesh or mesh input.") return self._send_to_td(vertices, colors, broadcast) except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def process_trimesh(self, mesh): try: import trimesh if isinstance(mesh, trimesh.Scene): meshes = [] for m in mesh.geometry.values(): if isinstance(m, trimesh.Trimesh): meshes.append(m) if meshes: mesh = trimesh.util.concatenate(meshes) else: raise ValueError("No valid mesh data found") if not isinstance(mesh, trimesh.Trimesh): raise ValueError("Input data is not a valid Trimesh object") mesh.fix_normals() vertices = np.asarray(mesh.vertices, dtype=np.float32) vertex_count = len(vertices) if hasattr(mesh.visual, "vertex_colors") and mesh.visual.vertex_colors is not None: colors = np.asarray(mesh.visual.vertex_colors) elif hasattr(mesh.visual, "uv") and mesh.visual.uv is not None: try: mesh.visual = mesh.visual.to_color() colors = np.asarray(mesh.visual.vertex_colors) except Exception: colors = np.tile([200, 200, 200, 255], (vertex_count, 1)) else: colors = np.tile([200, 200, 200, 255], (vertex_count, 1)) colors = self._normalize_colors(colors, vertex_count) return vertices, colors except ImportError: raise ImportError("Please install the trimesh library: pip install trimesh") except Exception as e: raise ValueError(f"Error processing trimesh: {str(e)}") def process_comfy_mesh(self, mesh): try: if isinstance(mesh, list): if len(mesh) == 0: raise ValueError("Empty mesh list received") mesh = mesh[0] vertices = self._extract_comfy_mesh_vertices(mesh) vertex_count = len(vertices) colors = self._extract_comfy_mesh_colors(mesh, vertex_count) colors = self._normalize_colors(colors, vertex_count) return vertices, colors except Exception as e: raise ValueError(f"Error processing ComfyUI mesh: {str(e)}") def _extract_comfy_mesh_vertices(self, mesh): vertices = None if hasattr(mesh, "vertices") and mesh.vertices is not None: vertices = mesh.vertices elif isinstance(mesh, dict) and "vertices" in mesh: vertices = mesh["vertices"] elif isinstance(mesh, (tuple, list)) and len(mesh) > 0: vertices = mesh[0] if vertices is None: raise ValueError("Cannot find vertices in ComfyUI mesh") vertices = self._to_numpy(vertices) vertices = self._squeeze_batch(vertices) vertices = np.asarray(vertices, dtype=np.float32) if vertices.ndim != 2 or vertices.shape[1] < 3: raise ValueError(f"Invalid vertices shape: {vertices.shape}") return vertices[:, :3] def _extract_comfy_mesh_colors(self, mesh, vertex_count): colors = None if hasattr(mesh, "vertex_colors") and mesh.vertex_colors is not None: colors = mesh.vertex_colors elif hasattr(mesh, "colors") and mesh.colors is not None: colors = mesh.colors elif hasattr(mesh, "visual") and mesh.visual is not None: if hasattr(mesh.visual, "vertex_colors") and mesh.visual.vertex_colors is not None: colors = mesh.visual.vertex_colors elif isinstance(mesh, dict) and "vertex_colors" in mesh: colors = mesh["vertex_colors"] elif isinstance(mesh, dict) and "colors" in mesh: colors = mesh["colors"] if colors is None: colors = np.tile([200, 200, 200, 255], (vertex_count, 1)) return colors def _to_numpy(self, value): if value is None: return None if hasattr(value, "detach"): value = value.detach() if hasattr(value, "cpu"): value = value.cpu() if hasattr(value, "numpy"): value = value.numpy() return np.asarray(value) def _squeeze_batch(self, value): value = np.asarray(value) if value.ndim == 3 and value.shape[0] == 1: value = value[0] return value def _normalize_colors(self, colors, vertex_count): colors = self._to_numpy(colors) if colors is None: colors = np.tile([200, 200, 200, 255], (vertex_count, 1)) colors = self._squeeze_batch(colors) colors = np.asarray(colors) if colors.ndim == 1: if colors.shape[0] in (3, 4): colors = np.tile(colors, (vertex_count, 1)) else: colors = np.column_stack([colors, colors, colors]) if colors.ndim != 2: colors = np.tile([200, 200, 200, 255], (vertex_count, 1)) if len(colors) != vertex_count: if len(colors) > vertex_count: colors = colors[:vertex_count] elif len(colors) > 0: last_color = colors[-1] padding = np.tile(last_color, (vertex_count - len(colors), 1)) colors = np.vstack([colors, padding]) else: colors = np.tile([200, 200, 200, 255], (vertex_count, 1)) if colors.dtype != np.uint8: if colors.size > 0 and colors.max() <= 1.0: colors = (np.clip(colors, 0, 1) * 255).astype(np.uint8) else: colors = np.clip(colors, 0, 255).astype(np.uint8) if colors.shape[1] == 1: colors = np.repeat(colors, 3, axis=1) if colors.shape[1] == 2: colors = np.repeat(colors[:, :1], 3, axis=1) if colors.shape[1] == 3: alpha = np.full((vertex_count, 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) if colors.shape[1] > 4: colors = colors[:, :4] if colors.shape[1] < 4: default_colors = np.tile([200, 200, 200, 255], (vertex_count, 1)).astype(np.uint8) default_colors[:, :colors.shape[1]] = colors colors = default_colors return colors[:, :4].astype(np.uint8) def _send_to_td(self, vertices, colors, broadcast): try: vertices = np.asarray(vertices, dtype=np.float32) vertex_count = len(vertices) colors = self._normalize_colors(colors, vertex_count) dtype_vertex = np.dtype([ ("pos", "I", type_num) bytesIO.write(header) image.save(bytesIO, format="JPEG", quality=quality) jpeg_bytes = bytesIO.getvalue() server = PromptServer.instance sid = None if broadcast else server.client_id server.send_sync( BinaryEventTypes.PREVIEW_IMAGE, jpeg_bytes, sid, ) results.append({ "source": "websocket", "content-type": "image/jpeg", "type": "output", }) return {"ui": {"images": results}} class Comfy3DPacktoTD: #https://github.com/MrForExample/ComfyUI-3D-Pack @classmethod def INPUT_TYPES(cls): return { "required": { "mesh": ("MESH",), "broadcast": ("BOOLEAN", {"default": False}), }, } RETURN_TYPES = () FUNCTION = "send_to_td" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_to_td(self, mesh, broadcast): try: vertices = mesh.v.cpu().numpy().astype(np.float32) colors = None color_source = "none" if hasattr(mesh, 'albedo') and mesh.albedo is not None: albedo = mesh.albedo if hasattr(albedo, 'cpu'): albedo = albedo.cpu() if hasattr(albedo, 'numpy'): albedo = albedo.numpy() if len(albedo.shape) > 1: if hasattr(mesh, 'vt') and mesh.vt is not None: uvs = mesh.vt.cpu().numpy() uvs = np.clip(uvs, 0, 1) tex_h, tex_w = albedo.shape[:2] mapped_u, mapped_v = uvs[:, 0], uvs[:, 1] tex_x = mapped_u * (tex_w - 1) tex_y = mapped_v * (tex_h - 1) tex_x0 = np.floor(tex_x).astype(np.int32) tex_y0 = np.floor(tex_y).astype(np.int32) tex_x1 = np.minimum(tex_x0 + 1, tex_w - 1) tex_y1 = np.minimum(tex_y0 + 1, tex_h - 1) wx = tex_x - tex_x0 wy = tex_y - tex_y0 c00 = albedo[tex_y0, tex_x0] c01 = albedo[tex_y0, tex_x1] c10 = albedo[tex_y1, tex_x0] c11 = albedo[tex_y1, tex_x1] c0 = c00 * (1 - wx[:, np.newaxis]) + c01 * wx[:, np.newaxis] c1 = c10 * (1 - wx[:, np.newaxis]) + c11 * wx[:, np.newaxis] colors = c0 * (1 - wy[:, np.newaxis]) + c1 * wy[:, np.newaxis] color_source = "albedo_with_uv_no_v_flip" else: if albedo.shape[0] == len(vertices): colors = albedo color_source = "albedo_direct" else: colors = albedo color_source = "albedo_scalar" if colors is None and hasattr(mesh, 'vc') and mesh.vc is not None: colors = mesh.vc.cpu().numpy() color_source = "vertex_colors" if colors is None: positions = vertices - vertices.min(axis=0) positions = positions / positions.max(axis=0) if positions.max() > 0 else positions colors = positions color_source = "generated" if colors is not None: if colors.max() <= 1.0: colors = (colors * 255).astype(np.uint8) else: colors = np.clip(colors, 0, 255).astype(np.uint8) if colors.shape[1] == 3: alpha = np.full((len(vertices), 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {len(vertices)}", "property float x", "property float y", "property float z", "property uchar red", "property uchar green", "property uchar blue", "property uchar alpha", "end_header\n" ] header = '\n'.join(header).encode('ascii') binary_data = bytearray() for i in range(len(vertices)): binary_data.extend(struct.pack(' 0: mesh_obj = mesh[0] else: raise ValueError("Empty mesh list received") else: mesh_obj = mesh if hasattr(mesh_obj, 'vertices'): vertices = mesh_obj.vertices if hasattr(vertices, 'cpu'): vertices = vertices.cpu() if hasattr(vertices, 'numpy'): vertices = vertices.numpy() vertices = vertices.astype(np.float32) else: raise ValueError("Cannot find vertex data in mesh object") rotated_vertices = vertices.copy() rotated_vertices[:, 0] = -vertices[:, 0] rotated_vertices[:, 1], rotated_vertices[:, 2] = vertices[:, 2], vertices[:, 1] final_vertices = rotated_vertices.copy() final_vertices[:, 0] = rotated_vertices[:, 2] final_vertices[:, 2] = -rotated_vertices[:, 0] vertices = final_vertices colors = None if hasattr(mesh_obj, 'visual') and mesh_obj.visual is not None: visual = mesh_obj.visual if hasattr(visual, 'vertex_colors') and visual.vertex_colors is not None: colors = visual.vertex_colors if hasattr(colors, 'cpu'): colors = colors.cpu() if hasattr(colors, 'numpy'): colors = colors.numpy() elif hasattr(visual, 'face_colors') and visual.face_colors is not None: face_colors = visual.face_colors if hasattr(face_colors, 'cpu'): face_colors = face_colors.cpu() if hasattr(face_colors, 'numpy'): face_colors = face_colors.numpy() if hasattr(mesh_obj, 'faces') and mesh_obj.faces is not None: faces = mesh_obj.faces if hasattr(faces, 'cpu'): faces = faces.cpu() if hasattr(faces, 'numpy'): faces = faces.numpy() colors = np.zeros((len(vertices), 4), dtype=np.uint8) vertex_color_count = np.zeros(len(vertices), dtype=np.int32) for i, face in enumerate(faces): for vertex_idx in face: colors[vertex_idx] += face_colors[i] vertex_color_count[vertex_idx] += 1 for i in range(len(vertices)): if vertex_color_count[i] > 0: colors[i] = colors[i] // vertex_color_count[i] elif hasattr(visual, 'material') and visual.material is not None: material = visual.material if hasattr(material, 'diffuse') and material.diffuse is not None: diffuse = material.diffuse colors = np.tile(diffuse, (len(vertices), 1)) if colors is None and hasattr(mesh_obj, '_visual') and mesh_obj._visual is not None: visual = mesh_obj._visual if hasattr(visual, 'vertex_colors') and visual.vertex_colors is not None: colors = visual.vertex_colors if hasattr(colors, 'cpu'): colors = colors.cpu() if hasattr(colors, 'numpy'): colors = colors.numpy() if colors is None and hasattr(mesh_obj, 'visual') and hasattr(mesh_obj.visual, 'uv') and mesh_obj.visual.uv is not None: if hasattr(mesh_obj.visual, 'material') and hasattr(mesh_obj.visual.material, 'image'): uvs = mesh_obj.visual.uv image = mesh_obj.visual.material.image h, w = image.shape[:2] u = np.clip(uvs[:, 0], 0, 1) * (w - 1) v = np.clip(uvs[:, 1], 0, 1) * (h - 1) u_int = np.round(u).astype(np.int32) v_int = np.round(v).astype(np.int32) u_int = np.clip(u_int, 0, w - 1) v_int = np.clip(v_int, 0, h - 1) colors = image[v_int, u_int] if colors is None: np.random.seed(42) colors = np.random.randint(100, 200, size=(len(vertices), 3), dtype=np.uint8) alpha = np.full((len(vertices), 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) if colors is not None: if len(colors.shape) == 1: colors = np.column_stack([colors, colors, colors]) if colors.max() <= 1.0 and colors.dtype != np.uint8: colors = (colors * 255).astype(np.uint8) else: colors = np.clip(colors, 0, 255).astype(np.uint8) if colors.shape[1] == 3: alpha = np.full((len(vertices), 1), 255, dtype=np.uint8) colors = np.concatenate([colors, alpha], axis=1) elif colors.shape[1] > 4: colors = colors[:, :4] if len(colors) != len(vertices): if len(colors) > len(vertices): colors = colors[:len(vertices)] else: last_color = colors[-1] padding = np.tile(last_color, (len(vertices) - len(colors), 1)) colors = np.vstack([colors, padding]) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {len(vertices)}", "property float x", "property float y", "property float z", "property uchar red", "property uchar green", "property uchar blue", "property uchar alpha", "end_header\n" ] header = '\n'.join(header).encode('ascii') binary_data = bytearray() for i in range(len(vertices)): binary_data.extend(struct.pack(' 1: waveform = waveform[0:1, ...] waveform = waveform[0] with torch.no_grad(): max_abs = waveform.abs().amax() if torch.isfinite(max_abs) and max_abs > 1.0: waveform = waveform / max_abs wav_np = waveform.detach().cpu().float().numpy() if wav_np.ndim != 2: raise ValueError( f"Waveform must be [C, T] after squeeze, got {wav_np.shape}" ) wav_np = np.transpose(wav_np, (1, 0)) audio_binary = self._encode_wav(wav_np, sample_rate) return self._send_to_td(audio_binary, broadcast) def _encode_wav(self, wav_np, sample_rate): import wave if wav_np.ndim == 1: wav_np = wav_np[:, None] channels = wav_np.shape[1] pcm16 = np.clip(wav_np, -1.0, 1.0) pcm16 = (pcm16 * 32767.0).astype(" 65535: raise ValueError("layer_tag is too long, max 65535 bytes") header = struct.pack( ">4sBBHI", self.MAGIC, self.VERSION, fmt_code, len(tag_bytes), len(image_bytes), ) return header + tag_bytes + image_bytes