| """ |
| Render Scene Overview: Multi-object tabletop |
| Rotating camera showing the full scene with 5 objects of different materials. |
| This is the "hero" shot for the playbook. |
| """ |
| import mujoco |
| import numpy as np |
| import imageio |
|
|
| WIDTH, HEIGHT = 1280, 720 |
| FPS = 60 |
| OUTPUT_DIR = "." |
| DURATION_SEC = 6.0 |
|
|
| XML = """ |
| <mujoco model="tabletop_scene"> |
| <option timestep="0.002" gravity="0 0 -9.81"/> |
| |
| <visual> |
| <global offwidth="{w}" offheight="{h}"/> |
| <quality shadowsize="4096"/> |
| <headlight ambient="0.25 0.25 0.28" diffuse="0.5 0.5 0.55"/> |
| </visual> |
| |
| <asset> |
| <texture name="grid" type="2d" builtin="checker" rgb1="0.32 0.32 0.35" rgb2="0.28 0.28 0.31" |
| width="256" height="256"/> |
| <material name="grid_mat" texture="grid" texrepeat="10 10" reflectance="0.15"/> |
| <texture name="wood" type="2d" builtin="flat" rgb1="0.52 0.38 0.24" width="1" height="1"/> |
| <material name="wood_mat" texture="wood" specular="0.15" shininess="0.3"/> |
| |
| <!-- Object materials --> |
| <material name="mat_wood" rgba="0.65 0.45 0.25 1" specular="0.2" shininess="0.3"/> |
| <material name="mat_rubber" rgba="0.18 0.18 0.20 1" specular="0.05" shininess="0.1"/> |
| <material name="mat_metal" rgba="0.72 0.73 0.76 1" specular="0.8" shininess="0.95"/> |
| <material name="mat_plastic" rgba="0.2 0.55 0.85 1" specular="0.4" shininess="0.6"/> |
| <material name="mat_ceramic" rgba="0.92 0.90 0.85 1" specular="0.3" shininess="0.5"/> |
| </asset> |
| |
| <worldbody> |
| <light pos="0.3 -0.5 1.8" dir="-0.15 0.3 -1" castshadow="true" diffuse="0.85 0.82 0.78"/> |
| <light pos="-0.5 0.3 1.2" dir="0.3 -0.15 -1" castshadow="true" diffuse="0.3 0.32 0.38"/> |
| |
| <geom name="floor" type="plane" size="3 3 0.1" material="grid_mat"/> |
| |
| <!-- Table --> |
| <body name="table" pos="0 0 0.4"> |
| <geom type="box" size="0.45 0.3 0.025" material="wood_mat"/> |
| <geom type="cylinder" size="0.018 0.19" pos="-0.40 -0.25 -0.21" rgba="0.42 0.32 0.22 1"/> |
| <geom type="cylinder" size="0.018 0.19" pos=" 0.40 -0.25 -0.21" rgba="0.42 0.32 0.22 1"/> |
| <geom type="cylinder" size="0.018 0.19" pos="-0.40 0.25 -0.21" rgba="0.42 0.32 0.22 1"/> |
| <geom type="cylinder" size="0.018 0.19" pos=" 0.40 0.25 -0.21" rgba="0.42 0.32 0.22 1"/> |
| </body> |
| |
| <!-- Wood block --> |
| <body name="wood_block" pos="-0.18 0.08 0.46"> |
| <joint type="free"/> |
| <geom type="box" size="0.035 0.035 0.035" mass="0.6" material="mat_wood" |
| friction="0.4 0.005 0.0001"/> |
| </body> |
| |
| <!-- Rubber ball --> |
| <body name="rubber_ball" pos="0.05 -0.1 0.455"> |
| <joint type="free"/> |
| <geom type="sphere" size="0.03" mass="1.1" material="mat_rubber" |
| friction="0.8 0.005 0.0001"/> |
| </body> |
| |
| <!-- Metal cylinder --> |
| <body name="metal_cylinder" pos="0.2 0.05 0.455"> |
| <joint type="free"/> |
| <geom type="cylinder" size="0.02 0.03" mass="3.0" material="mat_metal" |
| friction="0.2 0.005 0.0001"/> |
| </body> |
| |
| <!-- Plastic cup (hollow cylinder approximation) --> |
| <body name="plastic_cup" pos="-0.05 0.12 0.46"> |
| <joint type="free"/> |
| <geom type="cylinder" size="0.025 0.035" mass="0.12" material="mat_plastic" |
| friction="0.35 0.005 0.0001"/> |
| </body> |
| |
| <!-- Ceramic bowl --> |
| <body name="ceramic_bowl" pos="0.15 -0.08 0.45"> |
| <joint type="free"/> |
| <geom type="cylinder" size="0.04 0.015" mass="0.3" material="mat_ceramic" |
| friction="0.25 0.005 0.0001"/> |
| </body> |
| </worldbody> |
| </mujoco> |
| """.format(w=WIDTH, h=HEIGHT) |
|
|
|
|
| def render_orbit(filename, radius=0.75, height=0.65, n_frames=None): |
| """Render a smooth camera orbit around the tabletop.""" |
| model = mujoco.MjModel.from_xml_string(XML) |
| data = mujoco.MjData(model) |
| renderer = mujoco.Renderer(model, height=HEIGHT, width=WIDTH) |
| |
| mujoco.mj_forward(model, data) |
| |
| |
| for _ in range(500): |
| mujoco.mj_step(model, data) |
| |
| if n_frames is None: |
| n_frames = int(DURATION_SEC * FPS) |
| |
| frames = [] |
| scene = mujoco.MjvScene(model, maxgeom=1000) |
| camera = mujoco.MjvCamera() |
| option = mujoco.MjvOption() |
| |
| |
| camera.lookat[:] = [0, 0, 0.45] |
| camera.distance = radius |
| camera.elevation = -25 |
| |
| for i in range(n_frames): |
| t = i / n_frames |
| angle = t * 360 |
| camera.azimuth = angle + 135 |
| |
| |
| camera.elevation = -25 + 5 * np.sin(t * 2 * np.pi) |
| |
| mujoco.mjv_updateScene(model, data, option, None, camera, |
| mujoco.mjtCatBit.mjCAT_ALL, scene) |
| renderer.update_scene(data) |
| |
| |
| renderer._scene.camera[0].pos[:] = [ |
| radius * np.cos(np.radians(angle + 135)), |
| radius * np.sin(np.radians(angle + 135)), |
| height + 0.05 * np.sin(t * 2 * np.pi) |
| ] |
| |
| |
| |
| cam = mujoco.MjvCamera() |
| cam.lookat[:] = [0, 0, 0.43] |
| cam.distance = radius |
| cam.azimuth = angle + 135 |
| cam.elevation = -25 + 5 * np.sin(t * 2 * np.pi) |
| |
| renderer.update_scene(data, camera=cam) |
| frame = renderer.render() |
| frames.append(frame.copy()) |
| |
| if i % FPS == 0: |
| print(f" Frame {i}/{n_frames} ({t*100:.0f}%)") |
| |
| output_path = f"{OUTPUT_DIR}/{filename}" |
| writer = imageio.get_writer(output_path, fps=FPS, codec='libx264', |
| output_params=['-crf', '18', '-preset', 'medium']) |
| for frame in frames: |
| writer.append_data(frame) |
| writer.close() |
| print(f" Saved: {output_path} ({len(frames)} frames, {len(frames)/FPS:.1f}s)") |
| renderer.close() |
|
|
|
|
| def render_static_views(prefix="scene"): |
| """Render a few static views as individual frames (PNG).""" |
| model = mujoco.MjModel.from_xml_string(XML) |
| data = mujoco.MjData(model) |
| renderer = mujoco.Renderer(model, height=HEIGHT, width=WIDTH) |
| |
| mujoco.mj_forward(model, data) |
| for _ in range(500): |
| mujoco.mj_step(model, data) |
| |
| views = [ |
| ("front", 0.7, 180, -20), |
| ("angle", 0.75, 135, -25), |
| ("top", 0.9, 90, -80), |
| ("close", 0.45, 160, -15), |
| ] |
| |
| for name, dist, azimuth, elev in views: |
| cam = mujoco.MjvCamera() |
| cam.lookat[:] = [0, 0, 0.43] |
| cam.distance = dist |
| cam.azimuth = azimuth |
| cam.elevation = elev |
| |
| renderer.update_scene(data, camera=cam) |
| frame = renderer.render() |
| |
| output_path = f"{OUTPUT_DIR}/{prefix}_{name}.png" |
| imageio.imwrite(output_path, frame) |
| print(f" Saved: {output_path}") |
| |
| renderer.close() |
|
|
|
|
| if __name__ == "__main__": |
| print("=== Rendering Scene Overview ===\n") |
| |
| print("--- Static views ---") |
| render_static_views() |
| |
| print("\n--- Orbit video ---") |
| render_orbit("scene_overview.mp4") |
| |
| print("\nDone!") |
|
|