This commit is contained in:
Yousef Rafat 2026-05-20 20:18:54 +03:00
parent 9b52e24430
commit 1ccf2d413e
2 changed files with 21 additions and 109 deletions

View File

@ -284,80 +284,6 @@ def fill_holes_fn(vertices, faces, max_perimeter=0.03):
return v, f return v, f
def make_double_sided(vertices, faces, colors=None, normals=None, z_offset=1e-4):
"""
Creates double-sided mesh using PER-FACE normals for offset.
This avoids pole singularities completely.
"""
is_batched = vertices.ndim == 3
if is_batched:
v_list, f_list, c_list = [], [], []
for i in range(vertices.shape[0]):
# Compute face normals for this mesh
v0 = vertices[i][faces[i][:, 0]]
v1 = vertices[i][faces[i][:, 1]]
v2 = vertices[i][faces[i][:, 2]]
fn = torch.cross(v1 - v0, v2 - v0, dim=-1)
fn = fn / (torch.norm(fn, dim=-1, keepdim=True) + 1e-8)
# Offset each face's vertices along its face normal
front = torch.stack([v0, v1, v2], dim=1) + fn.unsqueeze(1) * z_offset
back = torch.stack([v0, v1, v2], dim=1) - fn.unsqueeze(1) * z_offset
front = front.reshape(-1, 3)
back = back.reshape(-1, 3)
f_front = torch.arange(faces[i].shape[0] * 3, device=vertices.device).reshape(-1, 3)
f_back = f_front + faces[i].shape[0] * 3
f_back = f_back[:, [0, 2, 1]] # flip winding for back faces
v_list.append(torch.cat([front, back], dim=0))
f_list.append(torch.cat([f_front, f_back], dim=0))
if colors is not None:
c_faces = colors[i][faces[i]]
c_front = c_faces.reshape(-1, colors[i].shape[-1])
c_back = c_front.clone()
c_list.append(torch.cat([c_front, c_back], dim=0))
out_v = torch.stack(v_list)
out_f = torch.stack(f_list)
if colors is not None:
return out_v, out_f, torch.stack(c_list)
return out_v, out_f
# --- Unbatched ---
v0 = vertices[faces[:, 0]]
v1 = vertices[faces[:, 1]]
v2 = vertices[faces[:, 2]]
fn = torch.cross(v1 - v0, v2 - v0, dim=-1)
fn = fn / (torch.norm(fn, dim=-1, keepdim=True) + 1e-8)
# Offset each face's vertices along its face normal
front = torch.stack([v0, v1, v2], dim=1) + fn.unsqueeze(1) * z_offset
back = torch.stack([v0, v1, v2], dim=1) - fn.unsqueeze(1) * z_offset
front = front.reshape(-1, 3)
back = back.reshape(-1, 3)
f_front = torch.arange(faces.shape[0] * 3, device=vertices.device).reshape(-1, 3)
f_back = f_front + faces.shape[0] * 3
f_back = f_back[:, [0, 2, 1]] # flip winding for back faces
v_dup = torch.cat([front, back], dim=0)
f_dup = torch.cat([f_front, f_back], dim=0)
if colors is not None:
c_faces = colors[faces]
c_front = c_faces.reshape(-1, colors.shape[-1])
c_back = c_front.clone()
c_dup = torch.cat([c_front, c_back], dim=0)
return v_dup, f_dup, c_dup
return v_dup, f_dup, None
def _cleanup_mesh(verts, faces, min_angle_deg=0.5, max_aspect=100.0): def _cleanup_mesh(verts, faces, min_angle_deg=0.5, max_aspect=100.0):
if faces.numel() == 0: if faces.numel() == 0:
return verts, faces return verts, faces
@ -905,32 +831,10 @@ class FillHoles(IO.ComfyNode):
return v, f, c return v, f, c
return _process_mesh_batch(mesh, _fn) return _process_mesh_batch(mesh, _fn)
class MakeDoubleSided(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="MakeDoubleSided",
display_name="Make Double Sided",
category="latent/3d",
description="Duplicates faces with flipped normals so the mesh renders from both sides.",
inputs=[IO.Mesh.Input("mesh")],
outputs=[IO.Mesh.Output("mesh")],
)
@classmethod
def execute(cls, mesh):
def _fn(v, f, c):
return make_double_sided(v, f, c)
return _process_mesh_batch(mesh, _fn)
class PostProcessMeshExtension(ComfyExtension): class PostProcessMeshExtension(ComfyExtension):
@override @override
async def get_node_list(self) -> list[type[IO.ComfyNode]]: async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [ return [
MakeDoubleSided,
FillHoles, FillHoles,
DecimateMesh, DecimateMesh,
PaintMesh PaintMesh

View File

@ -234,6 +234,12 @@ def save_glb(vertices, faces, filepath, metadata=None,
textures = [] textures = []
samplers = [] samplers = []
materials = [] materials = []
pbr = {
"metallicFactor": 0.0,
"roughnessFactor": 0.5,
"baseColorFactor": [0.22, 0.22, 0.22, 1.0],
}
if texture_png_bytes is not None and "TEXCOORD_0" in primitive_attributes: if texture_png_bytes is not None and "TEXCOORD_0" in primitive_attributes:
buffer_views.append({ buffer_views.append({
"buffer": 0, "buffer": 0,
@ -243,15 +249,13 @@ def save_glb(vertices, faces, filepath, metadata=None,
images.append({"bufferView": len(buffer_views) - 1, "mimeType": "image/png"}) images.append({"bufferView": len(buffer_views) - 1, "mimeType": "image/png"})
samplers.append({"magFilter": 9729, "minFilter": 9729, "wrapS": 33071, "wrapT": 33071}) samplers.append({"magFilter": 9729, "minFilter": 9729, "wrapS": 33071, "wrapT": 33071})
textures.append({"source": 0, "sampler": 0}) textures.append({"source": 0, "sampler": 0})
materials.append({ pbr["baseColorTexture"] = {"index": 0, "texCoord": 0}
"pbrMetallicRoughness": {
"baseColorTexture": {"index": 0, "texCoord": 0}, materials.append({
"metallicFactor": 0.0, "pbrMetallicRoughness": pbr,
"roughnessFactor": 1.0, "doubleSided": True,
}, })
"doubleSided": True, primitive["material"] = 0
})
primitive["material"] = 0
gltf = { gltf = {
"asset": {"version": "2.0", "generator": "ComfyUI"}, "asset": {"version": "2.0", "generator": "ComfyUI"},
@ -373,10 +377,14 @@ class SaveGLB(IO.ComfyNode):
continue continue
tex_img = Image.fromarray(texture_np[i], mode="RGB") if texture_np is not None else None tex_img = Image.fromarray(texture_np[i], mode="RGB") if texture_np is not None else None
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(
uvs=uvs_i, vertices_i, faces_i,
vertex_colors=v_colors, os.path.join(full_output_folder, f),
texture_image=tex_img) metadata,
uvs=uvs_i,
vertex_colors=v_colors,
texture_image=tex_img,
)
results.append({ results.append({
"filename": f, "filename": f,
"subfolder": subfolder, "subfolder": subfolder,