Skip to content

Commit ffa5ff8

Browse files
authored
Merge pull request #515 from DenisKochetov/partcrafter-fix
Partcrafter fix
2 parents 6fa0039 + 080747c commit ffa5ff8

6 files changed

Lines changed: 223 additions & 238 deletions

File tree

Gen_3D_Modules/PartCrafter/partcrafter_src/pipelines/pipeline_partcrafter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def __call__(
337337
max_num_expanded_coords=max_num_expanded_coords,
338338
# verbose=True
339339
)
340-
mesh = trimesh.Trimesh(mesh_v_f[0].astype(np.float32), mesh_v_f[1])
340+
mesh = trimesh.Trimesh(mesh_v_f[0].astype(np.float32), mesh_v_f[1], process=True)
341341
except:
342342
mesh_v_f = None
343343
mesh = None

Gen_3D_Modules/PartCrafter/partcrafter_src/utils/render_utils.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,60 @@
1010
from diffusers.utils.loading_utils import load_video
1111
import torch
1212
from torchvision.utils import make_grid
13+
import math
1314

1415
os.environ['PYOPENGL_PLATFORM'] = 'egl'
1516

17+
def explode_mesh(mesh, explosion_scale=0.4):
18+
# ensure we have a Scene
19+
if isinstance(mesh, trimesh.Trimesh):
20+
scene = trimesh.Scene(mesh)
21+
elif isinstance(mesh, trimesh.Scene):
22+
scene = mesh
23+
else:
24+
raise ValueError(f"Expected Trimesh or Scene, got {type(mesh)}")
25+
26+
if len(scene.geometry) <= 1:
27+
print("Nothing to explode")
28+
return scene
29+
30+
# 1) collect (name, geom, world_center)
31+
parts = []
32+
for name, geom in scene.geometry.items():
33+
# ← get(name) returns (4×4 world‐space matrix, parent_frame)
34+
world_tf, _ = scene.graph.get(name)
35+
pts = trimesh.transformations.transform_points(geom.vertices, world_tf)
36+
center = pts.mean(axis=0)
37+
parts.append((name, geom, center))
38+
39+
# compute global center
40+
all_centers = np.stack([c for _,_,c in parts], axis=0)
41+
global_center = all_centers.mean(axis=0)
42+
43+
exploded = trimesh.Scene()
44+
for name, geom, center in parts:
45+
dir_vec = center - global_center
46+
norm = np.linalg.norm(dir_vec)
47+
if norm < 1e-6:
48+
dir_vec = np.random.randn(3)
49+
dir_vec /= np.linalg.norm(dir_vec)
50+
else:
51+
dir_vec /= norm
52+
53+
offset = dir_vec * explosion_scale
54+
55+
# fetch the same 4×4, then bump just the translation
56+
world_tf, _ = scene.graph.get(name)
57+
world_tf = world_tf.copy()
58+
world_tf[:3, 3] += offset
59+
60+
exploded.add_geometry(geom, transform=world_tf, geom_name=name)
61+
print(f"[explode] {name} moved by {np.linalg.norm(offset):.4f}")
62+
63+
return exploded
64+
65+
66+
1667
def render(
1768
scene: pyrender.Scene,
1869
renderer: pyrender.Renderer,
@@ -123,13 +174,22 @@ def render_views_around_mesh(
123174
Tuple[List[Image.Image], List[Image.Image]],
124175
Tuple[List[np.ndarray], List[np.ndarray]]
125176
]:
177+
178+
meshes = []
179+
scenes = []
126180

127181
if not isinstance(mesh, (trimesh.Trimesh, trimesh.Scene)):
128182
raise ValueError("mesh must be a trimesh.Trimesh or trimesh.Scene object")
129183
if isinstance(mesh, trimesh.Trimesh):
130-
mesh = trimesh.Scene(mesh)
131-
132-
scene = pyrender.Scene.from_trimesh_scene(mesh)
184+
for i in range(num_views):
185+
scenes.append(pyrender.Scene.from_trimesh_scene(trimesh.Scene(mesh)))
186+
else:
187+
for i in range(num_views):
188+
value = math.sin(math.pi * (i - 1) / num_views)
189+
scenes.append(pyrender.Scene.from_trimesh_scene(explode_mesh(mesh, 0.2 * value),
190+
ambient_light=[0.02, 0.02, 0.02],
191+
bg_color=[0.0, 0.0, 0.0, 1.0]))
192+
133193
light = pyrender.DirectionalLight(
134194
color=np.ones(3),
135195
intensity=light_intensity
@@ -149,9 +209,9 @@ def render_views_around_mesh(
149209
)
150210

151211
images, depths = [], []
152-
for pose in camera_poses:
212+
for i, pose in enumerate(camera_poses):
153213
image, depth = render(
154-
scene, renderer, camera, pose, light,
214+
scenes[i], renderer, camera, pose, light,
155215
normalize_depth=normalize_depth,
156216
flags=flags,
157217
return_type=return_type

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ Your ComfyUI Root Directory\python_embeded\python.exe install.py
4545
## Features:
4646
- For use cases please check out [Example Workflows](./_Example_Workflows/). [**Last update: 5/June/2025**]
4747
- **Note:** you need to put [Example Inputs Files & Folders](_Example_Workflows/_Example_Inputs_Files/) under ComfyUI Root Directory\ComfyUI\input folder before you can run the example workflow
48+
49+
- **PartCrafter**: [wgsxm/PartCrafter](https://github.com/wgsxm/PartCrafter)
50+
- Pipeline:
51+
- Single image → 3D mesh with **part segmentation**
52+
- Optional background removal (`rembg`)
53+
- Output includes:
54+
- Merged full mesh
55+
- ZIP archive with individual part meshes
56+
- Model weights: [HuggingFace - PartCrafter](https://huggingface.co/wgsxm/PartCrafter)
57+
- [Workflow](./_Example_Workflows/PartCrafter.json)
58+
59+
<video controls autoplay loop src="https://github.com/user-attachments/assets/b80bcc97-7381-4cf7-9ec6-ee48c8d58217"></video>
4860

4961
- **Hunyuan3D_2.1**: [Tencent-Hunyuan/Hunyuan3D-2.1](https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1)
5062
- Updated two-stage pipeline:

0 commit comments

Comments
 (0)