Sleeping Bodies

what wakes a settled pile, and how to edit one without waking it.

▶ Run in browser

Tags: 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() and sleep() are the explicit controls, and can_sleep = False keeps a body simulated however long it rests (those crates turn blue).

  • Changing a support wakes what rests on it. Assigning a smaller shape to 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 default wake=True and 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 or wake() is called.

  • Sleep is per island, and so is waking. Pull the bottom crate out of a stack by assigning its position and 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.SLEEP never 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())