3D Joints

Pendulum chain and hinge door using physics joints.

▶ Run in browser

Tags: 3d

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 examples/features/3d/joints.py

Source

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