summaryrefslogtreecommitdiff
path: root/src/tests/test_physics.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-05 16:40:27 +0100
committerskal <pascal.massimino@gmail.com>2026-02-05 16:40:27 +0100
commitf6f3c13fcd287774a458730722854baab8a17366 (patch)
tree44420eecdd2e2dd84d68be12cb12641064eb1c5a /src/tests/test_physics.cc
parent93a65b43094641b4c188b4fc260b8ed44c883728 (diff)
feat(physics): Implement SDF-based physics engine and BVH
Completed Task #49. - Implemented CPU-side SDF library (sphere, box, torus, plane). - Implemented Dynamic BVH construction (rebuilt every frame). - Implemented PhysicsSystem with semi-implicit Euler integration and collision resolution. - Added visual debugging for BVH nodes. - Created test_3d_physics interactive test and test_physics unit tests. - Updated project docs and triaged new tasks.
Diffstat (limited to 'src/tests/test_physics.cc')
-rw-r--r--src/tests/test_physics.cc151
1 files changed, 151 insertions, 0 deletions
diff --git a/src/tests/test_physics.cc b/src/tests/test_physics.cc
new file mode 100644
index 0000000..a59502c
--- /dev/null
+++ b/src/tests/test_physics.cc
@@ -0,0 +1,151 @@
+// This file is part of the 64k demo project.
+// It tests the CPU-side SDF library and BVH for physics and collision.
+
+#include "3d/bvh.h"
+#include "3d/physics.h"
+#include "3d/sdf_cpu.h"
+#include <cassert>
+#include <cmath>
+#include <iostream>
+
+bool near(float a, float b, float e = 0.001f) {
+ return std::abs(a - b) < e;
+}
+
+void test_sdf_sphere() {
+ std::cout << "Testing sdSphere..." << std::endl;
+ float r = 1.0f;
+ assert(near(sdf::sdSphere({0, 0, 0}, r), -1.0f));
+ assert(near(sdf::sdSphere({1, 0, 0}, r), 0.0f));
+ assert(near(sdf::sdSphere({2, 0, 0}, r), 1.0f));
+}
+
+void test_sdf_box() {
+ std::cout << "Testing sdBox..." << std::endl;
+ vec3 b(1, 1, 1);
+ assert(near(sdf::sdBox({0, 0, 0}, b), -1.0f));
+ assert(near(sdf::sdBox({1, 1, 1}, b), 0.0f));
+ assert(near(sdf::sdBox({2, 0, 0}, b), 1.0f));
+}
+
+void test_sdf_torus() {
+ std::cout << "Testing sdTorus..." << std::endl;
+ vec2 t(1.0f, 0.2f);
+ // Point on the ring: length(p.xz) = 1.0, p.y = 0
+ assert(near(sdf::sdTorus({1, 0, 0}, t), -0.2f));
+ assert(near(sdf::sdTorus({1.2f, 0, 0}, t), 0.0f));
+}
+
+void test_sdf_plane() {
+ std::cout << "Testing sdPlane..." << std::endl;
+ vec3 n(0, 1, 0);
+ float h = 1.0f; // Plane is at y = -1 (dot(p,n) + 1 = 0 => y = -1)
+ assert(near(sdf::sdPlane({0, 0, 0}, n, h), 1.0f));
+ assert(near(sdf::sdPlane({0, -1, 0}, n, h), 0.0f));
+}
+
+void test_calc_normal() {
+ std::cout << "Testing calc_normal..." << std::endl;
+
+ // Sphere normal at (1,0,0) should be (1,0,0)
+ auto sphere_sdf = [](vec3 p) { return sdf::sdSphere(p, 1.0f); };
+ vec3 n = sdf::calc_normal({1, 0, 0}, sphere_sdf);
+ assert(near(n.x, 1.0f) && near(n.y, 0.0f) && near(n.z, 0.0f));
+
+ // Box normal at side
+ auto box_sdf = [](vec3 p) { return sdf::sdBox(p, {1, 1, 1}); };
+ n = sdf::calc_normal({1, 0, 0}, box_sdf);
+ assert(near(n.x, 1.0f) && near(n.y, 0.0f) && near(n.z, 0.0f));
+
+ // Plane normal should be n
+ vec3 plane_n(0, 1, 0);
+ auto plane_sdf = [plane_n](vec3 p) { return sdf::sdPlane(p, plane_n, 1.0f); };
+ n = sdf::calc_normal({0, 0, 0}, plane_sdf);
+ assert(near(n.x, plane_n.x) && near(n.y, plane_n.y) && near(n.z, plane_n.z));
+}
+
+void test_bvh() {
+ std::cout << "Testing BVH..." << std::endl;
+ std::vector<Object3D> objects;
+
+ // Object 0: Left side
+ Object3D obj0(ObjectType::BOX);
+ obj0.position = {-10, 0, 0};
+ objects.push_back(obj0);
+
+ // Object 1: Right side
+ Object3D obj1(ObjectType::BOX);
+ obj1.position = {10, 0, 0};
+ objects.push_back(obj1);
+
+ BVH bvh;
+ BVHBuilder::build(bvh, objects);
+
+ assert(bvh.nodes.size() == 3); // 1 root + 2 leaves
+
+ // Query left side
+ std::vector<int> results;
+ bvh.query({{-12, -2, -2}, {-8, 2, 2}}, results);
+ assert(results.size() == 1);
+ assert(results[0] == 0);
+
+ // Query right side
+ results.clear();
+ bvh.query({{8, -2, -2}, {12, 2, 2}}, results);
+ assert(results.size() == 1);
+ assert(results[0] == 1);
+
+ // Query center (should miss both)
+ results.clear();
+ bvh.query({{-2, -2, -2}, {2, 2, 2}}, results);
+ assert(results.size() == 0);
+
+ // Query both
+ results.clear();
+ bvh.query({{-12, -2, -2}, {12, 2, 2}}, results);
+ assert(results.size() == 2);
+ }
+
+ void test_physics_falling() {
+ std::cout << "Testing Physics falling..." << std::endl;
+ Scene scene;
+
+ // Plane at y = -1
+ Object3D plane(ObjectType::PLANE);
+ plane.position = {0, -1, 0};
+ plane.is_static = true;
+ scene.add_object(plane);
+
+ // Sphere at y = 5
+ Object3D sphere(ObjectType::SPHERE);
+ sphere.position = {0, 5, 0};
+ sphere.velocity = {0, 0, 0};
+ sphere.restitution = 0.0f; // No bounce for simple test
+ scene.add_object(sphere);
+
+ PhysicsSystem physics;
+ float dt = 0.016f;
+ for (int i = 0; i < 100; ++i) {
+ physics.update(scene, dt);
+ }
+
+ // Sphere should be above or at plane (y >= 0 because sphere radius is 1,
+ // plane is at -1)
+ assert(scene.objects[1].position.y >= -0.01f);
+ // Also should have slowed down
+ assert(scene.objects[1].velocity.y > -1.0f);
+ }
+
+ int main() {
+ test_sdf_sphere();
+ test_sdf_box();
+ test_sdf_torus();
+ test_sdf_plane();
+ test_calc_normal();
+ test_bvh();
+ test_physics_falling();
+
+ std::cout << "--- ALL PHYSICS TESTS PASSED ---" << std::endl;
+ return 0;
+ }
+ \ No newline at end of file