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