From eff8d43479e7704df65fae2a80eefa787213f502 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 9 Feb 2026 20:27:04 +0100 Subject: refactor: Reorganize tests into subsystem subdirectories Restructured test suite for better organization and targeted testing: **Structure:** - src/tests/audio/ - 15 audio system tests - src/tests/gpu/ - 12 GPU/shader tests - src/tests/3d/ - 6 3D rendering tests - src/tests/assets/ - 2 asset system tests - src/tests/util/ - 3 utility tests - src/tests/common/ - 3 shared test helpers - src/tests/scripts/ - 2 bash test scripts (moved conceptually, not physically) **CMake changes:** - Updated add_demo_test macro to accept LABEL parameter - Applied CTest labels to all 36 tests for subsystem filtering - Updated all test file paths in CMakeLists.txt - Fixed common helper paths (webgpu_test_fixture, etc.) - Added custom targets for subsystem testing: - run_audio_tests, run_gpu_tests, run_3d_tests - run_assets_tests, run_util_tests, run_all_tests **Include path updates:** - Fixed relative includes in GPU tests to reference ../common/ **Documentation:** - Updated doc/HOWTO.md with subsystem test commands - Updated doc/CONTRIBUTING.md with new test organization - Updated scripts/check_all.sh to reflect new structure **Verification:** - All 36 tests passing (100%) - ctest -L filters work correctly - make run__tests targets functional - scripts/check_all.sh passes Backward compatible: make test and ctest continue to work unchanged. handoff(Gemini): Test reorganization complete. 36/36 tests passing. --- src/tests/3d/test_3d.cc | 126 +++++++++++ src/tests/3d/test_3d_physics.cc | 296 ++++++++++++++++++++++++++ src/tests/3d/test_3d_render.cc | 326 +++++++++++++++++++++++++++++ src/tests/3d/test_mesh.cc | 425 ++++++++++++++++++++++++++++++++++++++ src/tests/3d/test_physics.cc | 150 ++++++++++++++ src/tests/3d/test_scene_loader.cc | 134 ++++++++++++ 6 files changed, 1457 insertions(+) create mode 100644 src/tests/3d/test_3d.cc create mode 100644 src/tests/3d/test_3d_physics.cc create mode 100644 src/tests/3d/test_3d_render.cc create mode 100644 src/tests/3d/test_mesh.cc create mode 100644 src/tests/3d/test_physics.cc create mode 100644 src/tests/3d/test_scene_loader.cc (limited to 'src/tests/3d') diff --git a/src/tests/3d/test_3d.cc b/src/tests/3d/test_3d.cc new file mode 100644 index 0000000..e0fb2e0 --- /dev/null +++ b/src/tests/3d/test_3d.cc @@ -0,0 +1,126 @@ +// This file is part of the 64k demo project. +// It tests the 3D system components (Camera, Object, Scene). + +#include "3d/camera.h" +#include "3d/object.h" +#include "3d/scene.h" +#include +#include +#include + +bool near(float a, float b, float e = 0.001f) { + return std::abs(a - b) < e; +} + +void test_camera() { + std::cout << "Testing Camera..." << std::endl; + Camera cam; + cam.position = vec3(0, 0, 10); + cam.target = vec3(0, 0, 0); + + mat4 view = cam.get_view_matrix(); + // Camera at (0,0,10) looking at (0,0,0). World (0,0,0) -> View (0,0,-10) + assert(near(view.m[14], -10.0f)); + + // Test Camera::set_look_at + cam.set_look_at({5, 0, 0}, {0, 0, 0}, + {0, 1, 0}); // Look at origin from (5,0,0) + mat4 view_shifted = cam.get_view_matrix(); + // The camera's forward vector (0,0,-1) should now point towards (-1,0,0) in + // world space. The translation part of the view matrix should be based on + // -dot(s, eye), -dot(u, eye), dot(f, eye) s = (0,0,-1), u = (0,1,0), f = + // (-1,0,0) m[12] = -dot({0,0,-1}, {5,0,0}) = 0 m[13] = -dot({0,1,0}, {5,0,0}) + // = 0 m[14] = dot({-1,0,0}, {5,0,0}) = -5 + assert(near(view_shifted.m[12], 0.0f)); + assert(near(view_shifted.m[13], 0.0f)); + assert(near(view_shifted.m[14], -5.0f)); + + // Test Camera::get_projection_matrix with varied parameters + // Change FOV and aspect ratio + mat4 proj = cam.get_projection_matrix(); + cam.fov_y_rad = 1.0472f; // 60 degrees + cam.aspect_ratio = 0.5f; // Narrower aspect ratio + mat4 proj_varied = cam.get_projection_matrix(); + // m[0] should increase due to narrower aspect ratio (1/tan(30deg)/0.5) + assert(proj_varied.m[0] > proj.m[0]); + // m[5] should increase due to larger FOV (1/tan(30deg)) + assert(proj_varied.m[5] < proj.m[5]); +} + +void test_object_transform() { + std::cout << "Testing Object Transform..." << std::endl; + Object3D obj; + obj.position = vec3(10, 0, 0); + + // Model matrix should translate by (10,0,0) + mat4 m = obj.get_model_matrix(); + assert(near(m.m[12], 10.0f)); + + // Test composed transformations (translate then rotate) + obj.position = vec3(5, 0, 0); + obj.rotation = quat::from_axis({0, 1, 0}, 1.570796f); // 90 deg Y rotation + m = obj.get_model_matrix(); + + // Transform point (1,0,0). Rotation around Y maps (1,0,0) to (0,0,-1). + // Translation moves it by (5,0,0). Final world pos: (5,0,-1). + vec4 p_comp(1, 0, 0, 1); + vec4 res_comp = m * p_comp; + assert(near(res_comp.x, 5.0f)); + assert(near(res_comp.z, -1.0f)); + + // Test Object3D::inv_model calculation + // Model matrix for translation (5,0,0) is just translation + obj.position = vec3(5, 0, 0); + obj.rotation = quat(); // Identity rotation + mat4 model_t = obj.get_model_matrix(); + mat4 inv_model_t = model_t.inverse(); + // Applying inv_model to a translated point should undo the translation. + // Point (5,0,0) should go to (0,0,0) + vec4 translated_point(5, 0, 0, 1); + vec4 original_space_t = + inv_model_t * + vec4(translated_point.x, translated_point.y, translated_point.z, 1.0); + assert(near(original_space_t.x, 0.0f) && near(original_space_t.y, 0.0f) && + near(original_space_t.z, 0.0f)); + + // Model matrix with rotation (90 deg Y) and translation (5,0,0) + obj.position = vec3(5, 0, 0); + obj.rotation = quat::from_axis({0, 1, 0}, 1.570796f); + mat4 model_trs = obj.get_model_matrix(); + mat4 inv_model_trs = model_trs.inverse(); + // Transform point (1,0,0) (local right) via TRS: Rotates to (0,0,-1), + // Translates to (5,0,-1) + vec4 p_trs(1, 0, 0, 1); + vec4 transformed_p = model_trs * p_trs; + assert(near(transformed_p.x, 5.0f) && near(transformed_p.z, -1.0f)); + // Apply inverse to transformed point to get back original point + vec4 original_space_trs = inv_model_trs * transformed_p; + assert(near(original_space_trs.x, 1.0f) && near(original_space_trs.y, 0.0f) && + near(original_space_trs.z, 0.0f)); +} + +void test_scene() { + std::cout << "Testing Scene..." << std::endl; + Scene scene; + scene.add_object(Object3D()); + assert(scene.objects.size() == 1); + scene.clear(); + assert(scene.objects.empty()); + + // Add multiple objects and check count + scene.add_object(Object3D()); + scene.add_object(Object3D()); + assert(scene.objects.size() == 2); + + // Test clearing the scene + scene.clear(); + assert(scene.objects.empty()); +} + +int main() { + test_camera(); + test_object_transform(); + test_scene(); + std::cout << "--- 3D SYSTEM TESTS PASSED ---" << std::endl; + return 0; +} diff --git a/src/tests/3d/test_3d_physics.cc b/src/tests/3d/test_3d_physics.cc new file mode 100644 index 0000000..eb1f5ef --- /dev/null +++ b/src/tests/3d/test_3d_physics.cc @@ -0,0 +1,296 @@ +// This file is part of the 64k demo project. +// Standalone "mini-demo" for testing the 3D physics engine. + +#include "3d/bvh.h" +#include "3d/camera.h" +#include "3d/object.h" +#include "3d/physics.h" +#include "3d/renderer.h" +#include "3d/scene.h" +#include "gpu/effects/shaders.h" +#include "gpu/texture_manager.h" +#include "platform/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); + } + if (strcmp(argv[i], "--no-bvh") == 0) { + g_renderer.SetBvhEnabled(false); + } + } +#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 diff --git a/src/tests/3d/test_3d_render.cc b/src/tests/3d/test_3d_render.cc new file mode 100644 index 0000000..eee46ba --- /dev/null +++ b/src/tests/3d/test_3d_render.cc @@ -0,0 +1,326 @@ +// This file is part of the 64k demo project. +// Standalone "mini-demo" for testing the 3D renderer. + +#include "3d/camera.h" +#include "3d/object.h" +#include "3d/renderer.h" +#include "3d/scene.h" +#include "generated/assets.h" +#include "gpu/effects/shaders.h" +#include "gpu/texture_manager.h" +#include "platform/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 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); + 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); + 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); + g_scene.add_object(sphere); + + // Mesh Object (Rasterized) + Object3D mesh_obj(ObjectType::MESH); + mesh_obj.position = vec3(-4.0f, 2.0f, 0); + mesh_obj.scale = vec3(2.0f, 2.0f, 2.0f); + mesh_obj.color = vec4(0.2, 0.2, 1, 1); + mesh_obj.mesh_asset_id = AssetId::ASSET_MESH_CUBE; + g_scene.add_object(mesh_obj); + + // Dodecahedron (Rasterized) + Object3D dodeca(ObjectType::MESH); + dodeca.position = vec3(4.0f, 2.0f, 2.0f); + dodeca.scale = vec3(1.0f, 1.0f, 1.0f); + dodeca.color = vec4(1.0, 0.5, 0.0, 1); // Orange + dodeca.mesh_asset_id = AssetId::ASSET_DODECAHEDRON; + g_scene.add_object(dodeca); + + // 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 = 0.5f + (rand() % 100) * 0.04f; + 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); + 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 Renderer Test...\n"); + +#if !defined(STRIP_ALL) + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--debug") == 0) { + Renderer3D::SetDebugEnabled(true); + } + if (strcmp(argv[i], "--no-bvh") == 0) { + g_renderer.SetBvhEnabled(false); + } + } +#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); + + // GPU Noise texture (replaces CPU procedural) + GpuProceduralParams noise_params = {}; + noise_params.width = 256; + noise_params.height = 256; + float noise_vals[2] = {1234.0f, 16.0f}; + noise_params.params = noise_vals; + noise_params.num_params = 2; + g_textures.create_gpu_noise_texture("noise", noise_params); + g_renderer.set_noise_texture(g_textures.get_texture_view("noise")); + + // GPU Perlin texture for sky (replaces CPU procedural) + GpuProceduralParams sky_params = {}; + sky_params.width = 512; + sky_params.height = 256; + float sky_vals[5] = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; + sky_params.params = sky_vals; + sky_params.num_params = 5; + g_textures.create_gpu_perlin_texture("sky", sky_params); + g_renderer.set_sky_texture(g_textures.get_texture_view("sky")); + + // GPU Grid texture (new!) + GpuProceduralParams grid_params = {}; + grid_params.width = 256; + grid_params.height = 256; + float grid_vals[2] = {32.0f, 2.0f}; // grid_size, thickness + grid_params.params = grid_vals; + grid_params.num_params = 2; + g_textures.create_gpu_grid_texture("grid", grid_params); + + 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; + + for (size_t i = 1; i < g_scene.objects.size(); ++i) { + // Rotation around a random-ish 3D axis + vec3 axis = + vec3(std::sin((float)i), std::cos((float)i), 0.5f).normalize(); + g_scene.objects[i].rotation = quat::from_axis(axis, time * 2.0f + i); + + // Non-uniform scaling variance + float s = 0.5f + 0.1f * std::sin(time * 0.5f + i); + g_scene.objects[i].scale = vec3(s, s * 1.4f, s * 0.8f); + + g_scene.objects[i].position.y = std::sin(time * 3.0f + i) * 1.5f; + } + +#if !defined(STRIP_ALL) + Renderer3D::SetDebugEnabled(true); + VisualDebug& dbg = g_renderer.GetVisualDebug(); + dbg.add_cross(vec3(0, 0, 0), 1.0f, vec3(1, 0, 0)); + dbg.add_sphere(vec3(std::sin(time) * 2.0f, 3.0f, std::cos(time) * 2.0f), + 0.5f, vec3(0, 1, 1)); + dbg.add_line(vec3(0, 0, 0), vec3(0, 5, 0), vec3(1, 0, 1)); + + // Cone (Spotlight visualization) + dbg.add_cone(vec3(0, 5, 0), vec3(0, -1, 0), 2.0f, 1.0f, vec3(1, 1, 0)); + + // Trajectory path + std::vector path; + for (int i = 0; i <= 32; ++i) { + float a = i * 6.28318f / 32.0f; + path.push_back(vec3(std::sin(a) * 4.0f, 0.5f, std::cos(a) * 4.0f)); + } + dbg.add_trajectory(path, vec3(0, 0.5f, 1.0f)); +#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 diff --git a/src/tests/3d/test_mesh.cc b/src/tests/3d/test_mesh.cc new file mode 100644 index 0000000..2129bc8 --- /dev/null +++ b/src/tests/3d/test_mesh.cc @@ -0,0 +1,425 @@ +// This file is part of the 64k demo project. +// Standalone test for loading and rendering a single mesh from a .obj file. + +#include "3d/camera.h" +#include "3d/object.h" +#include "3d/renderer.h" +#include "3d/scene.h" +#include "gpu/effects/shaders.h" +#include "gpu/texture_manager.h" +#include "platform/platform.h" +#include "procedural/generator.h" +#include "util/asset_manager_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Global State +static Renderer3D g_renderer; +static TextureManager g_textures; +static Scene g_scene; +static Camera g_camera; +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; + +// Test-specific storage for mesh buffers +static Renderer3D::MeshGpuData g_mesh_gpu_data; + +// Callbacks for asynchronous WGPU initialization (matches test_3d_render.cc) +void on_adapter_request_ended(WGPURequestAdapterStatus status, + WGPUAdapter adapter, WGPUStringView message, + void* userdata, void* user2) { + (void)user2; + if (status == WGPURequestAdapterStatus_Success) { + *(WGPUAdapter*)userdata = adapter; + } else { + fprintf(stderr, + "Failed to request adapter.\n"); // Avoid WGPUStringView::s issues + } +} + +void on_device_request_ended(WGPURequestDeviceStatus status, WGPUDevice device, + WGPUStringView message, void* userdata, + void* user2) { + (void)user2; + if (status == WGPURequestDeviceStatus_Success) { + *(WGPUDevice*)userdata = device; + } else { + fprintf(stderr, + "Failed to request device.\n"); // Avoid WGPUStringView::s issues + } +} + +// --- WGPU Boilerplate --- +void init_wgpu(WGPUInstance instance, PlatformState* platform_state) { + 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); + } + + // Request Adapter + WGPURequestAdapterOptions adapter_opts = {}; + adapter_opts.compatibleSurface = g_surface; + adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; + + WGPURequestAdapterCallbackInfo adapter_callback_info = {}; + adapter_callback_info.mode = WGPUCallbackMode_WaitAnyOnly; + adapter_callback_info.callback = on_adapter_request_ended; + adapter_callback_info.userdata1 = &g_adapter; // Corrected to userdata1 + + wgpuInstanceRequestAdapter(instance, &adapter_opts, adapter_callback_info); + + // Busy-wait for adapter + while (!g_adapter) { + platform_wgpu_wait_any(instance); + } + + // Request Device + WGPUDeviceDescriptor device_desc = {}; + WGPURequestDeviceCallbackInfo device_callback_info = {}; + device_callback_info.mode = WGPUCallbackMode_WaitAnyOnly; + device_callback_info.callback = on_device_request_ended; + device_callback_info.userdata1 = &g_device; // Corrected to userdata1 + + wgpuAdapterRequestDevice(g_adapter, &device_desc, device_callback_info); + + // Busy-wait for device + 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); +} + +// --- OBJ Loading Logic --- +#include // For std::sqrt + +struct Vec3 { + float x, y, z; + Vec3 operator+(const Vec3& o) const { + return {x + o.x, y + o.y, z + o.z}; + } + Vec3& operator+=(const Vec3& o) { + x += o.x; + y += o.y; + z += o.z; + return *this; + } + Vec3 operator-(const Vec3& o) const { + return {x - o.x, y - o.y, z - o.z}; + } + Vec3 operator*(float s) const { + return {x * s, y * s, z * s}; + } + static Vec3 cross(const Vec3& a, const Vec3& b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x}; + } + Vec3 normalize() const { + float len = std::sqrt(x * x + y * y + z * z); + if (len > 1e-6f) + return {x / len, y / len, z / len}; + return {0, 0, 0}; + } +}; + +bool load_obj_and_create_buffers(const char* path, Object3D& out_obj) { + std::ifstream obj_file(path); + if (!obj_file.is_open()) { + fprintf(stderr, "Error: Could not open mesh file: %s\n", path); + return false; + } + + std::vector v_pos, v_norm, v_uv; + struct RawFace { + int v[3], vt[3], vn[3]; + }; + std::vector raw_faces; + std::vector final_vertices; + std::vector final_indices; + std::map vertex_map; + + std::string obj_line; + while (std::getline(obj_file, obj_line)) { + if (obj_line.compare(0, 2, "v ") == 0) { + float x, y, z; + sscanf(obj_line.c_str(), "v %f %f %f", &x, &y, &z); + v_pos.insert(v_pos.end(), {x, y, z}); + } else if (obj_line.compare(0, 3, "vn ") == 0) { + float x, y, z; + sscanf(obj_line.c_str(), "vn %f %f %f", &x, &y, &z); + v_norm.insert(v_norm.end(), {x, y, z}); + } else if (obj_line.compare(0, 3, "vt ") == 0) { + float u, v; + sscanf(obj_line.c_str(), "vt %f %f", &u, &v); + v_uv.insert(v_uv.end(), {u, v}); + } else if (obj_line.compare(0, 2, "f ") == 0) { + char s1[64], s2[64], s3[64]; + if (sscanf(obj_line.c_str(), "f %s %s %s", s1, s2, s3) == 3) { + std::string parts[3] = {s1, s2, s3}; + RawFace face = {}; + for (int i = 0; i < 3; ++i) { + // Handle v//vn format + if (parts[i].find("//") != std::string::npos) { + sscanf(parts[i].c_str(), "%d//%d", &face.v[i], &face.vn[i]); + face.vt[i] = 0; + } else { + int res = sscanf(parts[i].c_str(), "%d/%d/%d", &face.v[i], + &face.vt[i], &face.vn[i]); + if (res == 2) + face.vn[i] = 0; + else if (res == 1) { + face.vt[i] = 0; + face.vn[i] = 0; + } + } + } + raw_faces.push_back(face); + } + } + } + + if (v_norm.empty() && !v_pos.empty()) { + std::vector temp_normals(v_pos.size() / 3, {0, 0, 0}); + for (auto& face : raw_faces) { + int i0 = face.v[0] - 1, i1 = face.v[1] - 1, i2 = face.v[2] - 1; + Vec3 p0 = {v_pos[i0 * 3], v_pos[i0 * 3 + 1], v_pos[i0 * 3 + 2]}; + Vec3 p1 = {v_pos[i1 * 3], v_pos[i1 * 3 + 1], v_pos[i1 * 3 + 2]}; + Vec3 p2 = {v_pos[i2 * 3], v_pos[i2 * 3 + 1], v_pos[i2 * 3 + 2]}; + Vec3 n = Vec3::cross(p1 - p0, p2 - p0).normalize(); + temp_normals[i0] += n; + temp_normals[i1] += n; + temp_normals[i2] += n; + } + for (const auto& n : temp_normals) { + Vec3 norm = n.normalize(); + v_norm.insert(v_norm.end(), {norm.x, norm.y, norm.z}); + } + for (auto& face : raw_faces) { + face.vn[0] = face.v[0]; + face.vn[1] = face.v[1]; + face.vn[2] = face.v[2]; + } + } + + for (const auto& face : raw_faces) { + for (int i = 0; i < 3; ++i) { + char key_buf[128]; + snprintf(key_buf, sizeof(key_buf), "%d/%d/%d", face.v[i], face.vt[i], + face.vn[i]); + std::string key = key_buf; + if (vertex_map.find(key) == vertex_map.end()) { + vertex_map[key] = (uint32_t)final_vertices.size(); + MeshVertex v = {}; + if (face.v[i] > 0) { + v.p[0] = v_pos[(face.v[i] - 1) * 3]; + v.p[1] = v_pos[(face.v[i] - 1) * 3 + 1]; + v.p[2] = v_pos[(face.v[i] - 1) * 3 + 2]; + } + if (face.vn[i] > 0) { + v.n[0] = v_norm[(face.vn[i] - 1) * 3]; + v.n[1] = v_norm[(face.vn[i] - 1) * 3 + 1]; + v.n[2] = v_norm[(face.vn[i] - 1) * 3 + 2]; + } + if (face.vt[i] > 0) { + v.u[0] = v_uv[(face.vt[i] - 1) * 2]; + v.u[1] = v_uv[(face.vt[i] - 1) * 2 + 1]; + } + final_vertices.push_back(v); + } + final_indices.push_back(vertex_map[key]); + } + } + + if (final_vertices.empty()) + return false; + + // Calculate AABB and center the mesh + float min_x = 1e10f, min_y = 1e10f, min_z = 1e10f; + float max_x = -1e10f, max_y = -1e10f, max_z = -1e10f; + for (const auto& v : final_vertices) { + min_x = std::min(min_x, v.p[0]); + min_y = std::min(min_y, v.p[1]); + min_z = std::min(min_z, v.p[2]); + max_x = std::max(max_x, v.p[0]); + max_y = std::max(max_y, v.p[1]); + max_z = std::max(max_z, v.p[2]); + } + float cx = (min_x + max_x) * 0.5f; + float cy = (min_y + max_y) * 0.5f; + float cz = (min_z + max_z) * 0.5f; + for (auto& v : final_vertices) { + v.p[0] -= cx; + v.p[1] -= cy; + v.p[2] -= cz; + } + out_obj.local_extent = vec3((max_x - min_x) * 0.5f, (max_y - min_y) * 0.5f, + (max_z - min_z) * 0.5f); + + g_mesh_gpu_data.num_indices = final_indices.size(); + g_mesh_gpu_data.vertex_buffer = + gpu_create_buffer(g_device, final_vertices.size() * sizeof(MeshVertex), + WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, + final_vertices.data()) + .buffer; + g_mesh_gpu_data.index_buffer = + gpu_create_buffer(g_device, final_indices.size() * sizeof(uint32_t), + WGPUBufferUsage_Index | WGPUBufferUsage_CopyDst, + final_indices.data()) + .buffer; + + struct MeshData { + std::vector vertices; + std::vector indices; + }; + MeshData* mesh_data = new MeshData(); + mesh_data->vertices = final_vertices; + mesh_data->indices = final_indices; + + out_obj.type = ObjectType::MESH; + out_obj.user_data = mesh_data; + + // This test doesn't use the asset system, so we override the renderer's + // internal cache lookup by manually setting the buffers on the renderer + // object. This is a HACK for this specific tool. + g_renderer.override_mesh_buffers(&g_mesh_gpu_data); + + return true; +} + +int main(int argc, char** argv) { + if (argc < 2) { + printf("Usage: %s [--debug]\n", argv[0]); + return 1; + } + const char* obj_path = argv[1]; + bool debug_mode = (argc > 2 && strcmp(argv[2], "--debug") == 0); + + printf("Loading mesh: %s\n", obj_path); + + PlatformState platform_state = platform_init(false, 1280, 720); + + WGPUInstance instance = wgpuCreateInstance(nullptr); + init_wgpu(instance, &platform_state); + InitShaderComposer(); + + g_renderer.init(g_device, g_queue, g_format); + g_renderer.resize(platform_state.width, platform_state.height); +#if !defined(STRIP_ALL) + if (debug_mode) { + Renderer3D::SetDebugEnabled(true); + } +#endif /* !defined(STRIP_ALL) */ + + g_textures.init(g_device, g_queue); + ProceduralTextureDef noise_def; + noise_def.width = 256; + noise_def.height = 256; + noise_def.gen_func = procedural::gen_noise; + noise_def.params = {1234.0f, 16.0f}; + g_textures.create_procedural_texture("noise", noise_def); + g_renderer.set_noise_texture(g_textures.get_texture_view("noise")); + + // --- Create Scene --- + Object3D floor(ObjectType::BOX); + floor.position = vec3(0, -2.0f, 0); + floor.scale = vec3(25.0f, 0.2f, 25.0f); + floor.color = vec4(0.5f, 0.5f, 0.5f, 1.0f); + g_scene.add_object(floor); + + Object3D mesh_obj; + if (!load_obj_and_create_buffers(obj_path, mesh_obj)) { + printf("Failed to load or process OBJ file.\n"); + return 1; + } + mesh_obj.color = vec4(1.0f, 0.7f, 0.2f, 1.0f); + mesh_obj.position = {0, 1.5, 0}; // Elevate a bit more + g_scene.add_object(mesh_obj); + + g_camera.position = vec3(0, 3, 5); + g_camera.target = vec3(0, 1.5, 0); + + while (!platform_should_close(&platform_state)) { + platform_poll(&platform_state); + float time = (float)platform_state.time; + + g_camera.aspect_ratio = platform_state.aspect_ratio; + + g_scene.objects[1].rotation = quat::from_axis({0.5f, 1.0f, 0.0f}, time); + +#if !defined(STRIP_ALL) + if (debug_mode) { + struct MeshData { + std::vector vertices; + std::vector indices; + }; + auto* data = (MeshData*)g_scene.objects[1].user_data; + VisualDebug& dbg = g_renderer.GetVisualDebug(); + dbg.add_mesh_normals(g_scene.objects[1].get_model_matrix(), + (uint32_t)data->vertices.size(), + data->vertices.data()); + // Wireframe is now handled automatically by renderer + } +#endif /* !defined(STRIP_ALL) */ + + WGPUSurfaceTexture surface_tex; + wgpuSurfaceGetCurrentTexture(g_surface, &surface_tex); + if (surface_tex.status == + WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal) { // WGPUSurfaceGetCurrentTextureStatus_Success + // is 0 + WGPUTextureView view = + wgpuTextureCreateView(surface_tex.texture, nullptr); + g_renderer.render(g_scene, g_camera, time, view); + wgpuTextureViewRelease(view); + wgpuSurfacePresent(g_surface); + } + wgpuTextureRelease( + surface_tex + .texture); // Release here, after present, outside the if block + } + +#if !defined(STRIP_ALL) + Renderer3D::SetDebugEnabled(false); // Reset debug mode +#endif + + struct MeshData { + std::vector vertices; + std::vector indices; + }; + delete (MeshData*)g_scene.objects[1].user_data; + wgpuBufferRelease(g_mesh_gpu_data.vertex_buffer); + wgpuBufferRelease(g_mesh_gpu_data.index_buffer); + + g_renderer.shutdown(); + g_textures.shutdown(); + platform_shutdown(&platform_state); + return 0; +} \ No newline at end of file diff --git a/src/tests/3d/test_physics.cc b/src/tests/3d/test_physics.cc new file mode 100644 index 0000000..df21e70 --- /dev/null +++ b/src/tests/3d/test_physics.cc @@ -0,0 +1,150 @@ +// 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 +#include +#include + +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 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 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; +} diff --git a/src/tests/3d/test_scene_loader.cc b/src/tests/3d/test_scene_loader.cc new file mode 100644 index 0000000..21bcbaa --- /dev/null +++ b/src/tests/3d/test_scene_loader.cc @@ -0,0 +1,134 @@ +#include "3d/scene_loader.h" +#include "generated/assets.h" +#include "util/asset_manager.h" +#include "util/mini_math.h" +#include +#include +#include +#include + +int main() { + Scene scene; + std::vector buffer; + + // Header + const char* magic = "SCN1"; + for (int i = 0; i < 4; ++i) + buffer.push_back(magic[i]); + + uint32_t num_obj = 2; // Increased to 2 + uint32_t num_cam = 0; + uint32_t num_light = 0; + + auto push_u32 = [&](uint32_t v) { + uint8_t* p = (uint8_t*)&v; + for (int i = 0; i < 4; ++i) + buffer.push_back(p[i]); + }; + auto push_f = [&](float v) { + uint8_t* p = (uint8_t*)&v; + for (int i = 0; i < 4; ++i) + buffer.push_back(p[i]); + }; + + push_u32(num_obj); + push_u32(num_cam); + push_u32(num_light); + + // --- Object 1: Basic Cube --- + char name1[64] = {0}; + std::strcpy(name1, "TestObject"); + for (int i = 0; i < 64; ++i) + buffer.push_back(name1[i]); + + push_u32(0); // CUBE + + // Pos + push_f(1.0f); + push_f(2.0f); + push_f(3.0f); + // Rot (0,0,0,1) + push_f(0.0f); + push_f(0.0f); + push_f(0.0f); + push_f(1.0f); + // Scale + push_f(1.0f); + push_f(1.0f); + push_f(1.0f); + // Color + push_f(1.0f); + push_f(0.0f); + push_f(0.0f); + push_f(1.0f); + + // Mesh Name length 0 + push_u32(0); + + // Physics + push_f(10.0f); // mass + push_f(0.8f); // restitution + push_u32(1); // static + + // --- Object 2: Mesh with Asset Ref --- + char name2[64] = {0}; + std::strcpy(name2, "MeshObject"); + for (int i = 0; i < 64; ++i) + buffer.push_back(name2[i]); + + push_u32(6); // MESH + + // Pos + push_f(0.0f); + push_f(0.0f); + push_f(0.0f); + // Rot + push_f(0.0f); + push_f(0.0f); + push_f(0.0f); + push_f(1.0f); + // Scale + push_f(1.0f); + push_f(1.0f); + push_f(1.0f); + // Color + push_f(0.0f); + push_f(1.0f); + push_f(0.0f); + push_f(1.0f); + + // Mesh Name "MESH_CUBE" + const char* mesh_name = "MESH_CUBE"; + uint32_t mesh_name_len = std::strlen(mesh_name); + push_u32(mesh_name_len); + for (size_t i = 0; i < mesh_name_len; ++i) + buffer.push_back(mesh_name[i]); + + // Physics + push_f(1.0f); + push_f(0.5f); + push_u32(0); // dynamic + + // --- Load --- + if (SceneLoader::LoadScene(scene, buffer.data(), buffer.size())) { + printf("Scene loaded successfully.\n"); + assert(scene.objects.size() == 2); + + // Check Obj 1 + assert(scene.objects[0].type == ObjectType::CUBE); + assert(scene.objects[0].position.x == 1.0f); + assert(scene.objects[0].is_static == true); + + // Check Obj 2 + assert(scene.objects[1].type == ObjectType::MESH); + assert(scene.objects[1].mesh_asset_id == AssetId::ASSET_MESH_CUBE); + printf("Mesh Asset ID resolved to: %d (Expected %d)\n", + (int)scene.objects[1].mesh_asset_id, (int)AssetId::ASSET_MESH_CUBE); + + } else { + printf("Scene load failed.\n"); + return 1; + } + + return 0; +} -- cgit v1.2.3