Physics Sandbox¶
Visual demo of the SimVX physics engine.
▶ Run in browserTags: 3d physics rigid-body
Demonstrates: - Falling cubes under gravity (PhysicsBody3D, mode=DYNAMIC) - Static ground plane + containing walls (mode=STATIC) - Bouncing spheres with restitution (PhysicsMaterial) - Impulse-driven interaction (PhysicsBody3D.push)
Everything lives under a single PhysicsRoot that owns one isolated
PhysicsWorld (gravity, body_count, clear()) – the seam replacement
for the old global PhysicsServer.
Controls: SPACE / click / tap : Spawn a new ball with random impulse R : Reset the scene ESC : Quit
Run with: uv run python examples/demos/physics_sandbox.py
Source¶
1#!/usr/bin/env python3
2"""Physics Sandbox: Visual demo of the SimVX physics engine.
3
4# /// simvx
5# tags = ["3d", "physics", "rigid-body"]
6# web = { width = 1280, height = 720, root = "PhysicsSandbox" }
7# ///
8
9Demonstrates:
10 - Falling cubes under gravity (PhysicsBody3D, mode=DYNAMIC)
11 - Static ground plane + containing walls (mode=STATIC)
12 - Bouncing spheres with restitution (PhysicsMaterial)
13 - Impulse-driven interaction (PhysicsBody3D.push)
14
15Everything lives under a single ``PhysicsRoot`` that owns one isolated
16``PhysicsWorld`` (gravity, ``body_count``, ``clear()``) -- the seam replacement
17for the old global ``PhysicsServer``.
18
19Controls:
20 SPACE / click / tap : Spawn a new ball with random impulse
21 R : Reset the scene
22 ESC : Quit
23
24Run with:
25 uv run python examples/demos/physics_sandbox.py
26"""
27
28import random
29
30from simvx.core import (
31 BodyMode,
32 BoxShape3D,
33 Camera3D,
34 Input,
35 InputMap,
36 Key,
37 Material,
38 Mesh,
39 MeshInstance3D,
40 MouseButton,
41 Node,
42 PhysicsBody3D,
43 PhysicsMaterial,
44 PhysicsRoot,
45 SphereShape3D,
46 Text2D,
47 Vec3,
48)
49from simvx.graphics import App
50
51# ============================================================================
52# Physics-aware mesh node helpers
53# ============================================================================
54
55
56class PhysicsCube(PhysicsBody3D):
57 """A falling cube with physics (DYNAMIC PhysicsBody3D)."""
58
59 def __init__(self, size: float = 1.0, colour: tuple = (0.8, 0.3, 0.2, 1.0), **kwargs):
60 half = size / 2
61 super().__init__(
62 mode=BodyMode.DYNAMIC,
63 shape=BoxShape3D(half_extents=Vec3(half, half, half)),
64 material=PhysicsMaterial(friction=0.6, restitution=0.3),
65 **kwargs,
66 )
67 self._size = size
68 self._colour = colour
69
70 def on_ready(self):
71 mesh_node = self.add_child(MeshInstance3D(name="Mesh"))
72 mesh_node.mesh = Mesh.cube(size=self._size)
73 mesh_node.material = Material(colour=self._colour)
74
75
76class PhysicsBall(PhysicsBody3D):
77 """A bouncing sphere with physics (DYNAMIC PhysicsBody3D)."""
78
79 def __init__(self, radius: float = 0.5, colour: tuple = (0.2, 0.6, 0.9, 1.0), **kwargs):
80 super().__init__(
81 mode=BodyMode.DYNAMIC,
82 shape=SphereShape3D(radius=radius),
83 material=PhysicsMaterial(friction=0.3, restitution=0.8),
84 **kwargs,
85 )
86 self._radius = radius
87 self._colour = colour
88
89 def on_ready(self):
90 mesh_node = self.add_child(MeshInstance3D(name="Mesh"))
91 mesh_node.mesh = Mesh.sphere(radius=self._radius)
92 mesh_node.material = Material(colour=self._colour)
93
94
95class Ground(PhysicsBody3D):
96 """Static ground plane (STATIC PhysicsBody3D)."""
97
98 def __init__(self, **kwargs):
99 super().__init__(
100 mode=BodyMode.STATIC,
101 shape=BoxShape3D(half_extents=Vec3(25, 0.5, 25)),
102 material=PhysicsMaterial(friction=0.8, restitution=0.5),
103 **kwargs,
104 )
105
106 def on_ready(self):
107 mesh_node = self.add_child(MeshInstance3D(name="Mesh"))
108 mesh_node.mesh = Mesh.cube(size=1)
109 mesh_node.material = Material(colour=(0.4, 0.5, 0.4, 1.0))
110 mesh_node.scale = Vec3(50, 1, 50)
111
112
113class Wall(PhysicsBody3D):
114 """Static wall for containing objects (STATIC PhysicsBody3D)."""
115
116 def __init__(self, half_extents=(0.5, 5, 25), colour=(0.5, 0.5, 0.6, 0.5), **kwargs):
117 super().__init__(
118 mode=BodyMode.STATIC,
119 shape=BoxShape3D(half_extents=Vec3(*half_extents)),
120 material=PhysicsMaterial(friction=0.5, restitution=0.7),
121 **kwargs,
122 )
123 self._half_extents = half_extents
124 self._colour = colour
125
126 def on_ready(self):
127 mesh_node = self.add_child(MeshInstance3D(name="Mesh"))
128 mesh_node.mesh = Mesh.cube(size=1)
129 mesh_node.material = Material(colour=self._colour)
130 he = self._half_extents
131 mesh_node.scale = Vec3(he[0] * 2, he[1] * 2, he[2] * 2)
132
133
134# ============================================================================
135# Main Scene
136# ============================================================================
137
138
139class PhysicsSandbox(Node):
140 """Main physics sandbox scene."""
141
142 def on_ready(self):
143 InputMap.add_action("space", [Key.SPACE, MouseButton.LEFT])
144 InputMap.add_action("reset", [Key.R])
145 InputMap.add_action("quit", [Key.ESCAPE])
146
147 # One isolated world for the whole sandbox (the PhysicsServer replacement).
148 self._world_root = self.add_child(PhysicsRoot(name="World", gravity=Vec3(0, -9.8, 0)))
149
150 # Camera
151 camera = self.add_child(Camera3D(name="Camera"))
152 camera.position = Vec3(0, 15, 25)
153 camera.look_at(Vec3(0, 3, 0))
154 camera.fov = 60.0
155
156 # Ground
157 self._world_root.add_child(Ground(name="Ground", position=Vec3(0, -0.5, 0)))
158
159 # Side walls
160 self._world_root.add_child(Wall(name="WallLeft", position=Vec3(-12, 5, 0), half_extents=(0.5, 5, 12)))
161 self._world_root.add_child(Wall(name="WallRight", position=Vec3(12, 5, 0), half_extents=(0.5, 5, 12)))
162 self._world_root.add_child(Wall(name="WallBack", position=Vec3(0, 5, -12), half_extents=(12, 5, 0.5)))
163 self._world_root.add_child(Wall(name="WallFront", position=Vec3(0, 5, 12), half_extents=(12, 5, 0.5)))
164
165 # Initial stack of cubes
166 colours = [
167 (0.9, 0.2, 0.2, 1),
168 (0.2, 0.9, 0.2, 1),
169 (0.2, 0.2, 0.9, 1),
170 (0.9, 0.9, 0.2, 1),
171 (0.9, 0.2, 0.9, 1),
172 (0.2, 0.9, 0.9, 1),
173 ]
174 for i in range(3):
175 for j in range(3 - i):
176 colour = colours[(i * 3 + j) % len(colours)]
177 self._world_root.add_child(
178 PhysicsCube(
179 name=f"Cube_{i}_{j}",
180 position=Vec3(-2 + j * 1.5, 1.5 + i * 1.5, 0),
181 size=1.2,
182 colour=colour,
183 )
184 )
185
186 # A couple of bouncy balls
187 for i in range(3):
188 x = -3 + i * 3
189 self._world_root.add_child(
190 PhysicsBall(
191 name=f"Ball_{i}",
192 position=Vec3(x, 8 + i * 2, 2),
193 radius=0.6,
194 colour=(0.1 + i * 0.3, 0.5, 0.9 - i * 0.2, 1),
195 )
196 )
197
198 # UI
199 self._spawn_count = 0
200 self.add_child(
201 Text2D(
202 name="Title",
203 text="Physics Sandbox",
204 position=(20, 20), font_scale=2.0,
205 colour=(1.0, 1.0, 1.0, 1.0),
206 )
207 )
208 self._info_text = self.add_child(
209 Text2D(
210 name="Info",
211 text="SPACE / click: spawn ball | R: reset | ESC: quit",
212 position=(20, 60), font_scale=1.0,
213 colour=(0.78, 0.78, 0.78, 1.0),
214 )
215 )
216 self._count_text = self.add_child(
217 Text2D(
218 name="Count",
219 text="Bodies: 0",
220 position=(20, 90), font_scale=1.0,
221 colour=(0.71, 0.71, 0.71, 1.0),
222 )
223 )
224
225 def on_update(self, dt: float):
226 # Quit on ESC
227 if Input.is_action_just_pressed("quit"):
228 self.app.quit()
229 return
230
231 # Spawn ball on space or click/tap
232 if Input.is_action_just_pressed("space"):
233 self._spawn_ball()
234
235 # Reset on R
236 if Input.is_action_just_pressed("reset"):
237 self.tree.change_scene(PhysicsSandbox())
238 return
239
240 # Update body count (per-world seam, not the old global server)
241 self._count_text.text = f"Bodies: {self._world_root.world.body_count}"
242
243 # Clean up fallen objects
244 for child in list(self._world_root.children):
245 if isinstance(child, PhysicsBody3D) and child.position.y < -20:
246 child.destroy()
247
248 def _spawn_ball(self):
249 self._spawn_count += 1
250 colour = (random.random(), random.random(), random.random(), 1.0)
251 ball = self._world_root.add_child(
252 PhysicsBall(
253 name=f"SpawnBall_{self._spawn_count}",
254 position=Vec3(random.uniform(-5, 5), 12, random.uniform(-5, 5)),
255 radius=random.uniform(0.3, 0.8),
256 colour=colour,
257 )
258 )
259 # Random impulse
260 ball.push(
261 Vec3(
262 random.uniform(-5, 5),
263 random.uniform(0, 5),
264 random.uniform(-5, 5),
265 )
266 )
267
268
269# ============================================================================
270# Entry Point
271# ============================================================================
272
273
274if __name__ == "__main__":
275 App(title="Physics Sandbox: SimVX", width=1280, height=720, target_fps=60, physics_fps=60).run(
276 PhysicsSandbox()
277 )