Skip to main content
Glama

stage_bake_simulation

Convert physics simulation data into keyframe animations for export to R3F/Remotion video rendering workflows.

Instructions

Bake physics simulation to keyframe animations.

Converts physics simulation data into keyframes that can be
exported to R3F/Remotion for video rendering.

Args:
    scene_id: Scene identifier
    simulation_id: Physics simulation ID from chuk-mcp-physics
    fps: Frames per second for sampling (default 60)
    duration: Duration in seconds to bake (if None, bakes entire simulation)
    physics_server_url: Optional Rapier HTTP server URL
        If None, defaults to public Rapier service (https://rapier.chukai.io)
        Can be overridden with RAPIER_SERVICE_URL environment variable

Returns:
    BakeSimulationResponse with frame count and baked object list

Tips for LLMs:
    - Run physics simulation first (chuk-mcp-physics step_simulation or record_trajectory)
    - Bind objects to physics bodies (stage_bind_physics)
    - Bake simulation to convert physics → animation keyframes
    - Then export scene to R3F/Remotion with animation data

Example:
    # After running simulation and binding objects
    result = await stage_bake_simulation(
        scene_id=scene_id,
        simulation_id=sim.sim_id,
        fps=60,
        duration=10.0
    )
    print(f"Baked {result.total_frames} frames for {len(result.baked_objects)} objects")

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
scene_idYes
simulation_idYes
fpsNo
durationNo
physics_server_urlNo

Implementation Reference

  • Main handler function stage_bake_simulation that converts physics simulation data into keyframe animations. It retrieves objects with physics bindings, uses PhysicsBridge to bake simulation data, stores animations in the scene VFS, and returns BakeSimulationResponse with frame counts and baked object IDs.
    @requires_auth()
    @tool  # type: ignore[arg-type]
    async def stage_bake_simulation(
        scene_id: str,
        simulation_id: str,
        fps: int = 60,
        duration: Optional[float] = None,
        physics_server_url: Optional[str] = None,
    ) -> BakeSimulationResponse:
        """Bake physics simulation to keyframe animations.
    
        Converts physics simulation data into keyframes that can be
        exported to R3F/Remotion for video rendering.
    
        Args:
            scene_id: Scene identifier
            simulation_id: Physics simulation ID from chuk-mcp-physics
            fps: Frames per second for sampling (default 60)
            duration: Duration in seconds to bake (if None, bakes entire simulation)
            physics_server_url: Optional Rapier HTTP server URL
                If None, defaults to public Rapier service (https://rapier.chukai.io)
                Can be overridden with RAPIER_SERVICE_URL environment variable
    
        Returns:
            BakeSimulationResponse with frame count and baked object list
    
        Tips for LLMs:
            - Run physics simulation first (chuk-mcp-physics step_simulation or record_trajectory)
            - Bind objects to physics bodies (stage_bind_physics)
            - Bake simulation to convert physics → animation keyframes
            - Then export scene to R3F/Remotion with animation data
    
        Example:
            # After running simulation and binding objects
            result = await stage_bake_simulation(
                scene_id=scene_id,
                simulation_id=sim.sim_id,
                fps=60,
                duration=10.0
            )
            print(f"Baked {result.total_frames} frames for {len(result.baked_objects)} objects")
        """
        manager = get_scene_manager()
        scene = await manager.get_scene(scene_id)
    
        # Find all objects with physics bindings
        bound_objects = [
            (obj_id, obj)
            for obj_id, obj in scene.objects.items()
            if obj.physics_binding and simulation_id in obj.physics_binding
        ]
    
        if not bound_objects:
            raise ValueError(f"No objects bound to simulation {simulation_id}")
    
        # Extract body IDs from bindings
        body_ids = []
        for obj_id, obj in bound_objects:
            # Parse "rapier://sim-{sim_id}/body-{body_id}"
            parts = obj.physics_binding.split("/")  # type: ignore
            if len(parts) >= 2:
                body_id = parts[-1].replace("body-", "")
                body_ids.append(body_id)
    
        logger.info(f"Baking simulation {simulation_id} for {len(body_ids)} bodies")
    
        # Use physics bridge to bake
        async with PhysicsBridge(physics_server_url) as bridge:
            baked_data = await bridge.bake_simulation(simulation_id, body_ids, fps, duration)
    
        # Store baked animations in scene workspace
        vfs = await manager.get_scene_vfs(scene_id)
        await vfs.mkdir("/animations")
    
        total_frames = 0
        baked_object_ids = []
    
        for obj_id, obj in bound_objects:
            # Get body ID
            body_id = obj.physics_binding.split("/")[-1].replace("body-", "")  # type: ignore
    
            if body_id in baked_data:
                keyframes = baked_data[body_id]
                total_frames = max(total_frames, len(keyframes))
    
                # Save keyframes to VFS
                keyframes_json = PhysicsBridge.keyframes_to_json(keyframes)
                animation_path = f"/animations/{obj_id}.json"
                await vfs.write_text(animation_path, keyframes_json)
    
                # Add baked animation to scene
                baked_anim = BakedAnimation(
                    object_id=obj_id,
                    source=simulation_id,
                    fps=fps,
                    frames=len(keyframes),
                    data_path=animation_path,
                )
                await manager.add_baked_animation(scene_id, obj_id, baked_anim)
    
                baked_object_ids.append(obj_id)
    
        return BakeSimulationResponse(
            scene_id=scene_id,
            baked_objects=baked_object_ids,
            total_frames=total_frames,
            fps=fps,
            message=f"Baked {total_frames} frames for {len(baked_object_ids)} objects",
        )
  • Tool registration using @tool decorator and @requires_auth() decorator to expose stage_bake_simulation as an MCP tool.
    @requires_auth()
    @tool  # type: ignore[arg-type]
    async def stage_bake_simulation(
        scene_id: str,
        simulation_id: str,
        fps: int = 60,
        duration: Optional[float] = None,
        physics_server_url: Optional[str] = None,
    ) -> BakeSimulationResponse:
  • BakeSimulationResponse schema defining the response structure with scene_id, baked_objects list, total_frames, fps, and message fields.
    class BakeSimulationResponse(BaseModel):
        """Response from baking simulation."""
    
        scene_id: str
        baked_objects: list[str]  # Object IDs that got animation data
        total_frames: int
        fps: int
        message: str = "Simulation baked successfully"
  • BakedAnimation schema defining stored animation metadata including object_id, source simulation ID, fps, frame count, and VFS data path.
    class BakedAnimation(BaseModel):
        """Baked animation data from physics simulation."""
    
        object_id: str
        source: str  # Physics simulation ID
        fps: int = Field(default=60, description="Frames per second")
        frames: int = Field(description="Total number of frames")
        data_path: str = Field(description="VFS path to animation data (binary or JSON)")
  • PhysicsBridge.bake_simulation method that calls the Rapier physics server HTTP API to retrieve trajectory data and convert it to keyframe format with position, rotation, and velocity data.
    async def bake_simulation(
        self,
        simulation_id: str,
        body_ids: list[str],
        fps: int = 60,
        duration: Optional[float] = None,
    ) -> dict[str, list[dict]]:
        """Bake physics simulation to keyframe data.
    
        Args:
            simulation_id: Physics simulation ID (e.g., from chuk-mcp-physics)
            body_ids: List of physics body IDs to bake
            fps: Frames per second for sampling
            duration: Duration in seconds (if None, bakes entire simulation)
    
        Returns:
            Dict mapping body_id to list of keyframes
            Each keyframe: {"time": float, "position": [x,y,z], "rotation": [x,y,z,w]}
    
        Raises:
            ValueError: If physics server is not configured
            httpx.HTTPError: If physics server request fails
        """
        if not self._client:
            raise ValueError(
                f"Physics client not initialized. Ensure PhysicsBridge is used as async context manager. "
                f"Rapier service URL: {self.physics_server_url}"
            )
    
        logger.info(f"Baking simulation {simulation_id} for {len(body_ids)} bodies at {fps} FPS")
    
        # Request trajectory data from physics server
        # This assumes chuk-mcp-physics has a compatible endpoint
        # For now, we'll use the record_trajectory tool via HTTP API
    
        baked_data: dict[str, list[dict]] = {}
    
        for body_id in body_ids:
            # Calculate number of steps from duration and fps
            if duration:
                steps = int(duration * fps)
            else:
                # Get current simulation time/steps
                steps = 600  # Default 10 seconds at 60 FPS
    
            # Call physics server to get trajectory
            # Rapier service endpoint: POST /simulations/{sim_id}/bodies/{body_id}/trajectory
            try:
                response = await self._client.post(
                    f"/simulations/{simulation_id}/bodies/{body_id}/trajectory",
                    json={
                        "steps": steps,
                        "dt": 1.0 / fps,
                    },
                )
                response.raise_for_status()
                trajectory_data = response.json()
    
                # Convert to our keyframe format
                keyframes = []
                for frame in trajectory_data.get("frames", []):
                    keyframes.append(
                        {
                            "time": frame["time"],
                            "position": frame["position"],
                            "rotation": frame["orientation"],
                            "velocity": frame.get("velocity", [0, 0, 0]),
                        }
                    )
    
                baked_data[body_id] = keyframes
                logger.info(f"Baked {len(keyframes)} frames for body {body_id}")
    
            except httpx.HTTPError as e:
                logger.error(f"Failed to bake trajectory for {body_id}: {e}")
                raise
    
        return baked_data

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-stage'

If you have feedback or need assistance with the MCP directory API, please join our Discord server