import trimesh import struct import numpy as np import io from server import PromptServer, BinaryEventTypes from pathlib import Path from PIL import Image import base64 from io import BytesIO import torch import tempfile import os class Hy3DtoTD: @classmethod def INPUT_TYPES(s): return { "required": { "trimesh": ("TRIMESH",), "broadcast": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = () FUNCTION = "send_mesh" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_mesh(self, trimesh, broadcast): processed_mesh = self.process_mesh(trimesh) return self._send_to_td(processed_mesh, broadcast) def process_mesh(self, mesh): try: 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() if hasattr(mesh.visual, 'vertex_colors') and mesh.visual.vertex_colors is not None: pass elif hasattr(mesh.visual, 'uv') and mesh.visual.uv is not None: mesh.visual = mesh.visual.to_color() else: mesh.visual = trimesh.visual.ColorVisuals(mesh) mesh.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(mesh.vertices), 1)) return mesh except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def _send_to_td(self, mesh, broadcast): try: buffer = io.BytesIO() mesh.export(file_obj=buffer, file_type='ply') binary_data = buffer.getvalue() buffer.close() server = PromptServer.instance sid = None if broadcast else server.client_id server.send_sync(1000, binary_data, sid=sid) return {"ui": { "mesh": [{ "source": "websocket", "content-type": "model/ply", "type": "output", }] }} except Exception as e: raise ValueError(f"Error sending mesh: {str(e)}") class Tripo3DtoTD: @classmethod def INPUT_TYPES(s): return { "required": { "model_file": ("STRING",), "broadcast": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = () FUNCTION = "send_mesh" OUTPUT_NODE = True CATEGORY = "TouchDesigner" def send_mesh(self, model_file, broadcast): processed_mesh = self.process_mesh(model_file) return self._send_to_td(processed_mesh, broadcast) def process_mesh(self, model_file): try: mesh = trimesh.load(model_file) if isinstance(mesh, trimesh.Scene): meshes = [] for m in mesh.geometry.values(): if isinstance(m, trimesh.Trimesh): self._process_vertex_colors(m) 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() if not hasattr(mesh.visual, 'vertex_colors') or mesh.visual.vertex_colors is None: mesh.visual = trimesh.visual.ColorVisuals(mesh) mesh.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(mesh.vertices), 1)) else: if mesh.visual.vertex_colors.max() <= 1.0: mesh.visual.vertex_colors = (mesh.visual.vertex_colors * 255).astype(np.uint8) else: mesh.visual.vertex_colors = mesh.visual.vertex_colors.astype(np.uint8) return mesh except Exception as e: raise ValueError(f"Error processing mesh: {str(e)}") def _process_vertex_colors(self, m): try: if not hasattr(m.visual, 'vertex_colors') or m.visual.vertex_colors is None: if hasattr(m.visual, 'material'): material = m.visual.material if hasattr(material, 'baseColorTexture') and material.baseColorTexture is not None: if hasattr(m.visual, 'uv') and m.visual.uv is not None: self._extract_colors_from_texture(m) elif hasattr(material, 'baseColorFactor'): color = np.array(material.baseColorFactor) if len(color) == 3: color = np.append(color, 1.0) color = np.clip(color, 0, 1) color = (color * 255).astype(np.uint8) m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile(color, (len(m.vertices), 1)) else: m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(m.vertices), 1)) else: m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(m.vertices), 1)) else: if m.visual.vertex_colors.max() <= 1.0: m.visual.vertex_colors = (m.visual.vertex_colors * 255).astype(np.uint8) else: m.visual.vertex_colors = m.visual.vertex_colors.astype(np.uint8) except Exception as e: raise ValueError(f"Error processing vertex colors: {str(e)}") def _extract_colors_from_texture(self, m): try: material = m.visual.material image = material.baseColorTexture if image is not None: image_data = np.array(image) if image_data.dtype != np.uint8: image_data = (np.clip(image_data, 0, 1) * 255).astype(np.uint8) uvs = m.visual.uv uvs = np.clip(uvs, 0, 1) h, w = image_data.shape[0], image_data.shape[1] i = ((1 - uvs[:, 1]) * (h - 1)).astype(int) j = (uvs[:, 0] * (w - 1)).astype(int) i = np.clip(i, 0, h - 1) j = np.clip(j, 0, w - 1) colors = image_data[i, j, :4] m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = colors else: m.visual = trimesh.visual.ColorVisuals(m) m.visual.vertex_colors = np.tile([200, 200, 200, 255], (len(m.vertices), 1)) except Exception as e: raise ValueError(f"Error extracting colors from texture: {str(e)}") def _send_to_td(self, mesh, broadcast): try: if not hasattr(mesh.visual, 'vertex_colors') or mesh.visual.vertex_colors is None: colors = np.tile([200, 200, 200, 255], (len(mesh.vertices), 1)).astype(np.uint8) else: colors = mesh.visual.vertex_colors.astype(np.uint8) header = [ "ply", "format binary_little_endian 1.0", f"element vertex {len(mesh.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') vertices = mesh.vertices.astype(' 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('