mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-05 06:01:39 +08:00
Qem decimate
This commit is contained in:
parent
35065d500a
commit
1aaa2eccef
@ -38,7 +38,8 @@ class MESH:
|
|||||||
metallic_roughness: torch.Tensor | None = None,
|
metallic_roughness: torch.Tensor | None = None,
|
||||||
vertex_counts: torch.Tensor | None = None,
|
vertex_counts: torch.Tensor | None = None,
|
||||||
face_counts: torch.Tensor | None = None,
|
face_counts: torch.Tensor | None = None,
|
||||||
unlit: bool = False):
|
unlit: bool = False,
|
||||||
|
normals: torch.Tensor | None = None):
|
||||||
|
|
||||||
assert (vertex_counts is None) == (face_counts is None), \
|
assert (vertex_counts is None) == (face_counts is None), \
|
||||||
"vertex_counts and face_counts must be provided together (both or neither)"
|
"vertex_counts and face_counts must be provided together (both or neither)"
|
||||||
@ -46,6 +47,9 @@ class MESH:
|
|||||||
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)
|
||||||
|
# Optional per-vertex normals: (B, N, 3). When None, SaveGLB computes smooth
|
||||||
|
# area-weighted normals so viewers don't fall back to flat (per-face) shading.
|
||||||
|
self.normals = normals
|
||||||
self.texture = texture # texture (baseColor): (B, H, W, 3)
|
self.texture = texture # texture (baseColor): (B, H, W, 3)
|
||||||
# glTF metallicRoughness texture: (B, H, W, 3), R unused, G=roughness, B=metallic
|
# glTF metallicRoughness texture: (B, H, W, 3), R unused, G=roughness, B=metallic
|
||||||
self.metallic_roughness = metallic_roughness
|
self.metallic_roughness = metallic_roughness
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,8 @@ from comfy.cli_args import args
|
|||||||
from comfy_api.latest import ComfyExtension, IO, Types
|
from comfy_api.latest import ComfyExtension, IO, Types
|
||||||
|
|
||||||
|
|
||||||
def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None, unlit=False):
|
def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None, unlit=False,
|
||||||
|
normals=None, metallic_roughness=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.
|
||||||
# colors and 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.
|
||||||
@ -55,9 +56,20 @@ def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=Non
|
|||||||
)
|
)
|
||||||
packed_uvs[i, :u.shape[0]] = u
|
packed_uvs[i, :u.shape[0]] = u
|
||||||
|
|
||||||
|
packed_normals = None
|
||||||
|
if normals is not None:
|
||||||
|
packed_normals = normals[0].new_zeros((batch_size, max_vertices, normals[0].shape[1]))
|
||||||
|
for i, nrm in enumerate(normals):
|
||||||
|
assert nrm.shape[0] == vertices[i].shape[0], (
|
||||||
|
f"normals[{i}] has {nrm.shape[0]} entries, expected {vertices[i].shape[0]} (1:1 with vertices)"
|
||||||
|
)
|
||||||
|
packed_normals[i, :nrm.shape[0]] = nrm
|
||||||
|
|
||||||
return Types.MESH(packed_vertices, packed_faces,
|
return Types.MESH(packed_vertices, packed_faces,
|
||||||
uvs=packed_uvs, vertex_colors=packed_colors, texture=texture,
|
uvs=packed_uvs, vertex_colors=packed_colors, texture=texture,
|
||||||
vertex_counts=vertex_counts, face_counts=face_counts, unlit=unlit)
|
metallic_roughness=metallic_roughness,
|
||||||
|
vertex_counts=vertex_counts, face_counts=face_counts, unlit=unlit,
|
||||||
|
normals=packed_normals)
|
||||||
|
|
||||||
|
|
||||||
def get_mesh_batch_item(mesh, index):
|
def get_mesh_batch_item(mesh, index):
|
||||||
@ -65,6 +77,7 @@ def get_mesh_batch_item(mesh, index):
|
|||||||
# if the mesh carries per-item counts (variable-size batch).
|
# if the mesh carries per-item counts (variable-size batch).
|
||||||
v_colors = getattr(mesh, "vertex_colors", None)
|
v_colors = getattr(mesh, "vertex_colors", None)
|
||||||
v_uvs = getattr(mesh, "uvs", None)
|
v_uvs = getattr(mesh, "uvs", None)
|
||||||
|
v_normals = getattr(mesh, "normals", None)
|
||||||
if getattr(mesh, "vertex_counts", None) is not 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())
|
||||||
@ -72,16 +85,102 @@ def get_mesh_batch_item(mesh, index):
|
|||||||
faces = mesh.faces[index, :face_count]
|
faces = mesh.faces[index, :face_count]
|
||||||
colors = v_colors[index, :vertex_count] if v_colors is not None else None
|
colors = v_colors[index, :vertex_count] if v_colors is not None else None
|
||||||
uvs = v_uvs[index, :vertex_count] if v_uvs is not None else None
|
uvs = v_uvs[index, :vertex_count] if v_uvs is not None else None
|
||||||
return vertices, faces, colors, uvs
|
normals = v_normals[index, :vertex_count] if v_normals is not None else None
|
||||||
|
return vertices, faces, colors, uvs, normals
|
||||||
|
|
||||||
colors = v_colors[index] if v_colors is not None else None
|
colors = v_colors[index] if v_colors is not None else None
|
||||||
uvs = v_uvs[index] if v_uvs is not None else None
|
uvs = v_uvs[index] if v_uvs is not None else None
|
||||||
return mesh.vertices[index], mesh.faces[index], colors, uvs
|
normals = v_normals[index] if v_normals is not None else None
|
||||||
|
return mesh.vertices[index], mesh.faces[index], colors, uvs, normals
|
||||||
|
|
||||||
|
|
||||||
|
def _smooth_vertex_normals(vertices_np, faces_np):
|
||||||
|
"""Area-weighted per-vertex normals (unit length), fully smooth — no vertex splitting.
|
||||||
|
|
||||||
|
Un-normalized face normals (the raw cross product) have magnitude 2*area, so
|
||||||
|
accumulating them onto their vertices yields an area-weighted average."""
|
||||||
|
tris = vertices_np[faces_np] # (M, 3, 3)
|
||||||
|
face_n = np.cross(tris[:, 1] - tris[:, 0], tris[:, 2] - tris[:, 0])
|
||||||
|
normals = np.zeros((vertices_np.shape[0], 3), dtype=np.float64)
|
||||||
|
for k in range(3):
|
||||||
|
np.add.at(normals, faces_np[:, k], face_n)
|
||||||
|
lens = np.linalg.norm(normals, axis=1, keepdims=True)
|
||||||
|
normals /= np.where(lens > 1e-12, lens, 1.0)
|
||||||
|
return normals.astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_vertex_normals(vertices_np, faces_np, crease_angle=None):
|
||||||
|
"""Compute per-vertex normals, returning (vertices, faces_uint32, normals, remap).
|
||||||
|
|
||||||
|
crease_angle is None (or >= 180) -> fully smooth normals; vertices/faces are
|
||||||
|
returned unchanged and remap is None.
|
||||||
|
|
||||||
|
Otherwise vertices are split along edges whose dihedral angle exceeds
|
||||||
|
crease_angle (degrees) so hard creases stay sharp while smooth regions still
|
||||||
|
interpolate. remap maps each output vertex back to its source index, so the
|
||||||
|
caller can duplicate any per-vertex attributes (uvs / colors) to match."""
|
||||||
|
faces_i = faces_np.astype(np.int64)
|
||||||
|
if crease_angle is None or crease_angle >= 180.0:
|
||||||
|
return (vertices_np, faces_i.astype(np.uint32),
|
||||||
|
_smooth_vertex_normals(vertices_np, faces_i), None)
|
||||||
|
|
||||||
|
M = faces_i.shape[0]
|
||||||
|
tris = vertices_np[faces_i]
|
||||||
|
face_n = np.cross(tris[:, 1] - tris[:, 0], tris[:, 2] - tris[:, 0])
|
||||||
|
areas = np.linalg.norm(face_n, axis=1, keepdims=True)
|
||||||
|
face_unit = face_n / np.where(areas > 1e-12, areas, 1.0)
|
||||||
|
cos_thresh = math.cos(math.radians(crease_angle))
|
||||||
|
|
||||||
|
# Union faces that share an edge whose dihedral angle is below the crease
|
||||||
|
# threshold; each connected component becomes one smoothing group.
|
||||||
|
parent = list(range(M))
|
||||||
|
|
||||||
|
def find(x):
|
||||||
|
while parent[x] != x:
|
||||||
|
parent[x] = parent[parent[x]]
|
||||||
|
x = parent[x]
|
||||||
|
return x
|
||||||
|
|
||||||
|
edge_faces = {}
|
||||||
|
for fi in range(M):
|
||||||
|
a, b, c = int(faces_i[fi, 0]), int(faces_i[fi, 1]), int(faces_i[fi, 2])
|
||||||
|
for u, v in ((a, b), (b, c), (c, a)):
|
||||||
|
edge_faces.setdefault((u, v) if u < v else (v, u), []).append(fi)
|
||||||
|
for fl in edge_faces.values():
|
||||||
|
if len(fl) == 2 and float(np.dot(face_unit[fl[0]], face_unit[fl[1]])) >= cos_thresh:
|
||||||
|
ra, rb = find(fl[0]), find(fl[1])
|
||||||
|
if ra != rb:
|
||||||
|
parent[ra] = rb
|
||||||
|
|
||||||
|
# Emit one output vertex per (original vertex, smoothing group) pair.
|
||||||
|
new_index = {}
|
||||||
|
remap = []
|
||||||
|
out_faces = np.empty((M, 3), dtype=np.int64)
|
||||||
|
for fi in range(M):
|
||||||
|
g = find(fi)
|
||||||
|
for k in range(3):
|
||||||
|
ov = int(faces_i[fi, k])
|
||||||
|
key = (ov, g)
|
||||||
|
ni = new_index.get(key)
|
||||||
|
if ni is None:
|
||||||
|
ni = len(remap)
|
||||||
|
new_index[key] = ni
|
||||||
|
remap.append(ov)
|
||||||
|
out_faces[fi, k] = ni
|
||||||
|
|
||||||
|
remap = np.asarray(remap, dtype=np.int64)
|
||||||
|
normals = np.zeros((remap.shape[0], 3), dtype=np.float64)
|
||||||
|
for k in range(3):
|
||||||
|
np.add.at(normals, out_faces[:, k], face_n)
|
||||||
|
lens = np.linalg.norm(normals, axis=1, keepdims=True)
|
||||||
|
normals /= np.where(lens > 1e-12, lens, 1.0)
|
||||||
|
return (vertices_np[remap], out_faces.astype(np.uint32), normals.astype(np.float32), remap)
|
||||||
|
|
||||||
|
|
||||||
def save_glb(vertices, faces, filepath, metadata=None,
|
def save_glb(vertices, faces, filepath, metadata=None,
|
||||||
uvs=None, vertex_colors=None, texture_image=None,
|
uvs=None, vertex_colors=None, texture_image=None,
|
||||||
metallic_roughness_image=None, unlit=False):
|
metallic_roughness_image=None, unlit=False,
|
||||||
|
normals=None):
|
||||||
"""
|
"""
|
||||||
Save PyTorch tensor vertices and faces as a GLB file without external dependencies.
|
Save PyTorch tensor vertices and faces as a GLB file without external dependencies.
|
||||||
|
|
||||||
@ -95,6 +194,9 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
texture_image: PIL.Image - Optional baseColor texture, embedded as PNG
|
texture_image: PIL.Image - Optional baseColor texture, embedded as PNG
|
||||||
metallic_roughness_image: PIL.Image - Optional glTF metallicRoughness texture
|
metallic_roughness_image: PIL.Image - Optional glTF metallicRoughness texture
|
||||||
(R unused, G=roughness, B=metallic), embedded as PNG
|
(R unused, G=roughness, B=metallic), embedded as PNG
|
||||||
|
normals: torch.Tensor of shape (N, 3) - Optional per-vertex normals, written as the
|
||||||
|
glTF NORMAL attribute. When omitted, NO normals are written and viewers fall back
|
||||||
|
to flat (per-face) shading — use the MeshSmoothNormals node to generate them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Convert tensors to numpy arrays
|
# Convert tensors to numpy arrays
|
||||||
@ -123,6 +225,12 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"save_glb: vertex_colors has {colors_np.shape[0]} entries but vertex count is {n_verts}"
|
f"save_glb: vertex_colors has {colors_np.shape[0]} entries but vertex count is {n_verts}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
normals_np = normals.cpu().numpy().astype(np.float32) if normals is not None else None
|
||||||
|
if normals_np is not None and normals_np.shape[0] != n_verts:
|
||||||
|
raise ValueError(
|
||||||
|
f"save_glb: normals has {normals_np.shape[0]} entries but vertex count is {n_verts}"
|
||||||
|
)
|
||||||
faces_np = faces_signed.astype(np.uint32)
|
faces_np = faces_signed.astype(np.uint32)
|
||||||
texture_png_bytes = None
|
texture_png_bytes = None
|
||||||
if texture_image is not None:
|
if texture_image is not None:
|
||||||
@ -139,6 +247,7 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
indices_buffer = faces_np.tobytes()
|
indices_buffer = faces_np.tobytes()
|
||||||
uvs_buffer = uvs_np.tobytes() if uvs_np is not None else b""
|
uvs_buffer = uvs_np.tobytes() if uvs_np is not None else b""
|
||||||
colors_buffer = colors_np.tobytes() if colors_np is not None else b""
|
colors_buffer = colors_np.tobytes() if colors_np is not None else b""
|
||||||
|
normals_buffer = normals_np.tobytes() if normals_np is not None else b""
|
||||||
texture_buffer = texture_png_bytes if texture_png_bytes is not None else b""
|
texture_buffer = texture_png_bytes if texture_png_bytes is not None else b""
|
||||||
mr_buffer = mr_png_bytes if mr_png_bytes is not None else b""
|
mr_buffer = mr_png_bytes if mr_png_bytes is not None else b""
|
||||||
|
|
||||||
@ -150,6 +259,7 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
indices_buffer_padded = pad_to_4_bytes(indices_buffer)
|
indices_buffer_padded = pad_to_4_bytes(indices_buffer)
|
||||||
uvs_buffer_padded = pad_to_4_bytes(uvs_buffer)
|
uvs_buffer_padded = pad_to_4_bytes(uvs_buffer)
|
||||||
colors_buffer_padded = pad_to_4_bytes(colors_buffer)
|
colors_buffer_padded = pad_to_4_bytes(colors_buffer)
|
||||||
|
normals_buffer_padded = pad_to_4_bytes(normals_buffer)
|
||||||
texture_buffer_padded = pad_to_4_bytes(texture_buffer)
|
texture_buffer_padded = pad_to_4_bytes(texture_buffer)
|
||||||
mr_buffer_padded = pad_to_4_bytes(mr_buffer)
|
mr_buffer_padded = pad_to_4_bytes(mr_buffer)
|
||||||
|
|
||||||
@ -158,6 +268,7 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
indices_buffer_padded,
|
indices_buffer_padded,
|
||||||
uvs_buffer_padded,
|
uvs_buffer_padded,
|
||||||
colors_buffer_padded,
|
colors_buffer_padded,
|
||||||
|
normals_buffer_padded,
|
||||||
texture_buffer_padded,
|
texture_buffer_padded,
|
||||||
mr_buffer_padded,
|
mr_buffer_padded,
|
||||||
])
|
])
|
||||||
@ -168,7 +279,8 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
indices_byte_offset = len(vertices_buffer_padded)
|
indices_byte_offset = len(vertices_buffer_padded)
|
||||||
uvs_byte_offset = indices_byte_offset + len(indices_buffer_padded)
|
uvs_byte_offset = indices_byte_offset + len(indices_buffer_padded)
|
||||||
colors_byte_offset = uvs_byte_offset + len(uvs_buffer_padded)
|
colors_byte_offset = uvs_byte_offset + len(uvs_buffer_padded)
|
||||||
texture_byte_offset = colors_byte_offset + len(colors_buffer_padded)
|
normals_byte_offset = colors_byte_offset + len(colors_buffer_padded)
|
||||||
|
texture_byte_offset = normals_byte_offset + len(normals_buffer_padded)
|
||||||
mr_byte_offset = texture_byte_offset + len(texture_buffer_padded)
|
mr_byte_offset = texture_byte_offset + len(texture_buffer_padded)
|
||||||
|
|
||||||
buffer_views = [
|
buffer_views = [
|
||||||
@ -239,6 +351,23 @@ def save_glb(vertices, faces, filepath, metadata=None,
|
|||||||
})
|
})
|
||||||
primitive_attributes["COLOR_0"] = accessor_idx
|
primitive_attributes["COLOR_0"] = accessor_idx
|
||||||
|
|
||||||
|
if normals_np is not None and len(normals_np) > 0:
|
||||||
|
buffer_views.append({
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": normals_byte_offset,
|
||||||
|
"byteLength": len(normals_buffer),
|
||||||
|
"target": 34962
|
||||||
|
})
|
||||||
|
accessor_idx = len(accessors)
|
||||||
|
accessors.append({
|
||||||
|
"bufferView": len(buffer_views) - 1,
|
||||||
|
"byteOffset": 0,
|
||||||
|
"componentType": 5126, # FLOAT
|
||||||
|
"count": len(normals_np),
|
||||||
|
"type": "VEC3",
|
||||||
|
})
|
||||||
|
primitive_attributes["NORMAL"] = accessor_idx
|
||||||
|
|
||||||
primitive = {
|
primitive = {
|
||||||
"attributes": primitive_attributes,
|
"attributes": primitive_attributes,
|
||||||
"indices": 1,
|
"indices": 1,
|
||||||
@ -428,7 +557,7 @@ class SaveGLB(IO.ComfyNode):
|
|||||||
f"metallic_roughness must be (B, H, W, 3), got shape {tuple(mr_np.shape)}"
|
f"metallic_roughness must be (B, H, W, 3), got shape {tuple(mr_np.shape)}"
|
||||||
)
|
)
|
||||||
for i in range(mesh.vertices.shape[0]):
|
for i in range(mesh.vertices.shape[0]):
|
||||||
vertices_i, faces_i, v_colors, uvs_i = get_mesh_batch_item(mesh, i)
|
vertices_i, faces_i, v_colors, uvs_i, normals_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
|
||||||
@ -444,6 +573,7 @@ class SaveGLB(IO.ComfyNode):
|
|||||||
texture_image=tex_img,
|
texture_image=tex_img,
|
||||||
metallic_roughness_image=mr_img,
|
metallic_roughness_image=mr_img,
|
||||||
unlit=getattr(mesh, "unlit", False),
|
unlit=getattr(mesh, "unlit", False),
|
||||||
|
normals=normals_i,
|
||||||
)
|
)
|
||||||
results.append({
|
results.append({
|
||||||
"filename": f,
|
"filename": f,
|
||||||
@ -542,13 +672,89 @@ class RotateMesh(IO.ComfyNode):
|
|||||||
out.vertices = [rotate(v) for v in mesh.vertices]
|
out.vertices = [rotate(v) for v in mesh.vertices]
|
||||||
else:
|
else:
|
||||||
out.vertices = rotate(mesh.vertices)
|
out.vertices = rotate(mesh.vertices)
|
||||||
|
# Normals are directions; rotate them too (R is orthogonal) so they stay valid.
|
||||||
|
nrm = getattr(mesh, "normals", None)
|
||||||
|
if nrm is not None:
|
||||||
|
out.normals = [rotate(n) for n in nrm] if isinstance(nrm, list) else rotate(nrm)
|
||||||
|
return IO.NodeOutput(out)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshSmoothNormals(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshSmoothNormals",
|
||||||
|
display_name="Smooth Mesh Normals",
|
||||||
|
category="3d",
|
||||||
|
description=(
|
||||||
|
"Compute smooth per-vertex normals and attach them to the mesh. Meshes "
|
||||||
|
"without normals are shaded flat (per-face) by glTF viewers; this makes "
|
||||||
|
"them shade smoothly. With crease_angle below 180, edges sharper than the "
|
||||||
|
"threshold are kept hard by splitting vertices along them."
|
||||||
|
),
|
||||||
|
inputs=[
|
||||||
|
IO.Mesh.Input("mesh"),
|
||||||
|
IO.Float.Input("crease_angle", default=180.0, min=0.0, max=180.0, step=1.0,
|
||||||
|
tooltip="Edges whose dihedral angle exceeds this (degrees) stay "
|
||||||
|
"hard (vertices are split). 180 = fully smooth; lower "
|
||||||
|
"preserves sharp edges (e.g. ~30-60 for hard-surface)."),
|
||||||
|
],
|
||||||
|
outputs=[IO.Mesh.Output("mesh")],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, mesh: Types.MESH, crease_angle: float) -> IO.NodeOutput:
|
||||||
|
crease = None if crease_angle >= 180.0 else float(crease_angle)
|
||||||
|
batch_size = mesh.vertices.shape[0]
|
||||||
|
|
||||||
|
if crease is None:
|
||||||
|
# Fully smooth: topology is unchanged, so just attach a normals tensor that
|
||||||
|
# matches the existing (possibly zero-padded) vertex layout and keep all fields.
|
||||||
|
normals_padded = torch.zeros_like(mesh.vertices)
|
||||||
|
for i in range(batch_size):
|
||||||
|
v_i, f_i, _, _, _ = get_mesh_batch_item(mesh, i)
|
||||||
|
if v_i.shape[0] == 0 or f_i.shape[0] == 0:
|
||||||
|
continue
|
||||||
|
n_i = _smooth_vertex_normals(v_i.cpu().numpy().astype(np.float32),
|
||||||
|
f_i.cpu().numpy().astype(np.int64))
|
||||||
|
normals_padded[i, :n_i.shape[0]] = torch.from_numpy(n_i).to(mesh.vertices)
|
||||||
|
out = copy.copy(mesh)
|
||||||
|
out.normals = normals_padded
|
||||||
|
return IO.NodeOutput(out)
|
||||||
|
|
||||||
|
# Crease split changes per-item vertex counts -> rebuild as a variable-size batch.
|
||||||
|
v_list, f_list, n_list = [], [], []
|
||||||
|
c_list = [] if mesh.vertex_colors is not None else None
|
||||||
|
u_list = [] if mesh.uvs is not None else None
|
||||||
|
for i in range(batch_size):
|
||||||
|
v_i, f_i, c_i, u_i, _ = get_mesh_batch_item(mesh, i)
|
||||||
|
if v_i.shape[0] == 0 or f_i.shape[0] == 0:
|
||||||
|
continue
|
||||||
|
dev = v_i.device
|
||||||
|
vo, fo, no, remap = _compute_vertex_normals(
|
||||||
|
v_i.cpu().numpy().astype(np.float32),
|
||||||
|
f_i.cpu().numpy().astype(np.int64), crease)
|
||||||
|
remap_t = torch.from_numpy(remap)
|
||||||
|
v_list.append(torch.from_numpy(vo).to(dev, mesh.vertices.dtype))
|
||||||
|
f_list.append(torch.from_numpy(fo.astype(np.int64)).to(dev, mesh.faces.dtype))
|
||||||
|
n_list.append(torch.from_numpy(no).to(dev, mesh.vertices.dtype))
|
||||||
|
if c_list is not None:
|
||||||
|
c_list.append(c_i[remap_t.to(c_i.device)])
|
||||||
|
if u_list is not None:
|
||||||
|
u_list.append(u_i[remap_t.to(u_i.device)])
|
||||||
|
if not v_list:
|
||||||
|
return IO.NodeOutput(mesh)
|
||||||
|
out = pack_variable_mesh_batch(
|
||||||
|
v_list, f_list, colors=c_list, uvs=u_list,
|
||||||
|
texture=mesh.texture, unlit=getattr(mesh, "unlit", False),
|
||||||
|
normals=n_list, metallic_roughness=getattr(mesh, "metallic_roughness", None))
|
||||||
return IO.NodeOutput(out)
|
return IO.NodeOutput(out)
|
||||||
|
|
||||||
|
|
||||||
class Save3DExtension(ComfyExtension):
|
class Save3DExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
return [SaveGLB, RotateMesh]
|
return [SaveGLB, RotateMesh, MeshSmoothNormals]
|
||||||
|
|
||||||
|
|
||||||
async def comfy_entrypoint() -> Save3DExtension:
|
async def comfy_entrypoint() -> Save3DExtension:
|
||||||
|
|||||||
@ -333,6 +333,7 @@ class VaeDecodeTextureTrellis(IO.ComfyNode):
|
|||||||
trellis_vae = vae.first_stage_model
|
trellis_vae = vae.first_stage_model
|
||||||
coord_counts = samples.get("coord_counts")
|
coord_counts = samples.get("coord_counts")
|
||||||
model_frame = samples.get("model_frame", "y_up")
|
model_frame = samples.get("model_frame", "y_up")
|
||||||
|
coord_resolution = samples.get("coord_resolution")
|
||||||
|
|
||||||
samples = samples["samples"]
|
samples = samples["samples"]
|
||||||
samples, coords = flatten_batched_sparse_latent(samples, coords, coord_counts)
|
samples, coords = flatten_batched_sparse_latent(samples, coords, coord_counts)
|
||||||
@ -358,7 +359,9 @@ class VaeDecodeTextureTrellis(IO.ComfyNode):
|
|||||||
if color_feats.shape[0] > 0 and color_feats.shape[-1] >= 3:
|
if color_feats.shape[0] > 0 and color_feats.shape[-1] >= 3:
|
||||||
_calibrate_tex_rgb(cal_in_latent, cal_in_coords, color_feats[:, :3], voxel_coords)
|
_calibrate_tex_rgb(cal_in_latent, cal_in_coords, color_feats[:, :3], voxel_coords)
|
||||||
|
|
||||||
if voxel_coords.numel() > 0 and voxel_coords.shape[-1] >= 3:
|
if coord_resolution is not None:
|
||||||
|
tex_resolution = int(coord_resolution) * 16
|
||||||
|
elif voxel_coords.numel() > 0 and voxel_coords.shape[-1] >= 3:
|
||||||
spatial = voxel_coords[:, -3:] if voxel_coords.shape[-1] == 4 else voxel_coords
|
spatial = voxel_coords[:, -3:] if voxel_coords.shape[-1] == 4 else voxel_coords
|
||||||
max_idx = int(spatial.max().item()) + 1
|
max_idx = int(spatial.max().item()) + 1
|
||||||
tex_resolution = next((r for r in (256, 512, 1024, 1536, 2048) if r >= max_idx), max_idx)
|
tex_resolution = next((r for r in (256, 512, 1024, 1536, 2048) if r >= max_idx), max_idx)
|
||||||
|
|||||||
1620
comfy_extras/qem_decimate/qem_core.py
Normal file
1620
comfy_extras/qem_decimate/qem_core.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user