bullet3 support

This commit is contained in:
Richard Ward
2025-12-19 16:39:30 +00:00
parent bdc2b8b841
commit 1a37903435
4 changed files with 332 additions and 10 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -4,18 +4,135 @@
#include <assimp/material.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <btBulletDynamicsCommon.h>
#include <array>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <string>
#include <system_error>
#include <unordered_map>
#include <utility>
#include <vector>
namespace sdl3cpp::script {
namespace {
struct PhysicsBridge {
struct BodyRecord {
std::unique_ptr<btCollisionShape> shape;
std::unique_ptr<btMotionState> motionState;
std::unique_ptr<btRigidBody> 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<btDefaultCollisionConfiguration> collisionConfig_;
std::unique_ptr<btCollisionDispatcher> dispatcher_;
std::unique_ptr<btBroadphaseInterface> broadphase_;
std::unique_ptr<btSequentialImpulseConstraintSolver> solver_;
std::unique_ptr<btDiscreteDynamicsWorld> world_;
std::unordered_map<std::string, BodyRecord> bodies_;
};
PhysicsBridge::PhysicsBridge()
: collisionConfig_(std::make_unique<btDefaultCollisionConfiguration>()),
dispatcher_(std::make_unique<btCollisionDispatcher>(collisionConfig_.get())),
broadphase_(std::make_unique<btDbvtBroadphase>()),
solver_(std::make_unique<btSequentialImpulseConstraintSolver>()),
world_(std::make_unique<btDiscreteDynamicsWorld>(
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<btBoxShape>(halfExtents);
btVector3 inertia(0.0f, 0.0f, 0.0f);
if (mass > 0.0f) {
shape->calculateLocalInertia(mass, inertia);
}
auto motionState = std::make_unique<btDefaultMotionState>(transform);
btRigidBody::btRigidBodyConstructionInfo constructionInfo(
mass,
motionState.get(),
shape.get(),
inertia);
auto body = std::make_unique<btRigidBody>(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<int>(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<std::array<float, 3>> positions;
std::vector<std::array<float, 3>> colors;
@@ -153,6 +270,83 @@ int LuaLoadMeshFromFile(lua_State* L) {
return 2;
}
int LuaPhysicsCreateBox(lua_State* L) {
auto* script = static_cast<CubeScript*>(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<float, 3> halfExtents = ReadVector3(L, 2);
float mass = static_cast<float>(luaL_checknumber(L, 3));
std::array<float, 3> origin = ReadVector3(L, 4);
std::array<float, 4> 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<CubeScript*>(lua_touserdata(L, lua_upvalueindex(1)));
float deltaTime = static_cast<float>(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<CubeScript*>(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<float, 16> IdentityMatrix() {
return {1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
@@ -163,7 +357,10 @@ std::array<float, 16> 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<PhysicsBridge>()) {
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<float, 16> CubeScript::ReadMatrix(lua_State* L, int index) {
return result;
}
std::array<float, 4> CubeScript::ReadQuaternion(lua_State* L, int index) {
std::array<float, 4> 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<int>(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<float>(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::GuiCommand> CubeScript::LoadGuiCommands() {
std::vector<GuiCommand> commands;
if (guiCommandsFnRef_ == LUA_REFNIL) {

View File

@@ -3,6 +3,7 @@
#include <array>
#include <filesystem>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
@@ -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<float, 3> ReadVector3(lua_State* L, int index);
static std::array<float, 16> ReadMatrix(lua_State* L, int index);
static std::array<float, 4> ReadQuaternion(lua_State* L, int index);
static std::vector<core::Vertex> ReadVertexArray(lua_State* L, int index);
static std::vector<uint16_t> 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> physicsBridge_;
};
} // namespace sdl3cpp::script