Some fixes and adjustments

This commit is contained in:
kijai 2026-05-10 19:55:31 +03:00
parent aaee77df7c
commit 603fc17d5b
2 changed files with 38 additions and 35 deletions

View File

@ -15,13 +15,21 @@ class MESH:
def __init__(self, vertices: torch.Tensor, faces: torch.Tensor, def __init__(self, vertices: torch.Tensor, faces: torch.Tensor,
uvs: torch.Tensor | None = None, uvs: torch.Tensor | None = None,
vertex_colors: torch.Tensor | None = None, vertex_colors: torch.Tensor | None = None,
texture: torch.Tensor | None = None): texture: torch.Tensor | None = None,
vertex_counts: torch.Tensor | None = None,
face_counts: torch.Tensor | None = None):
assert (vertex_counts is None) == (face_counts is None), \
"vertex_counts and face_counts must be provided together (both or neither)"
self.vertices = vertices # vertices: (B, N, 3) self.vertices = vertices # vertices: (B, N, 3)
self.faces = faces # faces: (B, M, 3) self.faces = faces # faces: (B, M, 3)
self.uvs = uvs # uvs: (B, N, 2) self.uvs = uvs # uvs: (B, N, 2)
self.vertex_colors = vertex_colors # vertex_colors: (B, N, 3 or 4) self.vertex_colors = vertex_colors # vertex_colors: (B, N, 3 or 4)
self.texture = texture # texture: (B, H, W, 3) self.texture = texture # texture: (B, H, W, 3)
# When vertices/faces are zero-padded to a common N/M across the batch (variable-size mesh batch),
# these hold the real per-item lengths (B,). None means rows are uniform and no slicing is needed.
self.vertex_counts = vertex_counts
self.face_counts = face_counts
class File3D: class File3D:

View File

@ -9,6 +9,7 @@ import os
import struct import struct
import numpy as np import numpy as np
from PIL import Image
import torch import torch
from typing_extensions import override from typing_extensions import override
@ -20,7 +21,7 @@ from comfy_api.latest import ComfyExtension, IO, Types
def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None): def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None):
# Pack lists of (Nᵢ, *) vertex/face/color/uv tensors into padded batched tensors, # Pack lists of (Nᵢ, *) vertex/face/color/uv tensors into padded batched tensors,
# stashing per-item lengths as runtime attrs so consumers can recover the real slice. # stashing per-item lengths as runtime attrs so consumers can recover the real slice.
# uvs are 1:1 with vertices, so they're padded to max_vertices and read with vertex_counts. # colors and uvs are 1:1 with vertices, so they're padded to max_vertices and read with vertex_counts.
# texture is (B, H, W, 3) — passed through unchanged since it doesn't need ragged packing. # texture is (B, H, W, 3) — passed through unchanged since it doesn't need ragged packing.
batch_size = len(vertices) batch_size = len(vertices)
max_vertices = max(v.shape[0] for v in vertices) max_vertices = max(v.shape[0] for v in vertices)
@ -36,51 +37,45 @@ def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=Non
packed_faces[i, :f.shape[0]] = f packed_faces[i, :f.shape[0]] = f
packed_colors = None packed_colors = None
color_counts = None
if colors is not None: if colors is not None:
max_colors = max(c.shape[0] for c in colors) packed_colors = colors[0].new_zeros((batch_size, max_vertices, colors[0].shape[1]))
packed_colors = colors[0].new_zeros((batch_size, max_colors, colors[0].shape[1]))
color_counts = torch.tensor([c.shape[0] for c in colors], device=colors[0].device, dtype=torch.int64)
for i, c in enumerate(colors): for i, c in enumerate(colors):
assert c.shape[0] == vertices[i].shape[0], (
f"vertex_colors[{i}] has {c.shape[0]} entries, expected {vertices[i].shape[0]} (1:1 with vertices)"
)
packed_colors[i, :c.shape[0]] = c packed_colors[i, :c.shape[0]] = c
packed_uvs = None packed_uvs = None
if uvs is not None: if uvs is not None:
packed_uvs = uvs[0].new_zeros((batch_size, max_vertices, uvs[0].shape[1])) packed_uvs = uvs[0].new_zeros((batch_size, max_vertices, uvs[0].shape[1]))
for i, u in enumerate(uvs): for i, u in enumerate(uvs):
assert u.shape[0] == vertices[i].shape[0], (
f"uvs[{i}] has {u.shape[0]} entries, expected {vertices[i].shape[0]} (1:1 with vertices)"
)
packed_uvs[i, :u.shape[0]] = u packed_uvs[i, :u.shape[0]] = u
mesh = Types.MESH(packed_vertices, packed_faces, uvs=packed_uvs, vertex_colors=packed_colors, texture=texture) return Types.MESH(packed_vertices, packed_faces,
mesh.vertex_counts = vertex_counts uvs=packed_uvs, vertex_colors=packed_colors, texture=texture,
mesh.face_counts = face_counts vertex_counts=vertex_counts, face_counts=face_counts)
if color_counts is not None:
mesh.color_counts = color_counts
return mesh
def get_mesh_batch_item(mesh, index): def get_mesh_batch_item(mesh, index):
# Returns (vertices, faces, colors) for batch index, slicing to real lengths # Returns (vertices, faces, colors, uvs) for batch index, slicing to real lengths
# if pack_variable_mesh_batch added per-item counts. # if the mesh carries per-item counts (variable-size batch).
if hasattr(mesh, "vertex_counts"): v_colors = getattr(mesh, "vertex_colors", None)
v_uvs = getattr(mesh, "uvs", None)
if getattr(mesh, "vertex_counts", None) is not None:
vertex_count = int(mesh.vertex_counts[index].item()) vertex_count = int(mesh.vertex_counts[index].item())
face_count = int(mesh.face_counts[index].item()) face_count = int(mesh.face_counts[index].item())
vertices = mesh.vertices[index, :vertex_count] vertices = mesh.vertices[index, :vertex_count]
faces = mesh.faces[index, :face_count] faces = mesh.faces[index, :face_count]
colors = None colors = v_colors[index, :vertex_count] if v_colors is not None else None
v_colors = getattr(mesh, "vertex_colors", None) uvs = v_uvs[index, :vertex_count] if v_uvs is not None else None
if v_colors is not None: return vertices, faces, colors, uvs
if hasattr(mesh, "color_counts"):
color_count = int(mesh.color_counts[index].item())
colors = v_colors[index, :color_count]
else:
colors = v_colors[index, :vertex_count]
return vertices, faces, colors
colors = None colors = v_colors[index] if v_colors is not None else None
v_colors = getattr(mesh, "vertex_colors", None) uvs = v_uvs[index] if v_uvs is not None else None
if v_colors is not None: return mesh.vertices[index], mesh.faces[index], colors, uvs
colors = v_colors[index]
return mesh.vertices[index], mesh.faces[index], colors
def save_glb(vertices, faces, filepath, metadata=None, def save_glb(vertices, faces, filepath, metadata=None,
@ -107,6 +102,8 @@ def save_glb(vertices, faces, filepath, metadata=None,
colors_np = np.clip(colors_np, 0.0, 1.0) colors_np = np.clip(colors_np, 0.0, 1.0)
n_verts = vertices_np.shape[0] n_verts = vertices_np.shape[0]
if n_verts == 0:
raise ValueError("save_glb: vertices is empty")
if faces_signed.size > 0: if faces_signed.size > 0:
fmin = int(faces_signed.min()) fmin = int(faces_signed.min())
fmax = int(faces_signed.max()) fmax = int(faces_signed.max())
@ -360,20 +357,18 @@ class SaveGLB(IO.ComfyNode):
}) })
else: else:
# Handle Mesh input - save vertices and faces as GLB; carry optional UVs / colors / texture. # Handle Mesh input - save vertices and faces as GLB; carry optional UVs / colors / texture.
uvs_b = getattr(mesh, "uvs", None)
texture_b = getattr(mesh, "texture", None) texture_b = getattr(mesh, "texture", None)
for i in range(mesh.vertices.shape[0]): for i in range(mesh.vertices.shape[0]):
vertices_i, faces_i, v_colors = get_mesh_batch_item(mesh, i) vertices_i, faces_i, v_colors, uvs_i = get_mesh_batch_item(mesh, i)
if vertices_i.shape[0] == 0 or faces_i.shape[0] == 0: if vertices_i.shape[0] == 0 or faces_i.shape[0] == 0:
logging.warning(f"SaveGLB: skipping empty mesh at batch index {i}") logging.warning(f"SaveGLB: skipping empty mesh at batch index {i}")
continue continue
uvs_i = None
if uvs_b is not None:
uvs_i = uvs_b[i, :vertices_i.shape[0]] if hasattr(mesh, "vertex_counts") else uvs_b[i]
tex_img = None tex_img = None
if texture_b is not None: if texture_b is not None:
from PIL import Image
arr = (texture_b[i].clamp(0.0, 1.0).cpu().numpy() * 255).astype(np.uint8) arr = (texture_b[i].clamp(0.0, 1.0).cpu().numpy() * 255).astype(np.uint8)
assert arr.ndim == 3 and arr.shape[-1] == 3, (
f"texture[{i}] must be (H, W, 3) RGB, got shape {tuple(arr.shape)}"
)
tex_img = Image.fromarray(arr, mode="RGB") tex_img = Image.fromarray(arr, mode="RGB")
f = f"{filename}_{counter:05}_.glb" f = f"{filename}_{counter:05}_.glb"
save_glb(vertices_i, faces_i, os.path.join(full_output_folder, f), metadata, save_glb(vertices_i, faces_i, os.path.join(full_output_folder, f), metadata,