fixed output mesh

This commit is contained in:
Denis Kochetov
2025-07-25 19:35:22 +00:00
parent 6fa0039ad5
commit 13dc7b3d3d
5 changed files with 243 additions and 270 deletions
@@ -337,7 +337,7 @@ class PartCrafterPipeline(DiffusionPipeline, TransformerDiffusionMixin):
max_num_expanded_coords=max_num_expanded_coords,
# verbose=True
)
mesh = trimesh.Trimesh(mesh_v_f[0].astype(np.float32), mesh_v_f[1])
mesh = trimesh.Trimesh(mesh_v_f[0].astype(np.float32), mesh_v_f[1], process=True)
except:
mesh_v_f = None
mesh = None
@@ -10,9 +10,60 @@ from diffusers.utils import export_to_video
from diffusers.utils.loading_utils import load_video
import torch
from torchvision.utils import make_grid
import math
os.environ['PYOPENGL_PLATFORM'] = 'egl'
def explode_mesh(mesh, explosion_scale=0.4):
# ensure we have a Scene
if isinstance(mesh, trimesh.Trimesh):
scene = trimesh.Scene(mesh)
elif isinstance(mesh, trimesh.Scene):
scene = mesh
else:
raise ValueError(f"Expected Trimesh or Scene, got {type(mesh)}")
if len(scene.geometry) <= 1:
print("Nothing to explode")
return scene
# 1) collect (name, geom, world_center)
parts = []
for name, geom in scene.geometry.items():
# ← get(name) returns (4×4 world‐space matrix, parent_frame)
world_tf, _ = scene.graph.get(name)
pts = trimesh.transformations.transform_points(geom.vertices, world_tf)
center = pts.mean(axis=0)
parts.append((name, geom, center))
# compute global center
all_centers = np.stack([c for _,_,c in parts], axis=0)
global_center = all_centers.mean(axis=0)
exploded = trimesh.Scene()
for name, geom, center in parts:
dir_vec = center - global_center
norm = np.linalg.norm(dir_vec)
if norm < 1e-6:
dir_vec = np.random.randn(3)
dir_vec /= np.linalg.norm(dir_vec)
else:
dir_vec /= norm
offset = dir_vec * explosion_scale
# fetch the same 4×4, then bump just the translation
world_tf, _ = scene.graph.get(name)
world_tf = world_tf.copy()
world_tf[:3, 3] += offset
exploded.add_geometry(geom, transform=world_tf, geom_name=name)
print(f"[explode] {name} moved by {np.linalg.norm(offset):.4f}")
return exploded
def render(
scene: pyrender.Scene,
renderer: pyrender.Renderer,
@@ -123,13 +174,22 @@ def render_views_around_mesh(
Tuple[List[Image.Image], List[Image.Image]],
Tuple[List[np.ndarray], List[np.ndarray]]
]:
meshes = []
scenes = []
if not isinstance(mesh, (trimesh.Trimesh, trimesh.Scene)):
raise ValueError("mesh must be a trimesh.Trimesh or trimesh.Scene object")
if isinstance(mesh, trimesh.Trimesh):
mesh = trimesh.Scene(mesh)
scene = pyrender.Scene.from_trimesh_scene(mesh)
for i in range(num_views):
scenes.append(pyrender.Scene.from_trimesh_scene(trimesh.Scene(mesh)))
else:
for i in range(num_views):
value = math.sin(math.pi * (i - 1) / num_views)
scenes.append(pyrender.Scene.from_trimesh_scene(explode_mesh(mesh, 0.2 * value),
ambient_light=[0.02, 0.02, 0.02],
bg_color=[0.0, 0.0, 0.0, 1.0]))
light = pyrender.DirectionalLight(
color=np.ones(3),
intensity=light_intensity
@@ -149,9 +209,9 @@ def render_views_around_mesh(
)
images, depths = [], []
for pose in camera_poses:
for i, pose in enumerate(camera_poses):
image, depth = render(
scene, renderer, camera, pose, light,
scenes[i], renderer, camera, pose, light,
normalize_depth=normalize_depth,
flags=flags,
return_type=return_type
+117 -200
View File
@@ -1,84 +1,22 @@
{
"id": "8fb4df3a-d3bc-4b42-95f2-434fb48a8992",
"revision": 0,
"last_node_id": 9,
"last_link_id": 7,
"last_node_id": 12,
"last_link_id": 12,
"nodes": [
{
"id": 1,
"type": "[Comfy3D] Load PartCrafter Pipeline",
"pos": [
2233.307861328125,
1132.6119384765625
],
"size": [
197.09921264648438,
26
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "partcrafter_pipe",
"type": "DIFFUSERS_PIPE",
"links": [
1
]
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Load PartCrafter Pipeline"
},
"widgets_values": []
},
{
"id": 8,
"type": "[Comfy3D] Preview 3DMesh",
"pos": [
3333.187744140625,
1183.9188232421875
],
"size": [
600,
500
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "mesh_file_path",
"type": "STRING",
"widget": {
"name": "mesh_file_path"
},
"link": 6
}
],
"outputs": [],
"properties": {
"Node name for S&R": "[Comfy3D] Preview 3DMesh"
},
"widgets_values": [
"",
null
]
},
{
"id": 3,
"type": "LoadImage",
"pos": [
2161.17333984375,
1232.0296630859375
2166.6142578125,
1234.06982421875
],
"size": [
353.18182373046875,
489.7575378417969
],
"flags": {},
"order": 1,
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
@@ -105,12 +43,101 @@
"image"
]
},
{
"id": 1,
"type": "[Comfy3D] Load PartCrafter Pipeline",
"pos": [
2166.07470703125,
1154.1156005859375
],
"size": [
346.0396423339844,
28.720361709594727
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "partcrafter_pipe",
"type": "DIFFUSERS_PIPE",
"links": [
1
]
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Load PartCrafter Pipeline"
},
"widgets_values": []
},
{
"id": 12,
"type": "[Comfy3D] Preview 3DMesh",
"pos": [
2927.549072265625,
1245.26904296875
],
"size": [
600,
500
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "mesh_file_path",
"type": "STRING",
"widget": {
"name": "mesh_file_path"
},
"link": 12
}
],
"outputs": [],
"properties": {
"Node name for S&R": "[Comfy3D] Preview 3DMesh"
},
"widgets_values": [
"",
null
]
},
{
"id": 9,
"type": "PreviewAny",
"pos": [
2927.620361328125,
1096.464111328125
],
"size": [
342.048828125,
88
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 7
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 2,
"type": "[Comfy3D] PartCrafter Generate",
"pos": [
2550.86669921875,
1193.7396240234375
2569.100341796875,
1241.7021484375
],
"size": [
318.76953125,
@@ -140,18 +167,16 @@
]
},
{
"name": "merged_mesh",
"type": "MESH",
"name": "glb_mesh_path",
"type": "STRING",
"links": [
5
12
]
},
{
"name": "processed_image",
"type": "IMAGE",
"links": [
3
]
"links": []
}
],
"properties": {
@@ -159,108 +184,16 @@
},
"widgets_values": [
4,
228764490511818,
836283434168025,
"randomize",
1024,
50,
7,
1000000000,
false,
false,
true,
2
]
},
{
"id": 9,
"type": "PreviewAny",
"pos": [
2912.02490234375,
1048.1806640625
],
"size": [
350.2099609375,
88
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 7
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 7,
"type": "[Comfy3D] Save 3D Mesh",
"pos": [
2933.991943359375,
1229.566162109375
],
"size": [
315,
58
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "mesh",
"type": "MESH",
"link": 5
}
],
"outputs": [
{
"name": "save_path",
"type": "STRING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Save 3D Mesh"
},
"widgets_values": [
"PartCrafter_Test/mesh.glb"
]
},
{
"id": 5,
"type": "PreviewImage",
"pos": [
2928.69384765625,
1368.6041259765625
],
"size": [
321.450439453125,
394.6763916015625
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 3
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
@@ -280,30 +213,6 @@
1,
"IMAGE"
],
[
3,
2,
2,
5,
0,
"IMAGE"
],
[
5,
2,
1,
7,
0,
"MESH"
],
[
6,
7,
0,
8,
0,
"STRING"
],
[
7,
2,
@@ -311,23 +220,31 @@
9,
0,
"*"
],
[
12,
2,
1,
12,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.9070294784580498,
"scale": 1.0292706480615603,
"offset": [
-2140.713805878563,
-975.661308705606
-2024.039719890923,
-1063.3672924124076
]
},
"node_versions": {
"ComfyUI-3D-Pack": "aedee80e370321bd78221b086445fe8a5f2f8906",
"comfy-core": "0.3.44"
},
"frontendVersion": "1.23.4",
"node_versions": {
"comfy-core": "0.3.44",
"ComfyUI-3D-Pack": "6fa0039ad586bdc6bc4f43af6be92ea3ebd479cb"
},
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

+60 -64
View File
@@ -126,7 +126,7 @@ from Gen_3D_Modules.Hunyuan3D_2_1.hy3dpaint.utils.torchvision_fix import apply_f
apply_fix()
from Gen_3D_Modules.PartCrafter.partcrafter_src.pipelines.pipeline_partcrafter import PartCrafterPipeline
from Gen_3D_Modules.PartCrafter.partcrafter_src.utils.data_utils import get_colored_mesh_composition
from Gen_3D_Modules.PartCrafter.partcrafter_src.utils.image_utils import prepare_image
from Gen_3D_Modules.PartCrafter.partcrafter_src.utils.render_utils import explode_mesh
import zipfile
@@ -262,12 +262,15 @@ class Preview_3DMesh:
mesh_folder_path, filename = os.path.split(mesh_file_path)
if not os.path.isabs(mesh_file_path):
mesh_file_path = os.path.join(comfy_paths.output_directory, mesh_folder_path)
mesh_file_path = os.path.join(comfy_paths.output_directory, mesh_folder_path, filename)
if not filename.lower().endswith(SUPPORTED_3D_EXTENSIONS):
cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3D file extensions: {SUPPORTED_3D_EXTENSIONS}").error.print()
mesh_file_path = ""
print(f"[Preview_3DMesh] Final mesh path: {mesh_file_path}")
print(f"[Preview_3DMesh] File exists: {os.path.exists(mesh_file_path) if mesh_file_path else False}")
previews = [
{
"filepath": mesh_file_path,
@@ -5584,8 +5587,7 @@ class Hunyuan3D_21_TexGen:
except:
pass
# --------------------- PARTCRAFTER ---------------------
class Load_PartCrafter_Pipeline:
"""Load PartCrafter Pipeline"""
@@ -5644,12 +5646,13 @@ class Load_PartCrafter_Pipeline:
print(f"PartCrafter pipeline loaded")
return (pipeline,)
class PartCrafter_Generate:
"""PartCrafter Generation"""
"""PartCrafter Generation - Creates multi-part 3D scenes with colored components"""
CATEGORY = "Comfy3D/Algorithm/PartCrafter"
RETURN_TYPES = ("STRING", "MESH", "IMAGE")
RETURN_NAMES = ("parts_zip_path", "merged_mesh", "processed_image")
RETURN_TYPES = ("STRING", "STRING", "IMAGE")
RETURN_NAMES = ("parts_zip_path", "glb_mesh_path", "processed_image")
FUNCTION = "generate"
@classmethod
@@ -5704,37 +5707,38 @@ class PartCrafter_Generate:
use_flash_decoder=use_flash_decoder,
).meshes
# Process results
for i in range(len(outputs)):
if outputs[i] is None:
# If mesh is None (decoding error), use dummy mesh
outputs[i] = trimesh.Trimesh(vertices=[[0, 0, 0]], faces=[[0, 0, 0]])
# Ensure no None outputs
for i, mesh in enumerate(outputs):
if mesh is None:
outputs[i] = trimesh.Trimesh(vertices=[[0,0,0]], faces=[[0,0,0]])
print(f"Replaced None mesh at index {i} with dummy mesh")
# Create merged mesh
merged_mesh_trimesh = get_colored_mesh_composition(outputs)
split_mesh = explode_mesh(merged_mesh_trimesh)
# Debug logging
print(f"Merged mesh type: {type(merged_mesh_trimesh)}")
print(f"Merged mesh attributes: {dir(merged_mesh_trimesh)}")
print(f"PartCrafter result type: {type(merged_mesh_trimesh)}")
if hasattr(merged_mesh_trimesh, 'geometry'):
print(f"Scene geometry keys: {list(merged_mesh_trimesh.geometry.keys())}")
print(f"Scene contains {len(merged_mesh_trimesh.geometry)} parts: {list(merged_mesh_trimesh.geometry.keys())}")
# Create ZIP with individual parts
parts_output_dir = "output/partcrafter_parts"
os.makedirs(parts_output_dir, exist_ok=True)
zip_path = os.path.join(parts_output_dir, f"parts_seed_{seed}.zip")
zip_path = os.path.join(parts_output_dir, f"parts.zip")
parts = []
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for i, part in enumerate(outputs):
print(f"Part {i} type: {type(part)}")
if part is not None:
for idx, mesh in enumerate(outputs):
print(f"Part {idx} type: {type(mesh)}")
if mesh is not None:
# Save part as GLB file
part_path = os.path.join(parts_output_dir, f"part_{i:02d}.glb")
part.export(part_path)
part_path = os.path.join(parts_output_dir, f"part_{idx:02d}.glb")
mesh.export(part_path)
parts.append(part_path)
# Add to ZIP
zipf.write(part_path, f"part_{i:02d}.glb")
zipf.write(part_path, f"part_{idx:02d}.glb")
# Clean up temporary file
try:
@@ -5742,56 +5746,48 @@ class PartCrafter_Generate:
except:
pass
print(f"Created parts ZIP: {zip_path}")
print(f"Created parts ZIP: {zip_path} with {len(parts)} parts")
# Handle Scene vs Mesh for merged result
# Save merged mesh
scene_output_dir = "output/partcrafter_scenes"
os.makedirs(scene_output_dir, exist_ok=True)
scene_file_path = os.path.join(scene_output_dir, f"scene.glb")
# For Preview_3DMesh, return relative path from ComfyUI output directory
relative_scene_path = "partcrafter_scenes/scene.glb"
# Export the merged mesh (same as app.py: merged.export(merged_path))
merged_mesh_trimesh.export(scene_file_path)
print(f"Saved merged colored mesh to: {scene_file_path}")
# Debug: check what was saved
if isinstance(merged_mesh_trimesh, trimesh.Scene):
print("Merged result is Scene, combining all geometries")
if merged_mesh_trimesh.geometry:
# Combine all geometries into one mesh
all_meshes = []
for geom_name in merged_mesh_trimesh.geometry:
geom = merged_mesh_trimesh.geometry[geom_name]
print(f"Geometry {geom_name}: {type(geom)}, vertices: {len(geom.vertices)}, faces: {len(geom.faces)}")
all_meshes.append(geom)
# Combine all meshes into one
if len(all_meshes) > 1:
combined_mesh = trimesh.util.concatenate(all_meshes)
print(f"Combined mesh: vertices: {len(combined_mesh.vertices)}, faces: {len(combined_mesh.faces)}")
else:
combined_mesh = all_meshes[0]
merged_mesh = Mesh.load_trimesh(given_mesh=combined_mesh)
else:
print("Scene has no geometry, creating dummy mesh")
# Create a dummy mesh if scene is empty
dummy_trimesh = trimesh.Trimesh(vertices=[[0, 0, 0]], faces=[[0, 0, 0]])
merged_mesh = Mesh.load_trimesh(given_mesh=dummy_trimesh)
print(f"Exported Scene with {len(merged_mesh_trimesh.geometry)} parts")
for geom_name in merged_mesh_trimesh.geometry:
geom = merged_mesh_trimesh.geometry[geom_name]
print(f" Part {geom_name}: vertices: {len(geom.vertices)}, faces: {len(geom.faces)}")
if hasattr(geom.visual, 'vertex_colors') and geom.visual.vertex_colors is not None:
print(f" Has vertex colors: {geom.visual.vertex_colors.shape}")
else:
print("Merged result is Mesh, loading directly")
merged_mesh = Mesh.load_trimesh(given_mesh=merged_mesh_trimesh)
print(f"Exported single mesh: vertices: {len(merged_mesh_trimesh.vertices)}, faces: {len(merged_mesh_trimesh.faces)}")
merged_mesh.auto_normal()
# Save debug merged result
# Debug info about the scene
try:
debug_dir = "output/partcrafter_debug"
os.makedirs(debug_dir, exist_ok=True)
# Save merged result for debugging
if isinstance(merged_mesh_trimesh, trimesh.Scene):
merged_mesh_trimesh.export(os.path.join(debug_dir, f"merged_scene_seed_{seed}.glb"))
print(f"Scene file saved successfully: {scene_file_path}")
if os.path.exists(scene_file_path):
file_size = os.path.getsize(scene_file_path) / (1024 * 1024) # MB
print(f"Scene file size: {file_size:.2f} MB")
print(f"Relative path for Preview_3DMesh: {relative_scene_path}")
else:
merged_mesh_trimesh.export(os.path.join(debug_dir, f"merged_mesh_seed_{seed}.obj"))
print(f"Debug merged file saved to {debug_dir}")
print(f"Warning: Scene file was not created properly")
except Exception as e:
print(f"Failed to save debug files: {e}")
print(f"Error checking scene file: {e}")
# Processed image
processed_image_tensor = pils_to_torch_imgs([pil_image])
print(f"Generated {len(outputs)} parts")
print(f"PartCrafter: Generated {len(outputs)} parts with explode_mesh processing")
print(f"GLB mesh path for Preview_3DMesh: {relative_scene_path}")
return (zip_path, merged_mesh, processed_image_tensor)
return (zip_path, relative_scene_path, processed_image_tensor)