3D Joints

Pendulum chain and hinge door using physics joints.

▶ Run in browser

Tags: 3d

Demonstrates:

  • PinJoint3D: chain of PhysicsBody3D(DYNAMIC) spheres swinging as a pendulum

  • HingeJoint3D: door panel rotating about a vertical hinge axis

  • PhysicsBody3D(STATIC) as fixed anchor / hinge post

  • The PhysicsWorld solves every constraint automatically each fixed step

Controls: Space or click - Push the door Left/Right keys or horizontal drag - Orbit camera R - Reset scene 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 }
  5# ///
  6
  7Demonstrates:
  8  - PinJoint3D: chain of PhysicsBody3D(DYNAMIC) spheres swinging as a pendulum
  9  - HingeJoint3D: door panel rotating about a vertical hinge axis
 10  - PhysicsBody3D(STATIC) as fixed anchor / hinge post
 11  - The PhysicsWorld solves every constraint automatically each fixed step
 12
 13Controls:
 14    Space or click  - Push the door
 15    Left/Right keys or horizontal drag - Orbit camera
 16    R               - Reset scene
 17    Escape          - Quit
 18
 19Run: uv run python examples/features/3d/joints.py
 20"""
 21
 22import math
 23
 24from simvx.core import (
 25    BodyMode,
 26    BoxShape3D,
 27    Camera3D,
 28    CollisionShape3D,
 29    DirectionalLight3D,
 30    HingeJoint3D,
 31    Input,
 32    InputMap,
 33    Key,
 34    Material,
 35    Mesh,
 36    MeshInstance3D,
 37    MouseButton,
 38    Node,
 39    PhysicsBody3D,
 40    PhysicsRoot,
 41    PinJoint3D,
 42    SphereShape3D,
 43    Text2D,
 44    Vec3,
 45)
 46from simvx.graphics import App
 47
 48CHAIN_LEN = 4
 49LINK_DIST = 1.2
 50
 51
 52class JointsDemo(Node):
 53    def on_ready(self):
 54        InputMap.add_action("push_door", [Key.SPACE])
 55        InputMap.add_action("reset", [Key.R])
 56        InputMap.add_action("orbit_left", [Key.LEFT])
 57        InputMap.add_action("orbit_right", [Key.RIGHT])
 58        InputMap.add_action("quit", [Key.ESCAPE])
 59
 60        # One isolated 3D world (Y-up, default gravity).
 61        self._root = self.add_child(PhysicsRoot(name="World"))
 62
 63        # Camera
 64        self._cam = self.add_child(Camera3D(
 65            name="Camera", position=Vec3(0, 4, 14), look_at=Vec3(0, 2, 0), fov=55.0,
 66        ))
 67        self._orbit = 0.0
 68        self._drag_dist = 0.0
 69
 70        # Light
 71        light = self.add_child(DirectionalLight3D(name="Sun"))
 72        light.look_at(Vec3(-1, -2, -1))
 73
 74        # Ground (visual only)
 75        ground = self.add_child(MeshInstance3D(name="Ground", mesh=Mesh.cube()))
 76        ground.material = Material(colour=(0.3, 0.35, 0.3), roughness=0.9)
 77        ground.scale = Vec3(20, 0.1, 20)
 78        ground.position = Vec3(0, -0.05, 0)
 79
 80        # --- Pendulum chain (left side) ---
 81        anchor_pos = Vec3(-4.0, 6.0, 0.0)
 82
 83        # Fixed anchor (static body). Disjoint mask so beads never self-collide.
 84        self._chain_anchor = self._make_body(
 85            "ChainAnchor", BodyMode.STATIC, anchor_pos, radius=0.15,
 86            colour=(1.0, 0.3, 0.3, 1.0), mask=0x0,
 87        )
 88
 89        # Chain bodies, each pinned to the one above at the upper pivot.
 90        self._chain_bodies: list[PhysicsBody3D] = []
 91        self._chain_start: list[Vec3] = []
 92        prev = self._chain_anchor
 93        prev_pos = anchor_pos
 94        for i in range(CHAIN_LEN):
 95            pos = Vec3(anchor_pos.x, anchor_pos.y - LINK_DIST * (i + 1), anchor_pos.z)
 96            body = self._make_body(
 97                f"ChainBody{i}", BodyMode.DYNAMIC, pos, radius=0.25,
 98                colour=(0.3, 0.6, 1.0, 1.0), mask=0x0,
 99            )
100            self._chain_bodies.append(body)
101            self._chain_start.append(pos)
102            self._root.add_child(PinJoint3D(body_a=prev, body_b=body, anchor=prev_pos))
103            prev = body
104            prev_pos = pos
105
106        # Give the first ball a sideways kick so the chain swings.
107        self._chain_bodies[0].velocity = Vec3(4.0, 0, 0)
108
109        # Anchor post (visual only)
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 the hinge position).
118        self._door_post = self._make_body(
119            "DoorPostBody", BodyMode.STATIC, hinge_pos, radius=0.12,
120            colour=(0.6, 0.6, 0.6, 1.0), mask=0x0, visible=False,
121        )
122
123        # Door body (dynamic, offset from the hinge), a box panel.
124        self._door_start = Vec3(hinge_pos.x + 1.0, hinge_pos.y, hinge_pos.z)
125        self._door_body = PhysicsBody3D(
126            name="DoorBody", mode=BodyMode.DYNAMIC, mass=5.0, position=self._door_start,
127            collision_mask=0x0,
128        )
129        self._door_body.add_child(CollisionShape3D(shape=BoxShape3D(half_extents=Vec3(1.0, 1.75, 0.06))))
130        self._door_body.add_child(MeshInstance3D(
131            name="DoorVis", mesh=Mesh.cube(),
132            material=Material(colour=(0.7, 0.4, 0.15), roughness=0.6),
133            scale=Vec3(2.0, 3.5, 0.12),
134        ))
135        self._root.add_child(self._door_body)
136
137        # Hinge joint -- rotates about the vertical Y axis at the hinge pivot.
138        self._root.add_child(HingeJoint3D(
139            body_a=self._door_post, body_b=self._door_body, anchor=hinge_pos, axis=Vec3(0, 1, 0),
140        ))
141
142        # Door post visual.
143        dp = self.add_child(MeshInstance3D(name="DoorPost", mesh=Mesh.cylinder(radius=0.1, height=4.0)))
144        dp.material = Material(colour=(0.6, 0.6, 0.6), roughness=0.5)
145        dp.position = hinge_pos
146
147        # HUD
148        self.add_child(Text2D(
149            name="HUD", text="3D Joints: Space/click=push door | arrows/drag=orbit | R=reset | ESC=quit",
150            position=(10, 10), font_scale=1.2,
151        ))
152
153    def _make_body(self, name, mode, pos, *, radius, colour, mask, visible=True):
154        """Build a sphere PhysicsBody3D with a collider + visual, add to the world."""
155        body = PhysicsBody3D(name=name, mode=mode, mass=1.0, position=pos, collision_mask=mask)
156        body.add_child(CollisionShape3D(shape=SphereShape3D(radius=radius)))
157        if visible:
158            body.add_child(MeshInstance3D(
159                mesh=Mesh.sphere(radius=radius), material=Material(colour=colour, roughness=0.3, metallic=0.5),
160            ))
161        self._root.add_child(body)
162        return body
163
164    def _reset(self):
165        for body, pos in zip(self._chain_bodies, self._chain_start, strict=True):
166            body.position = Vec3(pos.x, pos.y, pos.z)
167            body.velocity = Vec3()
168        self._chain_bodies[0].velocity = Vec3(4.0, 0, 0)
169        self._door_body.position = Vec3(self._door_start.x, self._door_start.y, self._door_start.z)
170        self._door_body.velocity = Vec3()
171
172    def on_update(self, dt: float):
173        if Input.is_action_just_pressed("quit"):
174            self.app.quit()
175            return
176        if Input.is_action_just_pressed("reset"):
177            self._reset()
178
179        push_door = Input.is_action_just_pressed("push_door")
180
181        # Mouse/touch: horizontal drag orbits, a tap (click without dragging) pushes the door.
182        if Input.is_mouse_button_pressed(MouseButton.LEFT):
183            dx = float(Input.mouse_delta.x)
184            self._drag_dist += abs(dx)
185            self._orbit -= dx * 0.005
186        if Input.is_mouse_button_just_released(MouseButton.LEFT):
187            push_door = push_door or self._drag_dist < 6.0
188            self._drag_dist = 0.0
189
190        # Push door: give it a sideways shove (the hinge converts it to swing).
191        if push_door:
192            self._door_body.velocity = Vec3(0, 0, 6.0)
193
194        # Camera orbit
195        if Input.is_action_pressed("orbit_left"):
196            self._orbit -= 1.5 * dt
197        if Input.is_action_pressed("orbit_right"):
198            self._orbit += 1.5 * dt
199        r = 14.0
200        self._cam.position = Vec3(math.sin(self._orbit) * r, 4, math.cos(self._orbit) * r)
201        self._cam.look_at(Vec3(0, 2, 0))
202
203
204if __name__ == "__main__":
205    App(title="3D Joints Demo", width=1280, height=720).run(JointsDemo())