From 2a7c5d12f2f0ecbbeb20103084016ecaf5ef06b1 Mon Sep 17 00:00:00 2001 From: Satvik-Singh192 Date: Sat, 25 Jul 2026 13:38:31 +0000 Subject: [PATCH] feat: decouple engine from renderer --- CMakeLists.txt | 197 ++-- app/main.cpp | 6 +- app/test_scenarios.cpp | 842 +++++----------- app/test_scenarios.hpp | 4 +- engine/api/AetherAPI.cpp | 502 ++++++++++ engine/api/AetherAPI.hpp | 169 ++++ engine/api/RenderBody.hpp | 24 + renderer/bodymenu.cpp | 844 +++++++--------- renderer/bodymenu.hpp | 12 +- renderer/bodyselection.hpp | 2 +- renderer/drawbodies.cpp | 1813 +++++++++++++--------------------- renderer/drawbodies.hpp | 4 +- renderer/drawconstraints.cpp | 581 ++++++----- renderer/drawconstraints.hpp | 6 +- renderer/window.cpp | 91 +- renderer/window.hpp | 4 +- wasm/main.cpp | 43 + 17 files changed, 2524 insertions(+), 2620 deletions(-) create mode 100644 engine/api/AetherAPI.cpp create mode 100644 engine/api/AetherAPI.hpp create mode 100644 engine/api/RenderBody.hpp create mode 100644 wasm/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cc001d3..a674bb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,101 +1,154 @@ cmake_minimum_required(VERSION 3.20) -project(Aether_Test) +project(Aether LANGUAGES CXX) set(CMAKE_POLICY_VERSION_MINIMUM 3.5) set(CMAKE_CXX_STANDARD 20) +set(AETHER_BUILD_TARGET_DEFAULT "Desktop") +if(EMSCRIPTEN OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(AETHER_BUILD_TARGET_DEFAULT "WASM") +endif() + +set(AETHER_BUILD_TARGET "${AETHER_BUILD_TARGET_DEFAULT}" CACHE STRING "Build target: Desktop or WASM") +set_property(CACHE AETHER_BUILD_TARGET PROPERTY STRINGS Desktop WASM) + include(FetchContent) -FetchContent_Declare( - glfw - GIT_REPOSITORY https://github.com/glfw/glfw.git - GIT_TAG 3.3.8 -) +set(_aether_deps_root "${CMAKE_BINARY_DIR}/_deps") -FetchContent_MakeAvailable(glfw) +function(aether_use_local_fetchcontent dependency_name source_dir_name) + string(TOUPPER "${dependency_name}" dependency_name_upper) + if(NOT DEFINED "FETCHCONTENT_SOURCE_DIR_${dependency_name_upper}") + if(EXISTS "${_aether_deps_root}/${source_dir_name}/CMakeLists.txt") + set("FETCHCONTENT_SOURCE_DIR_${dependency_name_upper}" "${_aether_deps_root}/${source_dir_name}" CACHE PATH "Local source dir for ${dependency_name}" FORCE) + endif() + endif() +endfunction() -FetchContent_Declare( - glad - GIT_REPOSITORY https://github.com/Dav1dde/glad.git - GIT_TAG v0.1.36 -) +aether_use_local_fetchcontent(glfw glfw-src) +aether_use_local_fetchcontent(glad glad-src) +aether_use_local_fetchcontent(glm glm-src) FetchContent_Declare( glm GIT_REPOSITORY https://github.com/g-truc/glm.git GIT_TAG 0.9.9.8 ) -FetchContent_Declare( - imgui - GIT_REPOSITORY https://github.com/ocornut/imgui.git - GIT_TAG v1.90.8 -) -FetchContent_MakeAvailable(imgui) - -FetchContent_MakeAvailable(glad) FetchContent_MakeAvailable(glm) -find_package(OpenGL REQUIRED) - -add_executable(Aether_Test - engine/engine_configs.cpp - app/main.cpp - app/test_scenarios.cpp - engine/math/vec3.cpp - engine/math/mat.cpp - engine/math/quat.cpp - engine/core/rigidbody.cpp - engine/core/buoyancy.cpp - engine/world/physicsworld.cpp - engine/collision/sphere_sphere.cpp - engine/collision/box_box.cpp - engine/collision/sphere_box.cpp - engine/collision/box_ramp.cpp - engine/collision/ramp_sphere.cpp - engine/collision/ramp_ramp.cpp - engine/collision/manifolds/buildBoxBoxManifold.cpp - engine/collision/manifolds/buildBoxSphereManifold.cpp - engine/collision/manifolds/buildSphereSphereManifold.cpp - engine/collision/manifolds/buildRampBoxManifold.cpp - engine/collision/manifolds/buildRampSphereManifold.cpp - engine/collision/manifolds/buildRampRampManifold.cpp - engine/collision/obb.hpp - renderer/window.cpp - renderer/camera.cpp - renderer/drawbodies.cpp - renderer/drawconstraints.cpp - renderer/bodyshaders.cpp - renderer/bodymenu.cpp - renderer/aether_theme.cpp +file(GLOB_RECURSE AETHER_ENGINE_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/engine/*.cpp" ) -add_library(imgui STATIC - ${imgui_SOURCE_DIR}/imgui.cpp - ${imgui_SOURCE_DIR}/imgui_draw.cpp - ${imgui_SOURCE_DIR}/imgui_tables.cpp - ${imgui_SOURCE_DIR}/imgui_widgets.cpp - ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp - ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp +file(GLOB_RECURSE AETHER_RENDERER_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/renderer/*.cpp" ) -target_include_directories(imgui PUBLIC - ${imgui_SOURCE_DIR} - ${imgui_SOURCE_DIR}/backends +add_library(AetherEngine STATIC + ${AETHER_ENGINE_SOURCES} + engine/api/AetherAPI.hpp + engine/api/RenderBody.hpp ) -target_compile_definitions(imgui PRIVATE IMGUI_IMPL_OPENGL_LOADER_GLAD) - -target_link_libraries(imgui PRIVATE glfw OpenGL::GL) - -target_include_directories(Aether_Test PRIVATE engine) +target_include_directories(AetherEngine PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/engine +) -target_link_libraries(Aether_Test - glfw - glad - OpenGL::GL +target_link_libraries(AetherEngine PRIVATE glm - imgui ) + +if(AETHER_BUILD_TARGET STREQUAL "Desktop") + aether_use_local_fetchcontent(imgui imgui-src) + + FetchContent_Declare( + glfw + GIT_REPOSITORY https://github.com/glfw/glfw.git + GIT_TAG 3.3.8 + ) + + FetchContent_MakeAvailable(glfw) + + FetchContent_Declare( + glad + GIT_REPOSITORY https://github.com/Dav1dde/glad.git + GIT_TAG v0.1.36 + ) + + FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG v1.90.8 + ) + + FetchContent_MakeAvailable(imgui) + + FetchContent_MakeAvailable(glad) + + find_package(OpenGL REQUIRED) + + add_library(imgui STATIC + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp + ) + + target_include_directories(imgui PUBLIC + ${imgui_SOURCE_DIR} + ${imgui_SOURCE_DIR}/backends + ) + + target_compile_definitions(imgui PRIVATE IMGUI_IMPL_OPENGL_LOADER_GLAD) + + target_link_libraries(imgui PRIVATE glfw OpenGL::GL) + + add_executable(AetherDesktop + app/main.cpp + app/test_scenarios.cpp + ${AETHER_RENDERER_SOURCES} + ) + + target_include_directories(AetherDesktop PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/app + ${CMAKE_CURRENT_SOURCE_DIR}/renderer + ) + + target_link_libraries(AetherDesktop PRIVATE + AetherEngine + glfw + glad + OpenGL::GL + glm + imgui + ) +elseif(AETHER_BUILD_TARGET STREQUAL "WASM") + + add_executable(AetherWasm + wasm/main.cpp + ) + + target_link_libraries(AetherWasm PRIVATE + AetherEngine + ) + + if(EMSCRIPTEN) + target_link_options(AetherWasm PRIVATE + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sALLOW_MEMORY_GROWTH=1" + "-sENVIRONMENT=web" + "-sEXPORTED_FUNCTIONS=['_Init','_Step']" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" +) + endif() + +else() + message(FATAL_ERROR "Unknown AETHER_BUILD_TARGET value: ${AETHER_BUILD_TARGET}. Use Desktop or WASM.") +endif() \ No newline at end of file diff --git a/app/main.cpp b/app/main.cpp index d3d1e74..2227dff 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,11 +1,11 @@ -#include "world/physicsworld.hpp" +#include "api/AetherAPI.hpp" #include "../renderer/window.hpp" // BoxRamp int main() { - PhysicsWorld world; + AetherAPI api; - CreateWindow(world); + CreateWindow(api); return 0; } \ No newline at end of file diff --git a/app/test_scenarios.cpp b/app/test_scenarios.cpp index d9b7026..c63c153 100644 --- a/app/test_scenarios.cpp +++ b/app/test_scenarios.cpp @@ -1,12 +1,12 @@ #include "test_scenarios.hpp" -#include "core/box_collider.hpp" -#include "core/rigidbody.hpp" -#include "core/ramp_collider.hpp" -#include "core/sphere_collider.hpp" -#include "math/vec3.hpp" -#include "world/physicsworld.hpp" + +#include +#include +#include #include -#include + +#include "api/AetherAPI.hpp" + #ifndef M_PI #define M_PI 3.14159265358979323846 #endif @@ -14,607 +14,247 @@ std::vector chapters = {"Kinematics", "Laws of Motion", "Collision", "Rotation", "Fluids", "Thermal Properties", "Fun Tests"}; std::unordered_map> testmap; - void InitializeTestMap() { - testmap["Kinematics"] = {TestCase::ProjectMotion, TestCase::RelativeVelocity, TestCase::InclinedPlane}; - testmap["Laws of Motion"] = {TestCase::NewtonThirdLaw, TestCase::MomentumTransfer}; - testmap["Collision"] = {TestCase::PerfectElasticCollision, TestCase::PerfectInelasticCollision, TestCase::Collision}; - testmap["Rotation"] = {TestCase::CenterOfMassTopple, TestCase::ConstraintPlayground, TestCase::AngularImpulse, TestCase::AngularStack, TestCase::CornerCollision, TestCase::RollingFriction, TestCase::BoxToppleOnRamp, TestCase::SphereToppleOnRamp, TestCase::CollisionCauseTopple, TestCase::CircularMotionRope, TestCase::CircularMotionSpring}; - testmap["Fluids"] = {TestCase::BuoyancyTest}; - testmap["Thermal Properties"] = {TestCase::HeatTransferDemo}; - testmap["Fun Tests"] = {TestCase::PyramidStack, TestCase::ManyBoxes, TestCase::ManySpheres, TestCase::RandomScatter}; + testmap["Kinematics"] = {TestCase::ProjectMotion, TestCase::RelativeVelocity, TestCase::InclinedPlane}; + testmap["Laws of Motion"] = {TestCase::NewtonThirdLaw, TestCase::MomentumTransfer}; + testmap["Collision"] = {TestCase::PerfectElasticCollision, TestCase::PerfectInelasticCollision, TestCase::Collision}; + testmap["Rotation"] = {TestCase::CenterOfMassTopple, TestCase::ConstraintPlayground, TestCase::AngularImpulse, TestCase::AngularStack, TestCase::CornerCollision, TestCase::RollingFriction, TestCase::BoxToppleOnRamp, TestCase::SphereToppleOnRamp, TestCase::CollisionCauseTopple, TestCase::CircularMotionRope, TestCase::CircularMotionSpring}; + testmap["Fluids"] = {TestCase::BuoyancyTest}; + testmap["Thermal Properties"] = {TestCase::HeatTransferDemo}; + testmap["Fun Tests"] = {TestCase::PyramidStack, TestCase::ManyBoxes, TestCase::ManySpheres, TestCase::RandomScatter}; } - namespace { - BoxCollider g_floor(Vec3(100.0f, 0.1f, 100.0f)); - SphereCollider g_small_sphere(0.5f); - SphereCollider g_big_sphere(0.8f); - BoxCollider g_small_box(Vec3(0.5f, 0.5f, 0.5f)); - BoxCollider g_wide_box(Vec3(1.0f, 0.5f, 0.8f)); - RampCollider g_gentle_ramp(0.35f, 8.0f, 1.5f); - RampCollider g_steep_ramp(0.70f, 6.0f, 1.2f); - - Camera spawn_projectile_demo(PhysicsWorld &world) -{ - const float speed = 20.0f; - const float angle1 = 30.0f * M_PI / 180.0f; - const float angle2 = 60.0f * M_PI / 180.0f; - - Vec3 start_pos(-10.0f, 0.5f, 0.0f); - Vec3 vel1( - speed * cos(angle1), - speed * sin(angle1), - 0.0f - ); - Vec3 vel2( - speed * cos(angle2), - speed * sin(angle2), - 0.0f - ); - - world.addBody(Rigidbody(start_pos, vel1, &g_small_sphere, 1.0f)); - world.addBody(Rigidbody(start_pos, vel2, &g_small_sphere, 1.0f)); - - return Camera().setPosition(glm::vec3(3.0,5.0,25.0)); -} - - Camera spawn_perfect_elastic_collision(PhysicsWorld &world){ - const float y = 0.5f; - Vec3 pos1(-8.0f, y, 0.0f); - Vec3 pos2(0.0f, y, 0.0f); - Vec3 vel1(10.0f, 0.0f, 0.0f); - Vec3 vel2(0.0f, 0.0f, 0.0f); - Rigidbody b1(pos1, vel1, &g_small_sphere, 1.0f); - Rigidbody b2(pos2, vel2, &g_small_sphere, 1.0f); - b1.restitution = 1.0f; - b2.restitution = 1.0f; - b1.friction = 0.0f; - b2.friction = 0.0f; - - world.addBody(b1); - world.addBody(b2); - return Camera(); - } - Camera spawn_perfect_inelastic_collision(PhysicsWorld &world) -{ - const float y = 0.5f; - - Vec3 pos1(-8.0f, y, 0.0f); - Vec3 pos2(0.0f, y, 0.0f); - - Vec3 vel1(10.0f, 0.0f, 0.0f); - Vec3 vel2(0.0f, 0.0f, 0.0f); - - Rigidbody b1(pos1, vel1, &g_small_sphere, 1.0f); - Rigidbody b2(pos2, vel2, &g_small_sphere, 1.0f); - - b1.restitution = 0.0f; - b2.restitution = 0.0f; - - b1.friction = 0.0f; - b2.friction = 0.0f; - - world.addBody(b1); - world.addBody(b2); - return Camera(); -} - Camera spawn_collision_partial(PhysicsWorld &world) -{ - const float y = 0.5f; - - Vec3 pos1(-8.0f, y, 0.0f); - Vec3 pos2(0.0f, y, 0.0f); - - Vec3 vel1(10.0f, 0.0f, 0.0f); - Vec3 vel2(0.0f, 0.0f, 0.0f); - - Rigidbody b1(pos1, vel1, &g_small_sphere, 1.0f); - Rigidbody b2(pos2, vel2, &g_small_sphere, 1.0f); - - b1.restitution = 0.5f; - b2.restitution = 0.5f; - - b1.friction = 0.1f; - b2.friction = 0.1f; - - world.addBody(b1); - world.addBody(b2); - return Camera(); -} - - Camera spawn_incline_demo(PhysicsWorld& world){ - world.addBody(Rigidbody(Vec3(-2.0f, 0.3f, 0.0f), Vec3(0.0f, 0.0f, 0.0f), &g_steep_ramp, 0.0f)); - world.addBody(Rigidbody(Vec3(3.4f,6.0f,0.0f),Vec3(0.0f,0.0f,0.0f),&g_small_sphere,2.0f)); - return Camera(); - } - - Camera spawn_relative_velocity(PhysicsWorld &world) -{ - - - const float y = 0.5f; - - Vec3 pos1(-8.0f, y, 0.0f); - Vec3 pos2(8.0f, y, 0.0f); - - Vec3 vel1(15.0f, 0.0f, 0.0f); - Vec3 vel2(5.0f, 0.0f, 0.0f); - - Rigidbody b1(pos1, vel1, &g_small_sphere, 1.0f); - Rigidbody b2(pos2, vel2, &g_small_sphere, 1.0f); - - b1.restitution = 0.0f; - b2.restitution = 0.0f; - b1.friction = 0.0f; - b2.friction = 0.0f; - - world.addBody(b1); - world.addBody(b2); - - return Camera().setPosition(glm::vec3(0.0f, 3.0f, 20.0f)); -} - - Camera spawn_newton_third_law(PhysicsWorld &world) -{ - - - const float y = 0.5f; - Vec3 pos1(-10.0f, y, 0.0f); - Vec3 vel1(12.0f, 0.0f, 0.0f); - Rigidbody light_body(pos1, vel1, &g_small_sphere, 0.5f); - - Vec3 pos2(10.0f, y, 0.0f); - Vec3 vel2(-6.0f, 0.0f, 0.0f); - Rigidbody heavy_body(pos2, vel2, &g_big_sphere, 2.0f); - light_body.restitution = 1.0f; - heavy_body.restitution = 1.0f; - light_body.friction = 0.0f; - heavy_body.friction = 0.0f; - - world.addBody(light_body); - world.addBody(heavy_body); - return Camera().setPosition(glm::vec3(0.0f, 3.0f, 25.0f)); -} - - Camera spawn_box_topple_on_ramp(PhysicsWorld &world) -{ - - world.addBody(Rigidbody(Vec3(0.0f, 0.0f, 0.0f), Vec3(), &g_steep_ramp, 0.0f)); - Rigidbody toppling_box(Vec3(4.5f, 8.0f, 0.0f), Vec3(), &g_small_box, 1.5f); - toppling_box.friction = 0.3f; - toppling_box.restitution = 0.4f; - world.addBody(toppling_box); - - return Camera().setPosition(glm::vec3(8.0f, 6.0f, 20.0f)); + BodyID AddBox(AetherAPI &api, const Vec3 &position, const Vec3 &halfSize, float mass = 1.0f, float friction = PHYSICS_DEFAULT_FRICTION, float restitution = PHYSICS_DEFAULT_RESTITUTION, const Vec3 &velocity = Vec3()) + { + BoxSpawnInfo info; + info.position = position; + info.velocity = velocity; + info.halfSize = halfSize; + info.mass = mass; + info.friction = friction; + info.restitution = restitution; + return api.createBox(info); + } + + BodyID AddSphere(AetherAPI &api, const Vec3 &position, float radius, float mass = 1.0f, float friction = PHYSICS_DEFAULT_FRICTION, float restitution = PHYSICS_DEFAULT_RESTITUTION, const Vec3 &velocity = Vec3()) + { + SphereSpawnInfo info; + info.position = position; + info.velocity = velocity; + info.radius = radius; + info.mass = mass; + info.friction = friction; + info.restitution = restitution; + return api.createSphere(info); + } + + BodyID AddRamp(AetherAPI &api, const Vec3 &position, float slope, float length, float halfWidthZ, float mass = 0.0f) + { + RampSpawnInfo info; + info.position = position; + info.slope = slope; + info.length = length; + info.halfWidthZ = halfWidthZ; + info.mass = mass; + return api.createRamp(info); + } + + Camera DefaultCamera(float x = 0.0f, float y = 5.0f, float z = 20.0f) + { + return Camera().setPosition(glm::vec3(x, y, z)); + } + + void AddGround(AetherAPI &api) + { + AddBox(api, Vec3(0.0f, -1.0f, 0.0f), Vec3(100.0f, 0.1f, 100.0f), 0.0f, 0.0f, 1.0f); + } + + void AddSimpleWalls(AetherAPI &api) + { + AddBox(api, Vec3(-12.0f, 5.0f, 0.0f), Vec3(0.5f, 8.0f, 5.0f), 0.0f, 0.0f, 1.0f); + AddBox(api, Vec3(12.0f, 5.0f, 0.0f), Vec3(0.5f, 8.0f, 5.0f), 0.0f, 0.0f, 1.0f); + } + + void AddBuoyancyTank(AetherAPI &api) + { + BuoyancySettings settings; + settings.enabled = true; + settings.beakerCenter = Vec3(0.0f, 4.0f, 0.0f); + settings.beakerHalfSize = 4.0f; + settings.waterHeight = 5.0f; + settings.fluidDensity = 2.0f; + settings.dragCoefficient = 0.3f; + api.setBuoyancySettings(settings); + AddBox(api, Vec3(-3.8f, 4.0f, 0.0f), Vec3(0.1f, 4.0f, 4.0f), 0.0f, 0.0f, 1.0f); + AddBox(api, Vec3(3.8f, 4.0f, 0.0f), Vec3(0.1f, 4.0f, 4.0f), 0.0f, 0.0f, 1.0f); + AddBox(api, Vec3(0.0f, 4.0f, -3.8f), Vec3(4.0f, 4.0f, 0.1f), 0.0f, 0.0f, 1.0f); + AddBox(api, Vec3(0.0f, 4.0f, 3.8f), Vec3(4.0f, 4.0f, 0.1f), 0.0f, 0.0f, 1.0f); + AddBox(api, Vec3(0.0f, 0.0f, 0.0f), Vec3(3.5f, 0.1f, 3.5f), 0.0f, 0.0f, 1.0f); + } + + void AddThermalDefaults(AetherAPI &api) + { + ThermalSettings thermal; + thermal.enabled = true; + thermal.conductionRate = 1.0f; + thermal.radiationRate = 0.02f; + thermal.ambientTemperature = 295.0f; + thermal.ambientCoupling = 0.05f; + thermal.radiationDistance = 3.5f; + thermal.minVisualTemperature = 250.0f; + thermal.maxVisualTemperature = 650.0f; + api.setThermalSettings(thermal); + + ThermalSpawnSettings spawn; + spawn.enabled = true; + spawn.lockToBasicShapes = true; + spawn.spawnTemperature = 320.0f; + spawn.spawnHeatCapacity = 900.0f; + spawn.spawnConductivity = 0.6f; + spawn.spawnEmissivity = 0.85f; + api.setThermalSpawnSettings(spawn); + } } - Camera spawn_sphere_topple_on_ramp(PhysicsWorld &world) +Camera LoadSingleTestScenario(AetherAPI &api, TestCase test_case) { - world.addBody(Rigidbody(Vec3(0.0f, 0.0f, 0.0f), Vec3(), &g_steep_ramp, 0.0f)); - Rigidbody rolling_sphere(Vec3(4.5f, 8.0f, 0.0f), Vec3(), &g_big_sphere, 1.2f); - rolling_sphere.friction = 0.2f; - rolling_sphere.restitution = 0.6f; - world.addBody(rolling_sphere); - - return Camera().setPosition(glm::vec3(8.0f, 6.0f, 20.0f)); -} - - Camera spawn_collision_cause_topple(PhysicsWorld &world) -{ world.addBody(Rigidbody(Vec3(0.0f, 0.0f, 0.0f), Vec3(), &g_gentle_ramp, 0.0f)); - - Rigidbody moving_box(Vec3(-8.0f, 6.0f, 0.0f), Vec3(8.0f, 0.0f, 0.0f), &g_small_box, 2.0f); - moving_box.friction = 0.1f; - moving_box.restitution = 0.5f; - world.addBody(moving_box); - Rigidbody target_sphere(Vec3(4.0f, 6.0f, 0.0f), Vec3(), &g_small_sphere, 1.0f); - target_sphere.friction = 0.2f; - target_sphere.restitution = 0.7f; - world.addBody(target_sphere); - - return Camera().setPosition(glm::vec3(0.0f, 4.0f, 20.0f)); -} - - void add_floor(PhysicsWorld &world) - { - // Keep floor top at y=0 so scenario bodies spawn above, not inside. - world.addBody(Rigidbody(Vec3(0.0f, -0.1f, 0.0f), Vec3(), &g_floor, 0.0f, PHYSICS_DEFAULT_FRICTION, 0.0f)); - } - - void spawn_pyramid_stack(PhysicsWorld &world) - { - // pyramid of boxes: spawn layers at increasing heights so each layer falls onto the previous one - const int base = 10; - const float base_spawn_y = 2.0f; // bottom layer spawn height (above ground) - const float layer_spacing = 2.2f; // vertical spacing between layers to allow visible falling - const float horizontal_spacing = 1.05f; - - for (int y = 0; y < base; ++y) - { - for (int x = 0; x < base - y; ++x) - { - float px = (x - (base - y - 1) * 0.5f) * horizontal_spacing; - float py = base_spawn_y + y * layer_spacing; - world.addBody(Rigidbody(Vec3(px, py, 0.0f), Vec3(), &g_small_box, 1.0f)); - } - } - } - - void spawn_many_spheres(PhysicsWorld &world) - { - for (int i = 0; i < 60; ++i) - { - float x = (i % 10) - 4.5f; - float y = 3.0f + (i / 10) * 0.9f; - float z = ((i / 5) % 2) * 0.6f; - if(i>=50) - world.addBody(Rigidbody(Vec3(x, y, z-0.8f), Vec3(), &g_small_sphere, 0.5f)); - else { - world.addBody(Rigidbody(Vec3(x, y, z ), Vec3(), &g_small_sphere, 0.5f)); - } - } - // world.addBody(Rigidbody(Vec3(0, , z), Vec3(), &g_small_sphere, 0.5f)); - - } - - void spawn_many_boxes(PhysicsWorld &world) - { - for (int i = 0; i < 80; ++i) - { - float x = (i % 8) - 3.5f; - float y = 0.6f + (i / 8) * 0.95f; - float z = ((i / 4) % 2) * 0.6f; - world.addBody(Rigidbody(Vec3(x, y, z), Vec3(), &g_small_box, 1.0f)); - } - } - - void spawn_chain_collide(PhysicsWorld &world) - { - // a row of spheres that knock into a second row - for (int i = 0; i < 10; ++i) - { - world.addBody(Rigidbody(Vec3(-10.0f + i * 1.2f, 3.0f, 0.0f), Vec3(6.0f, 0.0f, 0.0f), &g_small_sphere, 0.5f)); - } - for (int i = 0; i < 10; ++i) - { - world.addBody(Rigidbody(Vec3(2.0f + i * 1.2f, 3.0f, 0.0f), Vec3(), &g_small_sphere, 0.5f)); - } - } - - void spawn_random_scatter(PhysicsWorld &world) - { - for (int i = 0; i < 100; ++i) - { - float x = (rand() % 400 - 200) * 0.05f; - float y = 2.0f + (rand() % 200) * 0.02f; - float z = (rand() % 400 - 200) * 0.02f; - if (rand() % 2) - world.addBody(Rigidbody(Vec3(x, y, z), Vec3(), &g_small_sphere, 0.4f)); - else - world.addBody(Rigidbody(Vec3(x, y, z), Vec3(), &g_small_box, 0.9f)); - } - } - - Camera spawn_constraint_playground(PhysicsWorld &world) - { - // Rope pair (two dynamic bodies linked together) - auto rope_top = Rigidbody(Vec3(-9.0f, 7.0f, 0.0f), Vec3(0.4f, -0.2f, 0.0f), &g_small_sphere, 1.0f); - rope_top.friction = 0.2f; - auto rope_bottom = Rigidbody(Vec3(-9.2f, 3.2f, 0.0f), Vec3(-0.2f, 0.0f, 0.0f), &g_small_sphere, 1.1f); - rope_bottom.friction = 0.2f; - auto rope_top_id = world.addBody(rope_top); - auto rope_bottom_id = world.addBody(rope_bottom); - world.addDistanceConstraints(rope_top_id, rope_bottom_id, 4.0f, DistanceConstraint::ROPE, 0.0f, 0.0f); - - // Rod trio (short chain, no static anchor) - std::uint32_t rod_ids[3]; - for (int i = 0; i < 3; ++i) - { - float x = -1.5f + i * 2.0f; - rod_ids[i] = world.addBody(Rigidbody(Vec3(x, 5.0f, 0.0f), Vec3(0.0f, i == 2 ? -0.6f : 0.0f, 0.0f), &g_small_box, 1.0f)); - } - world.addDistanceConstraints(rod_ids[0], rod_ids[1], 2.0f, DistanceConstraint::ROD, 0.6f, 0.6f); - world.addDistanceConstraints(rod_ids[1], rod_ids[2], 2.0f, DistanceConstraint::ROD, 0.6f, 0.6f); - - // Spring pair (two moving spheres) - auto spring_a = world.addBody(Rigidbody(Vec3(4.8f, 6.5f, 0.0f), Vec3(-0.3f, 0.4f, 0.0f), &g_small_sphere, 0.9f)); - auto spring_b = world.addBody(Rigidbody(Vec3(7.0f, 3.5f, 0.0f), Vec3(0.5f, -0.3f, 0.0f), &g_small_sphere, 0.9f)); - world.addDistanceConstraints(spring_a, spring_b, 3.5f, DistanceConstraint::SPRING, 2.2f, 0.7f); - - // Tiny rope chain (3 bodies) to demonstrate sequential constraints without overload - std::vector chain; - chain.reserve(3); - for (int i = 0; i < 3; ++i) - { - float y = 7.0f - i * 1.0f; - chain.push_back(world.addBody(Rigidbody(Vec3(9.0f + i * 0.2f, y, 0.0f), Vec3(0.2f * i, 0.0f, 0.0f), &g_small_sphere, 0.8f + 0.1f * i))); - } - world.addDistanceConstraints(chain[0], chain[1], 1.0f, DistanceConstraint::ROPE, 0.0f, 0.0f); - world.addDistanceConstraints(chain[1], chain[2], 1.0f, DistanceConstraint::ROPE, 0.0f, 0.0f); - - return Camera().setPosition(glm::vec3(0.0f, 7.0f, 28.0f)); - } - - - void spawn_angular_stack(PhysicsWorld &world) - { - world.addBody(Rigidbody(Vec3(0.0f, 0.5f, 0.0f), Vec3(), &g_small_box, 1.0f)); - world.addBody(Rigidbody(Vec3(0.0f, 1.55f, 0.0f), Vec3(), &g_small_box, 1.0f)); - world.addBody(Rigidbody(Vec3(0.0f, 2.6f, 0.0f), Vec3(), &g_small_box, 1.0f)); - - world.addBody(Rigidbody(Vec3(-2.5f, 1.55f, 0.0f), Vec3(9.0f, 0.0f, 0.0f), &g_small_sphere, 0.5f)); - } - - void spawn_box_corner_collision(PhysicsWorld &world) - { - // Test: Box-to-box collision at corners - // Verify: Both boxes rotate from corner-to-corner impact - // Key: Collision at edges = both bodies get rotational energy - - // Box A - moving - world.addBody(Rigidbody(Vec3(-5.0f, 2.0f, 0.0f), Vec3(6.0f, 0.0f, 0.0f), &g_small_box, 1.0f)); - - // Box B - stationary, offset so collision is corner-to-corner - // A's right corner will hit B's left corner - world.addBody(Rigidbody(Vec3(3.0f, 2.0f, 0.0f), Vec3(), &g_small_box, 1.0f)); - - // Third test: perpendicular approach - world.addBody(Rigidbody(Vec3(0.0f, 5.0f, 0.0f), Vec3(0.0f, -5.0f, 0.0f), &g_wide_box, 1.5f)); - world.addBody(Rigidbody(Vec3(0.0f, 8.0f, 0.0f), Vec3(), &g_small_box, 1.0f)); - } - - void spawn_off_center_hit(PhysicsWorld &world) - { - // Test: Sphere hitting box corner point - // Verify: Box spins from off-center impact - // Key: Contact point far from box center = high torque - - // Target box (should spin markedly) - world.addBody(Rigidbody(Vec3(0.0f, 2.0f, 0.0f), Vec3(), &g_small_box, 2.0f)); - - // Sphere aimed at TOP-RIGHT corner (offset in X and Y) - // Box half-extent is (0.5, 0.5, 0.5), so corner is at (0.5, 0.5, 0) - // Contact point approximately: (0.5, 2.5, 0) - // This is (0.5, 0.5, 0) from center = maximum lever arm - world.addBody(Rigidbody(Vec3(-4.0f, 2.5f, 0.0f), Vec3(8.0f, 0.0f, 0.0f), &g_big_sphere, 1.0f)); - - // Second test: different corner - world.addBody(Rigidbody(Vec3(0.0f, 5.0f, 0.0f), Vec3(), &g_small_box, 1.5f)); - world.addBody(Rigidbody(Vec3(4.0f, 5.5f, 0.0f), Vec3(-7.0f, 0.0f, 0.0f), &g_big_sphere, 0.8f)); - } - - void spawn_stack_tipping(PhysicsWorld &world) - { - // Test: Stack toppling from side impact - // Verify: Stack doesn't just slide - it TIPS/ROTATES - // Key: Impact at height + side hit = rotation torque - - // Build tall stack (3 boxes) - world.addBody(Rigidbody(Vec3(0.0f, 0.5f, 0.0f), Vec3(), &g_small_box, 1.0f)); // base - world.addBody(Rigidbody(Vec3(0.0f, 1.55f, 0.0f), Vec3(), &g_small_box, 1.0f)); // middle - world.addBody(Rigidbody(Vec3(0.0f, 2.6f, 0.0f), Vec3(), &g_small_box, 1.0f)); // top - - // Side impact at MIDDLE box height (not center-mass) - // This creates lever arm: impact point height difference from COM - // Impact at y=1.55, if COM of stack is at y~1.2, lever arm is ~0.35 - world.addBody(Rigidbody(Vec3(-4.0f, 1.55f, 0.0f), Vec3(7.0f, 0.0f, 0.0f), &g_big_sphere, 1.2f)); - } - - void spawn_sphere_rolling(PhysicsWorld &world) - { - // Test: Sphere rolling with friction torque - // Verify: Sphere rotates due to friction at contact point - // Key: Sliding sphere → friction impulse creates torque → rolling motion - - // Create gentle ramp or flat surface with high friction - // Spawn sphere with sliding velocity (not rolling) - world.addBody(Rigidbody(Vec3(-8.0f, 3.0f, 0.0f), Vec3(8.0f, 0.0f, 0.0f), &g_big_sphere, 1.2f, 0.8f, 0.3f)); - - // Reference: non-sliding sphere for comparison - world.addBody(Rigidbody(Vec3(-8.0f, 5.0f, 0.0f), Vec3(6.0f, 0.0f, 0.0f), &g_small_sphere, 0.8f, 0.1f, 0.2f)); - - // Test on ramp: if it exists, rolling down will show rotation - world.addBody(Rigidbody(Vec3(2.0f, 0.0f, 0.0f), Vec3(), &g_gentle_ramp, 0.0f)); - world.addBody(Rigidbody(Vec3(4.0f, 3.5f, 0.0f), Vec3(), &g_small_sphere, 1.0f, 0.8f, 0.1f)); - } - - Camera spawn_heat_transfer_demo(PhysicsWorld &world) - { - world.thermal_settings.enabled = true; - world.thermal_settings.conduction_rate = 15.0f; - world.thermal_settings.radiation_rate = 0.02f; - world.thermal_settings.ambient_temperature = 295.0f; - world.thermal_settings.ambient_coupling = 0.03f; - world.thermal_settings.radiation_distance = 4.0f; - world.thermal_settings.min_visual_temperature = 240.0f; - world.thermal_settings.max_visual_temperature = 660.0f; - world.thermal_spawn_controls.enabled = true; - world.thermal_spawn_controls.lock_to_basic_shapes = true; - world.thermal_spawn_controls.spawn_temperature = 295.0f; - world.thermal_spawn_controls.spawn_heat_capacity = 930.0f; - world.thermal_spawn_controls.spawn_conductivity = 0.7f; - world.thermal_spawn_controls.spawn_emissivity = 0.9f; - - const int boxCount = 9; - const float spacing = 1.0f; - const float startX = -0.5f * spacing * (boxCount - 1); - const float coldTemp = 255.0f; - const float hotTemp = 650.0f; - - for (int i = 0; i < boxCount; ++i) - { - float lerp = (boxCount == 1) ? 0.0f : static_cast(i) / static_cast(boxCount - 1); - float temp = coldTemp + lerp * (hotTemp - coldTemp); - Vec3 pos(startX + i * spacing, 0.55f, 0.0f); - Rigidbody body(pos, Vec3(), &g_small_box, 1.8f); - body.thermal_enabled = true; - body.temperature = temp; - body.heat_capacity = 930.0f; - body.thermal_conductivity = 0.75f; - body.thermal_emissivity = 0.88f; - world.addBody(body); - } - Rigidbody striker(Vec3(0.0f, 2.0f, 0.0f), Vec3(), &g_small_sphere, 1.0f); - striker.thermal_enabled = true; - striker.temperature = 3000.0f; - striker.heat_capacity = 930.0f; - striker.thermal_conductivity = 0.75f; - striker.thermal_emissivity = 0.88f; - world.addBody(striker); - - return Camera().setPosition(glm::vec3(0.0f, 4.8f, 22.0f)); - } - - Camera spawn_circular_motion_rope(PhysicsWorld &world) - { - Rigidbody fixed_box(Vec3(0.0f, 0.0f, 0.0f), Vec3(), &g_small_box, 0.0f); - fixed_box.friction = 0.5f; - auto box_id = world.addBody(fixed_box); - const float rope_length = 3.0f; - const float orbital_speed = 50.0f; - Rigidbody orbiting_sphere( - Vec3(rope_length, 2.0f, 0.0f), - Vec3(0.0f, 0.0f, orbital_speed), - &g_small_sphere, - 1.0f - ); - orbiting_sphere.friction = 0.1f; - orbiting_sphere.restitution = 0.3f; - auto sphere_id = world.addBody(orbiting_sphere); - world.addDistanceConstraints(box_id, sphere_id, rope_length, DistanceConstraint::ROPE, 0.0f, 0.0f); - return Camera().setPosition(glm::vec3(0.0f, 15.0f, 0.0f)) - .setYaw(0.0f) - .setPitch(-90.0f); - } - - Camera spawn_circular_motion_spring(PhysicsWorld &world) - { - Rigidbody fixed_box(Vec3(0.0f, 0.0f, 0.0f), Vec3(), &g_small_box, 0.0f); - fixed_box.friction = 0.5f; - auto box_id = world.addBody(fixed_box); - const float spring_length = 3.0f; - const float spring_constant = 2.0f; - const float damping = 0.5f; - const float orbital_speed = 50.0f; - Rigidbody orbiting_sphere( - Vec3(spring_length, 2.0f, 0.0f), - Vec3(0.0f, 0.0f, orbital_speed), - &g_small_sphere, - 1.0f - ); - orbiting_sphere.friction = 0.1f; - orbiting_sphere.restitution = 0.3f; - auto sphere_id = world.addBody(orbiting_sphere); - world.addDistanceConstraints(box_id, sphere_id, spring_length, DistanceConstraint::SPRING, spring_constant, damping); - return Camera().setPosition(glm::vec3(0.0f, 15.0f, 0.0f)) - .setYaw(0.0f) - .setPitch(-90.0f); - } - - Camera spawn_pyramid_stack_scenario(PhysicsWorld &world) - { - spawn_pyramid_stack(world); - return Camera().setPosition(glm::vec3(0.0f, 5.0f, 25.0f)); - } - - Camera spawn_many_boxes_scenario(PhysicsWorld &world) - { - spawn_many_boxes(world); - return Camera().setPosition(glm::vec3(0.0f, 5.0f, 25.0f)); - } - - Camera spawn_many_spheres_scenario(PhysicsWorld &world) - { - spawn_many_spheres(world); - return Camera().setPosition(glm::vec3(0.0f, 5.0f, 25.0f)); - } - - Camera spawn_random_scatter_scenario(PhysicsWorld &world) - { - spawn_random_scatter(world); - return Camera().setPosition(glm::vec3(0.0f, 5.0f, 30.0f)); - } - -} - -Camera LoadSingleTestScenario(PhysicsWorld &world, TestCase test_case) -{ - world.enable_buoyancy = false; // only when boyancy testcase - world.thermal_settings = PhysicsWorld::ThermalSettings(); - world.thermal_spawn_controls = PhysicsWorld::ThermalSpawnControls(); - - add_floor(world); - - switch (test_case) - { - case TestCase::ProjectMotion: - return spawn_projectile_demo(world); - break; - case TestCase::PerfectElasticCollision: - return spawn_perfect_elastic_collision(world); - case TestCase::PerfectInelasticCollision: - return spawn_perfect_inelastic_collision(world); - case TestCase::Collision: - return spawn_collision_partial(world); - case TestCase::InclinedPlane: - return spawn_incline_demo(world); - case TestCase::MomentumTransfer: - // Conservation-of-momentum chain (Newton's cradle style) - spawn_chain_collide(world); - return Camera().setPosition(glm::vec3(-1.0f, 6.0f, 24.0f)); - case TestCase::CenterOfMassTopple: - // Demonstrates torque-induced tipping of a stacked tower - spawn_stack_tipping(world); - return Camera().setPosition(glm::vec3(0.0f, 5.0f, 20.0f)); - case TestCase::ConstraintPlayground: - return spawn_constraint_playground(world); - case TestCase::AngularImpulse: - // Off-center collisions that inject angular momentum - spawn_off_center_hit(world); - return Camera().setPosition(glm::vec3(0.0f, 6.0f, 22.0f)); - case TestCase::AngularStack: - spawn_angular_stack(world); - return Camera().setPosition(glm::vec3(0.0f, 4.0f, 22.0f)); - case TestCase::CornerCollision: - spawn_box_corner_collision(world); - return Camera().setPosition(glm::vec3(-1.0f, 6.5f, 24.0f)); - case TestCase::RollingFriction: - spawn_sphere_rolling(world); - return Camera().setPosition(glm::vec3(-1.0f, 5.0f, 26.0f)); - case TestCase::RelativeVelocity: - return spawn_relative_velocity(world); - case TestCase::NewtonThirdLaw: - return spawn_newton_third_law(world); - case TestCase::BoxToppleOnRamp: - return spawn_box_topple_on_ramp(world); - case TestCase::SphereToppleOnRamp: - return spawn_sphere_topple_on_ramp(world); - case TestCase::CollisionCauseTopple: - return spawn_collision_cause_topple(world); - case TestCase::BuoyancyTest: - world.enable_buoyancy = true; - world.water_fluid = Fluid(2.0f, 2.0f, 0.3f); - world.addBody(Rigidbody(Vec3(0.0f, 5.0f, 0.0f), Vec3(), &g_small_sphere, 0.5f)); - world.addBody(Rigidbody(Vec3(3.0f, 5.0f, 0.0f), Vec3(), &g_small_box, 0.6f)); - return Camera(); - case TestCase::HeatTransferDemo: - return spawn_heat_transfer_demo(world); - case TestCase::CircularMotionRope: - return spawn_circular_motion_rope(world); - case TestCase::CircularMotionSpring: - return spawn_circular_motion_spring(world); - case TestCase::PyramidStack: - return spawn_pyramid_stack_scenario(world); - case TestCase::ManyBoxes: - return spawn_many_boxes_scenario(world); - case TestCase::ManySpheres: - return spawn_many_spheres_scenario(world); - case TestCase::RandomScatter: - return spawn_random_scatter_scenario(world); - default: - return spawn_projectile_demo(world); - break; - } + api.reset(); + api.setGravity(Vec3(0.0f, -9.81f, 0.0f)); + AddGround(api); + + switch (test_case) + { + case TestCase::ProjectMotion: + AddSphere(api, Vec3(-10.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.2f, 0.7f, Vec3(15.0f, 12.0f, 0.0f)); + AddSphere(api, Vec3(-10.0f, 0.5f, 1.5f), 0.5f, 1.0f, 0.2f, 0.7f, Vec3(15.0f, 18.0f, 0.0f)); + return DefaultCamera(2.0f, 6.0f, 25.0f); + case TestCase::PerfectElasticCollision: + AddSphere(api, Vec3(-8.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.0f, 1.0f, Vec3(10.0f, 0.0f, 0.0f)); + AddSphere(api, Vec3(0.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.0f, 1.0f); + return DefaultCamera(); + case TestCase::PerfectInelasticCollision: + AddSphere(api, Vec3(-8.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.0f, 0.0f, Vec3(10.0f, 0.0f, 0.0f)); + AddSphere(api, Vec3(0.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.0f, 0.0f); + return DefaultCamera(); + case TestCase::Collision: + AddBox(api, Vec3(-4.0f, 0.5f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 1.0f, 0.1f, 0.6f, Vec3(8.0f, 0.0f, 0.0f)); + AddBox(api, Vec3(2.0f, 0.5f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 1.0f, 0.1f, 0.6f); + return DefaultCamera(); + case TestCase::InclinedPlane: + AddRamp(api, Vec3(-4.0f, 0.3f, 0.0f), 0.35f, 8.0f, 1.5f, 0.0f); + AddSphere(api, Vec3(2.0f, 4.0f, 0.0f), 0.5f, 2.0f); + return DefaultCamera(); + case TestCase::MomentumTransfer: + for (int i = 0; i < 5; ++i) + AddSphere(api, Vec3(-8.0f + i * 1.2f, 0.5f, 0.0f), 0.5f, 0.5f, 0.0f, 0.9f, Vec3(8.0f, 0.0f, 0.0f)); + return DefaultCamera(); + case TestCase::CenterOfMassTopple: + AddBox(api, Vec3(0.0f, 0.5f, 0.0f), Vec3(0.6f, 0.6f, 0.6f), 1.0f); + AddSphere(api, Vec3(-4.0f, 1.5f, 0.0f), 0.8f, 2.0f, 0.2f, 0.7f, Vec3(8.0f, 0.0f, 0.0f)); + return DefaultCamera(); + case TestCase::ConstraintPlayground: + { + BodyID a = AddSphere(api, Vec3(-9.0f, 7.0f, 0.0f), 0.5f, 1.0f, 0.2f, 0.8f, Vec3(0.4f, -0.2f, 0.0f)); + BodyID b = AddSphere(api, Vec3(-9.2f, 3.2f, 0.0f), 0.5f, 1.1f, 0.2f, 0.8f, Vec3(-0.2f, 0.0f, 0.0f)); + api.createDistanceConstraint(a, b, 4.0f, ConstraintType::Rope, 0.0f, 0.0f); + BodyID c = AddBox(api, Vec3(0.0f, 5.0f, 0.0f), Vec3(0.45f, 0.45f, 0.45f), 1.0f, 0.2f, 0.6f); + BodyID d = AddBox(api, Vec3(1.2f, 5.0f, 0.0f), Vec3(0.45f, 0.45f, 0.45f), 1.0f, 0.2f, 0.6f); + api.createDistanceConstraint(c, d, 2.0f, ConstraintType::Rod, 0.6f, 0.6f); + } + return DefaultCamera(); + case TestCase::AngularImpulse: + AddBox(api, Vec3(0.0f, 0.5f, 0.0f), Vec3(0.7f, 0.7f, 0.7f), 1.0f, 0.1f, 0.7f); + AddSphere(api, Vec3(-4.0f, 1.5f, 0.0f), 0.8f, 1.0f, 0.2f, 0.8f, Vec3(10.0f, 0.0f, 0.0f)); + return DefaultCamera(); + case TestCase::AngularStack: + for (int i = 0; i < 3; ++i) + AddBox(api, Vec3(0.0f, 0.5f + i * 1.05f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 1.0f); + AddSphere(api, Vec3(-2.5f, 1.55f, 0.0f), 0.5f, 0.5f, 0.2f, 0.7f, Vec3(9.0f, 0.0f, 0.0f)); + return DefaultCamera(); + case TestCase::CornerCollision: + AddBox(api, Vec3(-5.0f, 2.0f, 0.0f), Vec3(0.7f, 0.7f, 0.7f), 1.0f, 0.2f, 0.6f, Vec3(6.0f, 0.0f, 0.0f)); + AddBox(api, Vec3(3.0f, 2.0f, 0.0f), Vec3(0.7f, 0.7f, 0.7f), 1.0f, 0.2f, 0.6f); + return DefaultCamera(); + case TestCase::RollingFriction: + AddRamp(api, Vec3(-2.0f, 0.0f, 0.0f), 0.35f, 8.0f, 1.5f, 0.0f); + AddSphere(api, Vec3(2.0f, 4.0f, 0.0f), 0.8f, 1.0f, 0.8f, 0.1f); + return DefaultCamera(); + case TestCase::BuoyancyTest: + AddBuoyancyTank(api); + AddSphere(api, Vec3(0.0f, 6.5f, 0.0f), 0.6f, 0.5f, 0.1f, 0.5f); + AddBox(api, Vec3(2.0f, 6.0f, 0.0f), Vec3(0.4f, 0.4f, 0.4f), 0.9f, 0.1f, 0.5f); + return DefaultCamera(0.0f, 8.0f, 16.0f); + case TestCase::HeatTransferDemo: + AddThermalDefaults(api); + for (int i = 0; i < 6; ++i) + { + BodyID id = AddBox(api, Vec3(-4.0f + i * 1.6f, 2.0f + (i % 2) * 0.3f, 0.0f), Vec3(0.45f, 0.45f, 0.45f), 1.8f, 0.2f, 0.5f); + (void)id; + } + return DefaultCamera(0.0f, 5.0f, 18.0f); + case TestCase::RelativeVelocity: + AddSphere(api, Vec3(-4.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.1f, 0.6f, Vec3(4.0f, 0.0f, 0.0f)); + AddSphere(api, Vec3(4.0f, 0.5f, 0.0f), 0.5f, 1.0f, 0.1f, 0.6f, Vec3(-4.0f, 0.0f, 0.0f)); + return DefaultCamera(); + case TestCase::NewtonThirdLaw: + AddSphere(api, Vec3(-6.0f, 0.5f, 0.0f), 0.5f, 0.5f, 0.1f, 0.6f, Vec3(6.0f, 0.0f, 0.0f)); + AddSphere(api, Vec3(0.0f, 0.5f, 0.0f), 0.8f, 2.0f, 0.1f, 0.6f); + return DefaultCamera(); + case TestCase::BoxToppleOnRamp: + AddRamp(api, Vec3(-3.0f, 0.0f, 0.0f), 0.7f, 6.0f, 1.2f, 0.0f); + AddBox(api, Vec3(3.5f, 4.0f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 1.5f); + return DefaultCamera(); + case TestCase::SphereToppleOnRamp: + AddRamp(api, Vec3(-3.0f, 0.0f, 0.0f), 0.7f, 6.0f, 1.2f, 0.0f); + AddSphere(api, Vec3(3.5f, 4.0f, 0.0f), 0.8f, 1.2f); + return DefaultCamera(); + case TestCase::CollisionCauseTopple: + AddRamp(api, Vec3(-3.0f, 0.0f, 0.0f), 0.35f, 8.0f, 1.5f, 0.0f); + AddBox(api, Vec3(-8.0f, 6.0f, 0.0f), Vec3(0.6f, 0.6f, 0.6f), 2.0f, 0.1f, 0.6f, Vec3(8.0f, 0.0f, 0.0f)); + AddSphere(api, Vec3(4.0f, 6.0f, 0.0f), 0.8f, 1.0f); + return DefaultCamera(); + case TestCase::CircularMotionRope: + { + BodyID fixedBox = AddBox(api, Vec3(0.0f, 0.0f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 0.0f); + BodyID orbit = AddSphere(api, Vec3(4.0f, 2.0f, 0.0f), 0.5f, 1.0f, 0.2f, 0.6f, Vec3(0.0f, 0.0f, 4.0f)); + api.createDistanceConstraint(fixedBox, orbit, 4.5f, ConstraintType::Rope, 0.0f, 0.0f); + } + return DefaultCamera(4.0f, 4.0f, 18.0f); + case TestCase::CircularMotionSpring: + { + BodyID fixedBox = AddBox(api, Vec3(0.0f, 0.0f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 0.0f); + BodyID orbit = AddSphere(api, Vec3(4.0f, 2.0f, 0.0f), 0.5f, 1.0f, 0.2f, 0.6f, Vec3(0.0f, 0.0f, 4.0f)); + api.createDistanceConstraint(fixedBox, orbit, 4.5f, ConstraintType::Spring, 2.0f, 0.7f); + } + return DefaultCamera(4.0f, 4.0f, 18.0f); + case TestCase::PyramidStack: + for (int y = 0; y < 5; ++y) + for (int x = 0; x < 5 - y; ++x) + AddBox(api, Vec3(-4.0f + x * 1.05f, 0.5f + y * 1.05f, 0.0f), Vec3(0.5f, 0.5f, 0.5f), 1.0f); + return DefaultCamera(); + case TestCase::ManyBoxes: + for (int i = 0; i < 80; ++i) + AddBox(api, Vec3(-10.0f + (i % 10) * 2.0f, 1.0f + (i / 10) * 1.2f, 0.0f), Vec3(0.4f, 0.4f, 0.4f), 1.0f); + return DefaultCamera(); + case TestCase::ManySpheres: + for (int i = 0; i < 60; ++i) + AddSphere(api, Vec3(-8.0f + (i % 10) * 1.8f, 1.0f + (i / 10) * 1.0f, 0.0f), 0.5f, 0.5f); + return DefaultCamera(); + case TestCase::RandomScatter: + for (int i = 0; i < 100; ++i) + { + const float x = -8.0f + (i % 10) * 1.8f; + const float y = 1.0f + (i / 10) * 1.0f; + if (i % 2 == 0) + AddSphere(api, Vec3(x, y, 0.0f), 0.5f, 0.6f); + else + AddBox(api, Vec3(x, y, 0.0f), Vec3(0.4f, 0.4f, 0.4f), 0.9f); + } + return DefaultCamera(); + default: + return DefaultCamera(); + } } \ No newline at end of file diff --git a/app/test_scenarios.hpp b/app/test_scenarios.hpp index f4fb609..d95ec22 100644 --- a/app/test_scenarios.hpp +++ b/app/test_scenarios.hpp @@ -3,7 +3,7 @@ #include #include #include -class PhysicsWorld; +class AetherAPI; enum class TestCase { @@ -38,4 +38,4 @@ extern std::vector chapters; extern std::unordered_map> testmap; void InitializeTestMap(); -Camera LoadSingleTestScenario(PhysicsWorld &world, TestCase test_case); +Camera LoadSingleTestScenario(AetherAPI &api, TestCase test_case); diff --git a/engine/api/AetherAPI.cpp b/engine/api/AetherAPI.cpp new file mode 100644 index 0000000..961dfdc --- /dev/null +++ b/engine/api/AetherAPI.cpp @@ -0,0 +1,502 @@ +#include "api/AetherAPI.hpp" + +#include +#include + +#include "core/box_collider.hpp" +#include "core/ramp_collider.hpp" +#include "core/rigidbody.hpp" +#include "core/sphere_collider.hpp" +#include "world/physicsworld.hpp" + +namespace +{ + MeshType toMeshType(const Collider* collider) + { + if (collider == nullptr) + { + return MeshType::Unknown; + } + + switch (collider->type) + { + case ShapeType::Box: + return MeshType::Box; + case ShapeType::Sphere: + return MeshType::Sphere; + case ShapeType::Ramp: + return MeshType::Ramp; + default: + return MeshType::Unknown; + } + } + + ConstraintType toConstraintType(DistanceConstraint::TYPE type) + { + switch (type) + { + case DistanceConstraint::ROPE: + return ConstraintType::Rope; + case DistanceConstraint::ROD: + return ConstraintType::Rod; + case DistanceConstraint::SPRING: + return ConstraintType::Spring; + default: + return ConstraintType::Rope; + } + } + + DistanceConstraint::TYPE toDistanceConstraintType(ConstraintType type) + { + switch (type) + { + case ConstraintType::Rope: + return DistanceConstraint::ROPE; + case ConstraintType::Rod: + return DistanceConstraint::ROD; + case ConstraintType::Spring: + return DistanceConstraint::SPRING; + default: + return DistanceConstraint::ROPE; + } + } + + float computeMass(const BodyState& state) + { + if (state.mass > 0.0f) + { + return state.mass; + } + if (state.inverseMass > 0.0f) + { + return 1.0f / state.inverseMass; + } + return 0.0f; + } + + void applyMassAndInertia(Rigidbody& body, const BodyState& state) + { + const float mass = computeMass(state); + const float minMass = PHYSICS_EPSILON; + float effectiveMass = mass; + + if (mass <= 0.0f) + { + effectiveMass = 0.0f; + } + else if (mass < minMass) + { + effectiveMass = minMass; + } + + body.inverse_mass = (effectiveMass > 0.0f) ? (1.0f / effectiveMass) : 0.0f; + const float actualMass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; + + if (!body.collider || actualMass <= 0.0f) + { + body.inverse_inertia_body = Mat3::identity() * 0.0f; + body.updateworldinvinertia(); + return; + } + + if (body.collider->type == ShapeType::Sphere) + { + auto* sphere = static_cast(body.collider); + const float I = 0.4f * actualMass * sphere->radius * sphere->radius; + const float invI = (I > PHYSICS_EPSILON) ? (1.0f / I) : 0.0f; + body.inverse_inertia_body = Mat3::diag(invI, invI, invI); + } + else if (body.collider->type == ShapeType::Box) + { + auto* box = static_cast(body.collider); + const Vec3 e = box->halfsize * 2.0f; + const float Ix = (actualMass / 12.0f) * (e.y * e.y + e.z * e.z); + const float Iy = (actualMass / 12.0f) * (e.x * e.x + e.z * e.z); + const float Iz = (actualMass / 12.0f) * (e.x * e.x + e.y * e.y); + body.inverse_inertia_body = Mat3::diag( + Ix > PHYSICS_EPSILON ? (1.0f / Ix) : 0.0f, + Iy > PHYSICS_EPSILON ? (1.0f / Iy) : 0.0f, + Iz > PHYSICS_EPSILON ? (1.0f / Iz) : 0.0f); + } + else + { + body.inverse_inertia_body = Mat3::identity() * 0.0f; + } + + body.updateworldinvinertia(); + } + + BodyState makeState(const Rigidbody& body) + { + BodyState state; + state.id = body.id; + state.position = body.position; + state.velocity = body.velocity; + state.forceAccum = body.force_accum; + state.orientation = body.orientation; + state.angularVelocity = body.angvel; + state.renderAlpha = body.render_alpha; + state.thermalEnabled = body.thermal_enabled; + state.temperature = body.temperature; + state.heatCapacity = body.heat_capacity; + state.thermalConductivity = body.thermal_conductivity; + state.thermalEmissivity = body.thermal_emissivity; + state.friction = body.friction; + state.restitution = body.restitution; + state.inverseMass = body.inverse_mass; + state.mass = body.inverse_mass > 0.0f ? (1.0f / body.inverse_mass) : 0.0f; + + if (body.collider == nullptr) + { + state.meshType = MeshType::Unknown; + return state; + } + + state.meshType = toMeshType(body.collider); + switch (body.collider->type) + { + case ShapeType::Sphere: + state.sphereRadius = static_cast(body.collider)->radius; + break; + case ShapeType::Box: + state.boxHalfSize = static_cast(body.collider)->halfsize; + break; + case ShapeType::Ramp: + { + auto* ramp = static_cast(body.collider); + state.rampSlope = ramp->slope; + state.rampLength = ramp->length; + state.rampHalfWidthZ = ramp->half_width_z; + break; + } + default: + break; + } + + return state; + } + + void applyState(Rigidbody& body, const BodyState& state) + { + body.position = state.position; + body.velocity = state.velocity; + body.force_accum = state.forceAccum; + body.orientation = state.orientation; + body.angvel = state.angularVelocity; + body.render_alpha = state.renderAlpha; + body.thermal_enabled = state.thermalEnabled; + body.temperature = state.temperature; + body.heat_capacity = state.heatCapacity; + body.thermal_conductivity = state.thermalConductivity; + body.thermal_emissivity = state.thermalEmissivity; + body.friction = state.friction; + body.restitution = state.restitution; + applyMassAndInertia(body, state); + + if (body.collider == nullptr) + { + return; + } + + switch (body.collider->type) + { + case ShapeType::Sphere: + static_cast(body.collider)->radius = state.sphereRadius; + break; + case ShapeType::Box: + static_cast(body.collider)->halfsize = state.boxHalfSize; + break; + case ShapeType::Ramp: + { + auto* ramp = static_cast(body.collider); + ramp->slope = state.rampSlope; + ramp->length = state.rampLength; + ramp->half_width_z = state.rampHalfWidthZ; + break; + } + default: + break; + } + } +} + +struct AetherAPI::Impl +{ + PhysicsWorld world; + std::unordered_map> ownedColliders; + + void clearOwnedCollider(BodyID bodyId) + { + ownedColliders.erase(bodyId); + } + + template + std::unique_ptr createCollider(Args&&... args) + { + return std::make_unique(std::forward(args)...); + } +}; + +AetherAPI::AetherAPI() + : impl(std::make_unique()) +{ +} + +AetherAPI::~AetherAPI() = default; + +AetherAPI::AetherAPI(AetherAPI&&) noexcept = default; + +AetherAPI& AetherAPI::operator=(AetherAPI&&) noexcept = default; + +void AetherAPI::step(float dt) +{ + impl->world.step(dt); +} + +void AetherAPI::reset() +{ + impl = std::make_unique(); +} + +BodyID AetherAPI::createBox(const BoxSpawnInfo& info) +{ + auto collider = impl->createCollider(info.halfSize); + Rigidbody body(info.position, info.velocity, collider.get(), info.mass, info.friction, info.restitution); + body.force_accum = info.forceAccum; + body.render_alpha = info.renderAlpha; + BodyID id = impl->world.addBody(std::move(body)); + impl->ownedColliders.emplace(id, std::move(collider)); + return id; +} + +BodyID AetherAPI::createSphere(const SphereSpawnInfo& info) +{ + auto collider = impl->createCollider(info.radius); + Rigidbody body(info.position, info.velocity, collider.get(), info.mass, info.friction, info.restitution); + body.force_accum = info.forceAccum; + body.render_alpha = info.renderAlpha; + BodyID id = impl->world.addBody(std::move(body)); + impl->ownedColliders.emplace(id, std::move(collider)); + return id; +} + +BodyID AetherAPI::createRamp(const RampSpawnInfo& info) +{ + auto collider = impl->createCollider(info.slope, info.length, info.halfWidthZ); + Rigidbody body(info.position, info.velocity, collider.get(), info.mass, info.friction, info.restitution); + body.force_accum = info.forceAccum; + body.render_alpha = info.renderAlpha; + BodyID id = impl->world.addBody(std::move(body)); + impl->ownedColliders.emplace(id, std::move(collider)); + return id; +} + +bool AetherAPI::deleteBody(BodyID bodyId) +{ + const PhysicsResult result = impl->world.deleteBody(bodyId); + if (result.success) + { + impl->clearOwnedCollider(bodyId); + return true; + } + return false; +} + +bool AetherAPI::applyForce(BodyID bodyId, const Vec3& force) +{ + Rigidbody* body = impl->world.getBodyByID(bodyId); + if (body == nullptr) + { + return false; + } + body->applyForce(force); + return true; +} + +bool AetherAPI::applyImpulse(BodyID bodyId, const Vec3& impulse) +{ + Rigidbody* body = impl->world.getBodyByID(bodyId); + if (body == nullptr || body->inverse_mass == 0.0f) + { + return false; + } + body->velocity += impulse * body->inverse_mass; + return true; +} + +bool AetherAPI::updateBody(const BodyState& state) +{ + Rigidbody* body = impl->world.getBodyByID(state.id); + if (body == nullptr) + { + return false; + } + applyState(*body, state); + return true; +} + +std::vector AetherAPI::getRenderBodies() const +{ + std::vector renderBodies; + const auto& bodies = impl->world.getBodies(); + renderBodies.reserve(bodies.size()); + + for (const auto& body : bodies) + { + RenderBody renderBody; + renderBody.id = body.id; + renderBody.meshType = toMeshType(body.collider); + renderBody.position = body.position; + renderBody.orientation = body.orientation; + renderBodies.push_back(renderBody); + } + + return renderBodies; +} + +std::vector AetherAPI::getBodies() const +{ + std::vector result; + const auto& bodies = impl->world.getBodies(); + result.reserve(bodies.size()); + for (const auto& body : bodies) + { + result.push_back(makeState(body)); + } + return result; +} + +std::size_t AetherAPI::getBodyCount() const +{ + return impl->world.getBodies().size(); +} + +std::size_t AetherAPI::getContactCount() const +{ + return impl->world.getContactCount(); +} + +std::optional AetherAPI::getBody(BodyID bodyId) const +{ + const Rigidbody* body = impl->world.getBodyByID(bodyId); + if (body == nullptr) + { + return std::nullopt; + } + return makeState(*body); +} + +std::vector AetherAPI::getDistanceConstraints() const +{ + std::vector result; + const auto& constraints = impl->world.getDistanceConstraints(); + result.reserve(constraints.size()); + for (const auto& constraint : constraints) + { + DistanceConstraintState state; + state.firstBodyId = constraint.a_id; + state.secondBodyId = constraint.b_id; + state.type = toConstraintType(constraint.type); + state.restLength = constraint.rest_length; + state.stiffness = constraint.stiffness; + state.damping = constraint.damping; + result.push_back(state); + } + return result; +} + +bool AetherAPI::createDistanceConstraint(BodyID firstBodyId, BodyID secondBodyId, float restLength, ConstraintType type, float stiffness, float damping) +{ + const PhysicsResult result = impl->world.addDistanceConstraints(firstBodyId, secondBodyId, restLength, toDistanceConstraintType(type), stiffness, damping); + return result.success; +} + +bool AetherAPI::deleteConstraint(BodyID firstBodyId, BodyID secondBodyId) +{ + return impl->world.deleteConstraint(firstBodyId, secondBodyId).success; +} + +bool AetherAPI::deleteConstraint(BodyID bodyId) +{ + return impl->world.deleteConstraint(bodyId).success; +} + +BuoyancySettings AetherAPI::getBuoyancySettings() const +{ + BuoyancySettings settings; + settings.enabled = impl->world.enable_buoyancy; + settings.beakerCenter = impl->world.water_fluid.beaker_center; + settings.beakerHalfSize = impl->world.water_fluid.beaker_half_size; + settings.waterHeight = impl->world.water_fluid.height; + settings.fluidDensity = impl->world.water_fluid.density; + settings.dragCoefficient = impl->world.water_fluid.drag_force; + return settings; +} + +void AetherAPI::setBuoyancySettings(const BuoyancySettings& settings) +{ + impl->world.enable_buoyancy = settings.enabled; + impl->world.water_fluid.beaker_center = settings.beakerCenter; + impl->world.water_fluid.beaker_half_size = settings.beakerHalfSize; + impl->world.water_fluid.height = settings.waterHeight; + impl->world.water_fluid.density = settings.fluidDensity; + impl->world.water_fluid.drag_force = settings.dragCoefficient; +} + +ThermalSettings AetherAPI::getThermalSettings() const +{ + ThermalSettings settings; + settings.enabled = impl->world.thermal_settings.enabled; + settings.conductionRate = impl->world.thermal_settings.conduction_rate; + settings.radiationRate = impl->world.thermal_settings.radiation_rate; + settings.ambientTemperature = impl->world.thermal_settings.ambient_temperature; + settings.ambientCoupling = impl->world.thermal_settings.ambient_coupling; + settings.radiationDistance = impl->world.thermal_settings.radiation_distance; + settings.minVisualTemperature = impl->world.thermal_settings.min_visual_temperature; + settings.maxVisualTemperature = impl->world.thermal_settings.max_visual_temperature; + return settings; +} + +void AetherAPI::setThermalSettings(const ThermalSettings& settings) +{ + impl->world.thermal_settings.enabled = settings.enabled; + impl->world.thermal_settings.conduction_rate = settings.conductionRate; + impl->world.thermal_settings.radiation_rate = settings.radiationRate; + impl->world.thermal_settings.ambient_temperature = settings.ambientTemperature; + impl->world.thermal_settings.ambient_coupling = settings.ambientCoupling; + impl->world.thermal_settings.radiation_distance = settings.radiationDistance; + impl->world.thermal_settings.min_visual_temperature = settings.minVisualTemperature; + impl->world.thermal_settings.max_visual_temperature = settings.maxVisualTemperature; +} + +ThermalSpawnSettings AetherAPI::getThermalSpawnSettings() const +{ + ThermalSpawnSettings settings; + settings.enabled = impl->world.thermal_spawn_controls.enabled; + settings.lockToBasicShapes = impl->world.thermal_spawn_controls.lock_to_basic_shapes; + settings.spawnTemperature = impl->world.thermal_spawn_controls.spawn_temperature; + settings.spawnHeatCapacity = impl->world.thermal_spawn_controls.spawn_heat_capacity; + settings.spawnConductivity = impl->world.thermal_spawn_controls.spawn_conductivity; + settings.spawnEmissivity = impl->world.thermal_spawn_controls.spawn_emissivity; + return settings; +} + +void AetherAPI::setThermalSpawnSettings(const ThermalSpawnSettings& settings) +{ + impl->world.thermal_spawn_controls.enabled = settings.enabled; + impl->world.thermal_spawn_controls.lock_to_basic_shapes = settings.lockToBasicShapes; + impl->world.thermal_spawn_controls.spawn_temperature = settings.spawnTemperature; + impl->world.thermal_spawn_controls.spawn_heat_capacity = settings.spawnHeatCapacity; + impl->world.thermal_spawn_controls.spawn_conductivity = settings.spawnConductivity; + impl->world.thermal_spawn_controls.spawn_emissivity = settings.spawnEmissivity; +} + +const Vec3& AetherAPI::getGravity() const +{ + return impl->world.getGravity(); +} + +void AetherAPI::setGravity(const Vec3& gravity) +{ + impl->world.setGravity(gravity); +} \ No newline at end of file diff --git a/engine/api/AetherAPI.hpp b/engine/api/AetherAPI.hpp new file mode 100644 index 0000000..d0c7cb9 --- /dev/null +++ b/engine/api/AetherAPI.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include + +#include "engine_configs.hpp" +#include "math/vec3.hpp" +#include "api/RenderBody.hpp" + +enum class ConstraintType +{ + Rope, + Rod, + Spring +}; + +struct BodyState +{ + BodyID id = 0; + MeshType meshType = MeshType::Unknown; + Vec3 position{}; + Vec3 velocity{}; + Vec3 forceAccum{}; + Quat orientation{}; + Vec3 angularVelocity{}; + float mass = 1.0f; + float inverseMass = 1.0f; + float friction = PHYSICS_DEFAULT_FRICTION; + float restitution = PHYSICS_DEFAULT_RESTITUTION; + float renderAlpha = 1.0f; + bool thermalEnabled = false; + float temperature = 293.15f; + float heatCapacity = 900.0f; + float thermalConductivity = 0.5f; + float thermalEmissivity = 0.85f; + Vec3 boxHalfSize{0.5f, 0.5f, 0.5f}; + float sphereRadius = 0.5f; + float rampSlope = 0.35f; + float rampLength = 8.0f; + float rampHalfWidthZ = 1.5f; +}; + +struct DistanceConstraintState +{ + BodyID firstBodyId = 0; + BodyID secondBodyId = 0; + ConstraintType type = ConstraintType::Rope; + float restLength = 0.0f; + float stiffness = 0.0f; + float damping = 0.0f; +}; + +struct BuoyancySettings +{ + bool enabled = false; + Vec3 beakerCenter{0.0f, 0.0f, 0.0f}; + float beakerHalfSize = 4.0f; + float waterHeight = 0.0f; + float fluidDensity = 2.0f; + float dragCoefficient = 0.3f; +}; + +struct ThermalSettings +{ + bool enabled = false; + float conductionRate = 1.0f; + float radiationRate = 0.02f; + float ambientTemperature = 295.0f; + float ambientCoupling = 0.05f; + float radiationDistance = 3.5f; + float minVisualTemperature = 250.0f; + float maxVisualTemperature = 650.0f; +}; + +struct ThermalSpawnSettings +{ + bool enabled = false; + bool lockToBasicShapes = false; + float spawnTemperature = 320.0f; + float spawnHeatCapacity = 900.0f; + float spawnConductivity = 0.6f; + float spawnEmissivity = 0.85f; +}; + +struct BoxSpawnInfo +{ + Vec3 position{}; + Vec3 velocity{}; + Vec3 forceAccum{}; + Vec3 halfSize{0.5f, 0.5f, 0.5f}; + float mass = 1.0f; + float friction = PHYSICS_DEFAULT_FRICTION; + float restitution = PHYSICS_DEFAULT_RESTITUTION; + float renderAlpha = 1.0f; +}; + +struct SphereSpawnInfo +{ + Vec3 position{}; + Vec3 velocity{}; + Vec3 forceAccum{}; + float radius = 0.5f; + float mass = 1.0f; + float friction = PHYSICS_DEFAULT_FRICTION; + float restitution = PHYSICS_DEFAULT_RESTITUTION; + float renderAlpha = 1.0f; +}; + +struct RampSpawnInfo +{ + Vec3 position{}; + Vec3 velocity{}; + Vec3 forceAccum{}; + float slope = 0.35f; + float length = 8.0f; + float halfWidthZ = 1.5f; + float mass = 1.0f; + float friction = PHYSICS_DEFAULT_FRICTION; + float restitution = PHYSICS_DEFAULT_RESTITUTION; + float renderAlpha = 1.0f; +}; + +class AetherAPI +{ +public: + AetherAPI(); + ~AetherAPI(); + + AetherAPI(const AetherAPI&) = delete; + AetherAPI& operator=(const AetherAPI&) = delete; + AetherAPI(AetherAPI&&) noexcept; + AetherAPI& operator=(AetherAPI&&) noexcept; + + void step(float dt); + void reset(); + + BodyID createBox(const BoxSpawnInfo& info); + BodyID createSphere(const SphereSpawnInfo& info); + BodyID createRamp(const RampSpawnInfo& info); + bool deleteBody(BodyID bodyId); + bool applyForce(BodyID bodyId, const Vec3& force); + bool applyImpulse(BodyID bodyId, const Vec3& impulse); + bool updateBody(const BodyState& state); + + std::vector getRenderBodies() const; + std::vector getBodies() const; + std::size_t getBodyCount() const; + std::size_t getContactCount() const; + std::optional getBody(BodyID bodyId) const; + std::vector getDistanceConstraints() const; + bool createDistanceConstraint(BodyID firstBodyId, BodyID secondBodyId, float restLength, ConstraintType type, float stiffness, float damping); + bool deleteConstraint(BodyID firstBodyId, BodyID secondBodyId); + bool deleteConstraint(BodyID bodyId); + + BuoyancySettings getBuoyancySettings() const; + void setBuoyancySettings(const BuoyancySettings& settings); + ThermalSettings getThermalSettings() const; + void setThermalSettings(const ThermalSettings& settings); + ThermalSpawnSettings getThermalSpawnSettings() const; + void setThermalSpawnSettings(const ThermalSpawnSettings& settings); + + const Vec3& getGravity() const; + void setGravity(const Vec3& gravity); + +private: + struct Impl; + std::unique_ptr impl; +}; \ No newline at end of file diff --git a/engine/api/RenderBody.hpp b/engine/api/RenderBody.hpp new file mode 100644 index 0000000..307937d --- /dev/null +++ b/engine/api/RenderBody.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "math/quat.hpp" +#include "math/vec3.hpp" + +using BodyID = std::uint32_t; + +enum class MeshType +{ + Box, + Sphere, + Ramp, + Unknown +}; + +struct RenderBody +{ + std::uint32_t id = 0; + MeshType meshType = MeshType::Unknown; + Vec3 position{}; + Quat orientation{}; +}; \ No newline at end of file diff --git a/renderer/bodymenu.cpp b/renderer/bodymenu.cpp index 34f0a0e..96bc5bc 100644 --- a/renderer/bodymenu.cpp +++ b/renderer/bodymenu.cpp @@ -1,203 +1,248 @@ #include "bodymenu.hpp" -#include #include #include #include -#include +#include #include #include -#include "../engine/core/box_collider.hpp" #include "bodyselection.hpp" -#include "../engine/core/ramp_collider.hpp" -#include "../engine/core/rigidbody.hpp" -#include "../engine/core/sphere_collider.hpp" -#include "../engine/math/vec3.hpp" #include "thermal_palette.hpp" -static int shapeIndex = 0; -static int linkBodyAIndex = 0; -static int linkBodyBIndex = 1; -static int linkKindIndex = 0; -static float linkRestLength = 2.0f; -static float linkStiffness = 5.0f; -static float linkDamping = 2.0f; - -static float spawnPos[3] = {0.0f, 3.0f, 0.0f}; -static float spawnSpeed[3] = {0.0f, 0.0f, 0.0f}; -static float spawnForce[3] = {0.0f, 0.0f, 0.0f}; -static float spawnMass = 1.0f; -static float spawnDensity = 1.0f; -static float spawnVolume = 0.5235988f; - -static float sphereRadius = 0.5f; -static float boxHalfSize[3] = {0.5f, 0.5f, 0.5f}; -static float rampSlope = 0.35f; -static float rampLength = 8.0f; -static float rampHalfWidthZ = 1.5f; - -static std::vector> ownedColliders; - -static constexpr float kPi = 3.14159265358979323846f; - -static float sphereVolumeFromRadius(float radius) -{ - radius = std::max(0.0f, radius); - return (4.0f / 3.0f) * kPi * radius * radius * radius; -} - -static float sphereRadiusFromVolume(float volume) +namespace { - volume = std::max(0.0f, volume); - if (volume <= 0.0f) - return 0.0f; - return std::cbrt((3.0f * volume) / (4.0f * kPi)); -} - -static void setBodyMassAndInertia(Rigidbody &body, float mass) -{ - const float MIN_MASS = PHYSICS_EPSILON; - float effective_mass = mass; + static int shapeIndex = 0; + static int linkBodyAIndex = 0; + static int linkBodyBIndex = 1; + static int linkKindIndex = 0; + static float linkRestLength = 2.0f; + static float linkStiffness = 5.0f; + static float linkDamping = 2.0f; + + static float spawnPos[3] = {0.0f, 3.0f, 0.0f}; + static float spawnSpeed[3] = {0.0f, 0.0f, 0.0f}; + static float spawnForce[3] = {0.0f, 0.0f, 0.0f}; + static float spawnMass = 1.0f; + static float spawnDensity = 1.0f; + static float spawnVolume = 0.5235988f; + + static float sphereRadius = 0.5f; + static float boxHalfSize[3] = {0.5f, 0.5f, 0.5f}; + static float rampSlope = 0.35f; + static float rampLength = 8.0f; + static float rampHalfWidthZ = 1.5f; + + static constexpr float kPi = 3.14159265358979323846f; + + static float sphereVolumeFromRadius(float radius) + { + radius = std::max(0.0f, radius); + return (4.0f / 3.0f) * kPi * radius * radius * radius; + } - if (mass <= 0.0f) + static float sphereRadiusFromVolume(float volume) { - effective_mass = 0.0f; + volume = std::max(0.0f, volume); + if (volume <= 0.0f) + return 0.0f; + return std::cbrt((3.0f * volume) / (4.0f * kPi)); } - else if (mass < MIN_MASS) + + static void show_tooltip(const char *text) { - effective_mass = MIN_MASS; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) + ImGui::SetTooltip("%s", text); } - body.inverse_mass = (effective_mass > 0.0f) ? (1.0f / effective_mass) : 0.0f; - const float actual_mass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; + static std::string g_toast_text; + static double g_toast_until = 0.0; - if (!body.collider || actual_mass <= 0.0f) + static void RequestEngineToast(const std::string &text) { - body.inverse_inertia_body = Mat3::identity() * 0.0f; - body.updateworldinvinertia(); - return; + if (text.empty()) + return; + g_toast_text = text; + g_toast_until = ImGui::GetTime() + 2.0; } - if (body.collider->type == ShapeType::Sphere) + static void RenderThermalLegend(const ThermalSettings &settings) { - const auto *sphere = static_cast(body.collider); - const float I = 0.4f * actual_mass * sphere->radius * sphere->radius; - const float invI = (I > PHYSICS_EPSILON) ? (1.0f / I) : 0.0f; - body.inverse_inertia_body = Mat3::diag(invI, invI, invI); + if (!settings.enabled) + return; + + ImGui::SeparatorText("Heat Scale"); + float legendWidth = ImGui::GetContentRegionAvail().x; + if (legendWidth <= 0.0f) + legendWidth = 1.0f; + const float barHeight = 18.0f; + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImDrawList *drawList = ImGui::GetWindowDrawList(); + const int segments = 64; + for (int i = 0; i < segments; ++i) + { + float t0 = static_cast(i) / static_cast(segments); + float t1 = static_cast(i + 1) / static_cast(segments); + glm::vec3 c0 = SampleThermalGradient(t0); + glm::vec3 c1 = SampleThermalGradient(t1); + ImU32 col0 = ImColor(c0.r, c0.g, c0.b, 1.0f); + ImU32 col1 = ImColor(c1.r, c1.g, c1.b, 1.0f); + float x0 = pos.x + t0 * legendWidth; + float x1 = pos.x + t1 * legendWidth; + drawList->AddRectFilledMultiColor(ImVec2(x0, pos.y), ImVec2(x1, pos.y + barHeight), col0, col1, col1, col0); + } + drawList->AddRect(ImVec2(pos.x, pos.y), ImVec2(pos.x + legendWidth, pos.y + barHeight), ImGui::GetColorU32(ImGuiCol_Border)); + ImGui::Dummy(ImVec2(legendWidth, barHeight + 6.0f)); + + auto formatLabel = [](const char *prefix, float value) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%s %.0fK", prefix, value); + return std::string(buffer); + }; + const std::string coldLabel = formatLabel("Cold", settings.minVisualTemperature); + const std::string hotLabel = formatLabel("Hot", settings.maxVisualTemperature); + const float startX = ImGui::GetCursorPosX(); + ImGui::TextUnformatted(coldLabel.c_str()); + ImGui::SameLine(); + float hotWidth = ImGui::CalcTextSize(hotLabel.c_str()).x; + ImGui::SetCursorPosX(startX + legendWidth - hotWidth); + ImGui::TextUnformatted(hotLabel.c_str()); + ImGui::Spacing(); } - else if (body.collider->type == ShapeType::Box) - { - const auto *box = static_cast(body.collider); - const Vec3 e = box->halfsize * 2.0f; - const float Ix = (actual_mass / 12.0f) * (e.y * e.y + e.z * e.z); - const float Iy = (actual_mass / 12.0f) * (e.x * e.x + e.z * e.z); - const float Iz = (actual_mass / 12.0f) * (e.x * e.x + e.y * e.y); + static float approxRadius(const BodyState &body) + { + switch (body.meshType) + { + case MeshType::Sphere: + return body.sphereRadius; + case MeshType::Box: + return std::sqrt(body.boxHalfSize.x * body.boxHalfSize.x + body.boxHalfSize.y * body.boxHalfSize.y + body.boxHalfSize.z * body.boxHalfSize.z); + case MeshType::Ramp: + { + const float height = body.rampSlope * body.rampLength; + const float halfLen = body.rampLength * 0.5f; + const float halfH = height * 0.5f; + return std::sqrt(halfLen * halfLen + halfH * halfH + body.rampHalfWidthZ * body.rampHalfWidthZ); + } + default: + return 0.0f; + } + } - body.inverse_inertia_body = Mat3::diag( - Ix > PHYSICS_EPSILON ? (1.0f / Ix) : 0.0f, - Iy > PHYSICS_EPSILON ? (1.0f / Iy) : 0.0f, - Iz > PHYSICS_EPSILON ? (1.0f / Iz) : 0.0f); + static float approxNewShapeRadius() + { + if (shapeIndex == 0) + return sphereRadius; + if (shapeIndex == 1) + return std::sqrt(boxHalfSize[0] * boxHalfSize[0] + boxHalfSize[1] * boxHalfSize[1] + boxHalfSize[2] * boxHalfSize[2]); + const float height = rampSlope * rampLength; + const float halfLen = rampLength * 0.5f; + const float halfH = height * 0.5f; + return std::sqrt(halfLen * halfLen + halfH * halfH + rampHalfWidthZ * rampHalfWidthZ); } - else + + static bool isBuoyancyHelperWall(const BuoyancySettings &settings, const BodyState &body) { - body.inverse_inertia_body = Mat3::identity() * 0.0f; + if (body.meshType != MeshType::Box) + return false; + if (body.inverseMass != 0.0f) + return false; + + const float halfSize = settings.beakerHalfSize; + constexpr float wallThickness = 0.2f; + constexpr float wallHalf = wallThickness / 2.0f; + const float tol = 0.02f; + const Vec3 hs = body.boxHalfSize; + + const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); + const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); + + const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); + const float bottomHalfY = wallHalf; + const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); + const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && + (std::abs(hs.y - bottomHalfY) < tol) && + (std::abs(hs.z - bottomHalfZ) < tol); + + return matchesXWall || matchesZWall || matchesBottom; } - body.updateworldinvinertia(); -} + static void spawnBody(AetherAPI &api) + { + BodyState body; + const BuoyancySettings buoyancy = api.getBuoyancySettings(); + const ThermalSpawnSettings thermalSpawn = api.getThermalSpawnSettings(); -static bool isBuoyancyHelperWall(const PhysicsWorld &world, const Rigidbody &body) -{ - if (!world.enable_buoyancy) - return false; - if (body.inverse_mass != 0.0f) - return false; - if (!body.collider || body.collider->type != ShapeType::Box) - return false; - - const Fluid &fluid = world.water_fluid; - const float halfSize = fluid.beaker_half_size; - constexpr float wallThickness = 0.2f; - constexpr float wallHalf = wallThickness / 2.0f; - const float tol = 0.02f; - - const auto *box = static_cast(body.collider); - const Vec3 hs = box->halfsize; - - const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); - const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); - - const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); - const float bottomHalfY = wallHalf; - const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); - const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && - (std::abs(hs.y - bottomHalfY) < tol) && - (std::abs(hs.z - bottomHalfZ) < tol); - - return matchesXWall || matchesZWall || matchesBottom; -} + int shapeChoice = shapeIndex; + if (buoyancy.enabled) + { + if (shapeChoice < 0 || shapeChoice > 1) + shapeChoice = 0; + shapeIndex = shapeChoice; + spawnMass = spawnDensity * spawnVolume; + spawnSpeed[0] = spawnSpeed[1] = spawnSpeed[2] = 0.0f; + spawnForce[0] = spawnForce[1] = spawnForce[2] = 0.0f; + if (shapeChoice == 0) + sphereRadius = sphereRadiusFromVolume(spawnVolume); + else + { + const float h = std::cbrt(std::max(0.0f, spawnVolume) / 8.0f); + boxHalfSize[0] = boxHalfSize[1] = boxHalfSize[2] = h; + } + } -static void show_tooltip(const char *text) -{ - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) - ImGui::SetTooltip("%s", text); -} + if (thermalSpawn.lockToBasicShapes && shapeChoice > 1) + { + shapeChoice = std::min(shapeChoice, 1); + shapeIndex = shapeChoice; + } -static std::string g_toast_text; -static double g_toast_until = 0.0; + body.position = Vec3(spawnPos[0], spawnPos[1], spawnPos[2]); + body.velocity = Vec3(spawnSpeed[0], spawnSpeed[1], spawnSpeed[2]); + body.forceAccum = Vec3(spawnForce[0], spawnForce[1], spawnForce[2]); + body.mass = spawnMass; + body.friction = PHYSICS_DEFAULT_FRICTION; + body.restitution = PHYSICS_DEFAULT_RESTITUTION; -static void RequestEngineToast(const std::string &text) -{ - if (text.empty()) - return; - g_toast_text = text; - g_toast_until = ImGui::GetTime() + 2.0; -} + BodyID createdId = 0; + if (shapeChoice == 0) + { + body.meshType = MeshType::Sphere; + body.sphereRadius = sphereRadius; + createdId = api.createSphere(SphereSpawnInfo{body.position, body.velocity, body.forceAccum, body.sphereRadius, body.mass, body.friction, body.restitution, body.renderAlpha}); + } + else if (shapeChoice == 1) + { + body.meshType = MeshType::Box; + body.boxHalfSize = Vec3(boxHalfSize[0], boxHalfSize[1], boxHalfSize[2]); + createdId = api.createBox(BoxSpawnInfo{body.position, body.velocity, body.forceAccum, body.boxHalfSize, body.mass, body.friction, body.restitution, body.renderAlpha}); + } + else + { + body.meshType = MeshType::Ramp; + body.rampSlope = rampSlope; + body.rampLength = rampLength; + body.rampHalfWidthZ = rampHalfWidthZ; + createdId = api.createRamp(RampSpawnInfo{body.position, body.velocity, body.forceAccum, body.rampSlope, body.rampLength, body.rampHalfWidthZ, body.mass, body.friction, body.restitution, body.renderAlpha}); + } -static void RenderThermalLegend(const PhysicsWorld &world) -{ - if (!world.thermal_settings.enabled) - return; + if (thermalSpawn.enabled) + { + if (auto existing = api.getBody(createdId)) + { + BodyState edited = *existing; + edited.thermalEnabled = true; + edited.temperature = thermalSpawn.spawnTemperature; + edited.heatCapacity = thermalSpawn.spawnHeatCapacity; + edited.thermalConductivity = thermalSpawn.spawnConductivity; + edited.thermalEmissivity = thermalSpawn.spawnEmissivity; + api.updateBody(edited); + } + } - ImGui::SeparatorText("Heat Scale"); - float legendWidth = ImGui::GetContentRegionAvail().x; - if (legendWidth <= 0.0f) - legendWidth = 1.0f; - const float barHeight = 18.0f; - ImVec2 pos = ImGui::GetCursorScreenPos(); - ImDrawList *drawList = ImGui::GetWindowDrawList(); - const int segments = 64; - for (int i = 0; i < segments; ++i) - { - float t0 = static_cast(i) / static_cast(segments); - float t1 = static_cast(i + 1) / static_cast(segments); - glm::vec3 c0 = SampleThermalGradient(t0); - glm::vec3 c1 = SampleThermalGradient(t1); - ImU32 col0 = ImColor(c0.r, c0.g, c0.b, 1.0f); - ImU32 col1 = ImColor(c1.r, c1.g, c1.b, 1.0f); - float x0 = pos.x + t0 * legendWidth; - float x1 = pos.x + t1 * legendWidth; - drawList->AddRectFilledMultiColor(ImVec2(x0, pos.y), ImVec2(x1, pos.y + barHeight), col0, col1, col1, col0); + SetSelectedBodyId(createdId); } - drawList->AddRect(ImVec2(pos.x, pos.y), ImVec2(pos.x + legendWidth, pos.y + barHeight), ImGui::GetColorU32(ImGuiCol_Border)); - ImGui::Dummy(ImVec2(legendWidth, barHeight + 6.0f)); - - auto formatLabel = [](const char *prefix, float value) { - char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "%s %.0fK", prefix, value); - return std::string(buffer); - }; - const std::string coldLabel = formatLabel("Cold", world.thermal_settings.min_visual_temperature); - const std::string hotLabel = formatLabel("Hot", world.thermal_settings.max_visual_temperature); - const float startX = ImGui::GetCursorPosX(); - ImGui::TextUnformatted(coldLabel.c_str()); - ImGui::SameLine(); - float hotWidth = ImGui::CalcTextSize(hotLabel.c_str()).x; - ImGui::SetCursorPosX(startX + legendWidth - hotWidth); - ImGui::TextUnformatted(hotLabel.c_str()); - ImGui::Spacing(); } void RenderEnginePopups() @@ -217,117 +262,21 @@ void RenderEnginePopups() ImGui::End(); } -static float approx_radius_from_collider(const Collider *c) -{ - if (!c) - return 0.0f; - if (c->type == ShapeType::Sphere) - return static_cast(c)->radius; - if (c->type == ShapeType::Box) - { - const Vec3 &hs = static_cast(c)->halfsize; - return std::sqrt(hs.x * hs.x + hs.y * hs.y + hs.z * hs.z); - } - const RampCollider *rc = static_cast(c); - const float height = rc->getHeight(); - const float halfLen = rc->length * 0.5f; - const float halfH = height * 0.5f; - return std::sqrt(halfLen * halfLen + halfH * halfH + rc->half_width_z * rc->half_width_z); -} - -static float approx_radius_for_new_shape() -{ - if (shapeIndex == 0) - return sphereRadius; - if (shapeIndex == 1) - return std::sqrt(boxHalfSize[0] * boxHalfSize[0] + boxHalfSize[1] * boxHalfSize[1] + boxHalfSize[2] * boxHalfSize[2]); - const float height = rampSlope * rampLength; - const float halfLen = rampLength * 0.5f; - const float halfH = height * 0.5f; - return std::sqrt(halfLen * halfLen + halfH * halfH + rampHalfWidthZ * rampHalfWidthZ); -} - -static void spawn_body(PhysicsWorld &world) +void RenderAddBodyMenuContent(AetherAPI &api) { - Collider *collider_ptr = nullptr; - - int shapeChoice = shapeIndex; - if (world.enable_buoyancy) - { - if (shapeChoice < 0 || shapeChoice > 1) - shapeChoice = 0; - shapeIndex = shapeChoice; - - spawnMass = spawnDensity * spawnVolume; - spawnSpeed[0] = spawnSpeed[1] = spawnSpeed[2] = 0.0f; - spawnForce[0] = spawnForce[1] = spawnForce[2] = 0.0f; - - if (shapeChoice == 0) - { - sphereRadius = sphereRadiusFromVolume(spawnVolume); - } - else if (shapeChoice == 1) - { - const float h = std::cbrt(std::max(0.0f, spawnVolume) / 8.0f); - boxHalfSize[0] = boxHalfSize[1] = boxHalfSize[2] = h; - } - } - - if (world.thermal_spawn_controls.lock_to_basic_shapes && shapeChoice > 1) - { - shapeChoice = std::min(shapeChoice, 1); - shapeIndex = shapeChoice; - } - - if (shapeChoice == 0) // spawn sphere - { - auto c = std::make_unique(sphereRadius); - collider_ptr = c.get(); - ownedColliders.push_back(std::move(c)); - } - else if (shapeChoice == 1) // spawn box - { - auto c = std::make_unique(Vec3(boxHalfSize[0], boxHalfSize[1], boxHalfSize[2])); - collider_ptr = c.get(); - ownedColliders.push_back(std::move(c)); - } - else // spawn ramp - { - auto c = std::make_unique(rampSlope, rampLength, rampHalfWidthZ); - collider_ptr = c.get(); - ownedColliders.push_back(std::move(c)); - } - - // finally create the rigid body - Rigidbody b( - Vec3(spawnPos[0], spawnPos[1], spawnPos[2]), - Vec3(spawnSpeed[0], spawnSpeed[1], spawnSpeed[2]), - collider_ptr, - spawnMass); - b.force_accum = Vec3(spawnForce[0], spawnForce[1], spawnForce[2]); - if (world.thermal_spawn_controls.enabled) - { - b.thermal_enabled = true; - b.temperature = world.thermal_spawn_controls.spawn_temperature; - b.heat_capacity = world.thermal_spawn_controls.spawn_heat_capacity; - b.thermal_conductivity = world.thermal_spawn_controls.spawn_conductivity; - b.thermal_emissivity = world.thermal_spawn_controls.spawn_emissivity; - } - SetSelectedBodyId(world.addBody(std::move(b))); -} + const BuoyancySettings buoyancy = api.getBuoyancySettings(); + const ThermalSpawnSettings thermalSpawn = api.getThermalSpawnSettings(); -void RenderAddBodyMenuContent(PhysicsWorld &world) -{ ImGui::SeparatorText("Spawn Body"); - if (world.enable_buoyancy) + if (buoyancy.enabled) { if (shapeIndex < 0 || shapeIndex > 1) shapeIndex = 0; const char *buoyShapes[] = {"Sphere", "Box"}; ImGui::Combo("Add Shape", &shapeIndex, buoyShapes, 2); - const Vec3 fluidCenter = world.water_fluid.beaker_center; - const float halfSize = 4.0f; + const Vec3 fluidCenter = buoyancy.beakerCenter; + const float halfSize = buoyancy.beakerHalfSize; const float minX = fluidCenter.x - halfSize; const float maxX = fluidCenter.x + halfSize; const float minZ = fluidCenter.z - halfSize; @@ -356,9 +305,7 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) last_add_time = now; Vec3 pos(spawnPos[0], spawnPos[1], spawnPos[2]); if (std::abs(pos.x - fluidCenter.x) > halfSize || std::abs(pos.z - fluidCenter.z) > halfSize) - { RequestEngineToast("Body spawn must be inside beaker X/Z."); - } else { if (shapeIndex == 0) @@ -368,16 +315,14 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) const float h = std::cbrt(std::max(0.0f, spawnVolume) / 8.0f); boxHalfSize[0] = boxHalfSize[1] = boxHalfSize[2] = h; } - const float newRad = approx_radius_for_new_shape(); - const auto &bodies = world.getBodies(); + const float newRad = approxNewShapeRadius(); + const auto bodies = api.getBodies(); int overlap = 0; - for (auto &b : bodies) + for (const auto &b : bodies) { - if (!b.collider) - continue; - if (b.inverse_mass == 0.0f) + if (buoyancy.enabled && b.inverseMass == 0.0f) continue; - const float r = approx_radius_from_collider(b.collider); + const float r = approxRadius(b); const float dx = b.position.x - pos.x; const float dy = b.position.y - pos.y; const float dz = b.position.z - pos.z; @@ -393,7 +338,7 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) RequestEngineToast("area too clustered to add a body"); else { - spawn_body(world); + spawnBody(api); RequestEngineToast("Body was added successfully"); } } @@ -403,62 +348,39 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) return; } - const bool restrictShapes = world.thermal_spawn_controls.lock_to_basic_shapes; + const bool restrictShapes = thermalSpawn.lockToBasicShapes; const char *shapeNamesFull[] = {"Sphere", "Box", "Ramp"}; const char *shapeNamesLimited[] = {"Sphere", "Box"}; if (restrictShapes && shapeIndex > 1) - { shapeIndex = 0; - } const char **shapeNames = restrictShapes ? shapeNamesLimited : shapeNamesFull; - int shapeCount = restrictShapes ? 2 : 3; + const int shapeCount = restrictShapes ? 2 : 3; ImGui::Combo("Add Shape", &shapeIndex, shapeNames, shapeCount); show_tooltip("Choose which collider type to create for the next body."); - if (restrictShapes) - { - ImGui::TextColored(ImVec4(0.95f, 0.65f, 0.25f, 1.0f), "Thermal scenario: only spheres and boxes are available."); - } ImGui::DragFloat3("Position", spawnPos, 0.1f); - show_tooltip("Initial world-space position for the new body."); ImGui::DragFloat3("Speed", spawnSpeed, 0.1f); - show_tooltip("Initial linear velocity applied on spawn."); ImGui::DragFloat3("Force", spawnForce, 0.1f); - show_tooltip("Initial accumulated force. Useful for immediate pushes."); ImGui::DragFloat("Mass", &spawnMass, 0.1f, 0.0f, 100000.0f); - show_tooltip("Higher mass resists acceleration. 0.0 means static body."); if (shapeIndex == 0) - { ImGui::DragFloat("Sphere Radius", &sphereRadius, 0.01f, 0.0f, 100000.0f); - show_tooltip("Radius of the spawned sphere collider."); - } else if (shapeIndex == 1) - { ImGui::DragFloat3("Box Halfsize", boxHalfSize, 0.01f); - show_tooltip("Half extents of the box along X, Y, and Z."); - } else { ImGui::DragFloat("Ramp Slope", &rampSlope, 0.01f); - show_tooltip("Vertical rise per unit horizontal run for the ramp."); ImGui::DragFloat("Ramp Length", &rampLength, 0.1f); - show_tooltip("Ramp size along the forward axis."); ImGui::DragFloat("Ramp HalfWidthZ", &rampHalfWidthZ, 0.1f); - show_tooltip("Half-width of the ramp across the Z axis."); } - if (world.thermal_spawn_controls.enabled) + if (thermalSpawn.enabled) { ImGui::SeparatorText("Thermal Properties"); - ImGui::DragFloat("Spawn Temperature (K)", &world.thermal_spawn_controls.spawn_temperature, 1.0f, 100.0f, 1000.0f); - show_tooltip("Temperature assigned to newly spawned bodies inside the heat transfer lab."); - ImGui::DragFloat("Spawn Heat Capacity", &world.thermal_spawn_controls.spawn_heat_capacity, 5.0f, 10.0f, 5000.0f); - show_tooltip("Higher heat capacity slows down temperature changes."); - ImGui::DragFloat("Spawn Conductivity", &world.thermal_spawn_controls.spawn_conductivity, 0.01f, 0.0f, 5.0f); - show_tooltip("Controls how quickly bodies exchange heat via contacts."); - ImGui::DragFloat("Spawn Emissivity", &world.thermal_spawn_controls.spawn_emissivity, 0.01f, 0.0f, 1.5f); - show_tooltip("Higher emissivity radiates heat faster to nearby bodies."); + ImGui::DragFloat("Spawn Temperature (K)", const_cast(&thermalSpawn.spawnTemperature), 1.0f, 100.0f, 1000.0f); + ImGui::DragFloat("Spawn Heat Capacity", const_cast(&thermalSpawn.spawnHeatCapacity), 5.0f, 10.0f, 5000.0f); + ImGui::DragFloat("Spawn Conductivity", const_cast(&thermalSpawn.spawnConductivity), 0.01f, 0.0f, 5.0f); + ImGui::DragFloat("Spawn Emissivity", const_cast(&thermalSpawn.spawnEmissivity), 0.01f, 0.0f, 1.5f); } if (ImGui::Button("Add Body")) @@ -471,16 +393,14 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) { last_add_time = now; Vec3 pos(spawnPos[0], spawnPos[1], spawnPos[2]); - const float newRad = approx_radius_for_new_shape(); - const auto &bodies = world.getBodies(); + const float newRad = approxNewShapeRadius(); + const auto bodies = api.getBodies(); int overlap = 0; - for (auto &b : bodies) + for (const auto &b : bodies) { - if (!b.collider) + if (buoyancy.enabled && b.inverseMass == 0.0f) continue; - if (world.enable_buoyancy && b.inverse_mass == 0.0f) - continue; - const float r = approx_radius_from_collider(b.collider); + const float r = approxRadius(b); const float dx = b.position.x - pos.x; const float dy = b.position.y - pos.y; const float dz = b.position.z - pos.z; @@ -496,7 +416,7 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) RequestEngineToast("area too clustered to add a body"); else { - spawn_body(world); + spawnBody(api); RequestEngineToast("Body was added successfully"); } } @@ -504,101 +424,84 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) show_tooltip("Creates one rigid body with the current spawn parameters."); } -void RenderConstraintMenuContent(PhysicsWorld &world) +void RenderConstraintMenuContent(AetherAPI &api) { ImGui::SeparatorText("Constraints"); + const auto bodies = api.getBodies(); + const int n = static_cast(bodies.size()); + if (n < 2) { - auto &bodies = world.getBodies(); - const int n = static_cast(bodies.size()); - if (n < 2) - { - linkBodyAIndex = 0; - linkBodyBIndex = 0; - } - else - { - if (linkBodyAIndex >= n) - linkBodyAIndex = n - 1; - if (linkBodyBIndex >= n) - linkBodyBIndex = n - 1; - if (linkBodyAIndex == linkBodyBIndex) - linkBodyBIndex = (linkBodyAIndex + 1) % n; - } + linkBodyAIndex = 0; + linkBodyBIndex = 0; + } + else + { + if (linkBodyAIndex >= n) + linkBodyAIndex = n - 1; + if (linkBodyBIndex >= n) + linkBodyBIndex = n - 1; + if (linkBodyAIndex == linkBodyBIndex) + linkBodyBIndex = (linkBodyAIndex + 1) % n; + } - ImGui::Text("Link two bodies"); - std::vector linkLabels; - linkLabels.reserve(bodies.size()); - for (auto &b : bodies) - { - const char *t = "?"; - if (b.collider) - { - if (b.collider->type == ShapeType::Sphere) - t = "Sphere"; - else if (b.collider->type == ShapeType::Box) - t = "Box"; - else if (b.collider->type == ShapeType::Ramp) - t = "Ramp"; - } - linkLabels.push_back("ID " + std::to_string(b.id) + " (" + t + ")"); - } - std::vector linkItems; - linkItems.reserve(linkLabels.size()); - for (auto &s : linkLabels) - linkItems.push_back(s.c_str()); + ImGui::Text("Link two bodies"); + std::vector linkLabels; + linkLabels.reserve(bodies.size()); + for (const auto &b : bodies) + { + const char *t = "?"; + if (b.meshType == MeshType::Sphere) + t = "Sphere"; + else if (b.meshType == MeshType::Box) + t = "Box"; + else if (b.meshType == MeshType::Ramp) + t = "Ramp"; + linkLabels.push_back("ID " + std::to_string(b.id) + " (" + t + ")"); + } + std::vector linkItems; + linkItems.reserve(linkLabels.size()); + for (auto &s : linkLabels) + linkItems.push_back(s.c_str()); - if (n >= 2) + if (n >= 2) + { + ImGui::Combo("Body A", &linkBodyAIndex, linkItems.data(), n); + ImGui::Combo("Body B", &linkBodyBIndex, linkItems.data(), n); + const char *linkNames[] = {"Rope", "Rod", "Spring"}; + ImGui::Combo("Link type", &linkKindIndex, linkNames, 3); + ImGui::DragFloat("Rest length", &linkRestLength, 0.05f, 0.01f, 1000.0f); + ImGui::DragFloat("Stiffness", &linkStiffness, 0.05f, 0.0f, 1000.0f); + ImGui::DragFloat("Damping", &linkDamping, 0.05f, 0.0f, 1000.0f); + if (ImGui::Button("Add rope / rod / spring")) { - ImGui::Combo("Body A", &linkBodyAIndex, linkItems.data(), n); - show_tooltip("First body in the distance constraint pair."); - ImGui::Combo("Body B", &linkBodyBIndex, linkItems.data(), n); - show_tooltip("Second body in the distance constraint pair."); - const char *linkNames[] = {"Rope", "Rod", "Spring"}; - ImGui::Combo("Link type", &linkKindIndex, linkNames, 3); - show_tooltip("Rope = max length, Rod = fixed length, Spring = elastic."); - ImGui::DragFloat("Rest length", &linkRestLength, 0.05f, 0.01f, 1000.0f); - show_tooltip("Target distance used by rope, rod, and spring links."); - ImGui::DragFloat("Stiffness", &linkStiffness, 0.05f, 0.0f, 1000.0f); - show_tooltip("How strongly a spring pulls bodies toward its rest length."); - ImGui::DragFloat("Damping", &linkDamping, 0.05f, 0.0f, 1000.0f); - show_tooltip("Reduces oscillation and jitter in spring-like motion."); - if (ImGui::Button("Add rope / rod / spring")) + if (linkBodyAIndex != linkBodyBIndex) { - if (linkBodyAIndex != linkBodyBIndex) - { - std::uint32_t idA = bodies[static_cast(linkBodyAIndex)].id; - std::uint32_t idB = bodies[static_cast(linkBodyBIndex)].id; - DistanceConstraint::TYPE t = DistanceConstraint::ROPE; - if (linkKindIndex == 1) - t = DistanceConstraint::ROD; - else if (linkKindIndex == 2) - t = DistanceConstraint::SPRING; - PhysicsResult res = world.addDistanceConstraints(idA, idB, linkRestLength, t, linkStiffness, linkDamping); - RequestEngineToast(res.message); - } + BodyID idA = bodies[static_cast(linkBodyAIndex)].id; + BodyID idB = bodies[static_cast(linkBodyBIndex)].id; + ConstraintType type = ConstraintType::Rope; + if (linkKindIndex == 1) + type = ConstraintType::Rod; + else if (linkKindIndex == 2) + type = ConstraintType::Spring; + RequestEngineToast(api.createDistanceConstraint(idA, idB, linkRestLength, type, linkStiffness, linkDamping) ? "Constraint added successfully" : "Failed to add constraint"); } } - else - ImGui::TextDisabled("Need at least two bodies to add a link."); } + else + ImGui::TextDisabled("Need at least two bodies to add a link."); } -void RenderWorldMenuContent(PhysicsWorld &world) +void RenderWorldMenuContent(AetherAPI &api) { ImGui::SeparatorText("World"); - - // gravity section - { - Vec3 gravity = world.getGravity(); - float gravityY = gravity.y; - if (ImGui::DragFloat("Gravity Y", &gravityY, 0.1f, -100000.0f, 100000.0f)) - world.setGravity(Vec3(gravity.x, gravityY, gravity.z)); - show_tooltip("Negative values pull downward, positive values push upward."); - } + Vec3 gravity = api.getGravity(); + float gravityY = gravity.y; + if (ImGui::DragFloat("Gravity Y", &gravityY, 0.1f, -100000.0f, 100000.0f)) + api.setGravity(Vec3(gravity.x, gravityY, gravity.z)); } -void RenderBodyInspectorContent(PhysicsWorld &world, bool showCloseButton) +void RenderBodyInspectorContent(AetherAPI &api, bool showCloseButton) { if (showCloseButton && ImGui::Button("Back")) { @@ -610,129 +513,117 @@ void RenderBodyInspectorContent(PhysicsWorld &world, bool showCloseButton) ImGui::Separator(); ImGui::SeparatorText("Bodies"); - if (world.thermal_settings.enabled) - { - RenderThermalLegend(world); - } + RenderThermalLegend(api.getThermalSettings()); - // show active bodies in the scene float listHeight = ImGui::GetContentRegionAvail().y; if (showCloseButton) listHeight = (listHeight > 44.0f) ? (listHeight - 44.0f) : listHeight; ImGui::BeginChild("BodyList", ImVec2(0, listHeight), true); - auto &bodies = world.getBodies(); - for (auto &body : bodies) + const auto bodies = api.getBodies(); + for (const auto &body : bodies) { ImGui::PushID(body.id); - - if (isBuoyancyHelperWall(world, body)) - { - ImGui::PopID(); - continue; - } - - if (!body.collider) + if (isBuoyancyHelperWall(api.getBuoyancySettings(), body)) { ImGui::PopID(); continue; } const bool isSelected = body.id == GetSelectedBodyId(); - const bool isLive = body.inverse_mass != 0.0f; - + const bool isLive = body.inverseMass != 0.0f; const char *typeStr = "Unknown"; - if (body.collider->type == ShapeType::Sphere) + if (body.meshType == MeshType::Sphere) typeStr = "Sphere"; - else if (body.collider->type == ShapeType::Box) + else if (body.meshType == MeshType::Box) typeStr = "Box"; - else if (body.collider->type == ShapeType::Ramp) + else if (body.meshType == MeshType::Ramp) typeStr = "Ramp"; std::string label = "Body " + std::to_string(body.id) + " (" + typeStr + ")"; - if (ImGui::Selectable(label.c_str(), isSelected)) SetSelectedBodyId(body.id); ImGui::SameLine(); ImGui::TextUnformatted(isLive ? "Live" : "Static"); - ImGui::Text("Pos: %.3f %.3f %.3f", body.position.x, body.position.y, body.position.z); ImGui::Text("Speed: %.3f %.3f %.3f", body.velocity.x, body.velocity.y, body.velocity.z); - ImGui::Text("Force: %.3f %.3f %.3f", body.force_accum.x, body.force_accum.y, body.force_accum.z); - if (body.thermal_enabled) + ImGui::Text("Force: %.3f %.3f %.3f", body.forceAccum.x, body.forceAccum.y, body.forceAccum.z); + if (body.thermalEnabled) { ImGui::Text("Temp: %.1f K", body.temperature); - ImGui::Text("k: %.2f | emiss: %.2f", body.thermal_conductivity, body.thermal_emissivity); + ImGui::Text("k: %.2f | emiss: %.2f", body.thermalConductivity, body.thermalEmissivity); } - if (isSelected && isLive) + if (isSelected) { - if (world.enable_buoyancy) + BodyState edited = body; + bool changed = false; + if (body.meshType == MeshType::Sphere) { - if (body.collider->type == ShapeType::Sphere) + float currentVolume = sphereVolumeFromRadius(edited.sphereRadius); + float currentDensity = (currentVolume > 1e-6f) ? (edited.mass / currentVolume) : 0.0f; + float editVolume = currentVolume; + float editDensity = currentDensity; + changed |= ImGui::DragFloat("Volume", &editVolume, 0.01f, 0.0001f, 100.0f); + changed |= ImGui::DragFloat("Density", &editDensity, 0.05f, 0.01f, 100000.0f); + if (changed) { - SphereCollider *sphere = static_cast(body.collider); - const float currentVolume = sphereVolumeFromRadius(sphere->radius); - const float currentMass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; - const float currentDensity = (currentVolume > 1e-6f) ? (currentMass / currentVolume) : 0.0f; - - float editVolume = currentVolume; - float editDensity = currentDensity; - const float prevVolume = editVolume; - bool volChanged = ImGui::DragFloat("Volume", &editVolume, 0.01f, 0.0001f, 100.0f); - if (editVolume > 100.0f) - editVolume = prevVolume; - bool densChanged = ImGui::DragFloat("Density", &editDensity, 0.05f, 0.01f, 100000.0f); - - if (volChanged || densChanged) - { - sphere->radius = sphereRadiusFromVolume(editVolume); - setBodyMassAndInertia(body, editDensity * editVolume); - } + edited.sphereRadius = sphereRadiusFromVolume(editVolume); + edited.mass = editDensity * editVolume; } - else if (body.collider->type == ShapeType::Box) + } + else if (body.meshType == MeshType::Box) + { + float currentVolume = (2.0f * edited.boxHalfSize.x) * (2.0f * edited.boxHalfSize.y) * (2.0f * edited.boxHalfSize.z); + float currentDensity = (currentVolume > 1e-6f) ? (edited.mass / currentVolume) : 0.0f; + float editVolume = currentVolume; + float editDensity = currentDensity; + changed |= ImGui::DragFloat("Volume", &editVolume, 0.01f, 0.0001f, 100.0f); + changed |= ImGui::DragFloat("Density", &editDensity, 0.05f, 0.01f, 100000.0f); + if (changed) { - BoxCollider *box = static_cast(body.collider); - const Vec3 hs = box->halfsize; - const float currentVolume = (2.0f * hs.x) * (2.0f * hs.y) * (2.0f * hs.z); - const float currentMass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; - const float currentDensity = (currentVolume > 1e-6f) ? (currentMass / currentVolume) : 0.0f; - - float editVolume = currentVolume; - float editDensity = currentDensity; - const float prevVolume = editVolume; - bool volChanged = ImGui::DragFloat("Volume", &editVolume, 0.01f, 0.0001f, 100.0f); - if (editVolume > 100.0f) - editVolume = prevVolume; - bool densChanged = ImGui::DragFloat("Density", &editDensity, 0.05f, 0.01f, 100000.0f); - - if (volChanged || densChanged) - { - const float scale = std::cbrt(std::max(0.0001f, editVolume) / std::max(1e-6f, currentVolume)); - box->halfsize = Vec3(hs.x * scale, hs.y * scale, hs.z * scale); - setBodyMassAndInertia(body, editDensity * editVolume); - } + const float scale = std::cbrt(std::max(0.0001f, editVolume) / std::max(1e-6f, currentVolume)); + edited.boxHalfSize = Vec3(edited.boxHalfSize.x * scale, edited.boxHalfSize.y * scale, edited.boxHalfSize.z * scale); + edited.mass = editDensity * editVolume; } } - else + else if (body.meshType == MeshType::Ramp) { - float editSpeed[3] = {body.velocity.x, body.velocity.y, body.velocity.z}; + changed |= ImGui::DragFloat("Ramp Slope", &edited.rampSlope, 0.01f); + changed |= ImGui::DragFloat("Ramp Length", &edited.rampLength, 0.1f); + changed |= ImGui::DragFloat("Ramp HalfWidthZ", &edited.rampHalfWidthZ, 0.1f); + } + + if (!api.getThermalSettings().enabled) + { + float editSpeed[3] = {edited.velocity.x, edited.velocity.y, edited.velocity.z}; if (ImGui::DragFloat3("Edit Speed", editSpeed, 0.1f)) - body.velocity = Vec3(editSpeed[0], editSpeed[1], editSpeed[2]); + { + edited.velocity = Vec3(editSpeed[0], editSpeed[1], editSpeed[2]); + changed = true; + } - float editForce[3] = {body.force_accum.x, body.force_accum.y, body.force_accum.z}; + float editForce[3] = {edited.forceAccum.x, edited.forceAccum.y, edited.forceAccum.z}; if (ImGui::DragFloat3("Edit Force", editForce, 0.1f)) - body.force_accum = Vec3(editForce[0], editForce[1], editForce[2]); + { + edited.forceAccum = Vec3(editForce[0], editForce[1], editForce[2]); + changed = true; + } } - if (body.thermal_enabled) + + if (edited.thermalEnabled) { - float editTemp = body.temperature; + float editTemp = edited.temperature; if (ImGui::DragFloat("Edit Temperature", &editTemp, 0.5f, 100.0f, 1000.0f)) { - body.temperature = editTemp; + edited.temperature = editTemp; + changed = true; } } + + if (changed) + api.updateBody(edited); } ImGui::Separator(); @@ -748,24 +639,23 @@ void RenderBodyInspectorContent(PhysicsWorld &world, bool showCloseButton) if (ImGui::Button("Remove Selected Body")) { - PhysicsResult res = world.deleteBody(selectedId); - RequestEngineToast(res.message); - if (res.success) + const bool removed = api.deleteBody(selectedId); + RequestEngineToast(removed ? "Body was removed successfully" : "Failed to remove body"); + if (removed) SetSelectedBodyId(0); } - show_tooltip("Deletes the currently selected body from the world."); if (selectedId == 0) ImGui::EndDisabled(); } -void RenderBodyMenu(PhysicsWorld &world) +void RenderBodyMenu(AetherAPI &api) { ImGui::Begin("Body Menu"); - RenderAddBodyMenuContent(world); - if (!world.enable_buoyancy) - RenderConstraintMenuContent(world); - RenderWorldMenuContent(world); - RenderBodyInspectorContent(world, false); + RenderAddBodyMenuContent(api); + if (!api.getBuoyancySettings().enabled) + RenderConstraintMenuContent(api); + RenderWorldMenuContent(api); + RenderBodyInspectorContent(api, false); ImGui::End(); -} +} \ No newline at end of file diff --git a/renderer/bodymenu.hpp b/renderer/bodymenu.hpp index 7c5e28c..fa1d6ea 100644 --- a/renderer/bodymenu.hpp +++ b/renderer/bodymenu.hpp @@ -1,10 +1,10 @@ #pragma once -#include "../engine/world/physicsworld.hpp" +#include "api/AetherAPI.hpp" -void RenderBodyMenu(PhysicsWorld &world); -void RenderAddBodyMenuContent(PhysicsWorld &world); -void RenderConstraintMenuContent(PhysicsWorld &world); -void RenderWorldMenuContent(PhysicsWorld &world); -void RenderBodyInspectorContent(PhysicsWorld &world, bool showCloseButton = false); +void RenderBodyMenu(AetherAPI &api); +void RenderAddBodyMenuContent(AetherAPI &api); +void RenderConstraintMenuContent(AetherAPI &api); +void RenderWorldMenuContent(AetherAPI &api); +void RenderBodyInspectorContent(AetherAPI &api, bool showCloseButton = false); void RenderEnginePopups(); diff --git a/renderer/bodyselection.hpp b/renderer/bodyselection.hpp index 708bb2e..4d85e14 100644 --- a/renderer/bodyselection.hpp +++ b/renderer/bodyselection.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../engine/core/bodyid.hpp" +#include "api/RenderBody.hpp" inline BodyID g_selected_body_id = 0; diff --git a/renderer/drawbodies.cpp b/renderer/drawbodies.cpp index ed779aa..f10a140 100644 --- a/renderer/drawbodies.cpp +++ b/renderer/drawbodies.cpp @@ -1,24 +1,22 @@ #include "drawbodies.hpp" -#include + +#include +#include +#include +#include +#include #include + +#include #include #include #include -#include "../engine/core/box_collider.hpp" -#include "../engine/core/sphere_collider.hpp" -#include "../engine/core/ramp_collider.hpp" #include "../engine/math/mat3.hpp" -#include "../engine/math/vec3.hpp" #include "bodyselection.hpp" #include "bodyshaders.hpp" #include "drawconstraints.hpp" #include "thermal_palette.hpp" -#include -#include -#include -#include -#include static GLuint shaderProgram; static GLuint solidProgram; @@ -29,1131 +27,702 @@ static bool showVelocityArrows = true; static float g_bodyTintR = 1.0f; static float g_bodyTintG = 1.0f; static float g_bodyTintB = 1.0f; + struct ArrowRenderState { - glm::vec3 dir = glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 velSmooth = glm::vec3(0.0f); - float speed = 0.0f; - glm::vec3 lastPos = glm::vec3(0.0f); - float posMoveSmooth = 0.0f; - int stillFrames = 0; - bool visible = false; - bool initialized = false; + glm::vec3 dir = glm::vec3(1.0f, 0.0f, 0.0f); + glm::vec3 velSmooth = glm::vec3(0.0f); + float speed = 0.0f; + glm::vec3 lastPos = glm::vec3(0.0f); + float posMoveSmooth = 0.0f; + int stillFrames = 0; + bool visible = false; + bool initialized = false; }; + static std::unordered_map g_arrowRenderStates; namespace { - constexpr float ARROW_VEL_SMOOTH = 0.055f; - constexpr float ARROW_DIR_BLEND = 0.05f; - constexpr float ARROW_SPEED_SMOOTH = 0.065f; - constexpr float ARROW_POS_MOVE_SMOOTH = 0.28f; - constexpr float ARROW_SHOW_SPEED = 0.19f; - constexpr float ARROW_HIDE_SPEED = 0.048f; - constexpr float ARROW_MOVE_REST = 0.0011f; - constexpr int ARROW_STILL_FRAMES = 12; - constexpr float ARROW_DIR_UPDATE_MIN_SPEED = 0.06f; - constexpr float ARROW_SHOW_MOVE_FACTOR = 2.2f; - constexpr float ARROW_DRAW_MIN_SPEED = 0.042f; -} - -static void applyBodyTint(float &r, float &g, float &b) -{ - r = std::min(1.0f, r * g_bodyTintR); - g = std::min(1.0f, g * g_bodyTintG); - b = std::min(1.0f, b * g_bodyTintB); + constexpr float ARROW_VEL_SMOOTH = 0.055f; + constexpr float ARROW_DIR_BLEND = 0.05f; + constexpr float ARROW_SPEED_SMOOTH = 0.065f; + constexpr float ARROW_POS_MOVE_SMOOTH = 0.28f; + constexpr float ARROW_SHOW_SPEED = 0.19f; + constexpr float ARROW_HIDE_SPEED = 0.048f; + constexpr float ARROW_MOVE_REST = 0.0011f; + constexpr int ARROW_STILL_FRAMES = 12; + constexpr float ARROW_DIR_UPDATE_MIN_SPEED = 0.06f; + constexpr float ARROW_SHOW_MOVE_FACTOR = 2.2f; + constexpr float ARROW_DRAW_MIN_SPEED = 0.042f; + + static void applyBodyTint(float &r, float &g, float &b) + { + r = std::min(1.0f, r * g_bodyTintR); + g = std::min(1.0f, g * g_bodyTintG); + b = std::min(1.0f, b * g_bodyTintB); + } + + static glm::vec3 rotateOffset(const glm::mat4 &rot, const glm::vec3 &o) + { + return glm::vec3(rot * glm::vec4(o, 0.0f)); + } + + static glm::mat4 quatToMat4(const Quat &q) + { + const Mat3 r = q.toMat3(); + glm::mat4 m(1.0f); + m[0][0] = r.m[0][0]; m[0][1] = r.m[0][1]; m[0][2] = r.m[0][2]; + m[1][0] = r.m[1][0]; m[1][1] = r.m[1][1]; m[1][2] = r.m[1][2]; + m[2][0] = r.m[2][0]; m[2][1] = r.m[2][1]; m[2][2] = r.m[2][2]; + return m; + } + + static void pushLine(std::vector &v, const glm::vec3 &a, const glm::vec3 &b) + { + v.push_back(a.x); v.push_back(a.y); v.push_back(a.z); + v.push_back(b.x); v.push_back(b.y); v.push_back(b.z); + } + + static void pushTri(std::vector &v, const glm::vec3 &a, const glm::vec3 &na, const glm::vec3 &b, + const glm::vec3 &nb, const glm::vec3 &c, const glm::vec3 &nc) + { + v.push_back(a.x); v.push_back(a.y); v.push_back(a.z); v.push_back(na.x); v.push_back(na.y); v.push_back(na.z); + v.push_back(b.x); v.push_back(b.y); v.push_back(b.z); v.push_back(nb.x); v.push_back(nb.y); v.push_back(nb.z); + v.push_back(c.x); v.push_back(c.y); v.push_back(c.z); v.push_back(nc.x); v.push_back(nc.y); v.push_back(nc.z); + } + + static void pushFace4(std::vector &v, const glm::vec3 &a, const glm::vec3 &na, const glm::vec3 &b, + const glm::vec3 &nb, const glm::vec3 &c, const glm::vec3 &nc, const glm::vec3 &d, + const glm::vec3 &nd) + { + pushTri(v, a, na, b, nb, c, nc); + pushTri(v, a, na, c, nc, d, nd); + } + + static void pushBoxSolid(std::vector &v, const glm::vec3 &c, const glm::vec3 &h, const glm::mat4 &R) + { + const glm::vec3 p000 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, -h.z)); + const glm::vec3 p001 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, +h.z)); + const glm::vec3 p010 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, -h.z)); + const glm::vec3 p011 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, +h.z)); + const glm::vec3 p100 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, -h.z)); + const glm::vec3 p101 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, +h.z)); + const glm::vec3 p110 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, -h.z)); + const glm::vec3 p111 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, +h.z)); + const glm::vec3 nxp = rotateOffset(R, glm::vec3(-1.0f, 0.0f, 0.0f)); + const glm::vec3 nx = rotateOffset(R, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::vec3 nyn = rotateOffset(R, glm::vec3(0.0f, -1.0f, 0.0f)); + const glm::vec3 ny = rotateOffset(R, glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::vec3 nzn = rotateOffset(R, glm::vec3(0.0f, 0.0f, -1.0f)); + const glm::vec3 nz = rotateOffset(R, glm::vec3(0.0f, 0.0f, 1.0f)); + pushFace4(v, p000, nxp, p010, nxp, p011, nxp, p001, nxp); + pushFace4(v, p100, nx, p110, nx, p111, nx, p101, nx); + pushFace4(v, p000, nyn, p100, nyn, p101, nyn, p001, nyn); + pushFace4(v, p010, ny, p011, ny, p111, ny, p110, ny); + pushFace4(v, p000, nzn, p100, nzn, p110, nzn, p010, nzn); + pushFace4(v, p001, nz, p011, nz, p111, nz, p101, nz); + } + + static void pushSphereSolid(std::vector &v, const glm::vec3 &c, float r, int stacks, int slices) + { + const float pi = 3.14159265f; + for (int si = 0; si < stacks; ++si) + { + float t0 = (float)si / (float)stacks * pi; + float t1 = (float)(si + 1) / (float)stacks * pi; + for (int sj = 0; sj < slices; ++sj) + { + float p0 = (float)sj / (float)slices * 2.0f * pi; + float p1 = (float)(sj + 1) / (float)slices * 2.0f * pi; + glm::vec3 n00(std::sin(t0) * std::cos(p0), std::cos(t0), std::sin(t0) * std::sin(p0)); + glm::vec3 n01(std::sin(t0) * std::cos(p1), std::cos(t0), std::sin(t0) * std::sin(p1)); + glm::vec3 n10(std::sin(t1) * std::cos(p0), std::cos(t1), std::sin(t1) * std::sin(p0)); + glm::vec3 n11(std::sin(t1) * std::cos(p1), std::cos(t1), std::sin(t1) * std::sin(p1)); + n00 = glm::normalize(n00); + n01 = glm::normalize(n01); + n10 = glm::normalize(n10); + n11 = glm::normalize(n11); + glm::vec3 v00 = c + r * n00; + glm::vec3 v01 = c + r * n01; + glm::vec3 v10 = c + r * n10; + glm::vec3 v11 = c + r * n11; + pushTri(v, v00, n00, v01, n01, v10, n10); + pushTri(v, v01, n01, v11, n11, v10, n10); + } + } + } + + static void pushRampSolid(std::vector &v, const glm::vec3 &c, float L, float H, float w) + { + float x0 = c.x; + float y0 = c.y; + float x1 = c.x + L; + float z0 = c.z - w; + float z1 = c.z + w; + glm::vec3 p0z0(x0, y0, z0); + glm::vec3 p1z0(x1, y0, z0); + glm::vec3 p2z0(x1, y0 + H, z0); + glm::vec3 p0z1(x0, y0, z1); + glm::vec3 p1z1(x1, y0, z1); + glm::vec3 p2z1(x1, y0 + H, z1); + glm::vec3 nz0 = glm::normalize(glm::cross(p1z0 - p0z0, p2z0 - p0z0)); + glm::vec3 nz1 = glm::normalize(glm::cross(p2z1 - p0z1, p1z1 - p0z1)); + pushTri(v, p0z0, nz0, p1z0, nz0, p2z0, nz0); + pushTri(v, p0z1, nz1, p2z1, nz1, p1z1, nz1); + glm::vec3 n0 = glm::normalize(glm::cross(p1z0 - p0z0, p0z1 - p0z0)); + glm::vec3 n1 = glm::normalize(glm::cross(p2z0 - p1z0, p1z1 - p1z0)); + glm::vec3 n2 = glm::normalize(glm::cross(p0z0 - p2z0, p2z1 - p2z0)); + pushFace4(v, p0z0, n0, p1z0, n0, p1z1, n0, p0z1, n0); + pushFace4(v, p1z0, n1, p2z0, n1, p2z1, n1, p1z1, n1); + pushFace4(v, p2z0, n2, p0z0, n2, p0z1, n2, p2z1, n2); + } + + static bool looksLikeFloor(const BodyState &body) + { + if (body.meshType != MeshType::Box) + return false; + const float hx = body.boxHalfSize.x; + const float hy = body.boxHalfSize.y; + const float hz = body.boxHalfSize.z; + return body.inverseMass == 0.0f && hy <= 0.15f && hx >= 40.0f && hz >= 40.0f; + } + + static bool useThermalGradient(const ThermalSettings &settings, const BodyState &body) + { + return settings.enabled && body.thermalEnabled && !looksLikeFloor(body); + } + + static glm::vec3 temperatureColor(const ThermalSettings &settings, const BodyState &body) + { + const float minT = settings.minVisualTemperature; + const float maxT = settings.maxVisualTemperature; + float t = 0.0f; + if (maxT > minT) + t = (body.temperature - minT) / (maxT - minT); + t = std::clamp(t, 0.0f, 1.0f); + return SampleThermalGradient(t); + } + + static bool isBuoyancyHelperWall(const BuoyancySettings &settings, const BodyState &body) + { + if (!settings.enabled) + return false; + if (body.inverseMass != 0.0f) + return false; + if (body.meshType != MeshType::Box) + return false; + + const float halfSize = settings.beakerHalfSize; + constexpr float wallThickness = 0.2f; + constexpr float wallHalf = wallThickness / 2.0f; + const float tol = 0.02f; + const Vec3 hs = body.boxHalfSize; + + const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); + const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); + + const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); + const float bottomHalfY = wallHalf; + const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); + const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && (std::abs(hs.y - bottomHalfY) < tol) && (std::abs(hs.z - bottomHalfZ) < tol); + + return matchesXWall || matchesZWall || matchesBottom; + } + + static bool getArrowOrigin(const BodyState &body, const glm::vec3 &dir, glm::vec3 &origin, float &sizeScale) + { + if (body.meshType == MeshType::Sphere) + { + origin = glm::vec3(body.position.x, body.position.y, body.position.z); + sizeScale = std::max(0.35f, body.sphereRadius * 0.85f); + return true; + } + if (body.meshType == MeshType::Box) + { + origin = glm::vec3(body.position.x, body.position.y, body.position.z) + glm::vec3(0.0f, body.boxHalfSize.y * 0.5f, 0.0f); + sizeScale = std::max(0.35f, std::sqrt(body.boxHalfSize.x * body.boxHalfSize.x + body.boxHalfSize.y * body.boxHalfSize.y + body.boxHalfSize.z * body.boxHalfSize.z) * 0.45f); + return true; + } + if (body.meshType == MeshType::Ramp) + { + origin = glm::vec3(body.position.x, body.position.y, body.position.z) + glm::vec3(body.rampLength * 0.35f, body.rampSlope * body.rampLength * 0.2f, 0.0f); + sizeScale = std::max(0.35f, std::sqrt(body.rampLength * body.rampLength + body.rampSlope * body.rampSlope + body.rampHalfWidthZ * body.rampHalfWidthZ) * 0.4f); + return true; + } + (void)dir; + return false; + } + + static void pushVelocityArrow(std::vector &v, const BodyState &body, const glm::vec3 &vel) + { + const float speed = glm::length(vel); + if (speed < 1.0e-5f) + return; + glm::vec3 dir = vel * (1.0f / speed); + glm::vec3 origin; + float sizeScale = 1.0f; + if (!getArrowOrigin(body, dir, origin, sizeScale)) + return; + const glm::vec3 start = origin + dir * (sizeScale * 0.2f); + const glm::vec3 tip = start + dir * (0.7f * sizeScale + speed * 0.12f); + glm::vec3 side = glm::cross(dir, glm::vec3(0.0f, 1.0f, 0.0f)); + float slen = glm::length(side); + if (slen < 1e-5f) + side = glm::cross(dir, glm::vec3(1.0f, 0.0f, 0.0f)); + side = glm::normalize(side); + glm::vec3 up = glm::cross(side, dir); + float ulen = glm::length(up); + if (ulen < 1e-5f) + return; + up = up * (1.0f / ulen); + glm::vec3 shaftEnd = tip - dir * (0.23f * sizeScale); + glm::vec3 w0 = shaftEnd + side * (0.12f * sizeScale); + glm::vec3 w1 = shaftEnd - side * (0.12f * sizeScale); + glm::vec3 w2 = shaftEnd + up * (0.12f * sizeScale); + glm::vec3 w3 = shaftEnd - up * (0.12f * sizeScale); + pushLine(v, start, tip); + pushLine(v, tip, w0); + pushLine(v, tip, w1); + pushLine(v, tip, w2); + pushLine(v, tip, w3); + } + + static void drawVelocityArrows(AetherAPI &api, GLuint prog, GLuint vao, GLuint vbo, GLint modelLoc, + GLint viewLoc, GLint projLoc, GLint colorLoc, const glm::mat4 &model, + const glm::mat4 &view, const glm::mat4 &projection) + { + if (!showVelocityArrows) + return; + std::vector arrowVerts; + std::unordered_set alive; + const auto bodies = api.getBodies(); + alive.reserve(bodies.size()); + for (const auto &body : bodies) + { + if (looksLikeFloor(body) || body.meshType == MeshType::Unknown) + continue; + alive.insert(body.id); + auto &state = g_arrowRenderStates[body.id]; + glm::vec3 vel(body.velocity.x, body.velocity.y, body.velocity.z); + glm::vec3 pos(body.position.x, body.position.y, body.position.z); + float rawSpeed = glm::length(vel); + glm::vec3 rawDir = state.dir; + if (rawSpeed > 1e-5f) + rawDir = vel * (1.0f / rawSpeed); + if (!state.initialized) + { + state.dir = rawDir; + state.velSmooth = vel; + state.speed = rawSpeed; + state.lastPos = pos; + state.posMoveSmooth = 0.0f; + state.stillFrames = 0; + state.initialized = true; + } + float frameMove = glm::length(pos - state.lastPos); + state.lastPos = pos; + state.posMoveSmooth = state.posMoveSmooth + (frameMove - state.posMoveSmooth) * ARROW_POS_MOVE_SMOOTH; + state.velSmooth = glm::mix(state.velSmooth, vel, ARROW_VEL_SMOOTH); + float smoothSpeed = glm::length(state.velSmooth); + state.speed = state.speed + (smoothSpeed - state.speed) * ARROW_SPEED_SMOOTH; + if (smoothSpeed > ARROW_DIR_UPDATE_MIN_SPEED) + { + glm::vec3 sd = state.velSmooth * (1.0f / smoothSpeed); + state.dir = glm::normalize(glm::mix(state.dir, sd, ARROW_DIR_BLEND)); + } + bool likelyRest = (rawSpeed < ARROW_HIDE_SPEED && state.speed < ARROW_HIDE_SPEED && smoothSpeed < ARROW_HIDE_SPEED && state.posMoveSmooth < ARROW_MOVE_REST); + state.stillFrames = likelyRest ? (state.stillFrames + 1) : 0; + if (state.visible) + { + if (state.stillFrames > ARROW_STILL_FRAMES) + state.visible = false; + } + else + { + if ((state.speed > ARROW_SHOW_SPEED || rawSpeed > ARROW_SHOW_SPEED) && state.posMoveSmooth > ARROW_MOVE_REST * ARROW_SHOW_MOVE_FACTOR) + state.visible = true; + } + if (!state.visible) + continue; + glm::vec3 stableVel = state.dir * state.speed; + pushVelocityArrow(arrowVerts, body, stableVel); + } + + std::vector stale; + stale.reserve(g_arrowRenderStates.size()); + for (const auto &it : g_arrowRenderStates) + { + if (alive.find(it.first) == alive.end()) + stale.push_back(it.first); + } + for (BodyID id : stale) + g_arrowRenderStates.erase(id); + if (arrowVerts.empty()) + return; + glUseProgram(prog); + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); + glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, arrowVerts.size() * sizeof(float), arrowVerts.data(), GL_DYNAMIC_DRAW); + if (colorLoc >= 0) + glUniform4f(colorLoc, 0.82f, 0.98f, 1.0f, 1.0f); + glLineWidth(4.0f); + glDrawArrays(GL_LINES, 0, static_cast(arrowVerts.size() / 3)); + } + + static void drawSolidBody(const BodyState &body, float floorFlag, float r, float g, float b, float a, GLuint solidProgram) + { + if (body.renderAlpha <= 0.0f) + return; + if (body.meshType == MeshType::Unknown) + return; + const glm::vec3 c(body.position.x, body.position.y, body.position.z); + glm::mat4 R = quatToMat4(body.orientation); + std::vector solidVerts; + solidVerts.reserve(4096); + if (body.meshType == MeshType::Box) + pushBoxSolid(solidVerts, c, glm::vec3(body.boxHalfSize.x, body.boxHalfSize.y, body.boxHalfSize.z), R); + else if (body.meshType == MeshType::Sphere) + pushSphereSolid(solidVerts, c, body.sphereRadius, 14, 16); + else if (body.meshType == MeshType::Ramp) + { + std::vector tmp; + tmp.reserve(2048); + pushRampSolid(tmp, glm::vec3(0.0f), body.rampLength, body.rampSlope * body.rampLength, body.rampHalfWidthZ); + solidVerts.reserve(tmp.size()); + for (std::size_t i = 0; i + 5 < tmp.size(); i += 6) + { + glm::vec3 p(tmp[i + 0], tmp[i + 1], tmp[i + 2]); + glm::vec3 n(tmp[i + 3], tmp[i + 4], tmp[i + 5]); + p = c + rotateOffset(R, p); + n = rotateOffset(R, n); + solidVerts.push_back(p.x); + solidVerts.push_back(p.y); + solidVerts.push_back(p.z); + solidVerts.push_back(n.x); + solidVerts.push_back(n.y); + solidVerts.push_back(n.z); + } + } + if (solidVerts.empty()) + return; + + GLint smCol = glGetUniformLocation(solidProgram, "uColor"); + GLint smFloor = glGetUniformLocation(solidProgram, "uFloor"); + GLint smMatAmbient = glGetUniformLocation(solidProgram, "material.ambient"); + GLint smMatDiffuse = glGetUniformLocation(solidProgram, "material.diffuse"); + GLint smMatSpecular = glGetUniformLocation(solidProgram, "material.specular"); + GLint smMatShininess = glGetUniformLocation(solidProgram, "material.shininess"); + const glm::vec3 specularColor(0.95f, 0.97f, 1.0f); + if (smFloor >= 0) + glUniform1f(smFloor, floorFlag); + if (smCol >= 0) + glUniform4f(smCol, r, g, b, a); + if (smMatAmbient >= 0) + glUniform3f(smMatAmbient, r, g, b); + if (smMatDiffuse >= 0) + glUniform3f(smMatDiffuse, r, g, b); + if (smMatSpecular >= 0) + glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); + if (smMatShininess >= 0) + glUniform1f(smMatShininess, 64.0f); + glBufferData(GL_ARRAY_BUFFER, solidVerts.size() * sizeof(float), solidVerts.data(), GL_DYNAMIC_DRAW); + glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); + } + + static void drawWireBody(const BodyState &body) + { + if (body.meshType == MeshType::Unknown) + return; + const glm::vec3 c(body.position.x, body.position.y, body.position.z); + glm::mat4 R = quatToMat4(body.orientation); + std::vector bodyVertices; + bodyVertices.reserve(72); + + if (body.meshType == MeshType::Box) + { + const glm::vec3 h(body.boxHalfSize.x, body.boxHalfSize.y, body.boxHalfSize.z); + const glm::vec3 p000 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, -h.z)); + const glm::vec3 p001 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, +h.z)); + const glm::vec3 p010 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, -h.z)); + const glm::vec3 p011 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, +h.z)); + const glm::vec3 p100 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, -h.z)); + const glm::vec3 p101 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, +h.z)); + const glm::vec3 p110 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, -h.z)); + const glm::vec3 p111 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, +h.z)); + pushLine(bodyVertices, p000, p100); pushLine(bodyVertices, p100, p101); pushLine(bodyVertices, p101, p001); pushLine(bodyVertices, p001, p000); + pushLine(bodyVertices, p010, p110); pushLine(bodyVertices, p110, p111); pushLine(bodyVertices, p111, p011); pushLine(bodyVertices, p011, p010); + pushLine(bodyVertices, p000, p010); pushLine(bodyVertices, p100, p110); pushLine(bodyVertices, p101, p111); pushLine(bodyVertices, p001, p011); + } + else if (body.meshType == MeshType::Sphere) + { + const float r = body.sphereRadius; + const int segments = 24; + auto pushCircleLines = [&](const glm::vec3 ¢er, float radius, int planeAxis0, int planeAxis1) + { + auto point = [&](float t) -> glm::vec3 + { + glm::vec3 p = center; + p[planeAxis0] += radius * std::cos(t); + p[planeAxis1] += radius * std::sin(t); + return p; + }; + const float twoPi = 6.28318530718f; + for (int i = 0; i < segments; ++i) + { + float t0 = (twoPi * i) / segments; + float t1 = (twoPi * (i + 1)) / segments; + pushLine(bodyVertices, point(t0), point(t1)); + } + }; + pushCircleLines(c, r, 0, 1); + pushCircleLines(c, r, 0, 2); + pushCircleLines(c, r, 1, 2); + } + else if (body.meshType == MeshType::Ramp) + { + const float L = body.rampLength; + const float H = body.rampSlope * body.rampLength; + const float w = body.rampHalfWidthZ; + const glm::vec3 p0z0 = c + rotateOffset(R, glm::vec3(0.0f, 0.0f, -w)); + const glm::vec3 p1z0 = c + rotateOffset(R, glm::vec3(L, 0.0f, -w)); + const glm::vec3 p2z0 = c + rotateOffset(R, glm::vec3(L, H, -w)); + const glm::vec3 p0z1 = c + rotateOffset(R, glm::vec3(0.0f, 0.0f, +w)); + const glm::vec3 p1z1 = c + rotateOffset(R, glm::vec3(L, 0.0f, +w)); + const glm::vec3 p2z1 = c + rotateOffset(R, glm::vec3(L, H, +w)); + pushLine(bodyVertices, p0z0, p1z0); + pushLine(bodyVertices, p0z1, p1z1); + pushLine(bodyVertices, p1z0, p2z0); + pushLine(bodyVertices, p1z1, p2z1); + pushLine(bodyVertices, p0z0, p0z1); + pushLine(bodyVertices, p1z0, p1z1); + pushLine(bodyVertices, p2z0, p2z1); + } + + if (bodyVertices.empty()) + return; + GLint colorLoc = glGetUniformLocation(shaderProgram, "uColor"); + glBufferData(GL_ARRAY_BUFFER, bodyVertices.size() * sizeof(float), bodyVertices.data(), GL_DYNAMIC_DRAW); + if (colorLoc >= 0) + glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + glDrawArrays(GL_LINES, 0, static_cast(bodyVertices.size() / 3)); + } } void initDrawBodies() { - initBodyShaders(); - shaderProgram = getWireProgram(); // wireframe shader program - solidProgram = getSolidProgram(); // solid shader program - - glGenVertexArrays(1, &VAO); - glGenBuffers(1, &VBO); - - glBindVertexArray(VAO); - glBindBuffer(GL_ARRAY_BUFFER, VBO); - glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW); - - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); - glEnableVertexAttribArray(0); - - glBindVertexArray(0); - - glGenVertexArrays(1, &solidVAO); - glGenBuffers(1, &solidVBO); - glBindVertexArray(solidVAO); - glBindBuffer(GL_ARRAY_BUFFER, solidVBO); - glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - glBindVertexArray(0); -} - -static void pushLine(std::vector &v, const glm::vec3 &a, const glm::vec3 &b) -{ - v.push_back(a.x); - v.push_back(a.y); - v.push_back(a.z); - v.push_back(b.x); - v.push_back(b.y); - v.push_back(b.z); -} - -static void pushCircleLines(std::vector &v, const glm::vec3 ¢er, float radius, int segments, int planeAxis0, int planeAxis1) -{ - auto point = [&](float t) -> glm::vec3 - { - glm::vec3 p = center; - p[planeAxis0] += radius * std::cos(t); - p[planeAxis1] += radius * std::sin(t); - return p; - }; - - const float twoPi = 6.28318530718f; - for (int i = 0; i < segments; ++i) - { - float t0 = (twoPi * i) / segments; - float t1 = (twoPi * (i + 1)) / segments; - pushLine(v, point(t0), point(t1)); - } -} - -static void pushTri(std::vector &v, const glm::vec3 &a, const glm::vec3 &na, const glm::vec3 &b, - const glm::vec3 &nb, const glm::vec3 &c, const glm::vec3 &nc) // triangles required for lighting -{ - v.push_back(a.x); - v.push_back(a.y); - v.push_back(a.z); - v.push_back(na.x); - v.push_back(na.y); - v.push_back(na.z); - v.push_back(b.x); - v.push_back(b.y); - v.push_back(b.z); - v.push_back(nb.x); - v.push_back(nb.y); - v.push_back(nb.z); - v.push_back(c.x); - v.push_back(c.y); - v.push_back(c.z); - v.push_back(nc.x); - v.push_back(nc.y); - v.push_back(nc.z); -} - -static void pushFace4(std::vector &v, const glm::vec3 &a, const glm::vec3 &na, const glm::vec3 &b, - const glm::vec3 &nb, const glm::vec3 &cc, const glm::vec3 &nc, const glm::vec3 &d, - const glm::vec3 &nd) -{ - pushTri(v, a, na, b, nb, cc, nc); - pushTri(v, a, na, cc, nc, d, nd); // 2 tris for a rec -} - -static glm::vec3 rotateOffset(const Mat3 &R, const glm::vec3 &o) -{ - Vec3 ro = R * Vec3(o.x, o.y, o.z); - return glm::vec3(ro.x, ro.y, ro.z); -} - -static void pushBoxSolid(std::vector &v, const glm::vec3 &c, const glm::vec3 &h, const Mat3 &R) // creating solid shapes -{ - const glm::vec3 p000 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, -h.z)); - const glm::vec3 p001 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, +h.z)); - const glm::vec3 p010 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, -h.z)); - const glm::vec3 p011 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, +h.z)); - const glm::vec3 p100 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, -h.z)); - const glm::vec3 p101 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, +h.z)); - const glm::vec3 p110 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, -h.z)); - const glm::vec3 p111 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, +h.z)); - const glm::vec3 nxp = rotateOffset(R, glm::vec3(-1.0f, 0.0f, 0.0f)); - const glm::vec3 nx = rotateOffset(R, glm::vec3(1.0f, 0.0f, 0.0f)); - const glm::vec3 nyn = rotateOffset(R, glm::vec3(0.0f, -1.0f, 0.0f)); - const glm::vec3 ny = rotateOffset(R, glm::vec3(0.0f, 1.0f, 0.0f)); - const glm::vec3 nzn = rotateOffset(R, glm::vec3(0.0f, 0.0f, -1.0f)); - const glm::vec3 nz = rotateOffset(R, glm::vec3(0.0f, 0.0f, 1.0f)); - pushFace4(v, p000, nxp, p010, nxp, p011, nxp, p001, nxp); - pushFace4(v, p100, nx, p110, nx, p111, nx, p101, nx); - pushFace4(v, p000, nyn, p100, nyn, p101, nyn, p001, nyn); - pushFace4(v, p010, ny, p011, ny, p111, ny, p110, ny); - pushFace4(v, p000, nzn, p100, nzn, p110, nzn, p010, nzn); - pushFace4(v, p001, nz, p011, nz, p111, nz, p101, nz); -} - -static void pushSphereSolid(std::vector &v, const glm::vec3 &c, float r, int stacks, int slices) // creating spheres -{ - const float pi = 3.14159265f; // for angular calci - for (int si = 0; si < stacks; ++si) // vertical parse 0 -> pi - { - float t0 = (float)si / (float)stacks * pi; - float t1 = (float)(si + 1) / (float)stacks * pi; - for (int sj = 0; sj < slices; ++sj) // horizontal parse 0 -> 2 * pi - { - float p0 = (float)sj / (float)slices * 2.0f * pi; - float p1 = (float)(sj + 1) / (float)slices * 2.0f * pi; - glm::vec3 n00(std::sin(t0) * std::cos(p0), std::cos(t0), std::sin(t0) * std::sin(p0)); // radius given by sin(theta), using x = r * cos(phi) and z = r * sin(phi), y shows vertical movement, y = sin(theta) - glm::vec3 n01(std::sin(t0) * std::cos(p1), std::cos(t0), std::sin(t0) * std::sin(p1)); - glm::vec3 n10(std::sin(t1) * std::cos(p0), std::cos(t1), std::sin(t1) * std::sin(p0)); - glm::vec3 n11(std::sin(t1) * std::cos(p1), std::cos(t1), std::sin(t1) * std::sin(p1)); // create four vertices box - n00 = glm::normalize(n00); - n01 = glm::normalize(n01); - n10 = glm::normalize(n10); - n11 = glm::normalize(n11); // to unit length - glm::vec3 v00 = c + r * n00; - glm::vec3 v01 = c + r * n01; - glm::vec3 v10 = c + r * n10; - glm::vec3 v11 = c + r * n11; // resize to the actual size - pushTri(v, v00, n00, v01, n01, v10, n10); - pushTri(v, v01, n01, v11, n11, v10, n10); - } - } -} - -static void pushRampSolid(std::vector &v, const glm::vec3 &c, float L, float H, float w) // solid ramps -{ - float x0 = c.x; - float y0 = c.y; - float x1 = c.x + L; - float z0 = c.z - w; - float z1 = c.z + w; - glm::vec3 p0z0(x0, y0, z0); - glm::vec3 p1z0(x1, y0, z0); - glm::vec3 p2z0(x1, c.y + H, z0); - glm::vec3 p0z1(x0, y0, z1); - glm::vec3 p1z1(x1, y0, z1); - glm::vec3 p2z1(x1, c.y + H, z1); - glm::vec3 nz0 = glm::normalize(glm::cross(p1z0 - p0z0, p2z0 - p0z0)); - glm::vec3 nz1 = glm::normalize(glm::cross(p2z1 - p0z1, p1z1 - p0z1)); - pushTri(v, p0z0, nz0, p1z0, nz0, p2z0, nz0); - pushTri(v, p0z1, nz1, p2z1, nz1, p1z1, nz1); // side faces - glm::vec3 n0 = glm::normalize(glm::cross(p1z0 - p0z0, p0z1 - p0z0)); - glm::vec3 n1 = glm::normalize(glm::cross(p2z0 - p1z0, p1z1 - p1z0)); - glm::vec3 n2 = glm::normalize(glm::cross(p0z0 - p2z0, p2z1 - p2z0)); - pushFace4(v, p0z0, n0, p1z0, n0, p1z1, n0, p0z1, n0); - pushFace4(v, p1z0, n1, p2z0, n1, p2z1, n1, p1z1, n1); - pushFace4(v, p2z0, n2, p0z0, n2, p0z1, n2, p2z1, n2); // top, bottom, back face covered -} - -static bool looksLikeFloor(const Rigidbody &body) // check for floor -{ - if (!body.collider || body.collider->type != ShapeType::Box) - return false; - const auto *box = static_cast(body.collider); - const float hx = box->halfsize.x; - const float hy = box->halfsize.y; - const float hz = box->halfsize.z; - return hy <= 0.15f && hx >= 40.0f && hz >= 40.0f; -} - -static bool useThermalGradient(const PhysicsWorld &world, const Rigidbody &body) -{ - return world.thermal_settings.enabled && body.thermal_enabled && !looksLikeFloor(body); -} - -static glm::vec3 temperatureColor(const PhysicsWorld &world, const Rigidbody &body) -{ - const auto &settings = world.thermal_settings; - float minT = settings.min_visual_temperature; - float maxT = settings.max_visual_temperature; - float span = std::max(1.0f, maxT - minT); - float norm = (body.temperature - minT) / span; - return SampleThermalGradient(norm); -} - -static bool getArrowOrigin(const Rigidbody &body, const glm::vec3 &dir, glm::vec3 &origin, float &sizeScale) -{ - if (!body.collider) - return false; - const glm::vec3 c(body.position.x, body.position.y, body.position.z); - Mat3 R = body.orientation.toMat3(); - if (body.collider->type == ShapeType::Sphere) - { - const auto *sphere = static_cast(body.collider); - float r = std::max(0.12f, sphere->radius); - origin = c + dir * (r + 0.06f); - sizeScale = r; - return true; - } - if (body.collider->type == ShapeType::Box) - { - const auto *box = static_cast(body.collider); - float ex = std::abs(dir.x * box->halfsize.x); - float ey = std::abs(dir.y * box->halfsize.y); - float ez = std::abs(dir.z * box->halfsize.z); - float support = ex + ey + ez; - support = std::max(0.12f, support); - float maxHalf = std::max(box->halfsize.x, std::max(box->halfsize.y, box->halfsize.z)); - origin = c + dir * (support + 0.06f); - sizeScale = std::max(0.16f, maxHalf); - return true; - } - if (body.collider->type == ShapeType::Ramp) - { - const auto *ramp = static_cast(body.collider); - const Vec3 comOffset = ramp->getLocalCenterOfMassOffset(); - glm::vec3 localAnchor(ramp->length * 0.65f - comOffset.x, ramp->getHeight() * 0.72f - comOffset.y, 0.0f); - glm::vec3 worldAnchor = c + rotateOffset(R, localAnchor); - glm::vec3 up = rotateOffset(R, glm::vec3(0.0f, 1.0f, 0.0f)); - float upLen = glm::length(up); - if (upLen > 1e-5f) - up = up * (1.0f / upLen); - else - up = glm::vec3(0.0f, 1.0f, 0.0f); - origin = worldAnchor + up * 0.08f + dir * 0.06f; - sizeScale = std::max(0.18f, std::min(0.52f, std::max(ramp->length * 0.12f, ramp->getHeight() * 0.2f))); - return true; - } - origin = c + dir * 0.12f; - sizeScale = 0.22f; - return true; -} - -static void pushVelocityArrow(std::vector &v, const Rigidbody &body, const glm::vec3 &vel) -{ - float sqlen = vel.x * vel.x + vel.y * vel.y + vel.z * vel.z; - if (sqlen < ARROW_DRAW_MIN_SPEED * ARROW_DRAW_MIN_SPEED) - return; - float inv = 1.0f / std::sqrt(sqlen); - glm::vec3 dir(vel.x * inv, vel.y * inv, vel.z * inv); - glm::vec3 start; - float sizeScale = 0.22f; - if (!getArrowOrigin(body, dir, start, sizeScale)) - return; - float velocityMag = std::sqrt(sqlen); - constexpr float minArrowLen = 0.15f; - constexpr float maxArrowLen = 1.5f; - constexpr float velocityScale = 1.0f; - float totalLen = std::min(maxArrowLen, std::max(minArrowLen, velocityMag * velocityScale)); - const float headLen = totalLen * 0.34f; - const float headW = std::max(0.03f, totalLen * 0.18f); - glm::vec3 tip = start + dir * totalLen; - glm::vec3 shaftEnd = start + dir * (totalLen - headLen); - glm::vec3 aux(0.0f, 1.0f, 0.0f); - if (std::abs(dir.x * aux.x + dir.y * aux.y + dir.z * aux.z) > 0.92f) - aux = glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 side = glm::cross(dir, aux); - float slen = glm::length(side); - if (slen < 1e-5f) - return; - side = side * (1.0f / slen); - glm::vec3 up = glm::cross(side, dir); - float ulen = glm::length(up); - if (ulen < 1e-5f) - return; - up = up * (1.0f / ulen); - glm::vec3 w0 = shaftEnd + side * headW; - glm::vec3 w1 = shaftEnd - side * headW; - glm::vec3 w2 = shaftEnd + up * headW; - glm::vec3 w3 = shaftEnd - up * headW; - pushLine(v, start, tip); - pushLine(v, tip, w0); - pushLine(v, tip, w1); - pushLine(v, tip, w2); - pushLine(v, tip, w3); -} - -static void drawVelocityArrows(PhysicsWorld &world, GLuint prog, GLuint vao, GLuint vbo, GLint modelLoc, - GLint viewLoc, GLint projLoc, GLint colorLoc, const glm::mat4 &model, - const glm::mat4 &view, const glm::mat4 &projection) -{ - if (!showVelocityArrows) - return; - std::vector arrowVerts; - std::unordered_set alive; - alive.reserve(world.getBodies().size()); - for (auto &body : world.getBodies()) - { - if (looksLikeFloor(body)) - continue; - if (!body.collider) - continue; - alive.insert(body.id); - auto &state = g_arrowRenderStates[body.id]; - glm::vec3 vel(body.velocity.x, body.velocity.y, body.velocity.z); - glm::vec3 pos(body.position.x, body.position.y, body.position.z); - float rawSpeed = glm::length(vel); - glm::vec3 rawDir = state.dir; - if (rawSpeed > 1e-5f) - rawDir = vel * (1.0f / rawSpeed); - if (!state.initialized) - { - state.dir = rawDir; - state.velSmooth = vel; - state.speed = rawSpeed; - state.lastPos = pos; - state.posMoveSmooth = 0.0f; - state.stillFrames = 0; - state.initialized = true; - } - float frameMove = glm::length(pos - state.lastPos); - state.lastPos = pos; - state.posMoveSmooth = - state.posMoveSmooth + (frameMove - state.posMoveSmooth) * ARROW_POS_MOVE_SMOOTH; - state.velSmooth = glm::mix(state.velSmooth, vel, ARROW_VEL_SMOOTH); - float smoothSpeed = glm::length(state.velSmooth); - state.speed = state.speed + (smoothSpeed - state.speed) * ARROW_SPEED_SMOOTH; - if (smoothSpeed > ARROW_DIR_UPDATE_MIN_SPEED) - { - glm::vec3 sd = state.velSmooth * (1.0f / smoothSpeed); - state.dir = glm::normalize(glm::mix(state.dir, sd, ARROW_DIR_BLEND)); - } - bool likelyRest = (rawSpeed < ARROW_HIDE_SPEED && state.speed < ARROW_HIDE_SPEED && - smoothSpeed < ARROW_HIDE_SPEED && state.posMoveSmooth < ARROW_MOVE_REST); - if (likelyRest) - state.stillFrames += 1; - else - state.stillFrames = 0; - if (state.visible) - { - if (state.stillFrames > ARROW_STILL_FRAMES) - state.visible = false; - } - else - { - if ((state.speed > ARROW_SHOW_SPEED || rawSpeed > ARROW_SHOW_SPEED) && - state.posMoveSmooth > ARROW_MOVE_REST * ARROW_SHOW_MOVE_FACTOR) - state.visible = true; - } - if (!state.visible) - continue; - glm::vec3 stableVel = state.dir * state.speed; - pushVelocityArrow(arrowVerts, body, stableVel); - } - std::vector stale; - stale.reserve(g_arrowRenderStates.size()); - for (const auto &it : g_arrowRenderStates) - { - if (alive.find(it.first) == alive.end()) - stale.push_back(it.first); - } - for (BodyID id : stale) - { - g_arrowRenderStates.erase(id); - } - if (arrowVerts.empty()) - return; - glUseProgram(prog); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, arrowVerts.size() * sizeof(float), arrowVerts.data(), GL_DYNAMIC_DRAW); - if (colorLoc >= 0) - glUniform4f(colorLoc, 0.82f, 0.98f, 1.0f, 1.0f); - glLineWidth(4.0f); - glDrawArrays(GL_LINES, 0, static_cast(arrowVerts.size() / 3)); -} - -void RenderBodies(PhysicsWorld &world, const Camera &camera, float aspectRatio) -{ - if (world.enable_buoyancy) - { - const Fluid &fluid = world.water_fluid; - const float halfSize = fluid.beaker_half_size; - constexpr float wallThickness = 0.2f; - constexpr float wallHalf = wallThickness / 2.0f; - - float beakerCenterY = fluid.beaker_center.y; - if (std::abs(beakerCenterY) < 1e-4f) - beakerCenterY = halfSize; - - auto isWallLike = [&](const Rigidbody &body) -> bool - { - if (body.inverse_mass != 0.0f) - return false; - if (!body.collider || body.collider->type != ShapeType::Box) - return false; - const BoxCollider *box = static_cast(body.collider); - const Vec3 hs = box->halfsize; - const float tol = 0.02f; - - const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); - const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); - - const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); - const float bottomHalfY = wallHalf; - const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); - const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && - (std::abs(hs.y - bottomHalfY) < tol) && - (std::abs(hs.z - bottomHalfZ) < tol); - - return matchesXWall || matchesZWall || matchesBottom; - }; - - bool alreadyHas = false; - for (const auto &b : world.getBodies()) - { - if (isWallLike(b)) - { - alreadyHas = true; - break; - } - } - - static std::vector> s_owned; - if (!alreadyHas) - { - s_owned.clear(); - const float cx = fluid.beaker_center.x; - const float cz = fluid.beaker_center.z; - const float yCenter = beakerCenterY; - const float yHalf = halfSize; - - auto addBox = [&](const Vec3 &pos, const Vec3 &hs) - { - auto c = std::make_unique(hs); - Collider *ptr = c.get(); - s_owned.push_back(std::move(c)); - world.addBody(Rigidbody(pos, Vec3(), ptr, 0.0f)); - }; - - addBox(Vec3(cx - halfSize + wallHalf, yCenter, cz), Vec3(wallHalf, yHalf, halfSize)); - addBox(Vec3(cx + halfSize - wallHalf, yCenter, cz), Vec3(wallHalf, yHalf, halfSize)); - addBox(Vec3(cx, yCenter, cz - halfSize + wallHalf), Vec3(halfSize, yHalf, wallHalf)); - addBox(Vec3(cx, yCenter, cz + halfSize - wallHalf), Vec3(halfSize, yHalf, wallHalf)); - - const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); - const float bottomHalfY = wallHalf; - const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); - const float bottomCenterY = yCenter - halfSize + wallHalf; - addBox(Vec3(cx, bottomCenterY, cz), Vec3(bottomHalfX, bottomHalfY, bottomHalfZ)); - } - } - - auto isBuoyancyHelperWallBody = [&](const Rigidbody &body) -> bool - { - if (!world.enable_buoyancy) - return false; - if (body.inverse_mass != 0.0f) - return false; - if (!body.collider || body.collider->type != ShapeType::Box) - return false; - - const Fluid &fluid = world.water_fluid; - const float halfSize = fluid.beaker_half_size; - constexpr float wallThickness = 0.2f; - constexpr float wallHalf = wallThickness / 2.0f; - const float tol = 0.02f; - const BoxCollider *box = static_cast(body.collider); - const Vec3 hs = box->halfsize; - - const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); - const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); - - const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); - const float bottomHalfY = wallHalf; - const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); - const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && - (std::abs(hs.y - bottomHalfY) < tol) && - (std::abs(hs.z - bottomHalfZ) < tol); - - return matchesXWall || matchesZWall || matchesBottom; - }; - - // Separate vertex lists so we can color axes and body outlines differently - std::vector axisVertices; - axisVertices.reserve(18); // 3 axes * 2 endpoints * 3 components - - // Define world axes (X, Y, Z) - { - const float axisLen = 100.0f; - // X axis - pushLine(axisVertices, - glm::vec3(-axisLen, 0.0f, 0.0f), - glm::vec3(axisLen, 0.0f, 0.0f)); - // Y axis - pushLine(axisVertices, - glm::vec3(0.0f, -axisLen, 0.0f), - glm::vec3(0.0f, axisLen, 0.0f)); - // Z axis - pushLine(axisVertices, - glm::vec3(0.0f, 0.0f, -axisLen), - glm::vec3(0.0f, 0.0f, axisLen)); - } - - glm::mat4 model = glm::mat4(1.0f); - glm::mat4 view = camera.getViewMatrix(); - - glm::mat4 projection = glm::perspective( - glm::radians(45.0f), - aspectRatio, - 0.1f, - 100.0f); - - glUseProgram(shaderProgram); - - GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel"); - GLint viewLoc = glGetUniformLocation(shaderProgram, "uView"); - GLint projLoc = glGetUniformLocation(shaderProgram, "uProjection"); - GLint colorLoc = glGetUniformLocation(shaderProgram, "uColor"); - - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); - - glBindVertexArray(VAO); - glBindBuffer(GL_ARRAY_BUFFER, VBO); - - glEnable(GL_DEPTH_TEST); - if (!axisVertices.empty()) - { - glBufferData(GL_ARRAY_BUFFER, axisVertices.size() * sizeof(float), axisVertices.data(), GL_DYNAMIC_DRAW); - if (colorLoc >= 0) - glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); - glLineWidth(3.0f); - glDrawArrays(GL_LINES, 0, static_cast(axisVertices.size() / 3)); - } - - if (!g_wireframeMode) - { - glUseProgram(solidProgram); - GLint smModel = glGetUniformLocation(solidProgram, "uModel"); - GLint smView = glGetUniformLocation(solidProgram, "uView"); - GLint smProj = glGetUniformLocation(solidProgram, "uProjection"); - GLint smCol = glGetUniformLocation(solidProgram, "uColor"); - GLint smCam = glGetUniformLocation(solidProgram, "uCameraPos"); - GLint smLight = glGetUniformLocation(solidProgram, "uLightDir"); - GLint smSel = glGetUniformLocation(solidProgram, "uSelected"); - GLint smFloor = glGetUniformLocation(solidProgram, "uFloor"); - // Environment lighting + fog. - GLint smSky = glGetUniformLocation(solidProgram, "uSkyColor"); - GLint smGround = glGetUniformLocation(solidProgram, "uGroundColor"); - GLint smFogColor = glGetUniformLocation(solidProgram, "uFogColor"); - GLint smFogNear = glGetUniformLocation(solidProgram, "uFogNear"); - GLint smFogFar = glGetUniformLocation(solidProgram, "uFogFar"); - - // Material parameters (per body, derived from tint). - GLint smMatAmbient = glGetUniformLocation(solidProgram, "material.ambient"); - GLint smMatDiffuse = glGetUniformLocation(solidProgram, "material.diffuse"); - GLint smMatSpecular = glGetUniformLocation(solidProgram, "material.specular"); - GLint smMatShininess = glGetUniformLocation(solidProgram, "material.shininess"); - glm::vec3 lightDir = glm::normalize(glm::vec3(5.0f, -1.1f, 1.0f)); - glm::vec3 camPos = camera.getPosition(); - glUniformMatrix4fv(smView, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(smProj, 1, GL_FALSE, glm::value_ptr(projection)); - glUniformMatrix4fv(smModel, 1, GL_FALSE, glm::value_ptr(model)); - glUniform3fv(smLight, 1, glm::value_ptr(lightDir)); - glUniform3fv(smCam, 1, glm::value_ptr(camPos)); - - const glm::vec3 skyColor(0.03f, 0.02f, 0.07f); - const glm::vec3 groundColor(0.01f, 0.01f, 0.015f); - const glm::vec3 fogColor(0.015f, 0.01f, 0.035f); - const float fogNear = 28.0f; - const float fogFar = 80.0f; - - if (smSky >= 0) - glUniform3fv(smSky, 1, glm::value_ptr(skyColor)); - if (smGround >= 0) - glUniform3fv(smGround, 1, glm::value_ptr(groundColor)); - if (smFogColor >= 0) - glUniform3fv(smFogColor, 1, glm::value_ptr(fogColor)); - if (smFogNear >= 0) - glUniform1f(smFogNear, fogNear); - if (smFogFar >= 0) - glUniform1f(smFogFar, fogFar); - - const glm::vec3 specularColor(0.95f, 0.97f, 1.0f); - const float shininess = 64.0f; - - glBindVertexArray(solidVAO); - glBindBuffer(GL_ARRAY_BUFFER, solidVBO); - - auto drawSolidBody = [&](Rigidbody &body, float floorFlag, float ar, float ag, float ab, float aa) - { - if (!body.collider) - return; - const glm::vec3 c(body.position.x, body.position.y, body.position.z); - Mat3 R = body.orientation.toMat3(); - std::vector solidVerts; - solidVerts.reserve(4096); - if (body.collider->type == ShapeType::Box) - { - const auto *box = static_cast(body.collider); - pushBoxSolid(solidVerts, c, glm::vec3(box->halfsize.x, box->halfsize.y, box->halfsize.z), R); - } - else if (body.collider->type == ShapeType::Sphere) - { - const auto *sphere = static_cast(body.collider); - pushSphereSolid(solidVerts, c, sphere->radius, 14, 16); - } - else if (body.collider->type == ShapeType::Ramp) - { - const auto *ramp = static_cast(body.collider); - std::vector tmp; - tmp.reserve(2048); - pushRampSolid(tmp, glm::vec3(0.0f), ramp->length, ramp->getHeight(), ramp->half_width_z); - const Vec3 comOffset = ramp->getLocalCenterOfMassOffset(); - const glm::vec3 comLocal(comOffset.x, comOffset.y, comOffset.z); - - solidVerts.reserve(tmp.size()); - for (std::size_t i = 0; i + 5 < tmp.size(); i += 6) - { - glm::vec3 p(tmp[i + 0], tmp[i + 1], tmp[i + 2]); - glm::vec3 n(tmp[i + 3], tmp[i + 4], tmp[i + 5]); - p = c + rotateOffset(R, p - comLocal); - n = rotateOffset(R, n); - solidVerts.push_back(p.x); - solidVerts.push_back(p.y); - solidVerts.push_back(p.z); - solidVerts.push_back(n.x); - solidVerts.push_back(n.y); - solidVerts.push_back(n.z); - } - } - if (solidVerts.empty()) - return; - - const bool isSelected = (body.id == GetSelectedBodyId()); - float cr = ar; - float cg = ag; - float cb = ab; - float ca = aa; - if (ca < 0.0f) - ca = 0.0f; - if (ca > 1.0f) - ca = 1.0f; - if (isSelected && floorFlag < 0.5f) - { - cr = 1.0f; - cg = 0.92f; - cb = 0.35f; - ca = 1.0f; - } - - glColor4f(cr, cg, cb, ca); - if (ca <= 0.0f) - return; - - glBufferData(GL_ARRAY_BUFFER, solidVerts.size() * sizeof(float), solidVerts.data(), GL_DYNAMIC_DRAW); - if (smFloor >= 0) - glUniform1f(smFloor, floorFlag); - - if (smCol >= 0) - { - glUniform4f(smCol, cr, cg, cb, ca); - } - if (smSel >= 0) - glUniform1f(smSel, (isSelected && floorFlag < 0.5f) ? 1.0f : 0.0f); - - if (smMatAmbient >= 0) - glUniform3f(smMatAmbient, cr, cg, cb); - if (smMatDiffuse >= 0) - glUniform3f(smMatDiffuse, cr, cg, cb); - if (smMatSpecular >= 0) - glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); - if (smMatShininess >= 0) - glUniform1f(smMatShininess, shininess); - - glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); - }; - - auto pushBoxSolidOpenTop = [&](std::vector &v, const glm::vec3 &c, const glm::vec3 &h, const Mat3 &R) - { - const glm::vec3 p000 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, -h.z)); - const glm::vec3 p001 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, +h.z)); - const glm::vec3 p010 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, -h.z)); - const glm::vec3 p011 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, +h.z)); - const glm::vec3 p100 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, -h.z)); - const glm::vec3 p101 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, +h.z)); - const glm::vec3 p110 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, -h.z)); - const glm::vec3 p111 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, +h.z)); - const glm::vec3 nxp = rotateOffset(R, glm::vec3(-1.0f, 0.0f, 0.0f)); - const glm::vec3 nx = rotateOffset(R, glm::vec3(1.0f, 0.0f, 0.0f)); - const glm::vec3 nyn = rotateOffset(R, glm::vec3(0.0f, -1.0f, 0.0f)); - const glm::vec3 nzn = rotateOffset(R, glm::vec3(0.0f, 0.0f, -1.0f)); - const glm::vec3 nz = rotateOffset(R, glm::vec3(0.0f, 0.0f, 1.0f)); - pushFace4(v, p000, nxp, p010, nxp, p011, nxp, p001, nxp); - pushFace4(v, p100, nx, p110, nx, p111, nx, p101, nx); - pushFace4(v, p000, nyn, p100, nyn, p101, nyn, p001, nyn); - pushFace4(v, p000, nzn, p100, nzn, p110, nzn, p010, nzn); - pushFace4(v, p001, nz, p011, nz, p111, nz, p101, nz); - }; - - auto drawTransparentBoxOpenTop = [&](const glm::vec3 ¢er, const glm::vec3 &half, - const Mat3 &R, - float r, float g, float b, float a) - { - std::vector solidVerts; - solidVerts.reserve(4096); - pushBoxSolidOpenTop(solidVerts, center, half, R); - if (solidVerts.empty()) - return; - glBufferData(GL_ARRAY_BUFFER, solidVerts.size() * sizeof(float), solidVerts.data(), GL_DYNAMIC_DRAW); - if (smFloor >= 0) - glUniform1f(smFloor, 0.0f); - if (smCol >= 0) - glUniform4f(smCol, r, g, b, a); - if (smMatAmbient >= 0) - glUniform3f(smMatAmbient, r, g, b); - if (smMatDiffuse >= 0) - glUniform3f(smMatDiffuse, r, g, b); - if (smMatSpecular >= 0) - glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); - if (smMatShininess >= 0) - glUniform1f(smMatShininess, shininess); - glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); - }; - - auto drawTransparentBox = [&](const glm::vec3 ¢er, const glm::vec3 &half, - const Mat3 &R, - float r, float g, float b, float a) - { - std::vector solidVerts; - solidVerts.reserve(4096); - pushBoxSolid(solidVerts, center, half, R); - if (solidVerts.empty()) - return; - glBufferData(GL_ARRAY_BUFFER, solidVerts.size() * sizeof(float), solidVerts.data(), GL_DYNAMIC_DRAW); - if (smFloor >= 0) - glUniform1f(smFloor, 0.0f); - if (smCol >= 0) - glUniform4f(smCol, r, g, b, a); - if (smMatAmbient >= 0) - glUniform3f(smMatAmbient, r, g, b); - if (smMatDiffuse >= 0) - glUniform3f(smMatDiffuse, r, g, b); - if (smMatSpecular >= 0) - glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); - if (smMatShininess >= 0) - glUniform1f(smMatShininess, shininess); - glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); - }; - - if (world.enable_buoyancy) - { - const Fluid &fluid = world.water_fluid; - const float halfSize = fluid.beaker_half_size; - constexpr float wallThickness = 0.2f; - - float beakerCenterY = fluid.beaker_center.y; - if (std::abs(beakerCenterY) < 1e-4f) - beakerCenterY = halfSize; - - const Vec3 beakerCenter(fluid.beaker_center.x, beakerCenterY, fluid.beaker_center.z); - - const float innerHalfX = std::max(0.01f, halfSize - wallThickness); - const float innerHalfZ = std::max(0.01f, halfSize - wallThickness); - const float innerHalfY = std::max(0.01f, halfSize - wallThickness); - const float innerBottomY = beakerCenterY - innerHalfY; - - const float waterTopY = fluid.height; - const float waterHeight = std::max(0.0f, std::min(waterTopY - innerBottomY, 2.0f * innerHalfY)); - - if (waterHeight > 1e-4f) - { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - - const float waterHalfY = waterHeight * 0.5f; - const float waterCenterY = innerBottomY + waterHalfY; - drawTransparentBox(glm::vec3(beakerCenter.x, waterCenterY, beakerCenter.z), - glm::vec3(innerHalfX, waterHalfY, innerHalfZ), - Mat3::identity(), - 0.18f, 0.55f, 1.00f, 0.34f); - - constexpr float surfaceThickness = 0.08f; - const float surfaceHalfY = std::min(surfaceThickness * 0.5f, waterHalfY); - const float surfaceCenterY = waterTopY - surfaceHalfY; - drawTransparentBox(glm::vec3(beakerCenter.x, surfaceCenterY, beakerCenter.z), - glm::vec3(innerHalfX, surfaceHalfY, innerHalfZ), - Mat3::identity(), - 0.22f, 0.65f, 1.00f, 0.24f); - - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - } - } - - for (auto &body : world.getBodies()) - { - if (looksLikeFloor(body)) - continue; - if (isBuoyancyHelperWallBody(body)) - continue; - BodyID key = body.id; - float r = ((key * 73u) % 100) / 100.0f; - float g = ((key * 37u) % 100) / 100.0f; - float b = ((key * 19u) % 100) / 100.0f; - r = 0.5f + 0.5f * r; - g = 0.5f + 0.5f * g; - b = 0.5f + 0.5f * b; - if (useThermalGradient(world, body)) - { - glm::vec3 thermal = temperatureColor(world, body); - r = thermal.r; - g = thermal.g; - b = thermal.b; - } - applyBodyTint(r, g, b); - drawSolidBody(body, 0.0f, r, g, b, body.render_alpha); - } - - float tintR, tintG, tintB; - GetBodyTint(tintR, tintG, tintB); - RenderDistanceConstraintsSolid(world, view, projection, solidProgram, solidVAO, solidVBO, lightDir, camPos, - tintR, tintG, tintB); - - glBindVertexArray(solidVAO); - glBindBuffer(GL_ARRAY_BUFFER, solidVBO); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(2.5f, 25.0f); - for (auto &body : world.getBodies()) - { - if (!looksLikeFloor(body)) - continue; - drawSolidBody(body, 1.0f, 0.26f, 0.28f, 0.31f, 0.78f * body.render_alpha); - } - - if (world.enable_buoyancy) - { - const Fluid &fluid = world.water_fluid; - const float halfSize = fluid.beaker_half_size; - float beakerCenterY = fluid.beaker_center.y; - if (std::abs(beakerCenterY) < 1e-4f) - beakerCenterY = halfSize; - const Vec3 beakerCenter(fluid.beaker_center.x, beakerCenterY, fluid.beaker_center.z); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - drawTransparentBoxOpenTop(glm::vec3(beakerCenter.x, beakerCenter.y, beakerCenter.z), - glm::vec3(halfSize, halfSize, halfSize), - Mat3::identity(), - 0.75f, 0.92f, 1.00f, 0.08f); - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - } - glDisable(GL_POLYGON_OFFSET_FILL); - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - - glBindVertexArray(0); - glUseProgram(shaderProgram); - glBindVertexArray(VAO); - glBindBuffer(GL_ARRAY_BUFFER, VBO); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); - - float lineTintR, lineTintG, lineTintB; - GetBodyTint(lineTintR, lineTintG, lineTintB); - RenderDistanceConstraintsWire(world, model, view, projection, shaderProgram, VAO, VBO, lineTintR, lineTintG, - lineTintB); - drawVelocityArrows(world, shaderProgram, VAO, VBO, modelLoc, viewLoc, projLoc, colorLoc, model, view, - projection); - } - - if (g_wireframeMode) - { - for (auto &body : world.getBodies()) - { - if (!body.collider) - continue; - if (isBuoyancyHelperWallBody(body)) - continue; - if (body.render_alpha <= 0.0f) - { - glColor4f(0.0f, 0.0f, 0.0f, 0.0f); - continue; - } - - const glm::vec3 c(body.position.x, body.position.y, body.position.z); - Mat3 R = body.orientation.toMat3(); - - std::vector bodyVertices; - bodyVertices.reserve(72); - - if (body.collider->type == ShapeType::Box) - { - const auto *box = static_cast(body.collider); - const glm::vec3 h(box->halfsize.x, box->halfsize.y, box->halfsize.z); - - const glm::vec3 p000 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, -h.z)); - const glm::vec3 p001 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, +h.z)); - const glm::vec3 p010 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, -h.z)); - const glm::vec3 p011 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, +h.z)); - const glm::vec3 p100 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, -h.z)); - const glm::vec3 p101 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, +h.z)); - const glm::vec3 p110 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, -h.z)); - const glm::vec3 p111 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, +h.z)); - - // bottom - pushLine(bodyVertices, p000, p100); - pushLine(bodyVertices, p100, p101); - pushLine(bodyVertices, p101, p001); - pushLine(bodyVertices, p001, p000); - // top - pushLine(bodyVertices, p010, p110); - pushLine(bodyVertices, p110, p111); - pushLine(bodyVertices, p111, p011); - pushLine(bodyVertices, p011, p010); - // sides - pushLine(bodyVertices, p000, p010); - pushLine(bodyVertices, p100, p110); - pushLine(bodyVertices, p101, p111); - pushLine(bodyVertices, p001, p011); - } - else if (body.collider->type == ShapeType::Sphere) - { - const auto *sphere = static_cast(body.collider); - const float r = sphere->radius; - const int segments = 24; - - // 3 circles for a simple wire-sphere - pushCircleLines(bodyVertices, c, r, segments, 0, 1); // XY - pushCircleLines(bodyVertices, c, r, segments, 0, 2); // XZ - pushCircleLines(bodyVertices, c, r, segments, 1, 2); // YZ - } - else if (body.collider->type == ShapeType::Ramp) - { - const auto *ramp = static_cast(body.collider); - const float L = ramp->length; - const float H = ramp->getHeight(); - const float w = ramp->half_width_z; - const Vec3 comOffset = ramp->getLocalCenterOfMassOffset(); - const glm::vec3 comLocal(comOffset.x, comOffset.y, comOffset.z); - - const glm::vec3 p0z0 = c + rotateOffset(R, glm::vec3(0.0f, 0.0f, -w) - comLocal); - const glm::vec3 p1z0 = c + rotateOffset(R, glm::vec3(L, 0.0f, -w) - comLocal); - const glm::vec3 p2z0 = c + rotateOffset(R, glm::vec3(L, H, -w) - comLocal); - - const glm::vec3 p0z1 = c + rotateOffset(R, glm::vec3(0.0f, 0.0f, +w) - comLocal); - const glm::vec3 p1z1 = c + rotateOffset(R, glm::vec3(L, 0.0f, +w) - comLocal); - const glm::vec3 p2z1 = c + rotateOffset(R, glm::vec3(L, H, +w) - comLocal); - - // bottom face edges - pushLine(bodyVertices, p0z0, p1z0); - pushLine(bodyVertices, p0z1, p1z1); - - // side face edges - pushLine(bodyVertices, p1z0, p2z0); - pushLine(bodyVertices, p1z1, p2z1); - - // hypotenuse edges - pushLine(bodyVertices, p0z0, p2z0); - pushLine(bodyVertices, p0z1, p2z1); - - // connect slices along Z - pushLine(bodyVertices, p0z0, p0z1); - pushLine(bodyVertices, p1z0, p1z1); - pushLine(bodyVertices, p2z0, p2z1); - } - - if (!bodyVertices.empty()) - { - glBufferData(GL_ARRAY_BUFFER, bodyVertices.size() * sizeof(float), bodyVertices.data(), GL_DYNAMIC_DRAW); - - // Adding colour to wireframe based on stable body id - BodyID key = body.id; - float r = ((key * 73u) % 100) / 100.0f; - float g = ((key * 37u) % 100) / 100.0f; - float b = ((key * 19u) % 100) / 100.0f; - - r = 0.5f + 0.5f * r; - g = 0.5f + 0.5f * g; - b = 0.5f + 0.5f * b; - - const bool isSelected = (body.id == GetSelectedBodyId()); - if (colorLoc >= 0) - { - if (looksLikeFloor(body) && !isSelected) - { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - float alpha = 0.72f * body.render_alpha; - glUniform4f(colorLoc, 0.34f, 0.36f, 0.40f, alpha); - glColor4f(0.34f, 0.36f, 0.40f, alpha); - } - else if (isSelected) - { - float alpha = body.render_alpha; - glUniform4f(colorLoc, 1.0f, 1.0f, 0.2f, alpha); - glColor4f(1.0f, 1.0f, 0.2f, alpha); - } - else - { - float tr = r; - float tg = g; - float tb = b; - if (useThermalGradient(world, body)) - { - glm::vec3 thermal = temperatureColor(world, body); - tr = thermal.r; - tg = thermal.g; - tb = thermal.b; - } - applyBodyTint(tr, tg, tb); - float alpha = body.render_alpha; - glUniform4f(colorLoc, tr, tg, tb, alpha); - glColor4f(tr, tg, tb, alpha); - } - } - if (looksLikeFloor(body)) - { - glEnable(GL_POLYGON_OFFSET_LINE); - glPolygonOffset(2.5f, 25.0f); - } - if (body.id == GetSelectedBodyId()) - glLineWidth(4.0f); - else - glLineWidth(2.0f); - glDrawArrays(GL_LINES, 0, static_cast(bodyVertices.size() / 3)); - if (looksLikeFloor(body)) - glDisable(GL_POLYGON_OFFSET_LINE); - if (looksLikeFloor(body) && !isSelected) - glDisable(GL_BLEND); - } - } - - float wTintR, wTintG, wTintB; - GetBodyTint(wTintR, wTintG, wTintB); - RenderDistanceConstraintsWire(world, model, view, projection, shaderProgram, VAO, VBO, wTintR, wTintG, - wTintB); - drawVelocityArrows(world, shaderProgram, VAO, VBO, modelLoc, viewLoc, projLoc, colorLoc, model, view, - projection); - } -} - -void SetBodyDrawWireframeMode(bool wireframe) -{ - g_wireframeMode = wireframe; -} - -bool GetBodyDrawWireframeMode() -{ - return g_wireframeMode; -} - -void SetBodyTint(float r, float g, float b) -{ - g_bodyTintR = std::max(0.0f, r); - g_bodyTintG = std::max(0.0f, g); - g_bodyTintB = std::max(0.0f, b); + initBodyShaders(); + shaderProgram = getWireProgram(); + solidProgram = getSolidProgram(); + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); + glEnableVertexAttribArray(0); + glBindVertexArray(0); + + glGenVertexArrays(1, &solidVAO); + glGenBuffers(1, &solidVBO); + glBindVertexArray(solidVAO); + glBindBuffer(GL_ARRAY_BUFFER, solidVBO); + glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + glBindVertexArray(0); } -void GetBodyTint(float &r, float &g, float &b) +void RenderBodies(AetherAPI &api, const Camera &camera, float aspectRatio) { - r = g_bodyTintR; - g = g_bodyTintG; - b = g_bodyTintB; + const auto bodies = api.getBodies(); + std::unordered_map bodyMap; + bodyMap.reserve(bodies.size()); + for (const auto &body : bodies) + bodyMap.emplace(body.id, body); + + const BuoyancySettings buoyancy = api.getBuoyancySettings(); + const ThermalSettings thermal = api.getThermalSettings(); + + // Simple world axes. + std::vector axisVertices; + axisVertices.reserve(18); + { + const float axisLen = 100.0f; + pushLine(axisVertices, glm::vec3(-axisLen, 0.0f, 0.0f), glm::vec3(axisLen, 0.0f, 0.0f)); + pushLine(axisVertices, glm::vec3(0.0f, -axisLen, 0.0f), glm::vec3(0.0f, axisLen, 0.0f)); + pushLine(axisVertices, glm::vec3(0.0f, 0.0f, -axisLen), glm::vec3(0.0f, 0.0f, axisLen)); + } + + glm::mat4 model = glm::mat4(1.0f); + glm::mat4 view = camera.getViewMatrix(); + glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspectRatio, 0.1f, 100.0f); + + glUseProgram(shaderProgram); + GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel"); + GLint viewLoc = glGetUniformLocation(shaderProgram, "uView"); + GLint projLoc = glGetUniformLocation(shaderProgram, "uProjection"); + GLint colorLoc = glGetUniformLocation(shaderProgram, "uColor"); + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); + glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + + if (!axisVertices.empty()) + { + glBufferData(GL_ARRAY_BUFFER, axisVertices.size() * sizeof(float), axisVertices.data(), GL_DYNAMIC_DRAW); + if (colorLoc >= 0) + glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + glLineWidth(3.0f); + glDrawArrays(GL_LINES, 0, static_cast(axisVertices.size() / 3)); + } + + if (!g_wireframeMode) + { + glUseProgram(solidProgram); + GLint smModel = glGetUniformLocation(solidProgram, "uModel"); + GLint smView = glGetUniformLocation(solidProgram, "uView"); + GLint smProj = glGetUniformLocation(solidProgram, "uProjection"); + GLint smLight = glGetUniformLocation(solidProgram, "uLightDir"); + GLint smCam = glGetUniformLocation(solidProgram, "uCameraPos"); + GLint smSel = glGetUniformLocation(solidProgram, "uSelected"); + GLint smFloor = glGetUniformLocation(solidProgram, "uFloor"); + GLint smSky = glGetUniformLocation(solidProgram, "uSkyColor"); + GLint smGround = glGetUniformLocation(solidProgram, "uGroundColor"); + GLint smFogColor = glGetUniformLocation(solidProgram, "uFogColor"); + GLint smFogNear = glGetUniformLocation(solidProgram, "uFogNear"); + GLint smFogFar = glGetUniformLocation(solidProgram, "uFogFar"); + glm::vec3 lightDir = glm::normalize(glm::vec3(5.0f, -1.1f, 1.0f)); + glm::vec3 camPos = camera.getPosition(); + glUniformMatrix4fv(smView, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(smProj, 1, GL_FALSE, glm::value_ptr(projection)); + glUniformMatrix4fv(smModel, 1, GL_FALSE, glm::value_ptr(model)); + glUniform3fv(smLight, 1, glm::value_ptr(lightDir)); + glUniform3fv(smCam, 1, glm::value_ptr(camPos)); + if (smSel >= 0) glUniform1f(smSel, 0.0f); + if (smFloor >= 0) glUniform1f(smFloor, 0.0f); + if (smSky >= 0) glUniform3f(smSky, 0.03f, 0.02f, 0.07f); + if (smGround >= 0) glUniform3f(smGround, 0.01f, 0.01f, 0.015f); + if (smFogColor >= 0) glUniform3f(smFogColor, 0.015f, 0.01f, 0.035f); + if (smFogNear >= 0) glUniform1f(smFogNear, 28.0f); + if (smFogFar >= 0) glUniform1f(smFogFar, 80.0f); + + glBindVertexArray(solidVAO); + glBindBuffer(GL_ARRAY_BUFFER, solidVBO); + + if (buoyancy.enabled) + { + const float halfSize = buoyancy.beakerHalfSize; + float beakerCenterY = buoyancy.beakerCenter.y; + if (std::abs(beakerCenterY) < 1e-4f) + beakerCenterY = halfSize; + const Vec3 beakerCenter(buoyancy.beakerCenter.x, beakerCenterY, buoyancy.beakerCenter.z); + const float waterHeight = std::max(0.0f, std::min(buoyancy.waterHeight - (beakerCenterY - halfSize), 2.0f * halfSize)); + if (waterHeight > 1e-4f) + { + std::vector waterVerts; + pushBoxSolid(waterVerts, glm::vec3(beakerCenter.x, beakerCenter.y, beakerCenter.z), glm::vec3(halfSize, waterHeight * 0.5f, halfSize), glm::mat4(1.0f)); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + glBufferData(GL_ARRAY_BUFFER, waterVerts.size() * sizeof(float), waterVerts.data(), GL_DYNAMIC_DRAW); + glUniform4f(glGetUniformLocation(solidProgram, "uColor"), 0.18f, 0.55f, 1.0f, 0.20f); + glDrawArrays(GL_TRIANGLES, 0, static_cast(waterVerts.size() / 6)); + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + } + } + + for (const auto &renderBody : api.getRenderBodies()) + { + auto it = bodyMap.find(renderBody.id); + if (it == bodyMap.end()) + continue; + const BodyState &body = it->second; + if (body.renderAlpha <= 0.0f) + continue; + if (looksLikeFloor(body) || isBuoyancyHelperWall(buoyancy, body)) + continue; + float r = ((body.id * 73u) % 100) / 100.0f; + float g = ((body.id * 37u) % 100) / 100.0f; + float b = ((body.id * 19u) % 100) / 100.0f; + r = 0.5f + 0.5f * r; + g = 0.5f + 0.5f * g; + b = 0.5f + 0.5f * b; + if (useThermalGradient(thermal, body)) + { + glm::vec3 tcolor = temperatureColor(thermal, body); + r = tcolor.r; g = tcolor.g; b = tcolor.b; + } + applyBodyTint(r, g, b); + drawSolidBody(body, 0.0f, r, g, b, body.renderAlpha, solidProgram); + } + + float tintR, tintG, tintB; + GetBodyTint(tintR, tintG, tintB); + RenderDistanceConstraintsSolid(api, view, projection, solidProgram, solidVAO, solidVBO, lightDir, camPos, tintR, tintG, tintB); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(2.5f, 25.0f); + for (const auto &renderBody : api.getRenderBodies()) + { + auto it = bodyMap.find(renderBody.id); + if (it == bodyMap.end()) + continue; + const BodyState &body = it->second; + if (body.renderAlpha <= 0.0f) + continue; + if (!looksLikeFloor(body)) + continue; + drawSolidBody(body, 1.0f, 0.26f, 0.28f, 0.31f, 0.78f * body.renderAlpha, solidProgram); + } + glDisable(GL_POLYGON_OFFSET_FILL); + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + + glBindVertexArray(0); + glUseProgram(shaderProgram); + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); + glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); + RenderDistanceConstraintsWire(api, model, view, projection, shaderProgram, VAO, VBO, tintR, tintG, tintB); + drawVelocityArrows(api, shaderProgram, VAO, VBO, modelLoc, viewLoc, projLoc, colorLoc, model, view, projection); + } + + if (g_wireframeMode) + { + for (const auto &renderBody : api.getRenderBodies()) + { + auto it = bodyMap.find(renderBody.id); + if (it == bodyMap.end()) + continue; + const BodyState &body = it->second; + if (body.renderAlpha <= 0.0f) + continue; + if (isBuoyancyHelperWall(buoyancy, body)) + continue; + if (body.renderAlpha <= 0.0f) + continue; + drawWireBody(body); + } + } } -void SetBodyVelocityArrowVisible(bool enabled) -{ - showVelocityArrows = enabled; -} - -bool GetBodyVelocityArrowVisible() -{ - return showVelocityArrows; -} +void SetBodyDrawWireframeMode(bool wireframe) { g_wireframeMode = wireframe; } +bool GetBodyDrawWireframeMode() { return g_wireframeMode; } +void SetBodyTint(float r, float g, float b) { g_bodyTintR = r; g_bodyTintG = g; g_bodyTintB = b; } +void GetBodyTint(float &r, float &g, float &b) { r = g_bodyTintR; g = g_bodyTintG; b = g_bodyTintB; } +void SetBodyVelocityArrowVisible(bool enabled) { showVelocityArrows = enabled; } +bool GetBodyVelocityArrowVisible() { return showVelocityArrows; } \ No newline at end of file diff --git a/renderer/drawbodies.hpp b/renderer/drawbodies.hpp index 5da2966..e85eb5f 100644 --- a/renderer/drawbodies.hpp +++ b/renderer/drawbodies.hpp @@ -1,9 +1,9 @@ #pragma once -#include "../engine/world/physicsworld.hpp" +#include "api/AetherAPI.hpp" #include "camera.hpp" void initDrawBodies(); -void RenderBodies(PhysicsWorld &world, const Camera &camera, float aspectRatio); +void RenderBodies(AetherAPI &api, const Camera &camera, float aspectRatio); void SetBodyDrawWireframeMode(bool wireframe); bool GetBodyDrawWireframeMode(); void SetBodyTint(float r, float g, float b); diff --git a/renderer/drawconstraints.cpp b/renderer/drawconstraints.cpp index 9ee26a0..712d7c0 100644 --- a/renderer/drawconstraints.cpp +++ b/renderer/drawconstraints.cpp @@ -1,181 +1,205 @@ #include "drawconstraints.hpp" -#include -#include + #include #include #include +#include +#include + namespace { + static void tintRgb(float &r, float &g, float &b, float tr, float tg, float tb) + { + r = std::min(1.0f, r * tr); + g = std::min(1.0f, g * tg); + b = std::min(1.0f, b * tb); + } -static void tintRgb(float &r, float &g, float &b, float tr, float tg, float tb) -{ - r = std::min(1.0f, r * tr); - g = std::min(1.0f, g * tg); - b = std::min(1.0f, b * tb); -} + static void pushLine(std::vector &out, const glm::vec3 &a, const glm::vec3 &b) + { + out.push_back(a.x); + out.push_back(a.y); + out.push_back(a.z); + out.push_back(b.x); + out.push_back(b.y); + out.push_back(b.z); + } -static void pushLine(std::vector &out, const glm::vec3 &a, const glm::vec3 &b) -{ - out.push_back(a.x); - out.push_back(a.y); - out.push_back(a.z); - out.push_back(b.x); - out.push_back(b.y); - out.push_back(b.z); -} + static void pushTri(std::vector &v, const glm::vec3 &a, const glm::vec3 &na, const glm::vec3 &b, + const glm::vec3 &nb, const glm::vec3 &c, const glm::vec3 &nc) + { + v.push_back(a.x); + v.push_back(a.y); + v.push_back(a.z); + v.push_back(na.x); + v.push_back(na.y); + v.push_back(na.z); + v.push_back(b.x); + v.push_back(b.y); + v.push_back(b.z); + v.push_back(nb.x); + v.push_back(nb.y); + v.push_back(nb.z); + v.push_back(c.x); + v.push_back(c.y); + v.push_back(c.z); + v.push_back(nc.x); + v.push_back(nc.y); + v.push_back(nc.z); + } -static void pushTri(std::vector &v, const glm::vec3 &a, const glm::vec3 &na, const glm::vec3 &b, - const glm::vec3 &nb, const glm::vec3 &c, const glm::vec3 &nc) -{ - v.push_back(a.x); - v.push_back(a.y); - v.push_back(a.z); - v.push_back(na.x); - v.push_back(na.y); - v.push_back(na.z); - v.push_back(b.x); - v.push_back(b.y); - v.push_back(b.z); - v.push_back(nb.x); - v.push_back(nb.y); - v.push_back(nb.z); - v.push_back(c.x); - v.push_back(c.y); - v.push_back(c.z); - v.push_back(nc.x); - v.push_back(nc.y); - v.push_back(nc.z); -} + static void orthonormalBasis(const glm::vec3 &dir, glm::vec3 &u, glm::vec3 &v) + { + glm::vec3 up = (std::abs(dir.y) < 0.9f) ? glm::vec3(0.0f, 1.0f, 0.0f) : glm::vec3(1.0f, 0.0f, 0.0f); + u = glm::normalize(glm::cross(up, dir)); + v = glm::cross(dir, u); + } -static void orthonormalBasis(const glm::vec3 &dir, glm::vec3 &u, glm::vec3 &v) // create two vectors perpendicular to dir -{ - glm::vec3 up = (std::abs(dir.y) < 0.9f) ? glm::vec3(0.0f, 1.0f, 0.0f) : glm::vec3(1.0f, 0.0f, 0.0f); - u = glm::normalize(glm::cross(up, dir)); // vector 1 with cross product with dir - v = glm::cross(dir, u); // second vector -} + static void pushCylinderSolid(std::vector &v, const glm::vec3 &a, const glm::vec3 &b, float radius, + int slices) + { + glm::vec3 d = b - a; + float len = glm::length(d); + if (len < 1.0e-5f || radius < 1.0e-6f) + return; + glm::vec3 dir = d * (1.0f / len); + glm::vec3 u, w; + orthonormalBasis(dir, u, w); + glm::vec3 nBottom = -dir; + glm::vec3 nTop = dir; + for (int i = 0; i < slices; ++i) + { + float t0 = (float)i / (float)slices * 6.28318530718f; + float t1 = (float)(i + 1) / (float)slices * 6.28318530718f; + glm::vec3 c0 = std::cos(t0) * u + std::sin(t0) * w; + glm::vec3 c1 = std::cos(t1) * u + std::sin(t1) * w; + glm::vec3 ab0 = a + radius * c0; + glm::vec3 ab1 = a + radius * c1; + glm::vec3 bb0 = b + radius * c0; + glm::vec3 bb1 = b + radius * c1; + glm::vec3 n0 = glm::normalize(c0); + glm::vec3 n1 = glm::normalize(c1); + pushTri(v, ab0, n0, ab1, n1, bb1, n1); + pushTri(v, ab0, n0, bb1, n1, bb0, n0); + pushTri(v, a, nBottom, ab0, nBottom, ab1, nBottom); + pushTri(v, b, nTop, bb1, nTop, bb0, nTop); + } + } -static void pushCylinderSolid(std::vector &v, const glm::vec3 &a, const glm::vec3 &b, float radius, - int slices) -{ - glm::vec3 d = b - a; // axis vector - float len = glm::length(d); // length of axis - if (len < 1.0e-5f || radius < 1.0e-6f) // negligible length or radius - return; - glm::vec3 dir = d * (1.0f / len); // unit vector along direction of d - glm::vec3 u, w; // to create 2 vectors orthogonal to dir - orthonormalBasis(dir, u, w); // creates 2 orthogonal vectors - glm::vec3 nBottom = -dir; // bottom face - glm::vec3 nTop = dir; // top face - for (int i = 0; i < slices; ++i) - { - float t0 = (float)i / (float)slices * 6.28318530718f; // (slice no.) / (total slices) * 2 pi -> gives angle for that slice - float t1 = (float)(i + 1) / (float)slices * 6.28318530718f; // same - glm::vec3 c0 = std::cos(t0) * u + std::sin(t0) * w; // parametric circular equation in 3D - glm::vec3 c1 = std::cos(t1) * u + std::sin(t1) * w; // draw a circle in the plane defined by u an w - // scale the unit circle and center it at a, b - glm::vec3 ab0 = a + radius * c0; // bottom - glm::vec3 ab1 = a + radius * c1; // bottom - glm::vec3 bb0 = b + radius * c0; // top - glm::vec3 bb1 = b + radius * c1; // top - glm::vec3 n0 = glm::normalize(c0); // normal from c0 - glm::vec3 n1 = glm::normalize(c1); // normal from c1 - pushTri(v, ab0, n0, ab1, n1, bb1, n1); - pushTri(v, ab0, n0, bb1, n1, bb0, n0); - pushTri(v, a, nBottom, ab0, nBottom, ab1, nBottom); - pushTri(v, b, nTop, bb1, nTop, bb0, nTop); // draw the circle as triangles - } -} + static void pushHelixWire(std::vector &out, const glm::vec3 &a, const glm::vec3 &b, float coilRadius, + int numCoils, int segments) + { + glm::vec3 d = b - a; + float len = glm::length(d); + if (len < 1.0e-5f) + return; + glm::vec3 dir = d * (1.0f / len); + glm::vec3 u, w; + orthonormalBasis(dir, u, w); + glm::vec3 prev = a + coilRadius * u; + for (int i = 1; i <= segments; ++i) + { + float t = (float)i / (float)segments; + float ang = (float)numCoils * 6.28318530718f * t; + glm::vec3 center = a + d * t; + glm::vec3 p = center + coilRadius * (std::cos(ang) * u + std::sin(ang) * w); + pushLine(out, prev, p); + prev = p; + } + } -static void pushHelixWire(std::vector &out, const glm::vec3 &a, const glm::vec3 &b, float coilRadius, - int numCoils, int segments) -{ - glm::vec3 d = b - a; // direction vector - float len = glm::length(d); // len of the vector - if (len < 1.0e-5f) // negligible length - return; - glm::vec3 dir = d * (1.0f / len); // unit vector along d - glm::vec3 u, w; - orthonormalBasis(dir, u, w); // 2 orthogonal vectors to dir - glm::vec3 prev = a + coilRadius * u; // starting point - for (int i = 1; i <= segments; ++i) // iterate for the segments - { - float t = (float)i / (float)segments; // interpolation parameter - float ang = (float)numCoils * 6.28318530718f * t; // angle of rotation - glm::vec3 center = a + d * t; // moving center point - glm::vec3 p = center + coilRadius * (std::cos(ang) * u + std::sin(ang) * w); // helix equation to get the next point - pushLine(out, prev, p); - prev = p; - } -} + static void pushDashedLine(std::vector &out, const glm::vec3 &a, const glm::vec3 &b, float dashLen, + float gapLen) + { + glm::vec3 d = b - a; + float len = glm::length(d); + if (len < 1.0e-5f) + return; + glm::vec3 dir = d * (1.0f / len); + float posAlong = 0.0f; + while (posAlong < len) + { + float dash = std::min(dashLen, len - posAlong); + glm::vec3 p0 = a + dir * posAlong; + glm::vec3 p1 = a + dir * (posAlong + dash); + pushLine(out, p0, p1); + posAlong += dash + gapLen; + } + } -static void pushDashedLine(std::vector &out, const glm::vec3 &a, const glm::vec3 &b, float dashLen, - float gapLen) // draw the dahsed lines -{ - glm::vec3 d = b - a; - float len = glm::length(d); - if (len < 1.0e-5f) - return; - glm::vec3 dir = d * (1.0f / len); - float posAlong = 0.0f; - while (posAlong < len) - { - float dash = std::min(dashLen, len - posAlong); - glm::vec3 p0 = a + dir * posAlong; - glm::vec3 p1 = a + dir * (posAlong + dash); - pushLine(out, p0, p1); - posAlong += dash + gapLen; - } -} + static void pushSpringSolid(std::vector &v, const glm::vec3 &a, const glm::vec3 &b, float tubeRadius, + float coilRadius, int numCoils, int segmentsAlong, int cylinderSlices) + { + glm::vec3 d = b - a; + float len = glm::length(d); + if (len < 1.0e-5f) + return; + glm::vec3 dir = d * (1.0f / len); + glm::vec3 u, w; + orthonormalBasis(dir, u, w); + glm::vec3 prev = a + coilRadius * u; + for (int i = 1; i <= segmentsAlong; ++i) + { + float t = (float)i / (float)segmentsAlong; + float ang = (float)numCoils * 6.28318530718f * t; + glm::vec3 center = a + d * t; + glm::vec3 p = center + coilRadius * (std::cos(ang) * u + std::sin(ang) * w); + pushCylinderSolid(v, prev, p, tubeRadius, cylinderSlices); + prev = p; + } + } -static void pushSpringSolid(std::vector &v, const glm::vec3 &a, const glm::vec3 &b, float tubeRadius, - float coilRadius, int numCoils, int segmentsAlong, int cylinderSlices) // same as pushCylinder but rendering multiple cylinders around a coil -{ - glm::vec3 d = b - a; - float len = glm::length(d); - if (len < 1.0e-5f) - return; - glm::vec3 dir = d * (1.0f / len); - glm::vec3 u, w; - orthonormalBasis(dir, u, w); - glm::vec3 prev = a + coilRadius * u; - for (int i = 1; i <= segmentsAlong; ++i) - { - float t = (float)i / (float)segmentsAlong; - float ang = (float)numCoils * 6.28318530718f * t; - glm::vec3 center = a + d * t; - glm::vec3 p = center + coilRadius * (std::cos(ang) * u + std::sin(ang) * w); - pushCylinderSolid(v, prev, p, tubeRadius, cylinderSlices); - prev = p; - } -} + static void constraintBaseColor(ConstraintType type, float &r, float &g, float &b) + { + if (type == ConstraintType::Rope) + { + r = 0.92f; + g = 0.78f; + b = 0.28f; + } + else if (type == ConstraintType::Rod) + { + r = 0.32f; + g = 0.82f; + b = 0.98f; + } + else + { + r = 0.42f; + g = 0.95f; + b = 0.48f; + } + } -static void constraintBaseColor(DistanceConstraint::TYPE type, float &r, float &g, float &b) -{ - if (type == DistanceConstraint::ROPE) - { - r = 0.92f; - g = 0.78f; - b = 0.28f; - } - else if (type == DistanceConstraint::ROD) - { - r = 0.32f; - g = 0.82f; - b = 0.98f; - } - else - { - r = 0.42f; - g = 0.95f; - b = 0.48f; - } -} + static std::vector buildSolidConstraint(const DistanceConstraintState &constraint, const glm::vec3 &p0, const glm::vec3 &p1) + { + std::vector solid; + solid.reserve(4096); + if (constraint.type == ConstraintType::Rope) + pushCylinderSolid(solid, p0, p1, 0.045f, 12); + else if (constraint.type == ConstraintType::Rod) + pushCylinderSolid(solid, p0, p1, 0.07f, 14); + else + pushSpringSolid(solid, p0, p1, 0.028f, 0.22f, 10, 80, 8); + return solid; + } + static std::vector buildWireConstraint(const DistanceConstraintState &constraint, const glm::vec3 &p0, const glm::vec3 &p1) + { + std::vector lines; + if (constraint.type == ConstraintType::Rope) + pushDashedLine(lines, p0, p1, 0.12f, 0.08f); + else if (constraint.type == ConstraintType::Rod) + pushLine(lines, p0, p1); + else + pushHelixWire(lines, p0, p1, 0.22f, 10, 96); + return lines; + } } void RenderDistanceConstraintsSolid( - PhysicsWorld &world, + AetherAPI &api, const glm::mat4 &view, const glm::mat4 &projection, GLuint program, @@ -187,112 +211,86 @@ void RenderDistanceConstraintsSolid( float tintG, float tintB) { - GLint smModel = glGetUniformLocation(program, "uModel"); - GLint smView = glGetUniformLocation(program, "uView"); - GLint smProj = glGetUniformLocation(program, "uProjection"); - GLint smCol = glGetUniformLocation(program, "uColor"); - GLint smCam = glGetUniformLocation(program, "uCameraPos"); - GLint smLight = glGetUniformLocation(program, "uLightDir"); - GLint smSel = glGetUniformLocation(program, "uSelected"); - GLint smFloor = glGetUniformLocation(program, "uFloor"); - // Environment lighting + fog. - GLint smSky = glGetUniformLocation(program, "uSkyColor"); - GLint smGround = glGetUniformLocation(program, "uGroundColor"); - GLint smFogColor = glGetUniformLocation(program, "uFogColor"); - GLint smFogNear = glGetUniformLocation(program, "uFogNear"); - GLint smFogFar = glGetUniformLocation(program, "uFogFar"); + GLint smModel = glGetUniformLocation(program, "uModel"); + GLint smView = glGetUniformLocation(program, "uView"); + GLint smProj = glGetUniformLocation(program, "uProjection"); + GLint smCol = glGetUniformLocation(program, "uColor"); + GLint smCam = glGetUniformLocation(program, "uCameraPos"); + GLint smLight = glGetUniformLocation(program, "uLightDir"); + GLint smSel = glGetUniformLocation(program, "uSelected"); + GLint smFloor = glGetUniformLocation(program, "uFloor"); + GLint smSky = glGetUniformLocation(program, "uSkyColor"); + GLint smGround = glGetUniformLocation(program, "uGroundColor"); + GLint smFogColor = glGetUniformLocation(program, "uFogColor"); + GLint smFogNear = glGetUniformLocation(program, "uFogNear"); + GLint smFogFar = glGetUniformLocation(program, "uFogFar"); + GLint smMatAmbient = glGetUniformLocation(program, "material.ambient"); + GLint smMatDiffuse = glGetUniformLocation(program, "material.diffuse"); + GLint smMatSpecular = glGetUniformLocation(program, "material.specular"); + GLint smMatShininess = glGetUniformLocation(program, "material.shininess"); - // Material parameters. - GLint smMatAmbient = glGetUniformLocation(program, "material.ambient"); - GLint smMatDiffuse = glGetUniformLocation(program, "material.diffuse"); - GLint smMatSpecular = glGetUniformLocation(program, "material.specular"); - GLint smMatShininess = glGetUniformLocation(program, "material.shininess"); + glUseProgram(program); + glm::mat4 model = glm::mat4(1.0f); + glUniformMatrix4fv(smView, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(smProj, 1, GL_FALSE, glm::value_ptr(projection)); + glUniformMatrix4fv(smModel, 1, GL_FALSE, glm::value_ptr(model)); + glUniform3fv(smLight, 1, glm::value_ptr(lightDir)); + glUniform3fv(smCam, 1, glm::value_ptr(camPos)); + if (smSel >= 0) + glUniform1f(smSel, 0.0f); + if (smFloor >= 0) + glUniform1f(smFloor, 0.0f); - glm::mat4 model = glm::mat4(1.0f); - glUseProgram(program); - glUniformMatrix4fv(smView, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(smProj, 1, GL_FALSE, glm::value_ptr(projection)); - glUniformMatrix4fv(smModel, 1, GL_FALSE, glm::value_ptr(model)); - glUniform3fv(smLight, 1, glm::value_ptr(lightDir)); - glUniform3fv(smCam, 1, glm::value_ptr(camPos)); - if (smSel >= 0) - glUniform1f(smSel, 0.0f); - if (smFloor >= 0) - glUniform1f(smFloor, 0.0f); + const glm::vec3 skyColor(0.03f, 0.02f, 0.07f); + const glm::vec3 groundColor(0.01f, 0.01f, 0.015f); + const glm::vec3 fogColor(0.015f, 0.01f, 0.035f); + if (smSky >= 0) + glUniform3fv(smSky, 1, glm::value_ptr(skyColor)); + if (smGround >= 0) + glUniform3fv(smGround, 1, glm::value_ptr(groundColor)); + if (smFogColor >= 0) + glUniform3fv(smFogColor, 1, glm::value_ptr(fogColor)); + if (smFogNear >= 0) + glUniform1f(smFogNear, 28.0f); + if (smFogFar >= 0) + glUniform1f(smFogFar, 80.0f); - const glm::vec3 skyColor(0.03f, 0.02f, 0.07f); - const glm::vec3 groundColor(0.01f, 0.01f, 0.015f); - const glm::vec3 fogColor(0.015f, 0.01f, 0.035f); - const float fogNear = 28.0f; - const float fogFar = 80.0f; + const glm::vec3 specularColor(0.95f, 0.97f, 1.0f); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); - if (smSky >= 0) - glUniform3fv(smSky, 1, glm::value_ptr(skyColor)); - if (smGround >= 0) - glUniform3fv(smGround, 1, glm::value_ptr(groundColor)); - if (smFogColor >= 0) - glUniform3fv(smFogColor, 1, glm::value_ptr(fogColor)); - if (smFogNear >= 0) - glUniform1f(smFogNear, fogNear); - if (smFogFar >= 0) - glUniform1f(smFogFar, fogFar); - - const glm::vec3 specularColor(0.95f, 0.97f, 1.0f); - - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - - for (const auto &c : world.getDistanceConstraints()) - { - if (!c.a || !c.b) - continue; - glm::vec3 p0(c.a->position.x, c.a->position.y, c.a->position.z); - glm::vec3 p1(c.b->position.x, c.b->position.y, c.b->position.z); - std::vector solid; - solid.reserve(4096); - if (c.type == DistanceConstraint::ROPE) - pushCylinderSolid(solid, p0, p1, 0.045f, 12); - else if (c.type == DistanceConstraint::ROD) - pushCylinderSolid(solid, p0, p1, 0.07f, 14); - else - pushSpringSolid(solid, p0, p1, 0.028f, 0.22f, 10, 80, 8); - - if (solid.empty()) - continue; - glBufferData(GL_ARRAY_BUFFER, solid.size() * sizeof(float), solid.data(), GL_DYNAMIC_DRAW); - float r, g, b; - constraintBaseColor(c.type, r, g, b); - tintRgb(r, g, b, tintR, tintG, tintB); - - // Constraint shader uses Material for lighting, and uColor only for alpha. - if (smMatAmbient >= 0) - glUniform3f(smMatAmbient, r, g, b); - if (smMatDiffuse >= 0) - glUniform3f(smMatDiffuse, r, g, b); - if (smMatSpecular >= 0) - glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); - if (smMatShininess >= 0) - { - // Small variety makes different constraint types read better. - float shininess = 64.0f; - if (c.type == DistanceConstraint::ROPE) - shininess = 24.0f; - else if (c.type == DistanceConstraint::ROD) - shininess = 96.0f; - else - shininess = 48.0f; // spring - glUniform1f(smMatShininess, shininess); - } - - if (smCol >= 0) - glUniform4f(smCol, r, g, b, 1.0f); - glDrawArrays(GL_TRIANGLES, 0, static_cast(solid.size() / 6)); - } - glBindVertexArray(0); + for (const auto &constraint : api.getDistanceConstraints()) + { + auto bodyA = api.getBody(constraint.firstBodyId); + auto bodyB = api.getBody(constraint.secondBodyId); + if (!bodyA || !bodyB) + continue; + glm::vec3 p0(bodyA->position.x, bodyA->position.y, bodyA->position.z); + glm::vec3 p1(bodyB->position.x, bodyB->position.y, bodyB->position.z); + std::vector solid = buildSolidConstraint(constraint, p0, p1); + if (solid.empty()) + continue; + glBufferData(GL_ARRAY_BUFFER, solid.size() * sizeof(float), solid.data(), GL_DYNAMIC_DRAW); + float r, g, b; + constraintBaseColor(constraint.type, r, g, b); + tintRgb(r, g, b, tintR, tintG, tintB); + if (smMatAmbient >= 0) + glUniform3f(smMatAmbient, r, g, b); + if (smMatDiffuse >= 0) + glUniform3f(smMatDiffuse, r, g, b); + if (smMatSpecular >= 0) + glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); + if (smMatShininess >= 0) + glUniform1f(smMatShininess, constraint.type == ConstraintType::Rope ? 24.0f : constraint.type == ConstraintType::Rod ? 96.0f : 48.0f); + if (smCol >= 0) + glUniform4f(smCol, r, g, b, 1.0f); + glDrawArrays(GL_TRIANGLES, 0, static_cast(solid.size() / 6)); + } + glBindVertexArray(0); } void RenderDistanceConstraintsWire( - PhysicsWorld &world, + AetherAPI &api, const glm::mat4 &model, const glm::mat4 &view, const glm::mat4 &projection, @@ -303,41 +301,36 @@ void RenderDistanceConstraintsWire( float tintG, float tintB) { - GLint modelLoc = glGetUniformLocation(program, "uModel"); - GLint viewLoc = glGetUniformLocation(program, "uView"); - GLint projLoc = glGetUniformLocation(program, "uProjection"); - GLint colorLoc = glGetUniformLocation(program, "uColor"); - glUseProgram(program); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glLineWidth(2.5f); + GLint modelLoc = glGetUniformLocation(program, "uModel"); + GLint viewLoc = glGetUniformLocation(program, "uView"); + GLint projLoc = glGetUniformLocation(program, "uProjection"); + GLint colorLoc = glGetUniformLocation(program, "uColor"); + glUseProgram(program); + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); + glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glLineWidth(2.5f); - for (const auto &c : world.getDistanceConstraints()) - { - if (!c.a || !c.b) - continue; - glm::vec3 p0(c.a->position.x, c.a->position.y, c.a->position.z); - glm::vec3 p1(c.b->position.x, c.b->position.y, c.b->position.z); - std::vector lines; - if (c.type == DistanceConstraint::ROPE) - pushDashedLine(lines, p0, p1, 0.12f, 0.08f); - else if (c.type == DistanceConstraint::ROD) - pushLine(lines, p0, p1); - else - pushHelixWire(lines, p0, p1, 0.22f, 10, 96); - - if (lines.empty()) - continue; - glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_DYNAMIC_DRAW); - float r, g, b; - constraintBaseColor(c.type, r, g, b); - tintRgb(r, g, b, tintR, tintG, tintB); - if (colorLoc >= 0) - glUniform4f(colorLoc, r, g, b, 1.0f); - glDrawArrays(GL_LINES, 0, static_cast(lines.size() / 3)); - } - glBindVertexArray(0); -} + for (const auto &constraint : api.getDistanceConstraints()) + { + auto bodyA = api.getBody(constraint.firstBodyId); + auto bodyB = api.getBody(constraint.secondBodyId); + if (!bodyA || !bodyB) + continue; + glm::vec3 p0(bodyA->position.x, bodyA->position.y, bodyA->position.z); + glm::vec3 p1(bodyB->position.x, bodyB->position.y, bodyB->position.z); + std::vector lines = buildWireConstraint(constraint, p0, p1); + if (lines.empty()) + continue; + glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_DYNAMIC_DRAW); + float r, g, b; + constraintBaseColor(constraint.type, r, g, b); + tintRgb(r, g, b, tintR, tintG, tintB); + if (colorLoc >= 0) + glUniform4f(colorLoc, r, g, b, 1.0f); + glDrawArrays(GL_LINES, 0, static_cast(lines.size() / 3)); + } + glBindVertexArray(0); +} \ No newline at end of file diff --git a/renderer/drawconstraints.hpp b/renderer/drawconstraints.hpp index c0b96be..d6946a1 100644 --- a/renderer/drawconstraints.hpp +++ b/renderer/drawconstraints.hpp @@ -1,11 +1,11 @@ #pragma once -#include "../engine/world/physicsworld.hpp" +#include "api/AetherAPI.hpp" #include #include void RenderDistanceConstraintsSolid( - PhysicsWorld &world, + AetherAPI &api, const glm::mat4 &view, const glm::mat4 &projection, GLuint program, @@ -18,7 +18,7 @@ void RenderDistanceConstraintsSolid( float tintB); void RenderDistanceConstraintsWire( - PhysicsWorld &world, + AetherAPI &api, const glm::mat4 &model, const glm::mat4 &view, const glm::mat4 &projection, diff --git a/renderer/window.cpp b/renderer/window.cpp index e500100..e5b78a6 100644 --- a/renderer/window.cpp +++ b/renderer/window.cpp @@ -5,7 +5,7 @@ #include #include -#include "../engine/world/physicsworld.hpp" +#include "api/AetherAPI.hpp" #include "camera.hpp" #include "drawbodies.hpp" #include "bodymenu.hpp" @@ -153,7 +153,7 @@ static void ShowTooltip(const char *text) } } -void CreateWindow(PhysicsWorld &world) +void CreateWindow(AetherAPI &world) { // Initialize test scenario mapping InitializeTestMap(); @@ -248,7 +248,7 @@ void CreateWindow(PhysicsWorld &world) auto reloadSelectedScenario = [&]() { - world = PhysicsWorld(); + world.reset(); std::string chapterName = chapters[selectedChapterIndex]; std::vector scenariosInChapter = testmap[chapterName]; @@ -265,41 +265,59 @@ void CreateWindow(PhysicsWorld &world) simulation_time = 0.0f; frame = 0; }; -PhysicsWorld main_menu_world = PhysicsWorld(-6.0); -BoxCollider horizontal_bound(Vec3(10.0f, 0.5f, 5.0f)); -const uint32_t menu_floor_id = main_menu_world.addBody(Rigidbody(Vec3(0.0f, -1.5f, 0.0f), Vec3(), &horizontal_bound, 0.0f, 0.0f, 1.0f)); // floor -const uint32_t menu_ceiling_id = main_menu_world.addBody(Rigidbody(Vec3(0.0f, 13.5f, 0.0f), Vec3(), &horizontal_bound, 0.0f, 0.0f, 1.0f)); // ceiling -BoxCollider vertical_bound(Vec3(0.5f, 8.0f, 5.0f)); -const uint32_t menu_right_wall_id = main_menu_world.addBody(Rigidbody(Vec3(11.5f, 5.0f, 0.0f), Vec3(), &vertical_bound, 0.0f, 0.0f, 1.0f)); // right -const uint32_t menu_left_wall_id = main_menu_world.addBody(Rigidbody(Vec3(-11.5f, 5.0f, 0.0f), Vec3(), &vertical_bound, 0.0f, 0.0f, 1.0f)); // left -const std::array menu_wall_ids = {menu_floor_id, menu_ceiling_id, menu_right_wall_id, menu_left_wall_id}; -BoxCollider face_bound(Vec3(10.0f, 8.0f, 0.5f)); -// main_menu_world.addBody(Rigidbody(Vec3(0.0f, 5.0f, 5.0f), Vec3(), &face_bound, 0.0f)); // front -// main_menu_world.addBody(Rigidbody(Vec3(0.0f, 5.0f, -5.0f), Vec3(), &face_bound, 0.0f)); // back +AetherAPI main_menu_world; +main_menu_world.setGravity(Vec3(0.0f, -6.0f, 0.0f)); + +BoxSpawnInfo horizontal_bound_floor; +horizontal_bound_floor.position = Vec3(0.0f, -1.5f, 0.0f); +horizontal_bound_floor.halfSize = Vec3(10.0f, 0.5f, 5.0f); +horizontal_bound_floor.mass = 0.0f; +horizontal_bound_floor.friction = 0.0f; +horizontal_bound_floor.restitution = 1.0f; +const BodyID menu_floor_id = main_menu_world.createBox(horizontal_bound_floor); + +BoxSpawnInfo horizontal_bound_ceiling = horizontal_bound_floor; +horizontal_bound_ceiling.position = Vec3(0.0f, 13.5f, 0.0f); +const BodyID menu_ceiling_id = main_menu_world.createBox(horizontal_bound_ceiling); + +BoxSpawnInfo vertical_bound; +vertical_bound.halfSize = Vec3(0.5f, 8.0f, 5.0f); +vertical_bound.mass = 0.0f; +vertical_bound.friction = 0.0f; +vertical_bound.restitution = 1.0f; +vertical_bound.position = Vec3(11.5f, 5.0f, 0.0f); +const BodyID menu_right_wall_id = main_menu_world.createBox(vertical_bound); +vertical_bound.position = Vec3(-11.5f, 5.0f, 0.0f); +const BodyID menu_left_wall_id = main_menu_world.createBox(vertical_bound); +const std::array menu_wall_ids = {menu_floor_id, menu_ceiling_id, menu_right_wall_id, menu_left_wall_id}; double mouseX, mouseY; -SphereCollider cursor_ghost_collider(1.5f); glfwGetCursorPos(window, &mouseX, &mouseY); -SphereCollider nig(1.011f); +SphereSpawnInfo nig; +nig.radius = 1.011f; +nig.mass = 0.5f; +nig.restitution = 0.9f; for(int i=0; i<50; i++){ float px = -10.0f + (i % 10) * 3.3f + (rand() % 100 - 50) * 0.02f; float py = 1.5f + (i / 10) * 2.0f + (rand() % 100 - 50) * 0.02f; - main_menu_world.addBody(Rigidbody(Vec3(px, py, 0.0f),Vec3(),&nig, 0.5f, 0.0f, 0.9f)); + nig.position = Vec3(px, py, 0.0f); + main_menu_world.createSphere(nig); } -const uint32_t ghost_id=main_menu_world.addBody(Rigidbody(Vec3(0.0f,0.0f ,0.0f),Vec3(),&cursor_ghost_collider,0.0f,0.0f,1.0f)); -// PhysicsWorld can reallocate its body buffer during step(), so always re-fetch the ghost pointer by ID. -auto reacquireGhost = [&]() -> Rigidbody * -{ - return main_menu_world.getBodyByID(ghost_id); -}; +SphereSpawnInfo cursorGhost; +cursorGhost.radius = 1.5f; +cursorGhost.mass = 0.0f; +cursorGhost.restitution = 1.0f; +const BodyID ghost_id = main_menu_world.createSphere(cursorGhost); auto enforceMenuInvisibility = [&]() { for (uint32_t wall_id : menu_wall_ids) { - if (Rigidbody *wall = main_menu_world.getBodyByID(wall_id)) + if (auto wall = main_menu_world.getBody(wall_id)) { - wall->render_alpha = 0.0f; + BodyState edited = *wall; + edited.renderAlpha = 0.0f; + main_menu_world.updateBody(edited); } } }; @@ -350,7 +368,7 @@ glfwGetCursorPos(window, &mouseX, &mouseY); ? static_cast(framebufferWidth) / static_cast(framebufferHeight) : 1.0f; - RenderBodies(world, camera, aspectRatio); + RenderBodies(world, camera, aspectRatio); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); @@ -440,7 +458,7 @@ glfwGetCursorPos(window, &mouseX, &mouseY); ImGui::EndMenu(); } - if (!world.enable_buoyancy) + if (!world.getBuoyancySettings().enabled) { if (ImGui::BeginMenu("Constraints")) { @@ -546,10 +564,12 @@ glfwGetCursorPos(window, &mouseX, &mouseY); float x = (10.0 * mouseX / framebufferHeight) - 9.0f; float y = (10.0 * mouseY / framebufferHeight) -0.0f; Vec3 ghostTarget(x, y, 0.0f); - if (Rigidbody *ghost = reacquireGhost()) + if (auto ghost = main_menu_world.getBody(ghost_id)) { - ghost->render_alpha = 0.0f; - ghost->position = ghostTarget; + BodyState edited = *ghost; + edited.renderAlpha = 0.0f; + edited.position = ghostTarget; + main_menu_world.updateBody(edited); } menuAccumulator += frametime; @@ -566,13 +586,14 @@ glfwGetCursorPos(window, &mouseX, &mouseY); menuAccumulator = 0.0f; } - if (Rigidbody *ghost = reacquireGhost()) + if (auto ghost = main_menu_world.getBody(ghost_id)) { - ghost->render_alpha = 0.0f; - ghost->position = ghostTarget; - //std::cout<position.x<<" "<position.y<<'\n'; + BodyState edited = *ghost; + edited.renderAlpha = 0.0f; + edited.position = ghostTarget; + main_menu_world.updateBody(edited); } - RenderBodies(main_menu_world,camera,aspectRatio); + RenderBodies(main_menu_world,camera,aspectRatio); const char *startWindowTitle = hasActiveSim ? "Aether Studio - Menu" : "Aether Studio - Start"; if (ImGui::Begin(startWindowTitle, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { diff --git a/renderer/window.hpp b/renderer/window.hpp index 9d6a8e4..2297f1e 100644 --- a/renderer/window.hpp +++ b/renderer/window.hpp @@ -1,6 +1,6 @@ #pragma once #include"engine_configs.hpp" #include -class PhysicsWorld; +class AetherAPI; -void CreateWindow(PhysicsWorld& world); \ No newline at end of file +void CreateWindow(AetherAPI& api); \ No newline at end of file diff --git a/wasm/main.cpp b/wasm/main.cpp new file mode 100644 index 0000000..18f681e --- /dev/null +++ b/wasm/main.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include "api/AetherAPI.hpp" + +static AetherAPI engine; +static bool initialized = false; + +extern "C" { + +EMSCRIPTEN_KEEPALIVE +void Init() +{ + if (initialized) return; + + initialized = true; + + BoxSpawnInfo box; + box.position = Vec3(0, 5, 0); + box.mass = 1.0f; + box.velocity = Vec3(0, 5, 0); + engine.createBox(box); +} + +EMSCRIPTEN_KEEPALIVE +void Step() +{ + engine.step(1.0f / 60.0f); + + auto bodies = engine.getRenderBodies(); + + if (!bodies.empty()) + { + std::cout + << "Body position: " + << bodies[0].position.x << " " + << bodies[0].position.y << " " + << bodies[0].position.z + << std::endl; + } +} + +} \ No newline at end of file