3D Joints¶
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())