3D Joints

Play Demo

Demonstrates:

  • PinJoint3D: chain of RigidBody3D spheres swinging as a pendulum

  • HingeJoint3D: door panel rotating on a vertical hinge axis

  • StaticBody3D as fixed anchor / hinge post

  • PhysicsServer handles all constraint solving automatically

Controls: Space - Push the door R - Reset scene Left/Right - Orbit camera Escape - Quit

Run: uv run python packages/graphics/examples/3d_joints.py

Source Code

  1"""3D Joints Demo -- Pendulum chain and hinge door using physics joints.
  2
  3Demonstrates:
  4  - PinJoint3D: chain of RigidBody3D spheres swinging as a pendulum
  5  - HingeJoint3D: door panel rotating on a vertical hinge axis
  6  - StaticBody3D as fixed anchor / hinge post
  7  - PhysicsServer handles all constraint solving automatically
  8
  9Controls:
 10    Space       - Push the door
 11    R           - Reset scene
 12    Left/Right  - Orbit camera
 13    Escape      - Quit
 14
 15Run: uv run python packages/graphics/examples/3d_joints.py
 16"""
 17
 18
 19import math
 20
 21from simvx.core import (
 22    Camera3D,
 23    DirectionalLight3D,
 24    HingeJoint3D,
 25    Input,
 26    InputMap,
 27    Key,
 28    Material,
 29    Mesh,
 30    MeshInstance3D,
 31    Node,
 32    PinJoint3D,
 33    RigidBody3D,
 34    StaticBody3D,
 35    Text2D,
 36    Vec3,
 37)
 38from simvx.graphics import App
 39
 40CHAIN_LEN = 4
 41LINK_DIST = 1.2
 42
 43
 44class JointsDemo(Node):
 45    def ready(self):
 46        InputMap.add_action("push_door", [Key.SPACE])
 47        InputMap.add_action("reset", [Key.R])
 48        InputMap.add_action("orbit_left", [Key.LEFT])
 49        InputMap.add_action("orbit_right", [Key.RIGHT])
 50        InputMap.add_action("quit", [Key.ESCAPE])
 51
 52        # Camera
 53        self._cam = self.add_child(Camera3D(
 54            name="Camera", position=Vec3(0, 4, 14), look_at=Vec3(0, 2, 0), fov=55.0,
 55        ))
 56        self._orbit = 0.0
 57
 58        # Light
 59        light = self.add_child(DirectionalLight3D(name="Sun"))
 60        light.look_at(Vec3(-1, -2, -1))
 61
 62        # Ground
 63        ground = self.add_child(MeshInstance3D(name="Ground", mesh=Mesh.cube()))
 64        ground.material = Material(colour=(0.3, 0.35, 0.3), roughness=0.9)
 65        ground.scale = Vec3(20, 0.1, 20)
 66        ground.position = Vec3(0, -0.05, 0)
 67
 68        # --- Pendulum chain (left side) ---
 69        anchor_pos = Vec3(-4.0, 6.0, 0.0)
 70
 71        # Fixed anchor (static body)
 72        self._chain_anchor = self.add_child(StaticBody3D(name="ChainAnchor", position=anchor_pos))
 73
 74        # Chain bodies
 75        self._chain_bodies: list[RigidBody3D] = []
 76        for i in range(CHAIN_LEN):
 77            body = self.add_child(RigidBody3D(
 78                name=f"ChainBody{i}",
 79                position=Vec3(anchor_pos.x, anchor_pos.y - LINK_DIST * (i + 1), anchor_pos.z),
 80                mass=1.0, linear_damp=0.1,
 81            ))
 82            self._chain_bodies.append(body)
 83
 84        # Give initial sideways kick to first ball
 85        self._chain_bodies[0].linear_velocity = Vec3(4.0, 0, 0)
 86
 87        # Connect anchor to first body, then chain the rest with PinJoint3D
 88        prev = self._chain_anchor
 89        for body in self._chain_bodies:
 90            self.add_child(PinJoint3D(body_a=prev, body_b=body, distance=LINK_DIST, stiffness=1.0, damping=0.5))
 91            prev = body
 92
 93        # Visual spheres for chain
 94        self._chain_meshes: list[MeshInstance3D] = []
 95        # Anchor visual
 96        mi = self.add_child(MeshInstance3D(name="ChainAnchorVis", mesh=Mesh.sphere(radius=0.15)))
 97        mi.material = Material(colour=(1.0, 0.3, 0.3, 1.0), roughness=0.3, metallic=0.5)
 98        self._chain_meshes.append(mi)
 99        # Body visuals
100        for i in range(CHAIN_LEN):
101            mi = self.add_child(MeshInstance3D(name=f"ChainVis{i}", mesh=Mesh.sphere(radius=0.25)))
102            mi.material = Material(colour=(0.3, 0.6, 1.0, 1.0), roughness=0.3, metallic=0.5)
103            self._chain_meshes.append(mi)
104
105        # Anchor post
106        post = self.add_child(MeshInstance3D(name="AnchorPost", mesh=Mesh.cylinder(radius=0.08, height=1.0)))
107        post.material = Material(colour=(0.5, 0.5, 0.5), roughness=0.6)
108        post.position = Vec3(anchor_pos.x, anchor_pos.y + 0.5, anchor_pos.z)
109
110        # --- Hinge door (right side) ---
111        hinge_pos = Vec3(4, 2, 0)
112
113        # Door post (static body at hinge position)
114        self._door_post = self.add_child(StaticBody3D(name="DoorPostBody", position=hinge_pos))
115
116        # Door body (dynamic, offset from hinge)
117        self._door_body = self.add_child(RigidBody3D(
118            name="DoorBody", position=Vec3(hinge_pos.x + 1.0, hinge_pos.y, hinge_pos.z),
119            mass=5.0, linear_damp=0.5, angular_damp=2.0,
120        ))
121
122        # Hinge joint -- rotates around vertical Y axis, with angular limits
123        self._hinge = self.add_child(HingeJoint3D(
124            body_a=self._door_post, body_b=self._door_body, axis=Vec3(0, 1, 0),
125            stiffness=1.0, damping=0.3, bias_factor=0.5, angular_limit_min=-90, angular_limit_max=90,
126        ))
127
128        # Door post visual
129        dp = self.add_child(MeshInstance3D(name="DoorPost", mesh=Mesh.cylinder(radius=0.1, height=4.0)))
130        dp.material = Material(colour=(0.6, 0.6, 0.6), roughness=0.5)
131        dp.position = hinge_pos
132
133        # Door panel visual
134        self._door_vis = self.add_child(MeshInstance3D(name="Door", mesh=Mesh.cube()))
135        self._door_vis.material = Material(colour=(0.7, 0.4, 0.15), roughness=0.6)
136        self._door_vis.scale = Vec3(2.0, 3.5, 0.12)
137
138        # HUD
139        self.add_child(Text2D(
140            name="HUD", text="3D Joints: Space=push door | L/R=orbit | R=reset | ESC=quit", x=10, y=10, font_scale=1.2,
141        ))
142
143    def _reset(self):
144        anchor_pos = self._chain_anchor.position
145        for i, body in enumerate(self._chain_bodies):
146            body.position = Vec3(anchor_pos.x, anchor_pos.y - LINK_DIST * (i + 1), anchor_pos.z)
147            body.linear_velocity = Vec3()
148        self._chain_bodies[0].linear_velocity = Vec3(4.0, 0, 0)
149        hinge_pos = self._door_post.position
150        self._door_body.position = Vec3(hinge_pos.x + 1.0, hinge_pos.y, hinge_pos.z)
151        self._door_body.linear_velocity = Vec3()
152        self._door_body.angular_velocity = Vec3()
153        from simvx.core import Quat
154        self._door_body.rotation = Quat()  # Reset orientation to identity
155        self._hinge._initialized = False  # Recalculate reference angle
156        if hasattr(self._hinge, "_init_dir"):
157            del self._hinge._init_dir
158
159    def process(self, dt: float):
160        if Input.is_action_just_pressed("quit"):
161            self.app.quit()
162            return
163        if Input.is_action_just_pressed("reset"):
164            self._reset()
165
166        # Push door
167        if Input.is_action_just_pressed("push_door"):
168            self._door_body.apply_impulse(Vec3(0, 0, 15.0))
169
170        # Sync chain visuals to physics body positions
171        self._chain_meshes[0].position = self._chain_anchor.position
172        for i, body in enumerate(self._chain_bodies):
173            self._chain_meshes[i + 1].position = body.position
174
175        # Sync door visual to door body (HingeJoint3D sets rotation automatically)
176        self._door_vis.position = self._door_body.position
177        self._door_vis.rotation = self._door_body.rotation
178
179        # Camera orbit
180        if Input.is_action_pressed("orbit_left"):
181            self._orbit -= 1.5 * dt
182        if Input.is_action_pressed("orbit_right"):
183            self._orbit += 1.5 * dt
184        r = 14.0
185        self._cam.position = Vec3(math.sin(self._orbit) * r, 4, math.cos(self._orbit) * r)
186        self._cam.look_at(Vec3(0, 2, 0))
187
188
189if __name__ == "__main__":
190    App(title="3D Joints Demo", width=1280, height=720).run(JointsDemo())