diff --git a/CMakeLists.txt b/CMakeLists.txt index 62147fc..4e7fceb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,9 @@ if(BUILD_SDL3_APP) src/app/vulkan_api.cpp src/script/script_engine.cpp src/script/physics_bridge.cpp + src/script/lua_helpers.cpp + src/script/lua_bindings.cpp + src/script/mesh_loader.cpp ) target_include_directories(sdl3_app PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(sdl3_app PRIVATE ${SDL_TARGET} Vulkan::Vulkan lua::lua CLI11::CLI11 rapidjson assimp::assimp Bullet::Bullet glm::glm Vorbis::vorbisfile Vorbis::vorbis) @@ -138,6 +141,9 @@ add_executable(script_engine_tests tests/test_cube_script.cpp src/script/script_engine.cpp src/script/physics_bridge.cpp + src/script/lua_helpers.cpp + src/script/lua_bindings.cpp + src/script/mesh_loader.cpp src/app/audio_player.cpp ) target_include_directories(script_engine_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/src/script/lua_bindings.cpp b/src/script/lua_bindings.cpp new file mode 100644 index 0000000..eed0f56 --- /dev/null +++ b/src/script/lua_bindings.cpp @@ -0,0 +1,211 @@ +#include "script/lua_bindings.hpp" +#include "script/script_engine.hpp" +#include "script/lua_helpers.hpp" +#include "script/mesh_loader.hpp" + +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::script { + +namespace { + +glm::vec3 ToVec3(const std::array& value) { + return glm::vec3(value[0], value[1], value[2]); +} + +glm::quat ToQuat(const std::array& value) { + return glm::quat(value[3], value[0], value[1], value[2]); +} + +void PushMatrix(lua_State* L, const glm::mat4& matrix) { + lua_newtable(L); + const float* ptr = glm::value_ptr(matrix); + for (int i = 0; i < 16; ++i) { + lua_pushnumber(L, ptr[i]); + lua_rawseti(L, -2, i + 1); + } +} + +} // namespace + +void LuaBindings::RegisterBindings(lua_State* L, ScriptEngine* engine) { + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &LoadMeshFromFile, 1); + lua_setglobal(L, "load_mesh_from_file"); + + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &PhysicsCreateBox, 1); + lua_setglobal(L, "physics_create_box"); + + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &PhysicsStepSimulation, 1); + lua_setglobal(L, "physics_step_simulation"); + + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &PhysicsGetTransform, 1); + lua_setglobal(L, "physics_get_transform"); + + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &GlmMatrixFromTransform, 1); + lua_setglobal(L, "glm_matrix_from_transform"); + + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &AudioPlayBackground, 1); + lua_setglobal(L, "audio_play_background"); + + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, &AudioPlaySound, 1); + lua_setglobal(L, "audio_play_sound"); +} + +int LuaBindings::LoadMeshFromFile(lua_State* L) { + auto* engine = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* path = luaL_checkstring(L, 1); + + MeshPayload payload; + std::string error; + if (!MeshLoader::LoadFromFile(engine->GetScriptDirectory(), path, payload, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + MeshLoader::PushMeshToLua(L, payload); + lua_pushnil(L); + return 2; +} + +int LuaBindings::PhysicsCreateBox(lua_State* L) { + auto* engine = 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 (!engine->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 LuaBindings::PhysicsStepSimulation(lua_State* L) { + auto* engine = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + float deltaTime = static_cast(luaL_checknumber(L, 1)); + int steps = engine->GetPhysicsBridge().stepSimulation(deltaTime); + lua_pushinteger(L, steps); + return 1; +} + +int LuaBindings::PhysicsGetTransform(lua_State* L) { + auto* engine = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* name = luaL_checkstring(L, 1); + + btTransform transform; + std::string error; + if (!engine->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; +} + +int LuaBindings::AudioPlayBackground(lua_State* L) { + auto* engine = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* path = luaL_checkstring(L, 1); + bool loop = true; + if (lua_gettop(L) >= 2 && lua_isboolean(L, 2)) { + loop = lua_toboolean(L, 2); + } + + std::string error; + if (!engine->QueueAudioCommand(ScriptEngine::AudioCommandType::Background, path, loop, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int LuaBindings::AudioPlaySound(lua_State* L) { + auto* engine = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* path = luaL_checkstring(L, 1); + bool loop = false; + if (lua_gettop(L) >= 2 && lua_isboolean(L, 2)) { + loop = lua_toboolean(L, 2); + } + + std::string error; + if (!engine->QueueAudioCommand(ScriptEngine::AudioCommandType::Effect, path, loop, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +int LuaBindings::GlmMatrixFromTransform(lua_State* L) { + std::array translation = ReadVector3(L, 1); + std::array rotation = ReadQuaternion(L, 2); + glm::vec3 pos = ToVec3(translation); + glm::quat quat = ToQuat(rotation); + glm::mat4 matrix = glm::translate(glm::mat4(1.0f), pos) * glm::mat4_cast(quat); + PushMatrix(L, matrix); + return 1; +} + +} // namespace sdl3cpp::script diff --git a/src/script/lua_bindings.hpp b/src/script/lua_bindings.hpp new file mode 100644 index 0000000..26c807b --- /dev/null +++ b/src/script/lua_bindings.hpp @@ -0,0 +1,26 @@ +#ifndef SDL3CPP_SCRIPT_LUA_BINDINGS_HPP +#define SDL3CPP_SCRIPT_LUA_BINDINGS_HPP + +struct lua_State; + +namespace sdl3cpp::script { + +class ScriptEngine; + +class LuaBindings { +public: + static void RegisterBindings(lua_State* L, ScriptEngine* engine); + +private: + static int LoadMeshFromFile(lua_State* L); + static int PhysicsCreateBox(lua_State* L); + static int PhysicsStepSimulation(lua_State* L); + static int PhysicsGetTransform(lua_State* L); + static int AudioPlayBackground(lua_State* L); + static int AudioPlaySound(lua_State* L); + static int GlmMatrixFromTransform(lua_State* L); +}; + +} // namespace sdl3cpp::script + +#endif // SDL3CPP_SCRIPT_LUA_BINDINGS_HPP diff --git a/src/script/lua_helpers.cpp b/src/script/lua_helpers.cpp new file mode 100644 index 0000000..cd1a5a4 --- /dev/null +++ b/src/script/lua_helpers.cpp @@ -0,0 +1,77 @@ +#include "script/lua_helpers.hpp" + +#include +#include + +namespace sdl3cpp::script { + +std::array ReadVector3(lua_State* L, int index) { + std::array result{}; + int absIndex = lua_absindex(L, index); + size_t len = lua_rawlen(L, absIndex); + if (len != 3) { + throw std::runtime_error("Expected vector with 3 components"); + } + for (size_t i = 1; i <= 3; ++i) { + lua_rawgeti(L, absIndex, static_cast(i)); + if (!lua_isnumber(L, -1)) { + lua_pop(L, 1); + throw std::runtime_error("Vector component is not a number"); + } + result[i - 1] = static_cast(lua_tonumber(L, -1)); + lua_pop(L, 1); + } + return result; +} + +std::array 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::array ReadMatrix(lua_State* L, int index) { + std::array result{}; + int absIndex = lua_absindex(L, index); + size_t len = lua_rawlen(L, absIndex); + if (len != 16) { + throw std::runtime_error("Expected 4x4 matrix with 16 components"); + } + for (size_t i = 1; i <= 16; ++i) { + lua_rawgeti(L, absIndex, static_cast(i)); + if (!lua_isnumber(L, -1)) { + lua_pop(L, 1); + throw std::runtime_error("Matrix component is not a number"); + } + result[i - 1] = static_cast(lua_tonumber(L, -1)); + lua_pop(L, 1); + } + return result; +} + +std::string GetLuaError(lua_State* L) { + const char* message = lua_tostring(L, -1); + return message ? message : "unknown lua error"; +} + +std::array IdentityMatrix() { + return {1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; +} + +} // namespace sdl3cpp::script diff --git a/src/script/lua_helpers.hpp b/src/script/lua_helpers.hpp new file mode 100644 index 0000000..645ff0a --- /dev/null +++ b/src/script/lua_helpers.hpp @@ -0,0 +1,19 @@ +#ifndef SDL3CPP_SCRIPT_LUA_HELPERS_HPP +#define SDL3CPP_SCRIPT_LUA_HELPERS_HPP + +#include +#include + +struct lua_State; + +namespace sdl3cpp::script { + +std::array ReadVector3(lua_State* L, int index); +std::array ReadQuaternion(lua_State* L, int index); +std::array ReadMatrix(lua_State* L, int index); +std::string GetLuaError(lua_State* L); +std::array IdentityMatrix(); + +} // namespace sdl3cpp::script + +#endif // SDL3CPP_SCRIPT_LUA_HELPERS_HPP diff --git a/src/script/mesh_loader.cpp b/src/script/mesh_loader.cpp new file mode 100644 index 0000000..5b0e98b --- /dev/null +++ b/src/script/mesh_loader.cpp @@ -0,0 +1,135 @@ +#include "script/mesh_loader.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace sdl3cpp::script { + +bool MeshLoader::LoadFromFile(const std::filesystem::path& scriptDirectory, + const std::string& requestedPath, + MeshPayload& outPayload, + std::string& outError) { + std::filesystem::path resolved(requestedPath); + if (!resolved.is_absolute()) { + resolved = scriptDirectory / resolved; + } + + std::error_code ec; + resolved = std::filesystem::weakly_canonical(resolved, ec); + if (ec) { + outError = "Failed to resolve mesh path: " + ec.message(); + return false; + } + + if (!std::filesystem::exists(resolved)) { + outError = "Mesh file not found: " + resolved.string(); + return false; + } + + Assimp::Importer importer; + const aiScene* scene = importer.ReadFile( + resolved.string(), + aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_PreTransformVertices); + + if (!scene) { + outError = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load mesh"; + return false; + } + + if (scene->mNumMeshes == 0) { + outError = "Scene contains no meshes"; + return false; + } + + const aiMesh* mesh = scene->mMeshes[0]; + if (!mesh->mNumVertices) { + outError = "Mesh contains no vertices"; + return false; + } + + outPayload.positions.reserve(mesh->mNumVertices); + outPayload.colors.reserve(mesh->mNumVertices); + outPayload.indices.reserve(mesh->mNumFaces * 3); + + aiColor3D defaultColor(0.6f, 0.8f, 1.0f); + aiColor3D materialColor = defaultColor; + + if (mesh->mMaterialIndex < scene->mNumMaterials) { + const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; + aiColor4D diffuse; + if (material && material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS) { + materialColor = aiColor3D(diffuse.r, diffuse.g, diffuse.b); + } + } + + for (unsigned i = 0; i < mesh->mNumVertices; ++i) { + const aiVector3D& vertex = mesh->mVertices[i]; + outPayload.positions.push_back({vertex.x, vertex.y, vertex.z}); + + aiColor3D color = materialColor; + if (mesh->HasVertexColors(0) && mesh->mColors[0]) { + const aiColor4D& vertexColor = mesh->mColors[0][i]; + color = aiColor3D(vertexColor.r, vertexColor.g, vertexColor.b); + } + outPayload.colors.push_back({color.r, color.g, color.b}); + } + + for (unsigned faceIndex = 0; faceIndex < mesh->mNumFaces; ++faceIndex) { + const aiFace& face = mesh->mFaces[faceIndex]; + if (face.mNumIndices != 3) { + continue; + } + outPayload.indices.push_back(face.mIndices[0]); + outPayload.indices.push_back(face.mIndices[1]); + outPayload.indices.push_back(face.mIndices[2]); + } + + if (outPayload.indices.empty()) { + outError = "Mesh contains no triangle faces"; + return false; + } + + return true; +} + +int MeshLoader::PushMeshToLua(lua_State* L, const MeshPayload& payload) { + lua_newtable(L); + + lua_newtable(L); + for (size_t vertexIndex = 0; vertexIndex < payload.positions.size(); ++vertexIndex) { + lua_newtable(L); + + lua_newtable(L); + for (int component = 0; component < 3; ++component) { + lua_pushnumber(L, payload.positions[vertexIndex][component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "position"); + + lua_newtable(L); + for (int component = 0; component < 3; ++component) { + lua_pushnumber(L, payload.colors[vertexIndex][component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "color"); + + lua_rawseti(L, -2, static_cast(vertexIndex + 1)); + } + lua_setfield(L, -2, "vertices"); + + lua_newtable(L); + for (size_t index = 0; index < payload.indices.size(); ++index) { + lua_pushinteger(L, static_cast(payload.indices[index]) + 1); + lua_rawseti(L, -2, static_cast(index + 1)); + } + lua_setfield(L, -2, "indices"); + + return 1; +} + +} // namespace sdl3cpp::script diff --git a/src/script/mesh_loader.hpp b/src/script/mesh_loader.hpp new file mode 100644 index 0000000..b91f153 --- /dev/null +++ b/src/script/mesh_loader.hpp @@ -0,0 +1,31 @@ +#ifndef SDL3CPP_SCRIPT_MESH_LOADER_HPP +#define SDL3CPP_SCRIPT_MESH_LOADER_HPP + +#include +#include +#include +#include + +struct lua_State; + +namespace sdl3cpp::script { + +struct MeshPayload { + std::vector> positions; + std::vector> colors; + std::vector indices; +}; + +class MeshLoader { +public: + static bool LoadFromFile(const std::filesystem::path& scriptDirectory, + const std::string& requestedPath, + MeshPayload& outPayload, + std::string& outError); + + static int PushMeshToLua(lua_State* L, const MeshPayload& payload); +}; + +} // namespace sdl3cpp::script + +#endif // SDL3CPP_SCRIPT_MESH_LOADER_HPP