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