mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
feat: Add support for loading meshes from PK3 archives
- Updated CMakeLists.txt to find and link libzip. - Modified conanfile.py to include libzip as a dependency. - Created a new configuration file for Quake 3 runtime settings. - Implemented loading of Quake 3 maps from PK3 archives in mesh_service. - Added new Lua bindings for loading meshes from archives in script_engine_service. - Enhanced material handling in materialx_shader_generator to support vertex data blocks.
This commit is contained in:
@@ -137,6 +137,7 @@ find_package(bgfx CONFIG REQUIRED)
|
||||
find_package(MaterialX CONFIG REQUIRED)
|
||||
find_package(Freetype CONFIG REQUIRED)
|
||||
find_package(assimp CONFIG REQUIRED)
|
||||
find_package(libzip CONFIG REQUIRED)
|
||||
find_package(Bullet CONFIG REQUIRED)
|
||||
find_package(Vorbis CONFIG REQUIRED)
|
||||
find_package(glm CONFIG REQUIRED)
|
||||
@@ -181,6 +182,13 @@ if(TARGET Freetype::Freetype)
|
||||
elseif(TARGET freetype::freetype)
|
||||
list(APPEND SDL3CPP_FREETYPE_LIBS freetype::freetype)
|
||||
endif()
|
||||
|
||||
set(SDL3CPP_ZIP_LIBS)
|
||||
if(TARGET libzip::zip)
|
||||
list(APPEND SDL3CPP_ZIP_LIBS libzip::zip)
|
||||
elseif(TARGET libzip::libzip)
|
||||
list(APPEND SDL3CPP_ZIP_LIBS libzip::libzip)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(BUILD_SDL3_APP)
|
||||
@@ -226,6 +234,7 @@ if(BUILD_SDL3_APP)
|
||||
rapidjson
|
||||
${SDL3CPP_RENDER_STACK_LIBS}
|
||||
${SDL3CPP_FREETYPE_LIBS}
|
||||
${SDL3CPP_ZIP_LIBS}
|
||||
assimp::assimp
|
||||
Bullet::Bullet
|
||||
glm::glm
|
||||
|
||||
@@ -33,6 +33,7 @@ class SDL3CppConan(ConanFile):
|
||||
"freetype/2.13.2",
|
||||
"ffmpeg/8.0.1",
|
||||
"cairo/1.18.0",
|
||||
"libzip/1.10.1",
|
||||
)
|
||||
RENDER_STACK_REQUIRES = (
|
||||
"bgfx/1.129.8930-495",
|
||||
|
||||
83
config/quake3_runtime.json
Normal file
83
config/quake3_runtime.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"launcher": {
|
||||
"name": "Quake 3 Arena",
|
||||
"description": "Loads a Quake 3 BSP from a PK3 archive",
|
||||
"enabled": true
|
||||
},
|
||||
"window_width": 1280,
|
||||
"window_height": 720,
|
||||
"lua_script": "scripts/quake3_arena.lua",
|
||||
"scripts_directory": "scripts",
|
||||
"project_root": "../",
|
||||
"shaders_directory": "shaders",
|
||||
"bgfx": {
|
||||
"renderer": "vulkan"
|
||||
},
|
||||
"mouse_grab": {
|
||||
"enabled": true,
|
||||
"grab_on_click": true,
|
||||
"release_on_escape": true,
|
||||
"start_grabbed": false,
|
||||
"hide_cursor": true,
|
||||
"relative_mode": true,
|
||||
"grab_mouse_button": "left",
|
||||
"release_key": "escape"
|
||||
},
|
||||
"input_bindings": {
|
||||
"move_forward": "W",
|
||||
"move_back": "S",
|
||||
"move_left": "A",
|
||||
"move_right": "D",
|
||||
"fly_up": "Q",
|
||||
"fly_down": "Z",
|
||||
"jump": "Space",
|
||||
"noclip_toggle": "N",
|
||||
"music_toggle": "M",
|
||||
"music_toggle_gamepad": "start",
|
||||
"gamepad_move_x_axis": "leftx",
|
||||
"gamepad_move_y_axis": "lefty",
|
||||
"gamepad_look_x_axis": "rightx",
|
||||
"gamepad_look_y_axis": "righty",
|
||||
"gamepad_dpad_up": "dpup",
|
||||
"gamepad_dpad_down": "dpdown",
|
||||
"gamepad_dpad_left": "dpleft",
|
||||
"gamepad_dpad_right": "dpright",
|
||||
"gamepad_button_actions": {
|
||||
"a": "gamepad_a",
|
||||
"b": "gamepad_b",
|
||||
"x": "gamepad_x",
|
||||
"y": "gamepad_y",
|
||||
"leftshoulder": "gamepad_lb",
|
||||
"rightshoulder": "gamepad_rb",
|
||||
"leftstick": "gamepad_ls",
|
||||
"rightstick": "gamepad_rs",
|
||||
"back": "gamepad_back",
|
||||
"start": "gamepad_start"
|
||||
},
|
||||
"gamepad_axis_actions": {
|
||||
"lefttrigger": "gamepad_lt",
|
||||
"righttrigger": "gamepad_rt"
|
||||
},
|
||||
"gamepad_axis_action_threshold": 0.5
|
||||
},
|
||||
"quake3": {
|
||||
"pk3_path": "/home/rewrich/Documents/GitHub/q3/pak0.pk3",
|
||||
"map_path": "q3dm1",
|
||||
"scale": 0.01,
|
||||
"rotate_x_degrees": -90.0,
|
||||
"offset": [0.0, 0.0, 0.0],
|
||||
"shader_key": "pbr",
|
||||
"move_speed": 14.0,
|
||||
"fly_speed": 10.0,
|
||||
"mouse_sensitivity": 0.0025,
|
||||
"camera": {
|
||||
"position": [0.0, 48.0, 0.0],
|
||||
"yaw_degrees": 0.0,
|
||||
"pitch_degrees": 0.0,
|
||||
"fov": 0.85,
|
||||
"near": 0.1,
|
||||
"far": 2000.0
|
||||
}
|
||||
},
|
||||
"config_file": "config/quake3_runtime.json"
|
||||
}
|
||||
306
scripts/quake3_arena.lua
Normal file
306
scripts/quake3_arena.lua
Normal file
@@ -0,0 +1,306 @@
|
||||
local math3d = require("math3d")
|
||||
|
||||
local function log_debug(fmt, ...)
|
||||
if not lua_debug or not fmt then
|
||||
return
|
||||
end
|
||||
print(string.format(fmt, ...))
|
||||
end
|
||||
|
||||
local function resolve_table(value)
|
||||
if type(value) == "table" then
|
||||
return value
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function resolve_number(value, fallback)
|
||||
if type(value) == "number" then
|
||||
return value
|
||||
end
|
||||
return fallback
|
||||
end
|
||||
|
||||
local function resolve_vec3(value, fallback)
|
||||
if type(value) == "table"
|
||||
and type(value[1]) == "number"
|
||||
and type(value[2]) == "number"
|
||||
and type(value[3]) == "number" then
|
||||
return {value[1], value[2], value[3]}
|
||||
end
|
||||
return {fallback[1], fallback[2], fallback[3]}
|
||||
end
|
||||
|
||||
local quake3_config = resolve_table(type(config) == "table" and config.quake3)
|
||||
|
||||
local pk3_path = quake3_config.pk3_path or "/home/rewrich/Documents/GitHub/q3/pak0.pk3"
|
||||
local map_entry = quake3_config.map_path or "q3dm1"
|
||||
if not string.find(map_entry, "%.bsp$") then
|
||||
map_entry = map_entry .. ".bsp"
|
||||
end
|
||||
if not string.find(map_entry, "/") then
|
||||
map_entry = "maps/" .. map_entry
|
||||
end
|
||||
|
||||
local rotation_config = resolve_table(quake3_config.rotation_degrees)
|
||||
local map_rotate_x = resolve_number(rotation_config.x, resolve_number(quake3_config.rotate_x_degrees, -90.0))
|
||||
local map_rotate_y = resolve_number(rotation_config.y, resolve_number(quake3_config.rotate_y_degrees, 0.0))
|
||||
local map_rotate_z = resolve_number(rotation_config.z, resolve_number(quake3_config.rotate_z_degrees, 0.0))
|
||||
|
||||
local map_scale = resolve_number(quake3_config.scale, 0.01)
|
||||
local map_offset = resolve_vec3(quake3_config.offset, {0.0, 0.0, 0.0})
|
||||
local map_shader_key = quake3_config.shader_key or "pbr"
|
||||
|
||||
local function scale_matrix(x, y, z)
|
||||
return {
|
||||
x, 0.0, 0.0, 0.0,
|
||||
0.0, y, 0.0, 0.0,
|
||||
0.0, 0.0, z, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}
|
||||
end
|
||||
|
||||
local function rotation_z(radians)
|
||||
local c = math.cos(radians)
|
||||
local s = math.sin(radians)
|
||||
return {
|
||||
c, s, 0.0, 0.0,
|
||||
-s, c, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}
|
||||
end
|
||||
|
||||
local function clamp(value, min_value, max_value)
|
||||
if value < min_value then
|
||||
return min_value
|
||||
end
|
||||
if value > max_value then
|
||||
return max_value
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function normalize(vec)
|
||||
local x, y, z = vec[1], vec[2], vec[3]
|
||||
local len = math.sqrt(x * x + y * y + z * z)
|
||||
if len == 0.0 then
|
||||
return {x, y, z}
|
||||
end
|
||||
return {x / len, y / len, z / len}
|
||||
end
|
||||
|
||||
local function cross(a, b)
|
||||
return {
|
||||
a[2] * b[3] - a[3] * b[2],
|
||||
a[3] * b[1] - a[1] * b[3],
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
}
|
||||
end
|
||||
|
||||
local function forward_from_angles(yaw, pitch)
|
||||
local cos_pitch = math.cos(pitch)
|
||||
return {
|
||||
math.cos(yaw) * cos_pitch,
|
||||
math.sin(pitch),
|
||||
math.sin(yaw) * cos_pitch,
|
||||
}
|
||||
end
|
||||
|
||||
local function is_action_down(action_name, fallback_key)
|
||||
if type(input_is_action_down) == "function" then
|
||||
return input_is_action_down(action_name)
|
||||
end
|
||||
if type(input_is_key_down) == "function" and fallback_key then
|
||||
return input_is_key_down(fallback_key)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local fallback_bindings = {
|
||||
move_forward = "W",
|
||||
move_back = "S",
|
||||
move_left = "A",
|
||||
move_right = "D",
|
||||
fly_up = "Q",
|
||||
fly_down = "Z",
|
||||
}
|
||||
|
||||
local input_bindings = resolve_table(type(config) == "table" and config.input_bindings)
|
||||
local function get_binding(action_name)
|
||||
if type(input_bindings[action_name]) == "string" then
|
||||
return input_bindings[action_name]
|
||||
end
|
||||
return fallback_bindings[action_name]
|
||||
end
|
||||
|
||||
local function build_map_model_matrix()
|
||||
local translation = math3d.translation(map_offset[1], map_offset[2], map_offset[3])
|
||||
local rotation_x = math3d.rotation_x(math.rad(map_rotate_x))
|
||||
local rotation_y = math3d.rotation_y(math.rad(map_rotate_y))
|
||||
local rotation = math3d.multiply(rotation_y, rotation_x)
|
||||
if map_rotate_z ~= 0.0 then
|
||||
local rotation_z_matrix = rotation_z(math.rad(map_rotate_z))
|
||||
rotation = math3d.multiply(rotation_z_matrix, rotation)
|
||||
end
|
||||
local scaling = scale_matrix(map_scale, map_scale, map_scale)
|
||||
return math3d.multiply(translation, math3d.multiply(rotation, scaling))
|
||||
end
|
||||
|
||||
local map_model_matrix = build_map_model_matrix()
|
||||
|
||||
if type(load_mesh_from_pk3) ~= "function" then
|
||||
error("load_mesh_from_pk3() is unavailable; rebuild with libzip support")
|
||||
end
|
||||
|
||||
local map_mesh, map_error = load_mesh_from_pk3(pk3_path, map_entry)
|
||||
if not map_mesh then
|
||||
error("Unable to load Quake 3 map: " .. tostring(map_error))
|
||||
end
|
||||
|
||||
log_debug("Loaded Quake 3 map %s from %s (%d vertices, %d indices)",
|
||||
map_entry,
|
||||
pk3_path,
|
||||
#map_mesh.vertices,
|
||||
#map_mesh.indices)
|
||||
|
||||
local camera_config = resolve_table(quake3_config.camera)
|
||||
local camera = {
|
||||
position = resolve_vec3(camera_config.position, {0.0, 48.0, 0.0}),
|
||||
yaw = math.rad(resolve_number(camera_config.yaw_degrees, 0.0)),
|
||||
pitch = math.rad(resolve_number(camera_config.pitch_degrees, 0.0)),
|
||||
fov = resolve_number(camera_config.fov, 0.85),
|
||||
near = resolve_number(camera_config.near, 0.1),
|
||||
far = resolve_number(camera_config.far, 2000.0),
|
||||
}
|
||||
|
||||
local controls = {
|
||||
move_speed = resolve_number(quake3_config.move_speed, 14.0),
|
||||
fly_speed = resolve_number(quake3_config.fly_speed, 10.0),
|
||||
mouse_sensitivity = resolve_number(quake3_config.mouse_sensitivity, 0.0025),
|
||||
max_pitch = math.rad(85.0),
|
||||
}
|
||||
|
||||
local last_frame_time = nil
|
||||
local world_up = {0.0, 1.0, 0.0}
|
||||
|
||||
local function update_camera(dt)
|
||||
local look_delta_x = 0.0
|
||||
local look_delta_y = 0.0
|
||||
if type(input_get_mouse_delta) == "function" then
|
||||
local dx, dy = input_get_mouse_delta()
|
||||
if type(dx) == "number" then
|
||||
look_delta_x = dx * controls.mouse_sensitivity
|
||||
end
|
||||
if type(dy) == "number" then
|
||||
look_delta_y = -dy * controls.mouse_sensitivity
|
||||
end
|
||||
end
|
||||
|
||||
camera.yaw = camera.yaw + look_delta_x
|
||||
camera.pitch = clamp(camera.pitch + look_delta_y, -controls.max_pitch, controls.max_pitch)
|
||||
|
||||
local forward = forward_from_angles(camera.yaw, camera.pitch)
|
||||
local forward_flat = normalize({forward[1], 0.0, forward[3]})
|
||||
local right = normalize(cross(forward_flat, world_up))
|
||||
|
||||
local move_x = 0.0
|
||||
local move_z = 0.0
|
||||
local move_y = 0.0
|
||||
|
||||
if is_action_down("move_forward", get_binding("move_forward")) then
|
||||
move_z = move_z + 1.0
|
||||
end
|
||||
if is_action_down("move_back", get_binding("move_back")) then
|
||||
move_z = move_z - 1.0
|
||||
end
|
||||
if is_action_down("move_right", get_binding("move_right")) then
|
||||
move_x = move_x + 1.0
|
||||
end
|
||||
if is_action_down("move_left", get_binding("move_left")) then
|
||||
move_x = move_x - 1.0
|
||||
end
|
||||
if is_action_down("fly_up", get_binding("fly_up")) then
|
||||
move_y = move_y + 1.0
|
||||
end
|
||||
if is_action_down("fly_down", get_binding("fly_down")) then
|
||||
move_y = move_y - 1.0
|
||||
end
|
||||
|
||||
local length = math.sqrt(move_x * move_x + move_z * move_z)
|
||||
if length > 1.0 then
|
||||
move_x = move_x / length
|
||||
move_z = move_z / length
|
||||
end
|
||||
|
||||
local planar_speed = controls.move_speed * dt
|
||||
camera.position[1] = camera.position[1] + (right[1] * move_x + forward_flat[1] * move_z) * planar_speed
|
||||
camera.position[2] = camera.position[2] + (right[2] * move_x + forward_flat[2] * move_z) * planar_speed
|
||||
camera.position[3] = camera.position[3] + (right[3] * move_x + forward_flat[3] * move_z) * planar_speed
|
||||
|
||||
if move_y ~= 0.0 then
|
||||
camera.position[2] = camera.position[2] + move_y * controls.fly_speed * dt
|
||||
end
|
||||
end
|
||||
|
||||
local shader_variants_module = require("shader_variants")
|
||||
local shader_variants = shader_variants_module.build_cube_variants(config, log_debug)
|
||||
|
||||
function get_scene_objects()
|
||||
return {
|
||||
{
|
||||
vertices = map_mesh.vertices,
|
||||
indices = map_mesh.indices,
|
||||
shader_key = map_shader_key,
|
||||
compute_model_matrix = function()
|
||||
return map_model_matrix
|
||||
end,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function get_shader_paths()
|
||||
return shader_variants
|
||||
end
|
||||
|
||||
local function build_view_state(aspect)
|
||||
local now = os.clock()
|
||||
local dt = 0.0
|
||||
if last_frame_time then
|
||||
dt = now - last_frame_time
|
||||
end
|
||||
last_frame_time = now
|
||||
if dt < 0.0 then
|
||||
dt = 0.0
|
||||
elseif dt > 0.1 then
|
||||
dt = 0.1
|
||||
end
|
||||
|
||||
update_camera(dt)
|
||||
|
||||
local forward = forward_from_angles(camera.yaw, camera.pitch)
|
||||
local center = {
|
||||
camera.position[1] + forward[1],
|
||||
camera.position[2] + forward[2],
|
||||
camera.position[3] + forward[3],
|
||||
}
|
||||
|
||||
local view = math3d.look_at(camera.position, center, world_up)
|
||||
local projection = math3d.perspective(camera.fov, aspect, camera.near, camera.far)
|
||||
|
||||
return {
|
||||
view = view,
|
||||
proj = projection,
|
||||
view_proj = math3d.multiply(projection, view),
|
||||
camera_pos = {camera.position[1], camera.position[2], camera.position[3]},
|
||||
}
|
||||
end
|
||||
|
||||
function get_view_state(aspect)
|
||||
return build_view_state(aspect)
|
||||
end
|
||||
|
||||
function get_view_projection(aspect)
|
||||
local state = build_view_state(aspect)
|
||||
return state.view_proj
|
||||
end
|
||||
@@ -13,39 +13,52 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
namespace mx = MaterialX;
|
||||
|
||||
namespace {
|
||||
|
||||
bool HasVertexDataBlock(const std::string& source) {
|
||||
return source.find("VertexData") != std::string::npos
|
||||
&& source.find("vd;") != std::string::npos;
|
||||
std::optional<std::string> FindVertexDataBlock(const std::string& source) {
|
||||
const std::string blockName = "VertexData";
|
||||
const std::string instanceToken = "vd;";
|
||||
size_t searchPos = 0;
|
||||
while (true) {
|
||||
size_t blockPos = source.find(blockName, searchPos);
|
||||
if (blockPos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
size_t lineStart = source.rfind('\n', blockPos);
|
||||
if (lineStart == std::string::npos) {
|
||||
lineStart = 0;
|
||||
} else {
|
||||
++lineStart;
|
||||
}
|
||||
size_t lineEnd = source.find('\n', blockPos);
|
||||
if (lineEnd == std::string::npos) {
|
||||
lineEnd = source.size();
|
||||
}
|
||||
std::string_view header(source.data() + lineStart, lineEnd - lineStart);
|
||||
if (header.find("layout") == std::string_view::npos) {
|
||||
searchPos = blockPos + blockName.size();
|
||||
continue;
|
||||
}
|
||||
size_t instancePos = source.find(instanceToken, blockPos);
|
||||
if (instancePos == std::string::npos) {
|
||||
searchPos = blockPos + blockName.size();
|
||||
continue;
|
||||
}
|
||||
size_t blockEnd = source.find('\n', instancePos);
|
||||
if (blockEnd == std::string::npos) {
|
||||
blockEnd = source.size();
|
||||
}
|
||||
return source.substr(lineStart, blockEnd - lineStart);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> ExtractVertexDataBlock(const std::string& source) {
|
||||
const std::string marker = "VertexData";
|
||||
const std::string instance = "vd;";
|
||||
size_t blockPos = source.find(marker);
|
||||
if (blockPos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
size_t instancePos = source.find(instance, blockPos);
|
||||
if (instancePos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
size_t lineStart = source.rfind('\n', blockPos);
|
||||
if (lineStart == std::string::npos) {
|
||||
lineStart = 0;
|
||||
} else {
|
||||
++lineStart;
|
||||
}
|
||||
size_t lineEnd = source.find('\n', instancePos);
|
||||
if (lineEnd == std::string::npos) {
|
||||
lineEnd = source.size();
|
||||
}
|
||||
return source.substr(lineStart, lineEnd - lineStart);
|
||||
bool UsesVertexDataInstance(const std::string& source) {
|
||||
return source.find("vd.") != std::string::npos;
|
||||
}
|
||||
|
||||
std::string ToVertexOutputBlock(std::string block) {
|
||||
@@ -75,6 +88,15 @@ void InsertAfterVersion(std::string& source, const std::string& block) {
|
||||
source.insert(lineEnd, block + "\n");
|
||||
}
|
||||
|
||||
bool ReplaceFirstOccurrence(std::string& source, const std::string& before, const std::string& after) {
|
||||
size_t pos = source.find(before);
|
||||
if (pos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
source.replace(pos, before.size(), after);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MaterialXShaderGenerator::MaterialXShaderGenerator(std::shared_ptr<ILogger> logger)
|
||||
@@ -222,25 +244,40 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config,
|
||||
paths.vertexSource = shader->getSourceCode(mx::Stage::VERTEX);
|
||||
paths.fragmentSource = shader->getSourceCode(mx::Stage::PIXEL);
|
||||
|
||||
const bool vertexHasBlock = HasVertexDataBlock(paths.vertexSource);
|
||||
const bool fragmentHasBlock = HasVertexDataBlock(paths.fragmentSource);
|
||||
if (!vertexHasBlock && fragmentHasBlock) {
|
||||
auto fragmentBlock = ExtractVertexDataBlock(paths.fragmentSource);
|
||||
if (fragmentBlock) {
|
||||
std::string vertexBlock = ToVertexOutputBlock(*fragmentBlock);
|
||||
InsertAfterVersion(paths.vertexSource, vertexBlock);
|
||||
if (logger_) {
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate",
|
||||
"vertexDataBlock=inserted");
|
||||
auto vertexBlock = FindVertexDataBlock(paths.vertexSource);
|
||||
auto fragmentBlock = FindVertexDataBlock(paths.fragmentSource);
|
||||
const bool vertexUsesInstance = UsesVertexDataInstance(paths.vertexSource);
|
||||
bool vertexHasBlock = vertexBlock.has_value();
|
||||
const bool fragmentHasBlock = fragmentBlock.has_value();
|
||||
|
||||
if (vertexHasBlock) {
|
||||
std::string normalizedBlock = ToVertexOutputBlock(*vertexBlock);
|
||||
if (normalizedBlock != *vertexBlock) {
|
||||
if (ReplaceFirstOccurrence(paths.vertexSource, *vertexBlock, normalizedBlock)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate",
|
||||
"vertexDataBlock=normalized");
|
||||
}
|
||||
}
|
||||
} else if (logger_) {
|
||||
}
|
||||
} else if (fragmentHasBlock) {
|
||||
std::string vertexOutBlock = ToVertexOutputBlock(*fragmentBlock);
|
||||
InsertAfterVersion(paths.vertexSource, vertexOutBlock);
|
||||
vertexHasBlock = true;
|
||||
if (logger_) {
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate",
|
||||
"vertexDataBlock=missing");
|
||||
"vertexDataBlock=inserted");
|
||||
}
|
||||
} else if (logger_) {
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate",
|
||||
"vertexDataBlock=missing, fragmentVertexDataBlock=missing");
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate",
|
||||
"vertexDataBlock=" + std::string(vertexHasBlock ? "present" : "absent") +
|
||||
", fragmentVertexDataBlock=" + std::string(fragmentHasBlock ? "present" : "absent"));
|
||||
", fragmentVertexDataBlock=" + std::string(fragmentHasBlock ? "present" : "absent") +
|
||||
", vertexUsesVertexData=" + std::string(vertexUsesInstance ? "true" : "false"));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
@@ -6,91 +6,109 @@
|
||||
#include <assimp/postprocess.h>
|
||||
#include <assimp/scene.h>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <lua.hpp>
|
||||
#include <system_error>
|
||||
#include <zip.h>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
namespace {
|
||||
constexpr unsigned int kAssimpLoadFlags =
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_PreTransformVertices |
|
||||
aiProcess_GenNormals;
|
||||
|
||||
MeshService::MeshService(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: configService_(std::move(configService)),
|
||||
logger_(std::move(logger)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MeshService", "MeshService",
|
||||
"configService=" + std::string(configService_ ? "set" : "null"));
|
||||
struct ZipArchiveDeleter {
|
||||
void operator()(zip_t* archive) const {
|
||||
if (archive) {
|
||||
zip_close(archive);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct ZipFileDeleter {
|
||||
void operator()(zip_file_t* file) const {
|
||||
if (file) {
|
||||
zip_fclose(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::string BuildZipErrorMessage(int errorCode) {
|
||||
zip_error_t zipError;
|
||||
zip_error_init_with_code(&zipError, errorCode);
|
||||
std::string message = zip_error_strerror(&zipError);
|
||||
zip_error_fini(&zipError);
|
||||
return message;
|
||||
}
|
||||
|
||||
bool MeshService::LoadFromFile(const std::string& requestedPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MeshService", "LoadFromFile",
|
||||
"requestedPath=" + requestedPath);
|
||||
std::string BuildZipArchiveErrorMessage(zip_t* archive) {
|
||||
if (!archive) {
|
||||
return "unknown zip archive error";
|
||||
}
|
||||
if (!configService_) {
|
||||
outError = "Config service not available";
|
||||
return false;
|
||||
zip_error_t* zipError = zip_get_error(archive);
|
||||
if (!zipError) {
|
||||
return "unknown zip archive error";
|
||||
}
|
||||
return zip_error_strerror(zipError);
|
||||
}
|
||||
|
||||
std::filesystem::path resolved(requestedPath);
|
||||
if (!resolved.is_absolute()) {
|
||||
resolved = configService_->GetScriptPath().parent_path() / resolved;
|
||||
std::string GetExtensionHint(const std::string& entryPath, const std::string& fallback) {
|
||||
std::filesystem::path entry(entryPath);
|
||||
std::string ext = entry.extension().string();
|
||||
if (!ext.empty() && ext.front() == '.') {
|
||||
ext.erase(ext.begin());
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
resolved = std::filesystem::weakly_canonical(resolved, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to resolve mesh path: " + ec.message();
|
||||
return false;
|
||||
if (!ext.empty()) {
|
||||
return ext;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
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 | aiProcess_GenNormals);
|
||||
|
||||
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.clear();
|
||||
outPayload.normals.clear();
|
||||
outPayload.colors.clear();
|
||||
outPayload.indices.clear();
|
||||
outPayload.positions.reserve(mesh->mNumVertices);
|
||||
outPayload.normals.reserve(mesh->mNumVertices);
|
||||
outPayload.colors.reserve(mesh->mNumVertices);
|
||||
outPayload.indices.reserve(mesh->mNumFaces * 3);
|
||||
|
||||
aiColor3D ResolveMaterialColor(const aiScene* scene, const aiMesh* mesh) {
|
||||
aiColor3D defaultColor(0.6f, 0.8f, 1.0f);
|
||||
aiColor3D materialColor = defaultColor;
|
||||
|
||||
if (!scene || !mesh) {
|
||||
return 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);
|
||||
return aiColor3D(diffuse.r, diffuse.g, diffuse.b);
|
||||
}
|
||||
}
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
bool AppendMeshPayload(const aiScene* scene,
|
||||
const aiMesh* mesh,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError,
|
||||
size_t& outIndicesAdded) {
|
||||
outIndicesAdded = 0;
|
||||
if (!mesh || !mesh->mNumVertices) {
|
||||
outError = "Mesh contains no vertices";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t vertexOffset = outPayload.positions.size();
|
||||
if (vertexOffset > std::numeric_limits<uint32_t>::max()) {
|
||||
outError = "Mesh vertex count exceeds uint32_t index range";
|
||||
return false;
|
||||
}
|
||||
|
||||
aiColor3D materialColor = ResolveMaterialColor(scene, mesh);
|
||||
|
||||
size_t positionsStart = outPayload.positions.size();
|
||||
size_t normalsStart = outPayload.normals.size();
|
||||
size_t colorsStart = outPayload.colors.size();
|
||||
size_t indicesStart = outPayload.indices.size();
|
||||
|
||||
outPayload.positions.reserve(positionsStart + mesh->mNumVertices);
|
||||
outPayload.normals.reserve(normalsStart + mesh->mNumVertices);
|
||||
outPayload.colors.reserve(colorsStart + mesh->mNumVertices);
|
||||
outPayload.indices.reserve(indicesStart + mesh->mNumFaces * 3);
|
||||
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
|
||||
const aiVector3D& vertex = mesh->mVertices[i];
|
||||
@@ -115,12 +133,20 @@ bool MeshService::LoadFromFile(const std::string& requestedPath,
|
||||
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]);
|
||||
outPayload.indices.push_back(static_cast<uint32_t>(face.mIndices[0]) +
|
||||
static_cast<uint32_t>(vertexOffset));
|
||||
outPayload.indices.push_back(static_cast<uint32_t>(face.mIndices[1]) +
|
||||
static_cast<uint32_t>(vertexOffset));
|
||||
outPayload.indices.push_back(static_cast<uint32_t>(face.mIndices[2]) +
|
||||
static_cast<uint32_t>(vertexOffset));
|
||||
}
|
||||
|
||||
if (outPayload.indices.empty()) {
|
||||
outIndicesAdded = outPayload.indices.size() - indicesStart;
|
||||
if (outIndicesAdded == 0) {
|
||||
outPayload.positions.resize(positionsStart);
|
||||
outPayload.normals.resize(normalsStart);
|
||||
outPayload.colors.resize(colorsStart);
|
||||
outPayload.indices.resize(indicesStart);
|
||||
outError = "Mesh contains no triangle faces";
|
||||
return false;
|
||||
}
|
||||
@@ -128,6 +154,216 @@ bool MeshService::LoadFromFile(const std::string& requestedPath,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildPayloadFromScene(const aiScene* scene,
|
||||
bool combineMeshes,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError,
|
||||
const std::shared_ptr<ILogger>& logger) {
|
||||
if (!scene) {
|
||||
outError = "Assimp scene is null";
|
||||
return false;
|
||||
}
|
||||
if (scene->mNumMeshes == 0) {
|
||||
outError = "Scene contains no meshes";
|
||||
return false;
|
||||
}
|
||||
|
||||
outPayload.positions.clear();
|
||||
outPayload.normals.clear();
|
||||
outPayload.colors.clear();
|
||||
outPayload.indices.clear();
|
||||
|
||||
if (!combineMeshes) {
|
||||
size_t indicesAdded = 0;
|
||||
if (!AppendMeshPayload(scene, scene->mMeshes[0], outPayload, outError, indicesAdded)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t totalIndicesAdded = 0;
|
||||
for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) {
|
||||
const aiMesh* mesh = scene->mMeshes[meshIndex];
|
||||
std::string meshError;
|
||||
size_t indicesAdded = 0;
|
||||
if (!AppendMeshPayload(scene, mesh, outPayload, meshError, indicesAdded)) {
|
||||
if (logger) {
|
||||
logger->Trace("MeshService", "BuildPayloadFromScene",
|
||||
"Skipping mesh " + std::to_string(meshIndex) + ": " + meshError);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
totalIndicesAdded += indicesAdded;
|
||||
}
|
||||
|
||||
if (totalIndicesAdded == 0) {
|
||||
outError = "Scene contains no triangle faces";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MeshService::MeshService(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: configService_(std::move(configService)),
|
||||
logger_(std::move(logger)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MeshService", "MeshService",
|
||||
"configService=" + std::string(configService_ ? "set" : "null"));
|
||||
}
|
||||
}
|
||||
|
||||
bool MeshService::LoadFromFile(const std::string& requestedPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MeshService", "LoadFromFile",
|
||||
"requestedPath=" + requestedPath);
|
||||
}
|
||||
std::filesystem::path resolved;
|
||||
if (!ResolvePath(requestedPath, resolved, outError)) {
|
||||
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(), kAssimpLoadFlags);
|
||||
|
||||
if (!scene) {
|
||||
outError = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load mesh";
|
||||
return false;
|
||||
}
|
||||
return BuildPayloadFromScene(scene, false, outPayload, outError, logger_);
|
||||
}
|
||||
|
||||
bool MeshService::LoadFromArchive(const std::string& archivePath,
|
||||
const std::string& entryPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MeshService", "LoadFromArchive",
|
||||
"archivePath=" + archivePath +
|
||||
", entryPath=" + entryPath);
|
||||
}
|
||||
|
||||
std::filesystem::path resolvedArchive;
|
||||
if (!ResolvePath(archivePath, resolvedArchive, outError)) {
|
||||
return false;
|
||||
}
|
||||
if (!std::filesystem::exists(resolvedArchive)) {
|
||||
outError = "Archive file not found: " + resolvedArchive.string();
|
||||
return false;
|
||||
}
|
||||
if (entryPath.empty()) {
|
||||
outError = "Archive entry path is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
int errorCode = 0;
|
||||
std::unique_ptr<zip_t, ZipArchiveDeleter> archive(
|
||||
zip_open(resolvedArchive.string().c_str(), ZIP_RDONLY, &errorCode));
|
||||
if (!archive) {
|
||||
outError = "Failed to open archive: " + BuildZipErrorMessage(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
zip_stat_t entryStat;
|
||||
if (zip_stat(archive.get(), entryPath.c_str(), ZIP_FL_ENC_GUESS, &entryStat) != 0) {
|
||||
outError = "Archive entry not found: " + entryPath;
|
||||
return false;
|
||||
}
|
||||
if (entryStat.size == 0) {
|
||||
outError = "Archive entry is empty: " + entryPath;
|
||||
return false;
|
||||
}
|
||||
if (entryStat.size > std::numeric_limits<size_t>::max()) {
|
||||
outError = "Archive entry exceeds addressable size: " + entryPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<zip_file_t, ZipFileDeleter> file(
|
||||
zip_fopen(archive.get(), entryPath.c_str(), ZIP_FL_ENC_GUESS));
|
||||
if (!file) {
|
||||
outError = "Failed to open archive entry: " + BuildZipArchiveErrorMessage(archive.get());
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t entrySize = static_cast<size_t>(entryStat.size);
|
||||
std::vector<uint8_t> buffer(entrySize);
|
||||
zip_int64_t totalRead = 0;
|
||||
while (static_cast<size_t>(totalRead) < entrySize) {
|
||||
zip_int64_t bytesRead = zip_fread(file.get(),
|
||||
buffer.data() + totalRead,
|
||||
entrySize - static_cast<size_t>(totalRead));
|
||||
if (bytesRead < 0) {
|
||||
outError = "Failed to read archive entry: " + BuildZipArchiveErrorMessage(archive.get());
|
||||
return false;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
break;
|
||||
}
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
if (static_cast<size_t>(totalRead) != entrySize) {
|
||||
outError = "Archive entry read incomplete: " + entryPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string extensionHint = GetExtensionHint(entryPath, "bsp");
|
||||
Assimp::Importer importer;
|
||||
const aiScene* scene = importer.ReadFileFromMemory(
|
||||
buffer.data(),
|
||||
buffer.size(),
|
||||
kAssimpLoadFlags,
|
||||
extensionHint.c_str());
|
||||
if (!scene) {
|
||||
outError = importer.GetErrorString() ? importer.GetErrorString()
|
||||
: "Assimp failed to load archive entry";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BuildPayloadFromScene(scene, true, outPayload, outError, logger_)) {
|
||||
return false;
|
||||
}
|
||||
if (outPayload.positions.size() > std::numeric_limits<uint16_t>::max()) {
|
||||
outError = "Mesh vertex count exceeds uint16_t index range: " +
|
||||
std::to_string(outPayload.positions.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeshService::ResolvePath(const std::string& requestedPath,
|
||||
std::filesystem::path& resolvedPath,
|
||||
std::string& outError) const {
|
||||
if (!configService_) {
|
||||
outError = "Config service not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path resolved(requestedPath);
|
||||
if (!resolved.is_absolute()) {
|
||||
resolved = configService_->GetScriptPath().parent_path() / resolved;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
resolved = std::filesystem::weakly_canonical(resolved, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to resolve path: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
resolvedPath = std::move(resolved);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) {
|
||||
if (logger_) {
|
||||
logger_->Trace("MeshService", "PushMeshToLua",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../interfaces/i_mesh_service.hpp"
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
@@ -18,9 +19,17 @@ public:
|
||||
bool LoadFromFile(const std::string& requestedPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) override;
|
||||
bool LoadFromArchive(const std::string& archivePath,
|
||||
const std::string& entryPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) override;
|
||||
void PushMeshToLua(lua_State* L, const MeshPayload& payload) override;
|
||||
|
||||
private:
|
||||
bool ResolvePath(const std::string& requestedPath,
|
||||
std::filesystem::path& resolvedPath,
|
||||
std::string& outError) const;
|
||||
|
||||
std::shared_ptr<IConfigService> configService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
};
|
||||
|
||||
@@ -471,6 +471,7 @@ void ScriptEngineService::RegisterBindings(lua_State* L) {
|
||||
};
|
||||
|
||||
bind("load_mesh_from_file", &ScriptEngineService::LoadMeshFromFile);
|
||||
bind("load_mesh_from_pk3", &ScriptEngineService::LoadMeshFromArchive);
|
||||
bind("physics_create_box", &ScriptEngineService::PhysicsCreateBox);
|
||||
bind("physics_step_simulation", &ScriptEngineService::PhysicsStepSimulation);
|
||||
bind("physics_get_transform", &ScriptEngineService::PhysicsGetTransform);
|
||||
@@ -528,6 +529,36 @@ int ScriptEngineService::LoadMeshFromFile(lua_State* L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
int ScriptEngineService::LoadMeshFromArchive(lua_State* L) {
|
||||
auto* context = static_cast<LuaBindingContext*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||
auto logger = context ? context->logger : nullptr;
|
||||
if (!context || !context->meshService) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "Mesh service not available");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* archivePath = luaL_checkstring(L, 1);
|
||||
const char* entryPath = luaL_checkstring(L, 2);
|
||||
if (logger) {
|
||||
logger->Trace("ScriptEngineService", "LoadMeshFromArchive",
|
||||
"archivePath=" + std::string(archivePath) +
|
||||
", entryPath=" + std::string(entryPath));
|
||||
}
|
||||
|
||||
MeshPayload payload;
|
||||
std::string error;
|
||||
if (!context->meshService->LoadFromArchive(archivePath, entryPath, payload, error)) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
context->meshService->PushMeshToLua(L, payload);
|
||||
lua_pushnil(L);
|
||||
return 2;
|
||||
}
|
||||
|
||||
int ScriptEngineService::PhysicsCreateBox(lua_State* L) {
|
||||
auto* context = static_cast<LuaBindingContext*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||
auto logger = context ? context->logger : nullptr;
|
||||
|
||||
@@ -61,6 +61,7 @@ private:
|
||||
|
||||
void RegisterBindings(lua_State* L);
|
||||
static int LoadMeshFromFile(lua_State* L);
|
||||
static int LoadMeshFromArchive(lua_State* L);
|
||||
static int PhysicsCreateBox(lua_State* L);
|
||||
static int PhysicsStepSimulation(lua_State* L);
|
||||
static int PhysicsGetTransform(lua_State* L);
|
||||
|
||||
@@ -17,6 +17,10 @@ public:
|
||||
virtual bool LoadFromFile(const std::string& requestedPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) = 0;
|
||||
virtual bool LoadFromArchive(const std::string& archivePath,
|
||||
const std::string& entryPath,
|
||||
MeshPayload& outPayload,
|
||||
std::string& outError) = 0;
|
||||
virtual void PushMeshToLua(lua_State* L, const MeshPayload& payload) = 0;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user