From 1a379034355d27ef8615a8ce2ab4e1a14afea0b3 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Fri, 19 Dec 2025 16:39:30 +0000 Subject: [PATCH] bullet3 support --- CMakeLists.txt | 5 +- scripts/cube_logic.lua | 97 +++++++++++++-- src/script/cube_script.cpp | 234 ++++++++++++++++++++++++++++++++++++- src/script/cube_script.hpp | 6 + 4 files changed, 332 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c26ed5..c591dfc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ find_package(lua CONFIG REQUIRED) find_package(CLI11 CONFIG REQUIRED) find_package(rapidjson CONFIG REQUIRED) find_package(assimp CONFIG REQUIRED) +find_package(Bullet CONFIG REQUIRED) if(BUILD_SDL3_APP) add_executable(sdl3_app @@ -93,7 +94,7 @@ add_executable(sdl3_app src/script/cube_script.cpp ) target_include_directories(sdl3_app PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") - target_link_libraries(sdl3_app PRIVATE sdl::sdl Vulkan::Vulkan lua::lua CLI11::CLI11 rapidjson assimp::assimp) + target_link_libraries(sdl3_app PRIVATE sdl::sdl Vulkan::Vulkan lua::lua CLI11::CLI11 rapidjson assimp::assimp Bullet::Bullet) target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/shaders" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") @@ -107,5 +108,5 @@ add_executable(cube_script_tests src/script/cube_script.cpp ) target_include_directories(cube_script_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") -target_link_libraries(cube_script_tests PRIVATE lua::lua assimp::assimp) +target_link_libraries(cube_script_tests PRIVATE lua::lua assimp::assimp Bullet::Bullet) add_test(NAME cube_script_tests COMMAND cube_script_tests) diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 0efb99e..1f79c36 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -113,9 +113,72 @@ end if cube_mesh_info.loaded then log_debug("Loaded cube mesh from %s (%d vertices, %d indices)", cube_mesh_info.path, cube_mesh_info.vertex_count, cube_mesh_info.index_count) -else - log_debug("Failed to load cube mesh (%s); using fallback cube", - cube_mesh_info.error or "unknown") +end + +local cube_body_name = "cube_body" +local cube_state = { + position = {0.0, 0.0, 0.0}, + rotation = {0.0, 0.0, 0.0, 1.0}, +} +local physics_last_time = 0.0 + +local function quaternion_to_matrix(q) + local x, y, z, w = q[1], q[2], q[3], q[4] + local xx = x * x + local yy = y * y + local zz = z * z + local xy = x * y + local xz = x * z + local yz = y * z + local wx = w * x + local wy = w * y + local wz = w * z + return { + 1.0 - 2.0 * yy - 2.0 * zz, 2.0 * xy + 2.0 * wz, 2.0 * xz - 2.0 * wy, 0.0, + 2.0 * xy - 2.0 * wz, 1.0 - 2.0 * xx - 2.0 * zz, 2.0 * yz + 2.0 * wx, 0.0, + 2.0 * xz + 2.0 * wy, 2.0 * yz - 2.0 * wx, 1.0 - 2.0 * xx - 2.0 * yy, 0.0, + 0.0, 0.0, 0.0, 1.0, + } +end + +local function initialize_physics() + if type(physics_create_box) ~= "function" then + error("physics_create_box() is unavailable") + end + local ok, err = physics_create_box( + cube_body_name, + {1.0, 1.0, 1.0}, + 1.0, + {0.0, 2.0, 0.0}, + {0.0, 0.0, 0.0, 1.0} + ) + if not ok then + error("physics_create_box failed: " .. (err or "unknown")) + end + if type(physics_step_simulation) == "function" then + physics_step_simulation(0.0) + end +end +initialize_physics() + +local function sync_physics(time) + local dt = time - physics_last_time + if dt < 0.0 then + dt = 0.0 + end + if dt > 0.0 and type(physics_step_simulation) == "function" then + physics_step_simulation(dt) + end + physics_last_time = time + if type(physics_get_transform) ~= "function" then + error("physics_get_transform() is unavailable") + end + local transform, err = physics_get_transform(cube_body_name) + if not transform then + error("physics_get_transform failed: " .. (err or "unknown")) + end + cube_state.position = transform.position + cube_state.rotation = transform.rotation end local rotation_speeds = {x = 0.5, y = 0.7} @@ -188,7 +251,7 @@ local function build_model(time) return math3d.multiply(y, x) end -local function create_cube(position, speed_scale, shader_key) +local function create_rotating_cube(position, speed_scale, shader_key) local function compute_model_matrix(time) local base = build_model(time * speed_scale) local offset = math3d.translation(position[1], position[2], position[3]) @@ -203,6 +266,26 @@ local function create_cube(position, speed_scale, shader_key) } end +local function create_physics_cube(shader_key) + local function compute_model_matrix(time) + sync_physics(time) + local offset = math3d.translation( + cube_state.position[1], + cube_state.position[2], + cube_state.position[3] + ) + local rotation_matrix = quaternion_to_matrix(cube_state.rotation) + return math3d.multiply(offset, rotation_matrix) + end + + return { + vertices = cube_vertices, + indices = cube_indices, + compute_model_matrix = compute_model_matrix, + shader_key = shader_key or "cube", + } +end + local function create_pyramid(position, shader_key) local function compute_model_matrix(time) local base = build_model(time * 0.6) @@ -220,9 +303,9 @@ end function get_scene_objects() local objects = { - create_cube({0.0, 0.0, 0.0}, 1.0, "cube"), - create_cube({3.0, 0.0, 0.0}, 0.8, "cube"), - create_cube({-3.0, 0.0, 0.0}, 1.2, "cube"), + create_physics_cube("cube"), + create_rotating_cube({3.0, 0.0, 0.0}, 0.8, "cube"), + create_rotating_cube({-3.0, 0.0, 0.0}, 1.2, "cube"), create_pyramid({0.0, -0.5, -4.0}, "pyramid"), } if lua_debug then diff --git a/src/script/cube_script.cpp b/src/script/cube_script.cpp index 5cfcef7..8085a50 100644 --- a/src/script/cube_script.cpp +++ b/src/script/cube_script.cpp @@ -4,18 +4,135 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include +#include namespace sdl3cpp::script { namespace { +struct PhysicsBridge { + struct BodyRecord { + std::unique_ptr shape; + std::unique_ptr motionState; + std::unique_ptr body; + }; + + PhysicsBridge(); + ~PhysicsBridge(); + + bool addBoxRigidBody(const std::string& name, + const btVector3& halfExtents, + float mass, + const btTransform& transform, + std::string& error); + int stepSimulation(float deltaTime); + bool getRigidBodyTransform(const std::string& name, + btTransform& outTransform, + std::string& error) const; + +private: + std::unique_ptr collisionConfig_; + std::unique_ptr dispatcher_; + std::unique_ptr broadphase_; + std::unique_ptr solver_; + std::unique_ptr world_; + std::unordered_map bodies_; +}; + +PhysicsBridge::PhysicsBridge() + : collisionConfig_(std::make_unique()), + dispatcher_(std::make_unique(collisionConfig_.get())), + broadphase_(std::make_unique()), + solver_(std::make_unique()), + world_(std::make_unique( + dispatcher_.get(), + broadphase_.get(), + solver_.get(), + collisionConfig_.get())) { + world_->setGravity(btVector3(0.0f, -9.81f, 0.0f)); +} + +PhysicsBridge::~PhysicsBridge() { + if (world_) { + for (auto& [name, entry] : bodies_) { + if (entry.body) { + world_->removeRigidBody(entry.body.get()); + } + } + } +} + +bool PhysicsBridge::addBoxRigidBody(const std::string& name, + const btVector3& halfExtents, + float mass, + const btTransform& transform, + std::string& error) { + if (name.empty()) { + error = "Rigid body name must not be empty"; + return false; + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + if (bodies_.count(name)) { + error = "Rigid body already exists: " + name; + return false; + } + auto shape = std::make_unique(halfExtents); + btVector3 inertia(0.0f, 0.0f, 0.0f); + if (mass > 0.0f) { + shape->calculateLocalInertia(mass, inertia); + } + auto motionState = std::make_unique(transform); + btRigidBody::btRigidBodyConstructionInfo constructionInfo( + mass, + motionState.get(), + shape.get(), + inertia); + auto body = std::make_unique(constructionInfo); + world_->addRigidBody(body.get()); + bodies_.emplace(name, BodyRecord{ + std::move(shape), + std::move(motionState), + std::move(body), + }); + return true; +} + +int PhysicsBridge::stepSimulation(float deltaTime) { + if (!world_) { + return 0; + } + return static_cast(world_->stepSimulation(deltaTime, 10, 1.0f / 60.0f)); +} + +bool PhysicsBridge::getRigidBodyTransform(const std::string& name, + btTransform& outTransform, + std::string& error) const { + auto it = bodies_.find(name); + if (it == bodies_.end()) { + error = "Rigid body not found: " + name; + return false; + } + if (!it->second.motionState) { + error = "Rigid body motion state is missing"; + return false; + } + it->second.motionState->getWorldTransform(outTransform); + return true; +} + struct MeshPayload { std::vector> positions; std::vector> colors; @@ -153,6 +270,83 @@ int LuaLoadMeshFromFile(lua_State* L) { return 2; } +int LuaPhysicsCreateBox(lua_State* L) { + auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* name = luaL_checkstring(L, 1); + if (!lua_istable(L, 2) || !lua_istable(L, 4) || !lua_istable(L, 5)) { + luaL_error(L, "physics_create_box expects vector tables for half extents, origin, and rotation"); + } + std::array halfExtents = ReadVector3(L, 2); + float mass = static_cast(luaL_checknumber(L, 3)); + std::array origin = ReadVector3(L, 4); + std::array rotation = ReadQuaternion(L, 5); + + btTransform transform; + transform.setIdentity(); + transform.setOrigin(btVector3(origin[0], origin[1], origin[2])); + transform.setRotation(btQuaternion(rotation[0], rotation[1], rotation[2], rotation[3])); + + std::string error; + if (!script->GetPhysicsBridge().addBoxRigidBody( + name, + btVector3(halfExtents[0], halfExtents[1], halfExtents[2]), + mass, + transform, + error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int LuaPhysicsStepSimulation(lua_State* L) { + auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + float deltaTime = static_cast(luaL_checknumber(L, 1)); + int steps = script->GetPhysicsBridge().stepSimulation(deltaTime); + lua_pushinteger(L, steps); + return 1; +} + +int LuaPhysicsGetTransform(lua_State* L) { + auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* name = luaL_checkstring(L, 1); + btTransform transform; + std::string error; + if (!script->GetPhysicsBridge().getRigidBodyTransform(name, transform, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_newtable(L); + lua_newtable(L); + const btVector3& origin = transform.getOrigin(); + lua_pushnumber(L, origin.x()); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, origin.y()); + lua_rawseti(L, -2, 2); + lua_pushnumber(L, origin.z()); + lua_rawseti(L, -2, 3); + lua_setfield(L, -2, "position"); + + lua_newtable(L); + const btQuaternion& orientation = transform.getRotation(); + lua_pushnumber(L, orientation.x()); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, orientation.y()); + lua_rawseti(L, -2, 2); + lua_pushnumber(L, orientation.z()); + lua_rawseti(L, -2, 3); + lua_pushnumber(L, orientation.w()); + lua_rawseti(L, -2, 4); + lua_setfield(L, -2, "rotation"); + + return 1; +} + std::array IdentityMatrix() { return {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, @@ -163,7 +357,10 @@ std::array IdentityMatrix() { } // namespace CubeScript::CubeScript(const std::filesystem::path& scriptPath, bool debugEnabled) - : L_(luaL_newstate()), scriptDirectory_(scriptPath.parent_path()), debugEnabled_(debugEnabled) { + : L_(luaL_newstate()), + scriptDirectory_(scriptPath.parent_path()), + debugEnabled_(debugEnabled), + physicsBridge_(std::make_unique()) { if (!L_) { throw std::runtime_error("Failed to create Lua state"); } @@ -171,6 +368,18 @@ CubeScript::CubeScript(const std::filesystem::path& scriptPath, bool debugEnable lua_pushlightuserdata(L_, this); lua_pushcclosure(L_, &LuaLoadMeshFromFile, 1); lua_setglobal(L_, "load_mesh_from_file"); + lua_pushlightuserdata(L_, this); + lua_pushcclosure(L_, &LuaPhysicsCreateBox, 1); + lua_setglobal(L_, "physics_create_box"); + lua_pushlightuserdata(L_, this); + lua_pushcclosure(L_, &LuaPhysicsStepSimulation, 1); + lua_setglobal(L_, "physics_step_simulation"); + lua_pushlightuserdata(L_, this); + lua_pushcclosure(L_, &LuaPhysicsGetTransform, 1); + lua_setglobal(L_, "physics_get_transform"); + lua_pushlightuserdata(L_, this); + lua_pushcclosure(L_, &LuaLoadMeshFromFile, 1); + lua_setglobal(L_, "load_mesh_from_file"); lua_pushboolean(L_, debugEnabled_); lua_setglobal(L_, "lua_debug"); auto scriptDir = scriptPath.parent_path(); @@ -494,11 +703,34 @@ std::array CubeScript::ReadMatrix(lua_State* L, int index) { return result; } +std::array CubeScript::ReadQuaternion(lua_State* L, int index) { + std::array result{}; + int absIndex = lua_absindex(L, index); + size_t len = lua_rawlen(L, absIndex); + if (len != 4) { + throw std::runtime_error("Expected quaternion with 4 components"); + } + for (size_t i = 1; i <= 4; ++i) { + lua_rawgeti(L, absIndex, static_cast(i)); + if (!lua_isnumber(L, -1)) { + lua_pop(L, 1); + throw std::runtime_error("Quaternion component is not a number"); + } + result[i - 1] = static_cast(lua_tonumber(L, -1)); + lua_pop(L, 1); + } + return result; +} + std::string CubeScript::LuaErrorMessage(lua_State* L) { const char* message = lua_tostring(L, -1); return message ? message : "unknown lua error"; } +PhysicsBridge& CubeScript::GetPhysicsBridge() { + return *physicsBridge_; +} + std::vector CubeScript::LoadGuiCommands() { std::vector commands; if (guiCommandsFnRef_ == LUA_REFNIL) { diff --git a/src/script/cube_script.hpp b/src/script/cube_script.hpp index d98235d..32debe3 100644 --- a/src/script/cube_script.hpp +++ b/src/script/cube_script.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -13,6 +14,8 @@ namespace sdl3cpp::script { +struct PhysicsBridge; + struct GuiInputSnapshot { float mouseX = 0.0f; float mouseY = 0.0f; @@ -91,10 +94,12 @@ public: void UpdateGuiInput(const GuiInputSnapshot& input); bool HasGuiCommands() const; std::filesystem::path GetScriptDirectory() const; + PhysicsBridge& GetPhysicsBridge(); private: static std::array ReadVector3(lua_State* L, int index); static std::array ReadMatrix(lua_State* L, int index); + static std::array ReadQuaternion(lua_State* L, int index); static std::vector ReadVertexArray(lua_State* L, int index); static std::vector ReadIndexArray(lua_State* L, int index); static std::string LuaErrorMessage(lua_State* L); @@ -108,6 +113,7 @@ private: int guiCommandsFnRef_ = LUA_REFNIL; std::filesystem::path scriptDirectory_; bool debugEnabled_ = false; + std::unique_ptr physicsBridge_; }; } // namespace sdl3cpp::script