load from stl

This commit is contained in:
Richard Ward
2025-12-19 16:30:00 +00:00
parent a9ad53ca8d
commit 96ac0fbba6
3 changed files with 189 additions and 109 deletions

View File

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

View File

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

View File

@@ -1,14 +1,158 @@
#include "script/cube_script.hpp"
#include <assimp/Importer.hpp>
#include <assimp/material.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <array>
#include <cstring>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>
namespace sdl3cpp::script {
namespace {
struct MeshPayload {
std::vector<std::array<float, 3>> positions;
std::vector<std::array<float, 3>> colors;
std::vector<uint32_t> 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<int>(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<lua_Integer>(payload.indices[index]) + 1);
lua_rawseti(L, -2, static_cast<int>(index + 1));
}
lua_setfield(L, -2, "indices");
return 1;
}
int LuaLoadMeshFromFile(lua_State* L) {
auto* script = static_cast<CubeScript*>(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<float, 16> 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();