Physics Sandbox

Play Demo

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    )