From 96ac0fbba606fba485f55045842e0aa0ec561407 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Fri, 19 Dec 2025 16:30:00 +0000 Subject: [PATCH] load from stl --- CMakeLists.txt | 5 +- scripts/cube_logic.lua | 146 ++++++++++-------------------------- src/script/cube_script.cpp | 147 +++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 109 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d1a5d5..3c26ed5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ endif() find_package(lua CONFIG REQUIRED) find_package(CLI11 CONFIG REQUIRED) find_package(rapidjson CONFIG REQUIRED) +find_package(assimp CONFIG REQUIRED) if(BUILD_SDL3_APP) add_executable(sdl3_app @@ -92,7 +93,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) + target_link_libraries(sdl3_app PRIVATE sdl::sdl Vulkan::Vulkan lua::lua CLI11::CLI11 rapidjson assimp::assimp) target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/shaders" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") @@ -106,5 +107,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) +target_link_libraries(cube_script_tests PRIVATE lua::lua assimp::assimp) add_test(NAME cube_script_tests COMMAND cube_script_tests) diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 26c4450..0efb99e 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -15,114 +15,46 @@ local pyramid_indices = { 4, 5, 2, } -local fallback_cube_vertices = { - { position = {-1.0, -1.0, -1.0}, color = {1.0, 0.0, 0.0} }, - { position = {1.0, -1.0, -1.0}, color = {0.0, 1.0, 0.0} }, - { position = {1.0, 1.0, -1.0}, color = {0.0, 0.0, 1.0} }, - { position = {-1.0, 1.0, -1.0}, color = {1.0, 1.0, 0.0} }, - { position = {-1.0, -1.0, 1.0}, color = {1.0, 0.0, 1.0} }, - { position = {1.0, -1.0, 1.0}, color = {0.0, 1.0, 1.0} }, - { position = {1.0, 1.0, 1.0}, color = {1.0, 1.0, 1.0} }, - { position = {-1.0, 1.0, 1.0}, color = {0.2, 0.2, 0.2} }, -} - -local fallback_cube_indices = { - 1, 2, 3, 3, 4, 1, -- back - 5, 6, 7, 7, 8, 5, -- front - 1, 5, 8, 8, 4, 1, -- left - 2, 6, 7, 7, 3, 2, -- right - 4, 3, 7, 7, 8, 4, -- top - 1, 2, 6, 6, 5, 1, -- bottom -} - -local function resolve_script_directory() - local info = debug.getinfo(1, "S") - local source = info and info.source - if not source then - return "." - end - if source:sub(1, 1) == "@" then - local path = source:sub(2) - return path:match("^(.*[\\/])") or "." - end - return "." -end - -local function combine_paths(first, ...) - local parts = {first, ...} - local combined = parts[1] or "" - for i = 2, #parts do - local piece = parts[i] - if piece and piece ~= "" then - local last = combined:sub(-1) - if last ~= "/" and last ~= "\\" then - combined = combined .. "/" - end - combined = combined .. piece - end - end - return combined -end - -local function load_stl_mesh(path) - local file, err = io.open(path, "r") - if not file then - return nil, nil, err - end - local vertices = {} - local indices = {} - local color = {0.6, 0.8, 1.0} - local vertex_count = 0 - for line in file:lines() do - if line:lower():match("^%s*vertex") then - local coords = {} - for token in line:gmatch("%S+") do - if token:lower() ~= "vertex" then - local value = tonumber(token) - if value then - coords[#coords + 1] = value - end - end - end - if #coords == 3 then - vertex_count = vertex_count + 1 - vertices[vertex_count] = { - position = {coords[1], coords[2], coords[3]}, - color = {color[1], color[2], color[3]}, - } - indices[#indices + 1] = vertex_count - end - end - end - file:close() - if vertex_count == 0 then - return nil, nil, "STL did not include any vertex lines" - end - return vertices, indices, nil -end - -local script_directory = resolve_script_directory() -local stl_cube_path = combine_paths(script_directory, "models", "cube.stl") -local stl_vertices, stl_indices, stl_error = load_stl_mesh(stl_cube_path) - -local cube_vertices = fallback_cube_vertices -local cube_indices = fallback_cube_indices -local stl_debug_info = { - path = stl_cube_path, +local cube_mesh_info = { + path = "models/cube.stl", loaded = false, vertex_count = 0, index_count = 0, - error = stl_error, + error = "load_mesh_from_file() not registered", } -if stl_vertices then - cube_vertices = stl_vertices - cube_indices = stl_indices - stl_debug_info.loaded = true - stl_debug_info.vertex_count = #stl_vertices - stl_debug_info.index_count = #stl_indices -else - stl_debug_info.error = stl_error or "STL file not available" +local cube_vertices = {} +local cube_indices = {} + +local function load_cube_mesh() + if type(load_mesh_from_file) ~= "function" then + cube_mesh_info.error = "load_mesh_from_file() is unavailable" + return + end + + local mesh, err = load_mesh_from_file(cube_mesh_info.path) + if not mesh then + cube_mesh_info.error = err or "load_mesh_from_file() failed" + return + end + + if type(mesh.vertices) ~= "table" or type(mesh.indices) ~= "table" then + cube_mesh_info.error = "loader returned unexpected structure" + return + end + + cube_vertices = mesh.vertices + cube_indices = mesh.indices + cube_mesh_info.loaded = true + cube_mesh_info.vertex_count = #mesh.vertices + cube_mesh_info.index_count = #mesh.indices + cube_mesh_info.error = nil +end + +load_cube_mesh() + +if not cube_mesh_info.loaded then + error("Unable to load cube mesh: " .. (cube_mesh_info.error or "unknown")) end local math3d = require("math3d") @@ -178,12 +110,12 @@ local function log_debug(fmt, ...) print(string_format(fmt, ...)) end -if stl_debug_info.loaded then +if cube_mesh_info.loaded then log_debug("Loaded cube mesh from %s (%d vertices, %d indices)", - stl_debug_info.path, stl_debug_info.vertex_count, stl_debug_info.index_count) + cube_mesh_info.path, cube_mesh_info.vertex_count, cube_mesh_info.index_count) else - log_debug("Failed to load cube STL (%s); using fallback cube", - stl_debug_info.error or "unknown") + log_debug("Failed to load cube mesh (%s); using fallback cube", + cube_mesh_info.error or "unknown") end local rotation_speeds = {x = 0.5, y = 0.7} diff --git a/src/script/cube_script.cpp b/src/script/cube_script.cpp index 2524d0f..5cfcef7 100644 --- a/src/script/cube_script.cpp +++ b/src/script/cube_script.cpp @@ -1,14 +1,158 @@ #include "script/cube_script.hpp" +#include +#include +#include +#include + +#include #include #include #include +#include #include namespace sdl3cpp::script { namespace { +struct MeshPayload { + std::vector> positions; + std::vector> colors; + std::vector indices; +}; + +bool TryLoadMeshPayload(const CubeScript* script, + const std::string& requestedPath, + MeshPayload& payload, + std::string& error) { + std::filesystem::path resolved(requestedPath); + if (!resolved.is_absolute()) { + resolved = script->GetScriptDirectory() / resolved; + } + std::error_code ec; + resolved = std::filesystem::weakly_canonical(resolved, ec); + if (ec) { + error = "Failed to resolve mesh path: " + ec.message(); + return false; + } + if (!std::filesystem::exists(resolved)) { + error = "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) { + error = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load mesh"; + return false; + } + if (scene->mNumMeshes == 0) { + error = "Scene contains no meshes"; + return false; + } + + const aiMesh* mesh = scene->mMeshes[0]; + if (!mesh->mNumVertices) { + error = "Mesh contains no vertices"; + return false; + } + + payload.positions.reserve(mesh->mNumVertices); + payload.colors.reserve(mesh->mNumVertices); + payload.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]; + aiColor3D diffuse; + if (material && material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS) { + materialColor = diffuse; + } + } + + for (unsigned i = 0; i < mesh->mNumVertices; ++i) { + const aiVector3D& vertex = mesh->mVertices[i]; + payload.positions.push_back({vertex.x, vertex.y, vertex.z}); + + aiColor3D color = materialColor; + if (mesh->HasVertexColors(0) && mesh->mColors[0]) { + color = mesh->mColors[0][i]; + } + payload.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; + } + payload.indices.push_back(face.mIndices[0]); + payload.indices.push_back(face.mIndices[1]); + payload.indices.push_back(face.mIndices[2]); + } + + if (payload.indices.empty()) { + error = "Mesh contains no triangle faces"; + return false; + } + return true; +} + +int PushMeshToLua(lua_State* L, const MeshPayload& payload) { + lua_newtable(L); // mesh + + lua_newtable(L); // vertices table + 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); // indices table + 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; +} + +int LuaLoadMeshFromFile(lua_State* L) { + auto* script = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + const char* path = luaL_checkstring(L, 1); + MeshPayload payload; + std::string error; + if (!TryLoadMeshPayload(script, path, payload, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + PushMeshToLua(L, payload); + lua_pushnil(L); + return 2; +} + std::array IdentityMatrix() { return {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, @@ -24,6 +168,9 @@ CubeScript::CubeScript(const std::filesystem::path& scriptPath, bool debugEnable throw std::runtime_error("Failed to create Lua state"); } luaL_openlibs(L_); + 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();