Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions embodichain/gen_sim/simready_pipeline/configs/gen_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@
},
"llm": {
"openai_compatible": {
"api_key": "",
"model": "gpt-4o",
"base_url": "",
"api_key": "sk-7hjyRgBLrhUYUSCpLgPSARk8sz1Sc2vZ2bnt3fy1bkHsI7ak",
"model": "gpt-5",
"base_url": "https://airouter.cloud/v1",
"default_query": {}
}
}
Expand Down
89 changes: 60 additions & 29 deletions embodichain/gen_sim/simready_pipeline/parser/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,58 +26,89 @@
class InternalParser(AssetParser):
name = "internal"

@staticmethod
def _look_at_pose(
eye: np.ndarray, target: np.ndarray, up: np.ndarray
) -> np.ndarray:
"""Build a pose matrix whose -Z axis points from ``eye`` toward ``target``.

This is the convention pyrender uses for both cameras (look direction is
-Z) and directional lights (light travels along -Z), so the same matrix
can orient a camera or aim a light at the model.
"""
forward = eye - target
forward = forward / np.linalg.norm(forward)

right = np.cross(up, forward)
right = right / np.linalg.norm(right)

corrected_up = np.cross(forward, right)

pose = np.eye(4)
pose[:3, 0] = right
pose[:3, 1] = corrected_up
pose[:3, 2] = forward
pose[:3, 3] = eye
return pose

@staticmethod
def _render_thumbnail(mesh: trimesh.Trimesh, output_path: Path) -> None:
"""
Internal static function to handle the rendering logic.
Camera is on X-axis positive, looking at the mesh's bounding box center.
Camera looks at the mesh's bounding box center from a 3/4 front angle.
Z-axis is up.
"""
bounds = mesh.bounds
model_center = (bounds[0] + bounds[1]) / 2.0
size = bounds[1] - bounds[0]

target_frustum_size = max(size[1], size[2]) * 1.5
# Frame against the bounding sphere so the 3/4 view always fits nicely.
radius = float(np.linalg.norm(size) / 2.0)
yfov = np.pi / 4.0
img_width, img_height = 512, 512
camera_distance = (target_frustum_size / 2.0) / np.tan(yfov / 2.0)
camera_distance = (radius * 1.3) / np.tan(yfov / 2.0)

eye = model_center + np.array([camera_distance, 0.0, 0.0])
target = model_center # Look at the mesh center, not origin
up = np.array([0.0, 0.0, 1.0]) # Z-up

forward = eye - target
forward = forward / np.linalg.norm(forward)

right = np.cross(up, forward)
right = right / np.linalg.norm(right)

corrected_up = np.cross(forward, right)

camera_pose = np.eye(4)
camera_pose[:3, 0] = right
camera_pose[:3, 1] = corrected_up
camera_pose[:3, 2] = forward
camera_pose[:3, 3] = eye
# Classic 3/4 product shot: front (+X), slightly to the side (+Y),
# slightly from above (+Z). A flat dead-on side view looks lifeless.
view_dir = np.array([1.0, 0.55, 0.45])
view_dir = view_dir / np.linalg.norm(view_dir)
eye = model_center + view_dir * camera_distance
camera_pose = InternalParser._look_at_pose(eye, model_center, up)

scene = pyrender.Scene(bg_color=[1.0, 1.0, 1.0, 1.0])
pyrender_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False)
scene = pyrender.Scene(
bg_color=[1.0, 1.0, 1.0, 1.0],
ambient_light=[0.45, 0.45, 0.45], # soft base fill, avoids black shadows
)
pyrender_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=True)
scene.add(pyrender_mesh)

camera = pyrender.PerspectiveCamera(
yfov=yfov, aspectRatio=img_width / img_height
)
scene.add(camera, pose=camera_pose)

key_light = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=3.0)
key_pose = np.eye(4)
key_pose[:3, 3] = eye + np.array([0, camera_distance, camera_distance])
scene.add(key_light, pose=key_pose)
# Three-point lighting, all aimed at the model center so the
# camera-facing side is properly lit (directional lights shine along
# their pose -Z, so they must be oriented, not just positioned).
key_pos = model_center + np.array([1.0, 0.8, 1.2]) * camera_distance
key_light = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=4.0)
scene.add(
key_light, pose=InternalParser._look_at_pose(key_pos, model_center, up)
)

fill_pos = model_center + np.array([0.6, -1.0, 0.3]) * camera_distance
fill_light = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=2.0)
scene.add(
fill_light, pose=InternalParser._look_at_pose(fill_pos, model_center, up)
)

fill_light = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=1.0)
fill_pose = np.eye(4)
fill_pose[:3, 3] = eye + np.array([0, -camera_distance, 0.5 * camera_distance])
scene.add(fill_light, pose=fill_pose)
rim_pos = model_center + np.array([-1.0, -0.5, 0.8]) * camera_distance
rim_light = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=2.5)
scene.add(
rim_light, pose=InternalParser._look_at_pose(rim_pos, model_center, up)
)

renderer = pyrender.OffscreenRenderer(
viewport_width=img_width, viewport_height=img_height
Expand All @@ -93,7 +124,7 @@ def parse(self, asset: Asset, asset_root: Path) -> None:
asset.internal.setdefault("error", None)

mesh_path_ori = asset_root / asset.asset_data.get("path")
mesh_path_sr = asset_root / "asset_simready" / "asset_simready.obj"
mesh_path_sr = asset_root / "asset_simready" / "asset_simready.glb"
mesh_path = None
if mesh_path_sr.exists():
mesh_path = mesh_path_sr
Expand Down
9 changes: 7 additions & 2 deletions embodichain/gen_sim/simready_pipeline/parser/physics.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,12 @@ def _ensure_sections(self, asset: Asset) -> None:
asset.simulation.setdefault("blockers", [])

def _simready_process(self, asset: Asset, asset_root: Path) -> None:
mesh_path = asset_root / asset.asset_data.get("path")

# mesh_path = asset_root / asset.asset_data.get("path")
archive_dir = asset_root / "asset_archive"
src_name = str(asset.identity.get("source_file"))
mesh_path = next(archive_dir.rglob(src_name), None)

out_path = asset_root / "asset_simready"

result = process_mesh(
Expand All @@ -277,7 +282,7 @@ def _simready_process(self, asset: Asset, asset_root: Path) -> None:
asset.semantics.update(semantics_generated)
delete_rendered_pngs(out_path)
asset.simulation["sim_ready"]["is_sim_ready"] = True
sim_ready_path = asset_root / "asset_simready" / "asset_simready.obj"
sim_ready_path = asset_root / "asset_simready" / "asset_simready.glb"
rel_path = sim_ready_path.relative_to(asset_root)
asset.simulation["sim_ready"]["sim_ready_path"] = str(rel_path)
return
Expand Down
Loading
Loading