3D Joints¶
A pinned chain and a hinged door, built from PinJoint3D and HingeJoint3D.
▶ Run in browserTags: 3d
A joint constrains two PhysicsBody3D nodes in the same PhysicsRoot, and
the world solves every joint on each fixed step. Add a joint after both of its
bodies: it resolves them once, when it enters the tree.
world.add_child(PinJoint3D(body_a=above, body_b=bead, anchor=above.position))
world.add_child(HingeJoint3D(body_a=post, body_b=door, anchor=(4, 2, 0), axis=(0, 1, 0)))
PinJoint3D holds the two bodies together at anchor and leaves them free
to rotate about it, so a column of pinned beads hanging from a static anchor
swings as a chain. HingeJoint3D is a pin plus a lock on the two off-axis
rotations, leaving one free turn about axis: the door panel is shoved
sideways and swings round its post while staying upright. Both joints hold
their pivot in the frame of the body carrying it, which is why the door orbits
the post instead of spinning about its own centre.
The anchor and the hinge post are BodyMode.STATIC bodies: the solver reads
them and never moves them. Every body has collision_mask=0, so the beads
never collide with each other or the door; only the joints act on them.
PhysicsRoot() runs the builtin solver. PhysicsRoot(backend="jolt") runs
the same scene on Jolt, with the same behaviour; the built-in solver has no
inertia tensor, so its chain swings a little less freely.
Controls: Space / click or tap / Push door - Shove the door R / Reset - Put every body back and kick the chain again Drag / Left, Right arrows - Orbit the camera Scroll / pinch - Zoom
Source¶
1# /// simvx
2# web = { width = 1280, height = 720 }
3# ///
4"""3D Joints: A pinned chain and a hinged door, built from PinJoint3D and HingeJoint3D.
5
6A joint constrains two ``PhysicsBody3D`` nodes in the same ``PhysicsRoot``, and
7the world solves every joint on each fixed step. Add a joint after both of its
8bodies: it resolves them once, when it enters the tree.
9
10 world.add_child(PinJoint3D(body_a=above, body_b=bead, anchor=above.position))
11 world.add_child(HingeJoint3D(body_a=post, body_b=door, anchor=(4, 2, 0), axis=(0, 1, 0)))
12
13``PinJoint3D`` holds the two bodies together at ``anchor`` and leaves them free
14to rotate about it, so a column of pinned beads hanging from a static anchor
15swings as a chain. ``HingeJoint3D`` is a pin plus a lock on the two off-axis
16rotations, leaving one free turn about ``axis``: the door panel is shoved
17sideways and swings round its post while staying upright. Both joints hold
18their pivot in the frame of the body carrying it, which is why the door orbits
19the post instead of spinning about its own centre.
20
21The anchor and the hinge post are ``BodyMode.STATIC`` bodies: the solver reads
22them and never moves them. Every body has ``collision_mask=0``, so the beads
23never collide with each other or the door; only the joints act on them.
24
25``PhysicsRoot()`` runs the builtin solver. ``PhysicsRoot(backend="jolt")`` runs
26the same scene on Jolt, with the same behaviour; the built-in solver has no
27inertia tensor, so its chain swings a little less freely.
28
29Controls:
30 Space / click or tap / Push door - Shove the door
31 R / Reset - Put every body back and kick the chain again
32 Drag / Left, Right arrows - Orbit the camera
33 Scroll / pinch - Zoom
34"""
35
36import math
37
38from simvx.core import (
39 Anchor,
40 Axis1D,
41 BodyMode,
42 BoxShape3D,
43 Button,
44 CanvasLayer,
45 DirectionalLight3D,
46 HBoxContainer,
47 HingeJoint3D,
48 Input,
49 Key,
50 Label,
51 Material,
52 Mesh,
53 MeshInstance3D,
54 Node3D,
55 OrbitCamera3D,
56 PhysicsBody3D,
57 PhysicsRoot,
58 PinJoint3D,
59 Pointer,
60 Quat,
61 SphereShape3D,
62 VBoxContainer,
63 Vec3,
64 on_input,
65)
66from simvx.graphics import App
67
68CHAIN_LEN = 4
69LINK_DIST = 1.2
70CHAIN_ANCHOR = Vec3(-4, 6, 0)
71HINGE = Vec3(4, 2, 0)
72KICK = Vec3(4, 0, 0) # the top bead's starting velocity, so the chain swings
73SHOVE = Vec3(0, 0, 6) # the door's velocity when pushed; the hinge turns it into a swing
74
75METAL = Material(colour=(0.6, 0.6, 0.6), roughness=0.5)
76
77
78def ball(name: str, position: Vec3, radius: float, colour, mode=BodyMode.DYNAMIC) -> PhysicsBody3D:
79 body = PhysicsBody3D(name=name, mode=mode, position=position, shape=SphereShape3D(radius=radius), collision_mask=0)
80 body.add_child(
81 MeshInstance3D(mesh=Mesh.sphere(radius), material=Material(colour=colour, roughness=0.3, metallic=0.5))
82 )
83 return body
84
85
86class JointsDemo(Node3D):
87 input_context = {
88 "push_door": [Key.SPACE],
89 "reset": [Key.R],
90 "orbit": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
91 }
92
93 def on_ready(self):
94 self.camera = self.add_child(
95 OrbitCamera3D(pivot=(0, 2, 0), distance=14.1, yaw=0.0, pitch=math.radians(-8), fov=55.0)
96 )
97 self.add_child(DirectionalLight3D(direction=(-1, -2, -1)))
98 self.add_child(
99 MeshInstance3D(
100 name="Ground", mesh=Mesh.plane(20), material=Material(colour=(0.3, 0.35, 0.3), roughness=0.9)
101 )
102 )
103 world = self.add_child(PhysicsRoot(name="World"))
104
105 # The chain: each bead pinned to the one above it, at that body's centre.
106 above = world.add_child(ball("ChainAnchor", CHAIN_ANCHOR, 0.15, (1.0, 0.3, 0.3), BodyMode.STATIC))
107 above.add_child(MeshInstance3D(mesh=Mesh.cylinder(0.08, 1.0), material=METAL, position=(0, 0.5, 0)))
108 self.beads = []
109 for i in range(CHAIN_LEN):
110 bead = world.add_child(
111 ball(f"ChainBody{i}", CHAIN_ANCHOR - Vec3(0, LINK_DIST * (i + 1), 0), 0.25, (0.3, 0.6, 1.0))
112 )
113 world.add_child(PinJoint3D(body_a=above, body_b=bead, anchor=above.position))
114 self.beads.append(bead)
115 above = bead
116
117 # The door: a panel whose edge is on the post, hinged about the vertical axis.
118 post = world.add_child(
119 PhysicsBody3D(
120 name="DoorPostBody",
121 mode=BodyMode.STATIC,
122 position=HINGE,
123 shape=SphereShape3D(radius=0.12),
124 collision_mask=0,
125 )
126 )
127 post.add_child(MeshInstance3D(mesh=Mesh.cylinder(0.1, 4.0), material=METAL))
128 self.door = world.add_child(
129 PhysicsBody3D(
130 name="DoorBody",
131 mass=5.0,
132 position=HINGE + Vec3(1, 0, 0),
133 shape=BoxShape3D(half_extents=(1.0, 1.75, 0.06)),
134 collision_mask=0,
135 )
136 )
137 self.door.add_child(
138 MeshInstance3D(mesh=Mesh.box((2.0, 3.5, 0.12)), material=Material(colour=(0.7, 0.4, 0.15), roughness=0.6))
139 )
140 world.add_child(HingeJoint3D(body_a=post, body_b=self.door, anchor=HINGE, axis=(0, 1, 0)))
141
142 self.homes = {body: Vec3(body.position) for body in [*self.beads, self.door]}
143 self.beads[0].velocity = KICK
144
145 # Example help overlay: not part of the technique shown.
146 footer = self.add_child(CanvasLayer()).add_child(
147 VBoxContainer(anchor=Anchor.BOTTOM_LEFT, inset=12, separation=8)
148 )
149 footer.add_child(Label("Space / click: push door R: reset Drag / Left, Right: orbit Scroll: zoom"))
150 bar = footer.add_child(HBoxContainer(separation=8))
151 bar.add_child(Button("Push door", action="push_door"))
152 bar.add_child(Button("Reset", action="reset"))
153 # A container keeps its own size rather than growing to its content, so a
154 # bottom-anchored stack is fitted by hand or it runs off the bottom edge.
155 footer.size = footer.get_minimum_size()
156
157 @on_input("push_door")
158 def push_door(self, _event=None):
159 self.door.velocity = SHOVE
160
161 @on_input(Pointer.PRIMARY, edge="release")
162 def _on_tap(self, event):
163 # A drag orbits the camera; a press that never became a drag pushes the door.
164 if not event.dragging:
165 self.push_door()
166
167 @on_input("reset")
168 def reset(self, _event):
169 # Position and rotation teleport a body; velocity and spin clear its momentum.
170 for body, home in self.homes.items():
171 body.position = home
172 body.rotation = Quat()
173 body.velocity = Vec3()
174 body.spin = Vec3()
175 self.beads[0].velocity = KICK
176
177 def on_update(self, dt):
178 self.camera.orbit(Input.value("orbit") * 1.5 * dt, 0.0)
179
180
181if __name__ == "__main__":
182 App(title="3D Joints", width=1280, height=720).run(JointsDemo())