Sleeping Bodies¶
what wakes a settled pile, and how to edit one without waking it.
▶ Run in browserTags: physics 3d sleeping
A dynamic body that has come to rest falls asleep: the step skips it, neither
integrating it nor solving its contacts, until something disturbs it. That is
what keeps a large settled scene cheap. Each crate here is tinted by its live
is_sleeping, so the pile dims as it settles and lights up when it is woken.
wake()andsleep()are the explicit controls, andcan_sleep = Falsekeeps a body simulated however long it rests (those crates turn blue).Changing a support wakes what rests on it. Assigning a smaller
shapeto the static pillar wakes the crate on top, which falls onto the shorter pillar.Moving a body through the world seam takes
wake=:body.world.set_body_transform(body.handle, pose, wake=False). Slide the pillar away that way and the crate on it stays asleep in mid-air; slide it with the defaultwake=Trueand the crate falls. Suppression is for level streaming and pooled respawns, and the consequences are then yours.A body can be handed to the simulation parked: add it and call
sleep()at once, and it hangs where it was placed, costing nothing, until something reaches it orwake()is called.Sleep is per island, and so is waking. Pull the bottom crate out of a stack by assigning its
positionand every crate above it wakes in the same step, not only the one it touched.Asleep is frozen: the readout tracks how far any sleeping crate has moved since it fell asleep, and it stays at zero.
A backend that does not advertise
Capability.SLEEPnever sleeps anything, and the readout says so.
The buttons along the bottom run each disturbance (keys 1 to 5), wake or sleep
the whole pile (W, S), toggle can_sleep (C) and rebuild the scene (R). Drag
or use the arrow keys to orbit the camera.
Source¶
1# /// simvx
2# tags = ["3d", "physics", "sleeping"]
3# ///
4"""Sleeping Bodies: what wakes a settled pile, and how to edit one without waking it.
5
6A dynamic body that has come to rest falls asleep: the step skips it, neither
7integrating it nor solving its contacts, until something disturbs it. That is
8what keeps a large settled scene cheap. Each crate here is tinted by its live
9`is_sleeping`, so the pile dims as it settles and lights up when it is woken.
10
11- `wake()` and `sleep()` are the explicit controls, and `can_sleep = False`
12 keeps a body simulated however long it rests (those crates turn blue).
13- Changing a support wakes what rests on it. Assigning a smaller `shape` to
14 the static pillar wakes the crate on top, which falls onto the shorter pillar.
15- Moving a body through the world seam takes `wake=`:
16 `body.world.set_body_transform(body.handle, pose, wake=False)`. Slide the
17 pillar away that way and the crate on it stays asleep in mid-air; slide it
18 with the default `wake=True` and the crate falls. Suppression is for level
19 streaming and pooled respawns, and the consequences are then yours.
20- A body can be handed to the simulation parked: add it and call `sleep()` at
21 once, and it hangs where it was placed, costing nothing, until something
22 reaches it or `wake()` is called.
23- Sleep is per island, and so is waking. Pull the bottom crate out of a stack
24 by assigning its `position` and every crate above it wakes in the same step,
25 not only the one it touched.
26- Asleep is frozen: the readout tracks how far any sleeping crate has moved
27 since it fell asleep, and it stays at zero.
28- A backend that does not advertise `Capability.SLEEP` never sleeps anything,
29 and the readout says so.
30
31The buttons along the bottom run each disturbance (keys 1 to 5), wake or sleep
32the whole pile (W, S), toggle `can_sleep` (C) and rebuild the scene (R). Drag
33or use the arrow keys to orbit the camera.
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 Input,
48 Key,
49 Label,
50 Material,
51 Mesh,
52 MeshInstance3D,
53 Node,
54 OrbitCamera3D,
55 PhysicsBody3D,
56 VBoxContainer,
57 Vec3,
58 on_input,
59)
60from simvx.core.physics import Capability
61from simvx.graphics import App
62
63PILLAR_X = 4.0
64PILLAR = Vec3(2.0, 4.0, 2.0)
65SHRUNK = Vec3(2.0, 1.0, 2.0)
66SLIDE = 8.0
67ORBIT_SPEED = 1.5
68
69CRATE_MESH = Mesh.box(1.0)
70ASLEEP = Material(colour=(0.30, 0.34, 0.42, 1.0), roughness=0.9)
71AWAKE = Material(colour=(0.95, 0.72, 0.25, 1.0), emissive_colour=(0.7, 0.45, 0.05, 0.8), roughness=0.4)
72NEVER_SLEEPS = Material(colour=(0.35, 0.80, 0.95, 1.0), emissive_colour=(0.1, 0.4, 0.5, 0.6), roughness=0.4)
73
74
75class Crate(PhysicsBody3D):
76 """A 1 m dynamic crate that tints itself by its live sleep state."""
77
78 def __init__(self, **kwargs):
79 super().__init__(shape=BoxShape3D(), mass=1.0, **kwargs)
80 self.mesh = self.add_child(MeshInstance3D(mesh=CRATE_MESH, material=AWAKE))
81
82 def on_update(self, dt):
83 if not self.can_sleep:
84 self.mesh.material = NEVER_SLEEPS
85 else:
86 self.mesh.material = ASLEEP if self.is_sleeping else AWAKE
87
88
89class SleepingScene(Node):
90 input_context = {
91 "shrink_pillar": [Key.KEY_1],
92 "slide_quietly": [Key.KEY_2],
93 "slide": [Key.KEY_3],
94 "stream_in": [Key.KEY_4],
95 "yank_bottom": [Key.KEY_5],
96 "wake_all": [Key.W],
97 "sleep_all": [Key.S],
98 "toggle_can_sleep": [Key.C],
99 "rebuild": [Key.R],
100 "orbit": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
101 }
102
103 def on_ready(self):
104 self.camera = self.add_child(OrbitCamera3D(pivot=(0, 2, 0), distance=21.0, yaw=0.7, pitch=-0.3))
105 self.add_child(DirectionalLight3D(direction=(-6, -12, -8), colour=(1.0, 0.96, 0.85), intensity=3.0))
106
107 ground = self.add_child(
108 PhysicsBody3D(mode=BodyMode.STATIC, shape=BoxShape3D((14, 0.5, 14)), position=(0, -0.5, 0))
109 )
110 ground.add_child(
111 MeshInstance3D(mesh=Mesh.box((28, 1, 28)), material=Material(colour=(0.22, 0.24, 0.28, 1), roughness=0.95))
112 )
113
114 # A static pillar with one crate resting on top: the support the first three disturbances edit.
115 self.pillar = self.add_child(
116 PhysicsBody3D(mode=BodyMode.STATIC, shape=BoxShape3D(PILLAR / 2), position=(PILLAR_X, PILLAR.y / 2, 0))
117 )
118 self.pillar_mesh = self.pillar.add_child(
119 MeshInstance3D(mesh=Mesh.box(PILLAR), material=Material(colour=(0.45, 0.42, 0.55, 1.0), roughness=0.8))
120 )
121 self.rider = self.add_child(Crate(position=(PILLAR_X, PILLAR.y + 0.55, 0)))
122
123 # Three short stacks on the ground; the first is the one the yank pulls apart.
124 self.stacks = [
125 [self.add_child(Crate(position=(x, 0.55 + level * 1.02, 0))) for level in range(3)]
126 for x in (-4.4, -3.0, -1.6)
127 ]
128 self.crates = [self.rider, *(crate for stack in self.stacks for crate in stack)]
129 self.streamed = 0
130 # Where each sleeping crate was when it fell asleep, for the drift readout.
131 self.slept_at: dict[Crate, Vec3] = {}
132
133 hud = self.add_child(CanvasLayer())
134 self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=16, font_size=18))
135 rows = hud.add_child(VBoxContainer(anchor=Anchor.CENTER_BOTTOM, inset=16))
136 for row in (
137 (("Shrink pillar", "shrink_pillar"), ("Slide (wake=False)", "slide_quietly"), ("Slide", "slide")),
138 (("Stream in", "stream_in"), ("Yank bottom", "yank_bottom"), ("Rebuild", "rebuild")),
139 (("Wake all", "wake_all"), ("Sleep all", "sleep_all"), ("can_sleep", "toggle_can_sleep")),
140 ):
141 buttons = rows.add_child(HBoxContainer())
142 for text, action in row:
143 buttons.add_child(Button(text, action=action))
144 # A container keeps its default size until told to fit its children.
145 rows.autosize_to_content()
146 self.note = ""
147
148 @on_input("shrink_pillar")
149 def shrink_pillar(self, event):
150 # A new shape is a disturbance: the crate the old one held up wakes and falls.
151 self.pillar.shape = BoxShape3D(SHRUNK / 2)
152 self.pillar_mesh.mesh = Mesh.box(SHRUNK)
153 self.note = "pillar shrank, waking what rested on it"
154
155 @on_input(("slide_quietly", "slide"))
156 def slide_pillar(self, event):
157 wake = event.action == "slide"
158 pose = (Vec3(PILLAR_X + SLIDE, PILLAR.y / 2, 0), self.pillar.world_rotation)
159 self.pillar.world.set_body_transform(self.pillar.handle, pose, wake=wake)
160 # The seam moves the body but not its node, and assigning the node's position
161 # would always wake, so the mesh is offset to draw the pillar where it now is.
162 self.pillar_mesh.position = (SLIDE, 0, 0)
163 self.note = f"pillar slid away with wake={wake}"
164
165 @on_input("stream_in")
166 def stream_in(self, event):
167 crate = self.add_child(Crate(position=(-0.5 + self.streamed * 1.3, 7.0, 0)))
168 crate.sleep()
169 self.crates.append(crate)
170 self.streamed += 1
171 self.note = "streamed a crate in parked: it hangs there until something wakes it"
172
173 @on_input("yank_bottom")
174 def yank_bottom(self, event):
175 bottom = self.stacks[0][0]
176 bottom.position = (bottom.position.x, 0.55, -6.0)
177 self.note = "yanked the bottom crate: the whole stack woke, not only the crate above it"
178
179 @on_input("wake_all")
180 def wake_all(self, event):
181 for crate in self.crates:
182 crate.wake()
183 self.note = "woke every crate"
184
185 @on_input("sleep_all")
186 def sleep_all(self, event):
187 for crate in self.crates:
188 crate.sleep()
189 self.note = "put every crate to sleep (no effect where can_sleep is False)"
190
191 @on_input("toggle_can_sleep")
192 def toggle_can_sleep(self, event):
193 enabled = not self.rider.can_sleep
194 for crate in self.crates:
195 crate.can_sleep = enabled
196 self.note = f"can_sleep = {enabled} on every crate"
197
198 @on_input("rebuild")
199 def rebuild(self, event):
200 self.tree.change_scene(SleepingScene())
201
202 def on_update(self, dt):
203 self.camera.orbit(Input.value("orbit") * ORBIT_SPEED * dt, 0.0)
204
205 crates = self.crates
206 asleep = [c for c in crates if c.is_sleeping]
207 self.slept_at = {c: self.slept_at.get(c, Vec3(c.position)) for c in asleep}
208 drift = max((math.dist(c.position, at) for c, at in self.slept_at.items()), default=0.0)
209 if Capability.SLEEP in self.rider.world.capabilities():
210 summary = (
211 f"asleep: {len(asleep)}/{len(crates)} crate on the pillar: "
212 f"{'asleep' if self.rider.is_sleeping else 'awake'} sleeper drift: {drift * 1000:.2f} mm"
213 )
214 else:
215 summary = "this physics backend does not advertise Capability.SLEEP: nothing here ever sleeps"
216 self.status.text = f"{summary}\n{self.note}"
217
218
219if __name__ == "__main__":
220 App(title="Sleeping Bodies", width=1280, height=720).run(SleepingScene())