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