3D Joints¶
Pendulum chain and hinge door using physics joints.
▶ Run in browserTags: 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())