From f6f3c13fcd287774a458730722854baab8a17366 Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 5 Feb 2026 16:40:27 +0100 Subject: 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. --- src/tests/test_3d_physics.cc | 291 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 src/tests/test_3d_physics.cc (limited to 'src/tests/test_3d_physics.cc') diff --git a/src/tests/test_3d_physics.cc b/src/tests/test_3d_physics.cc new file mode 100644 index 0000000..6d7f476 --- /dev/null +++ b/src/tests/test_3d_physics.cc @@ -0,0 +1,291 @@ +// This file is part of the 64k demo project. +// Standalone "mini-demo" for testing the 3D physics engine. + +#include "3d/camera.h" +#include "3d/object.h" +#include "3d/renderer.h" +#include "3d/scene.h" +#include "3d/bvh.h" +#include "3d/physics.h" +#include "gpu/effects/shaders.h" +#include "gpu/texture_manager.h" +#include "platform.h" +#include "procedural/generator.h" +#include +#include +#include +#include + +// Global State +static Renderer3D g_renderer; +static TextureManager g_textures; +static Scene g_scene; +static Camera g_camera; +static PhysicsSystem g_physics; +static WGPUDevice g_device = nullptr; +static WGPUQueue g_queue = nullptr; +static WGPUSurface g_surface = nullptr; +static WGPUAdapter g_adapter = nullptr; +static WGPUTextureFormat g_format = WGPUTextureFormat_Undefined; + +// ... (init_wgpu implementation same as before) +void init_wgpu(PlatformState* platform_state) { + WGPUInstance instance = wgpuCreateInstance(nullptr); + if (!instance) { + fprintf(stderr, "Failed to create WGPU instance.\n"); + exit(1); + } + + g_surface = platform_create_wgpu_surface(instance, platform_state); + if (!g_surface) { + fprintf(stderr, "Failed to create WGPU surface.\n"); + exit(1); + } + + WGPURequestAdapterOptions adapter_opts = {}; + adapter_opts.compatibleSurface = g_surface; + adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; + +#if defined(DEMO_CROSS_COMPILE_WIN32) + auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, + const char* message, void* userdata) { + if (status == WGPURequestAdapterStatus_Success) { + *(WGPUAdapter*)userdata = adapter; + } + }; + wgpuInstanceRequestAdapter(instance, &adapter_opts, on_adapter, &g_adapter); +#else + auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, + WGPUStringView message, void* userdata, void* user2) { + (void)user2; + if (status == WGPURequestAdapterStatus_Success) { + *(WGPUAdapter*)userdata = adapter; + } + }; + WGPURequestAdapterCallbackInfo adapter_cb = {}; + adapter_cb.mode = WGPUCallbackMode_WaitAnyOnly; + adapter_cb.callback = on_adapter; + adapter_cb.userdata1 = &g_adapter; + wgpuInstanceRequestAdapter(instance, &adapter_opts, adapter_cb); +#endif + + while (!g_adapter) { + platform_wgpu_wait_any(instance); + } + + WGPUDeviceDescriptor device_desc = {}; + +#if defined(DEMO_CROSS_COMPILE_WIN32) + auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice device, + const char* message, void* userdata) { + if (status == WGPURequestDeviceStatus_Success) { + *(WGPUDevice*)userdata = device; + } + }; + wgpuAdapterRequestDevice(g_adapter, &device_desc, on_device, &g_device); +#else + auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice device, + WGPUStringView message, void* userdata, void* user2) { + (void)user2; + if (status == WGPURequestDeviceStatus_Success) { + *(WGPUDevice*)userdata = device; + } + }; + WGPURequestDeviceCallbackInfo device_cb = {}; + device_cb.mode = WGPUCallbackMode_WaitAnyOnly; + device_cb.callback = on_device; + device_cb.userdata1 = &g_device; + wgpuAdapterRequestDevice(g_adapter, &device_desc, device_cb); +#endif + + while (!g_device) { + platform_wgpu_wait_any(instance); + } + + g_queue = wgpuDeviceGetQueue(g_device); + + WGPUSurfaceCapabilities caps = {}; + wgpuSurfaceGetCapabilities(g_surface, g_adapter, &caps); + g_format = caps.formats[0]; + + WGPUSurfaceConfiguration config = {}; + config.device = g_device; + config.format = g_format; + config.usage = WGPUTextureUsage_RenderAttachment; + config.width = platform_state->width; + config.height = platform_state->height; + config.presentMode = WGPUPresentMode_Fifo; + config.alphaMode = WGPUCompositeAlphaMode_Opaque; + wgpuSurfaceConfigure(g_surface, &config); +} + +void setup_scene() { + g_scene.clear(); + srand(12345); // Fixed seed + + // Large floor, use BOX type (SDF) at index 0 + Object3D floor(ObjectType::BOX); + floor.position = vec3(0, -2.0f, 0); + floor.scale = vec3(25.0f, 0.2f, 25.0f); + floor.color = vec4(0.8f, 0.8f, 0.8f, 1.0f); + floor.is_static = true; + g_scene.add_object(floor); + + // Large center Torus (SDF) + Object3D center(ObjectType::TORUS); + center.position = vec3(0, 1.0f, 0); + center.scale = vec3(2.5f, 2.5f, 2.5f); + center.color = vec4(1, 0.2, 0.2, 1); + center.is_static = false; + center.restitution = 0.8f; + g_scene.add_object(center); + + // Moving Sphere (SDF) + Object3D sphere(ObjectType::SPHERE); + sphere.position = vec3(4.0f, 2.0f, 0); + sphere.scale = vec3(1.5f, 1.5f, 1.5f); + sphere.color = vec4(0.2, 1, 0.2, 1); + sphere.is_static = false; + sphere.velocity = vec3(-2.0f, 5.0f, 1.0f); + g_scene.add_object(sphere); + + // Random objects + for (int i = 0; i < 30; ++i) { + ObjectType type = ObjectType::SPHERE; + int r = rand() % 3; + if (r == 1) + type = ObjectType::TORUS; + if (r == 2) + type = ObjectType::BOX; + + Object3D obj(type); + float angle = (rand() % 360) * 0.01745f; + float dist = 3.0f + (rand() % 100) * 0.05f; + float height = 5.0f + (rand() % 100) * 0.04f; // Start higher + obj.position = vec3(std::cos(angle) * dist, height, std::sin(angle) * dist); + + // Random non-uniform scale for debugging + float s = 0.6f + (rand() % 100) * 0.008f; + obj.scale = vec3(s, s * 1.2f, s * 0.8f); + + obj.color = vec4((rand() % 100) / 100.0f, (rand() % 100) / 100.0f, + (rand() % 100) / 100.0f, 1.0f); + obj.is_static = false; + obj.velocity = vec3((rand() % 100 - 50) * 0.01f, 0, (rand() % 100 - 50) * 0.01f); + g_scene.add_object(obj); + } +} + +// Wrapper to generate periodic noise +bool gen_periodic_noise(uint8_t* buffer, int w, int h, const float* params, + int num_params) { + if (!procedural::gen_noise(buffer, w, h, params, num_params)) + return false; + float p_params[] = {0.1f}; // 10% overlap + return procedural::make_periodic(buffer, w, h, p_params, 1); +} + +int main(int argc, char** argv) { + printf("Running 3D Physics Test...\n"); + +#if !defined(STRIP_ALL) + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--debug") == 0) { + Renderer3D::SetDebugEnabled(true); + } + } +#else + (void)argc; + (void)argv; +#endif + + PlatformState platform_state = platform_init(false, 1280, 720); + + // The test's own WGPU init sequence + init_wgpu(&platform_state); + + InitShaderComposer(); + + g_renderer.init(g_device, g_queue, g_format); + g_renderer.resize(platform_state.width, platform_state.height); + + g_textures.init(g_device, g_queue); + ProceduralTextureDef noise_def; + noise_def.width = 256; + noise_def.height = 256; + noise_def.gen_func = gen_periodic_noise; + noise_def.params.push_back(1234.0f); + noise_def.params.push_back(16.0f); + g_textures.create_procedural_texture("noise", noise_def); + + g_renderer.set_noise_texture(g_textures.get_texture_view("noise")); + + ProceduralTextureDef sky_def; + sky_def.width = 512; + sky_def.height = 256; + sky_def.gen_func = procedural::gen_perlin; + sky_def.params = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; + g_textures.create_procedural_texture("sky", sky_def); + + g_renderer.set_sky_texture(g_textures.get_texture_view("sky")); + + setup_scene(); + + g_camera.position = vec3(0, 5, 10); + g_camera.target = vec3(0, 0, 0); + + while (!platform_should_close(&platform_state)) { + platform_poll(&platform_state); + float time = (float)platform_state.time; + + float cam_radius = 10.0f + std::sin(time * 0.3f) * 4.0f; + float cam_height = 5.0f + std::cos(time * 0.4f) * 3.0f; + g_camera.set_look_at(vec3(std::sin(time * 0.5f) * cam_radius, cam_height, + std::cos(time * 0.5f) * cam_radius), + vec3(0, 0, 0), vec3(0, 1, 0)); + g_camera.aspect_ratio = platform_state.aspect_ratio; + + static double last_time = 0; + float dt = (float)(platform_state.time - last_time); + if (dt > 0.1f) dt = 0.1f; // Cap dt for stability + last_time = platform_state.time; + + g_physics.update(g_scene, dt); + + BVH bvh; + BVHBuilder::build(bvh, g_scene.objects); + for (const auto& node : bvh.nodes) { + g_renderer.add_debug_aabb({node.min_x, node.min_y, node.min_z}, + {node.max_x, node.max_y, node.max_z}, + {0.0f, 1.0f, 0.0f}); + } + +#if !defined(STRIP_ALL) + Renderer3D::SetDebugEnabled(true); +#endif + + WGPUSurfaceTexture surface_tex; + wgpuSurfaceGetCurrentTexture(g_surface, &surface_tex); + if (surface_tex.status == + WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal) { + const WGPUTextureViewDescriptor view_desc = { + .format = g_format, + .dimension = WGPUTextureViewDimension_2D, + .mipLevelCount = 1, + .arrayLayerCount = 1, + }; + + const WGPUTextureView view = + wgpuTextureCreateView(surface_tex.texture, &view_desc); + g_renderer.render(g_scene, g_camera, time, view); + wgpuTextureViewRelease(view); + wgpuSurfacePresent(g_surface); + wgpuTextureRelease(surface_tex.texture); + } + } + + g_renderer.shutdown(); + g_textures.shutdown(); + platform_shutdown(&platform_state); + return 0; +} \ No newline at end of file -- cgit v1.2.3