diff --git a/scripts/quake3_arena.lua b/scripts/quake3_arena.lua index c7e91b9..a22f4f2 100644 --- a/scripts/quake3_arena.lua +++ b/scripts/quake3_arena.lua @@ -21,6 +21,13 @@ local function resolve_number(value, fallback) return fallback end +local function resolve_boolean(value, fallback) + if type(value) == "boolean" then + return value + end + return fallback +end + local function resolve_vec3(value, fallback) if type(value) == "table" and type(value[1]) == "number" @@ -81,6 +88,54 @@ local function clamp(value, min_value, max_value) return value end +local function transform_point(matrix, point) + local x = point[1] + local y = point[2] + local z = point[3] + return { + matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13], + matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14], + matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15], + } +end + +local function compute_bounds(vertices, matrix) + if type(vertices) ~= "table" then + return nil + end + local min_bounds = {math.huge, math.huge, math.huge} + local max_bounds = {-math.huge, -math.huge, -math.huge} + for i = 1, #vertices do + local vertex = vertices[i] + local position = vertex and (vertex.position or vertex) + if type(position) == "table" then + local world_pos = transform_point(matrix, position) + if world_pos[1] < min_bounds[1] then + min_bounds[1] = world_pos[1] + end + if world_pos[2] < min_bounds[2] then + min_bounds[2] = world_pos[2] + end + if world_pos[3] < min_bounds[3] then + min_bounds[3] = world_pos[3] + end + if world_pos[1] > max_bounds[1] then + max_bounds[1] = world_pos[1] + end + if world_pos[2] > max_bounds[2] then + max_bounds[2] = world_pos[2] + end + if world_pos[3] > max_bounds[3] then + max_bounds[3] = world_pos[3] + end + end + end + if min_bounds[1] == math.huge then + return nil + end + return {min = min_bounds, max = max_bounds} +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) @@ -117,6 +172,14 @@ local function is_action_down(action_name, fallback_key) return false end +local action_states = {} +local function is_action_pressed(action_name, fallback_key) + local is_down = is_action_down(action_name, fallback_key) + local was_down = action_states[action_name] + action_states[action_name] = is_down + return is_down and not was_down +end + local fallback_bindings = { move_forward = "W", move_back = "S", @@ -124,6 +187,8 @@ local fallback_bindings = { move_right = "D", fly_up = "Q", fly_down = "Z", + jump = "Space", + noclip_toggle = "N", } local input_bindings = resolve_table(type(config) == "table" and config.input_bindings) @@ -164,9 +229,72 @@ log_debug("Loaded Quake 3 map %s from %s (%d vertices, %d indices)", #map_mesh.vertices, #map_mesh.indices) +local map_bounds = compute_bounds(map_mesh.vertices, map_model_matrix) +if map_bounds then + log_debug("Map bounds min=(%.2f, %.2f, %.2f) max=(%.2f, %.2f, %.2f)", + map_bounds.min[1], map_bounds.min[2], map_bounds.min[3], + map_bounds.max[1], map_bounds.max[2], map_bounds.max[3]) +end + local camera_config = resolve_table(quake3_config.camera) + +local function compute_spawn_position(bounds) + if not bounds then + return {0.0, 48.0, 0.0} + end + local min_bounds = bounds.min + local max_bounds = bounds.max + local center = { + (min_bounds[1] + max_bounds[1]) * 0.5, + (min_bounds[2] + max_bounds[2]) * 0.5, + (min_bounds[3] + max_bounds[3]) * 0.5, + } + local height = math.max(2.0, (max_bounds[2] - min_bounds[2]) * 0.1) + local spawn_height = resolve_number(quake3_config.spawn_height, height) + local spawn_offset = resolve_vec3(quake3_config.spawn_offset, {0.0, 0.0, 0.0}) + return { + center[1] + spawn_offset[1], + max_bounds[2] + spawn_height + spawn_offset[2], + center[3] + spawn_offset[3], + } +end + +local function is_position_far_from_bounds(position, bounds) + if not bounds then + return false + end + local min_bounds = bounds.min + local max_bounds = bounds.max + local extent_x = max_bounds[1] - min_bounds[1] + local extent_y = max_bounds[2] - min_bounds[2] + local extent_z = max_bounds[3] - min_bounds[3] + local margin = math.max(extent_x, extent_y, extent_z) * 0.5 + if position[1] < min_bounds[1] - margin or position[1] > max_bounds[1] + margin then + return true + end + if position[2] < min_bounds[2] - margin or position[2] > max_bounds[2] + margin then + return true + end + if position[3] < min_bounds[3] - margin or position[3] > max_bounds[3] + margin then + return true + end + return false +end + +local camera_position = resolve_vec3(camera_config.position, {0.0, 48.0, 0.0}) +local spawn_position = compute_spawn_position(map_bounds) +local auto_spawn = quake3_config.auto_spawn +if auto_spawn == nil then + auto_spawn = true +end +if auto_spawn and is_position_far_from_bounds(camera_position, map_bounds) then + camera_position = {spawn_position[1], spawn_position[2], spawn_position[3]} + log_debug("Camera spawn adjusted to map bounds (%.2f, %.2f, %.2f)", + camera_position[1], camera_position[2], camera_position[3]) +end + local camera = { - position = resolve_vec3(camera_config.position, {0.0, 48.0, 0.0}), + position = camera_position, 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), @@ -181,10 +309,46 @@ local controls = { max_pitch = math.rad(85.0), } +local physics_config = resolve_table(quake3_config.physics) +local physics_enabled = resolve_boolean(physics_config.enabled, true) +local physics_available = type(physics_create_static_mesh) == "function" + and type(physics_create_sphere) == "function" + and type(physics_get_transform) == "function" + and type(physics_set_linear_velocity) == "function" + and type(physics_get_linear_velocity) == "function" + and type(physics_step_simulation) == "function" + +if physics_enabled and not physics_available then + log_debug("Physics disabled: required bindings are unavailable") +end + +local physics_state = { + enabled = physics_enabled and physics_available, + ready = false, + noclip = false, + map_body_name = physics_config.map_body_name or "quake3_map", + player_body_name = physics_config.player_body_name or "quake3_player", + player_radius = resolve_number(physics_config.player_radius, resolve_number(quake3_config.player_radius, 0.6)), + player_mass = resolve_number(physics_config.player_mass, resolve_number(quake3_config.player_mass, 1.0)), + eye_height = resolve_number(physics_config.eye_height, resolve_number(quake3_config.eye_height, 0.6)), + gravity = resolve_vec3(physics_config.gravity, {0.0, -9.8, 0.0}), + jump_impulse = resolve_number(physics_config.jump_impulse, resolve_number(quake3_config.jump_impulse, 4.5)), + jump_velocity_threshold = resolve_number( + physics_config.jump_velocity_threshold, + resolve_number(quake3_config.jump_velocity_threshold, 0.2)), + max_sub_steps = resolve_number(physics_config.max_sub_steps, resolve_number(quake3_config.max_sub_steps, 8)), +} + +physics_state.spawn_position = { + camera.position[1], + camera.position[2] - physics_state.eye_height, + camera.position[3], +} + local last_frame_time = nil local world_up = {0.0, 1.0, 0.0} -local function update_camera(dt) +local function update_camera_angles() local look_delta_x = 0.0 local look_delta_y = 0.0 if type(input_get_mouse_delta) == "function" then @@ -199,11 +363,9 @@ local function update_camera(dt) camera.yaw = camera.yaw + look_delta_x camera.pitch = clamp(camera.pitch + look_delta_y, -controls.max_pitch, controls.max_pitch) +end - 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 function resolve_move_input() local move_x = 0.0 local move_z = 0.0 local move_y = 0.0 @@ -233,6 +395,11 @@ local function update_camera(dt) move_z = move_z / length end + return move_x, move_y, move_z +end + +local function update_free_fly(dt, forward_flat, right) + local move_x, move_y, move_z = resolve_move_input() 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 @@ -243,6 +410,96 @@ local function update_camera(dt) end end +local function ensure_physics_setup() + if not physics_state.enabled then + return false + end + if physics_state.ready then + return true + end + + if type(physics_clear) == "function" then + physics_clear() + end + if type(physics_set_gravity) == "function" then + local ok, err = physics_set_gravity(physics_state.gravity) + if not ok then + log_debug("Physics gravity failed: %s", err or "unknown") + end + end + + local ok, err = physics_create_static_mesh( + physics_state.map_body_name, + map_mesh.vertices, + map_mesh.indices, + map_model_matrix) + if not ok then + log_debug("Physics map creation failed: %s", err or "unknown") + return false + end + + local rotation = {0.0, 0.0, 0.0, 1.0} + ok, err = physics_create_sphere( + physics_state.player_body_name, + physics_state.player_radius, + physics_state.player_mass, + physics_state.spawn_position, + rotation) + if not ok then + log_debug("Physics player creation failed: %s", err or "unknown") + return false + end + + if type(physics_set_linear_velocity) == "function" then + physics_set_linear_velocity(physics_state.player_body_name, {0.0, 0.0, 0.0}) + end + + physics_state.ready = true + log_debug("Physics world initialized for Quake 3 map") + return true +end + +local function apply_physics_controls(forward_flat, right) + local move_x, _, move_z = resolve_move_input() + local desired_x = (right[1] * move_x + forward_flat[1] * move_z) * controls.move_speed + local desired_z = (right[3] * move_x + forward_flat[3] * move_z) * controls.move_speed + + local current_velocity = {0.0, 0.0, 0.0} + local velocity, velocity_err = physics_get_linear_velocity(physics_state.player_body_name) + if type(velocity) == "table" then + current_velocity = velocity + elseif velocity_err then + log_debug("Physics velocity query failed: %s", velocity_err) + end + + local desired_velocity = {desired_x, current_velocity[2] or 0.0, desired_z} + local ok, err = physics_set_linear_velocity(physics_state.player_body_name, desired_velocity) + if not ok then + log_debug("Physics velocity failed: %s", err or "unknown") + end + + if type(physics_apply_impulse) == "function" + and is_action_pressed("jump", get_binding("jump")) + and math.abs(current_velocity[2] or 0.0) < physics_state.jump_velocity_threshold then + local impulse = {0.0, physics_state.jump_impulse, 0.0} + local jump_ok, jump_err = physics_apply_impulse(physics_state.player_body_name, impulse) + if not jump_ok then + log_debug("Physics jump failed: %s", jump_err or "unknown") + end + end +end + +local function sync_camera_from_physics() + local transform, transform_err = physics_get_transform(physics_state.player_body_name) + if type(transform) == "table" and type(transform.position) == "table" then + camera.position[1] = transform.position[1] + camera.position[2] = transform.position[2] + physics_state.eye_height + camera.position[3] = transform.position[3] + elseif transform_err then + log_debug("Physics transform query failed: %s", transform_err) + end +end + local shader_variants_module = require("shader_variants") local shader_variants = shader_variants_module.build_cube_variants(config, log_debug) @@ -276,9 +533,47 @@ local function build_view_state(aspect) dt = 0.1 end - update_camera(dt) + update_camera_angles() 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 physics_ready = physics_state.enabled and ensure_physics_setup() + if physics_ready then + if is_action_pressed("noclip_toggle", get_binding("noclip_toggle")) then + physics_state.noclip = not physics_state.noclip + log_debug("Noclip toggled: %s", tostring(physics_state.noclip)) + if not physics_state.noclip and type(physics_set_transform) == "function" then + local rotation = {0.0, 0.0, 0.0, 1.0} + local reset_position = { + camera.position[1], + camera.position[2] - physics_state.eye_height, + camera.position[3], + } + local ok, err = physics_set_transform( + physics_state.player_body_name, + reset_position, + rotation) + if not ok then + log_debug("Physics reset failed: %s", err or "unknown") + elseif type(physics_set_linear_velocity) == "function" then + physics_set_linear_velocity(physics_state.player_body_name, {0.0, 0.0, 0.0}) + end + end + end + if physics_state.noclip then + update_free_fly(dt, forward_flat, right) + else + apply_physics_controls(forward_flat, right) + if dt > 0.0 then + physics_step_simulation(dt, physics_state.max_sub_steps) + end + sync_camera_from_physics() + end + else + update_free_fly(dt, forward_flat, right) + end local center = { camera.position[1] + forward[1], camera.position[2] + forward[2], diff --git a/src/services/impl/bgfx_graphics_backend.cpp b/src/services/impl/bgfx_graphics_backend.cpp index c6c8bf9..fd3a38e 100644 --- a/src/services/impl/bgfx_graphics_backend.cpp +++ b/src/services/impl/bgfx_graphics_backend.cpp @@ -671,6 +671,8 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label, shaderc::Compiler compiler; shaderc::CompileOptions options; options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + options.SetAutoBindUniforms(true); + options.SetAutoMapLocations(true); shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader; @@ -686,7 +688,11 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label, std::vector spirv(result.cbegin(), result.cend()); const bgfx::Memory* mem = bgfx::copy(spirv.data(), static_cast(spirv.size() * sizeof(uint32_t))); - return bgfx::createShader(mem); + bgfx::ShaderHandle handle = bgfx::createShader(mem); + if (!bgfx::isValid(handle) && logger_) { + logger_->Error("BgfxGraphicsBackend::CreateShader: Failed to create shader handle for " + label); + } + return handle; } bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& path, diff --git a/src/services/impl/bgfx_gui_service.cpp b/src/services/impl/bgfx_gui_service.cpp index ee7720c..015e53f 100644 --- a/src/services/impl/bgfx_gui_service.cpp +++ b/src/services/impl/bgfx_gui_service.cpp @@ -35,9 +35,7 @@ layout(location = 2) in vec2 inTexCoord; layout(location = 0) out vec4 fragColor; layout(location = 1) out vec2 fragTexCoord; -layout(set = 0, binding = 1) uniform GuiUniforms { - mat4 u_modelViewProj; -}; +uniform mat4 u_modelViewProj; void main() { fragColor = inColor; @@ -54,7 +52,7 @@ layout(location = 1) in vec2 fragTexCoord; layout(location = 0) out vec4 outColor; -layout(binding = 0) uniform sampler2D s_tex; +uniform sampler2D s_tex; void main() { outColor = fragColor * texture(s_tex, fragTexCoord); @@ -105,11 +103,16 @@ void BgfxGuiService::PrepareFrame(const std::vector& commands, } if (!bgfx::isValid(program_) || !bgfx::isValid(whiteTexture_)) { - if (logger_) { + if (logger_ && !loggedMissingResources_) { logger_->Warn("BgfxGuiService::PrepareFrame: GUI resources not initialized"); } + loggedMissingResources_ = true; return; } + if (loggedMissingResources_ && logger_) { + logger_->Trace("BgfxGuiService", "PrepareFrame", "GUI resources recovered"); + } + loggedMissingResources_ = false; ApplyGuiView(width, height); scissorStack_.clear(); @@ -830,6 +833,8 @@ bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label, shaderc::Compiler compiler; shaderc::CompileOptions options; options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + options.SetAutoBindUniforms(true); + options.SetAutoMapLocations(true); if (logger_) { logger_->Trace("BgfxGuiService", "CreateShader", @@ -848,7 +853,11 @@ bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label, std::vector spirv(result.cbegin(), result.cend()); const bgfx::Memory* mem = bgfx::copy(spirv.data(), static_cast(spirv.size() * sizeof(uint32_t))); - return bgfx::createShader(mem); + bgfx::ShaderHandle handle = bgfx::createShader(mem); + if (!bgfx::isValid(handle) && logger_) { + logger_->Error("BgfxGuiService::CreateShader: Failed to create shader handle for " + label); + } + return handle; } void BgfxGuiService::PruneTextCache() { diff --git a/src/services/impl/bgfx_gui_service.hpp b/src/services/impl/bgfx_gui_service.hpp index 9cd3247..d75cd52 100644 --- a/src/services/impl/bgfx_gui_service.hpp +++ b/src/services/impl/bgfx_gui_service.hpp @@ -159,6 +159,7 @@ private: uint32_t frameHeight_ = 0; uint16_t viewId_ = 1; bool initialized_ = false; + bool loggedMissingResources_ = false; uint64_t frameIndex_ = 0; size_t maxTextCacheEntries_ = 256; size_t maxSvgCacheEntries_ = 64; diff --git a/src/services/impl/materialx_shader_generator.cpp b/src/services/impl/materialx_shader_generator.cpp index f5eb71a..c1881c2 100644 --- a/src/services/impl/materialx_shader_generator.cpp +++ b/src/services/impl/materialx_shader_generator.cpp @@ -265,32 +265,47 @@ bool ReplaceFirstOccurrence(std::string& source, const std::string& before, cons return true; } -std::string ConvertIndividualOutputsToBlock(const std::string& source) { +std::string ConvertIndividualOutputsToBlock(const std::string& source, + const std::shared_ptr& logger) { // Find individual output declarations like: // layout (location = N) out vec3 varname; // And convert them to a VertexData block std::vector> outputs; // location, type, name + const std::string layoutToken = "layout (location ="; + const std::string layoutTokenCompact = "layout(location ="; size_t searchPos = 0; size_t firstOutputStart = std::string::npos; size_t lastOutputEnd = 0; while (true) { - size_t layoutPos = source.find("layout (location =", searchPos); - if (layoutPos == std::string::npos) break; + size_t layoutPos = source.find(layoutToken, searchPos); + size_t compactPos = source.find(layoutTokenCompact, searchPos); + size_t tokenLength = 0; + if (compactPos != std::string::npos && + (layoutPos == std::string::npos || compactPos < layoutPos)) { + layoutPos = compactPos; + tokenLength = layoutTokenCompact.size(); + } else { + tokenLength = layoutToken.size(); + } + if (layoutPos == std::string::npos) { + break; + } // Check if this line contains "out" (to confirm it's an output) size_t lineEnd = source.find('\n', layoutPos); if (lineEnd == std::string::npos) lineEnd = source.size(); std::string line = source.substr(layoutPos, lineEnd - layoutPos); - if (line.find(" out ") == std::string::npos) { + if (line.find(" out ") == std::string::npos || + line.find("VertexData") != std::string::npos) { searchPos = lineEnd; continue; } // Extract location number - size_t locStart = layoutPos + 18; // after "layout (location =" + size_t locStart = layoutPos + tokenLength; // after "layout (location =" while (locStart < source.size() && std::isspace(source[locStart])) ++locStart; size_t locEnd = locStart; while (locEnd < source.size() && std::isdigit(source[locEnd])) ++locEnd; @@ -344,10 +359,16 @@ std::string ConvertIndividualOutputsToBlock(const std::string& source) { return source; } - // Build the VertexData block - std::string block = "layout (location = 0) out VertexData\n{\n"; + std::sort(outputs.begin(), outputs.end(), + [](const auto& left, const auto& right) { + return std::get<0>(left) < std::get<0>(right); + }); + + // Build the VertexData block while preserving explicit locations. + std::string block = "out VertexData\n{\n"; for (const auto& [loc, type, name] : outputs) { - block += " " + type + " " + name + ";\n"; + block += " layout (location = " + std::to_string(loc) + ") " + + type + " " + name + ";\n"; } block += "} vd;\n\n"; @@ -356,36 +377,54 @@ std::string ConvertIndividualOutputsToBlock(const std::string& source) { result += block; result += source.substr(lastOutputEnd); + if (logger) { + logger->Trace("MaterialXShaderGenerator", "Generate", + "vertexOutputsConverted=" + std::to_string(outputs.size())); + } return result; } -std::string ConvertIndividualInputsToBlock(const std::string& source) { +std::string ConvertIndividualInputsToBlock(const std::string& source, + const std::shared_ptr& logger) { // Find individual input declarations like: // layout (location = N) in vec3 varname; // And convert them to a VertexData block std::vector> inputs; // location, type, name + const std::string layoutToken = "layout (location ="; + const std::string layoutTokenCompact = "layout(location ="; size_t searchPos = 0; size_t firstInputStart = std::string::npos; size_t lastInputEnd = 0; while (true) { - size_t layoutPos = source.find("layout (location =", searchPos); - if (layoutPos == std::string::npos) break; + size_t layoutPos = source.find(layoutToken, searchPos); + size_t compactPos = source.find(layoutTokenCompact, searchPos); + size_t tokenLength = 0; + if (compactPos != std::string::npos && + (layoutPos == std::string::npos || compactPos < layoutPos)) { + layoutPos = compactPos; + tokenLength = layoutTokenCompact.size(); + } else { + tokenLength = layoutToken.size(); + } + if (layoutPos == std::string::npos) { + break; + } // Check if this line contains "in" (to confirm it's an input) size_t lineEnd = source.find('\n', layoutPos); if (lineEnd == std::string::npos) lineEnd = source.size(); std::string line = source.substr(layoutPos, lineEnd - layoutPos); - // Skip lines with "in vec3 i_" (vertex inputs) - if (line.find(" in ") == std::string::npos || line.find(" in vec3 i_") != std::string::npos) { + if (line.find(" in ") == std::string::npos || + line.find("VertexData") != std::string::npos) { searchPos = lineEnd; continue; } // Extract location number - size_t locStart = layoutPos + 18; // after "layout (location =" + size_t locStart = layoutPos + tokenLength; // after "layout (location =" while (locStart < source.size() && std::isspace(source[locStart])) ++locStart; size_t locEnd = locStart; while (locEnd < source.size() && std::isdigit(source[locEnd])) ++locEnd; @@ -439,10 +478,16 @@ std::string ConvertIndividualInputsToBlock(const std::string& source) { return source; } - // Build the VertexData block - std::string block = "layout (location = 0) in VertexData\n{\n"; + std::sort(inputs.begin(), inputs.end(), + [](const auto& left, const auto& right) { + return std::get<0>(left) < std::get<0>(right); + }); + + // Build the VertexData block while preserving explicit locations. + std::string block = "in VertexData\n{\n"; for (const auto& [loc, type, name] : inputs) { - block += " " + type + " " + name + ";\n"; + block += " layout (location = " + std::to_string(loc) + ") " + + type + " " + name + ";\n"; } block += "} vd;\n\n"; @@ -451,6 +496,10 @@ std::string ConvertIndividualInputsToBlock(const std::string& source) { result += block; result += source.substr(lastInputEnd); + if (logger) { + logger->Trace("MaterialXShaderGenerator", "Generate", + "fragmentInputsConverted=" + std::to_string(inputs.size())); + } return result; } @@ -778,10 +827,10 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config, // MaterialX VkShaderGenerator incorrectly emits individual out variables instead of // a VertexData struct block, which causes compilation errors when the shader code // references vd.normalWorld etc. We convert them here as a workaround. - paths.vertexSource = ConvertIndividualOutputsToBlock(paths.vertexSource); + paths.vertexSource = ConvertIndividualOutputsToBlock(paths.vertexSource, logger_); // Fix fragment shader inputs: convert individual layout inputs to VertexData block - paths.fragmentSource = ConvertIndividualInputsToBlock(paths.fragmentSource); + paths.fragmentSource = ConvertIndividualInputsToBlock(paths.fragmentSource, logger_); // Ensure any remaining MaterialX tokens are substituted using the generator's map. const unsigned int airyIterations = ResolveAiryFresnelIterations(context, logger_); diff --git a/src/services/impl/physics_bridge_service.cpp b/src/services/impl/physics_bridge_service.cpp index 615e4e1..4b68fed 100644 --- a/src/services/impl/physics_bridge_service.cpp +++ b/src/services/impl/physics_bridge_service.cpp @@ -105,6 +105,7 @@ bool PhysicsBridgeService::AddBoxRigidBody(const std::string& name, std::move(shape), std::move(motionState), std::move(body), + nullptr, }); return true; } @@ -160,6 +161,84 @@ bool PhysicsBridgeService::AddSphereRigidBody(const std::string& name, std::move(shape), std::move(motionState), std::move(body), + nullptr, + }); + return true; +} + +bool PhysicsBridgeService::AddTriangleMeshRigidBody(const std::string& name, + const std::vector>& vertices, + const std::vector& indices, + const btTransform& transform, + std::string& error) { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "AddTriangleMeshRigidBody", + "name=" + name + + ", vertexCount=" + std::to_string(vertices.size()) + + ", indexCount=" + std::to_string(indices.size()) + + ", origin.x=" + std::to_string(transform.getOrigin().getX()) + + ", origin.y=" + std::to_string(transform.getOrigin().getY()) + + ", origin.z=" + std::to_string(transform.getOrigin().getZ())); + } + if (name.empty()) { + error = "Rigid body name must not be empty"; + return false; + } + if (vertices.empty()) { + error = "Triangle mesh vertices must not be empty"; + return false; + } + if (indices.empty()) { + error = "Triangle mesh indices must not be empty"; + return false; + } + if (indices.size() % 3 != 0) { + error = "Triangle mesh indices must be a multiple of 3"; + 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 triangleMesh = std::make_unique(); + for (size_t index = 0; index < indices.size(); index += 3) { + uint32_t i0 = indices[index]; + uint32_t i1 = indices[index + 1]; + uint32_t i2 = indices[index + 2]; + if (i0 >= vertices.size() || i1 >= vertices.size() || i2 >= vertices.size()) { + error = "Triangle mesh index out of range"; + return false; + } + const auto& v0 = vertices[i0]; + const auto& v1 = vertices[i1]; + const auto& v2 = vertices[i2]; + triangleMesh->addTriangle( + btVector3(v0[0], v0[1], v0[2]), + btVector3(v1[0], v1[1], v1[2]), + btVector3(v2[0], v2[1], v2[2]), + true); + } + + auto shape = std::make_unique(triangleMesh.get(), true, true); + btVector3 inertia(0.0f, 0.0f, 0.0f); + auto motionState = std::make_unique(transform); + btRigidBody::btRigidBodyConstructionInfo constructionInfo( + 0.0f, + motionState.get(), + shape.get(), + inertia); + auto body = std::make_unique(constructionInfo); + world_->addRigidBody(body.get()); + bodies_.emplace(name, BodyRecord{ + std::move(shape), + std::move(motionState), + std::move(body), + std::move(triangleMesh), }); return true; } @@ -284,6 +363,23 @@ bool PhysicsBridgeService::SetLinearVelocity(const std::string& name, return true; } +bool PhysicsBridgeService::GetLinearVelocity(const std::string& name, + btVector3& outVelocity, + std::string& error) const { + if (logger_) { + logger_->Trace("PhysicsBridgeService", "GetLinearVelocity", "name=" + name); + } + if (!world_) { + error = "Physics world is not initialized"; + return false; + } + auto* record = FindBodyRecord(name, error); + if (!record || !record->body) { + return false; + } + outVelocity = record->body->getLinearVelocity(); + return true; +} int PhysicsBridgeService::StepSimulation(float deltaTime, int maxSubSteps) { if (logger_) { logger_->Trace("PhysicsBridgeService", "StepSimulation", diff --git a/src/services/impl/physics_bridge_service.hpp b/src/services/impl/physics_bridge_service.hpp index 26ac218..f4d679f 100644 --- a/src/services/impl/physics_bridge_service.hpp +++ b/src/services/impl/physics_bridge_service.hpp @@ -2,15 +2,18 @@ #include "../interfaces/i_physics_bridge_service.hpp" #include "../interfaces/i_logger.hpp" +#include #include #include #include +#include class btVector3; class btTransform; class btCollisionShape; class btMotionState; class btRigidBody; +class btTriangleMesh; class btDefaultCollisionConfiguration; class btCollisionDispatcher; class btBroadphaseInterface; @@ -40,6 +43,11 @@ public: float mass, const btTransform& transform, std::string& error) override; + bool AddTriangleMeshRigidBody(const std::string& name, + const std::vector>& vertices, + const std::vector& indices, + const btTransform& transform, + std::string& error) override; bool RemoveRigidBody(const std::string& name, std::string& error) override; bool SetRigidBodyTransform(const std::string& name, @@ -54,6 +62,9 @@ public: bool SetLinearVelocity(const std::string& name, const btVector3& velocity, std::string& error) override; + bool GetLinearVelocity(const std::string& name, + btVector3& outVelocity, + std::string& error) const override; int StepSimulation(float deltaTime, int maxSubSteps = 10) override; bool GetRigidBodyTransform(const std::string& name, btTransform& outTransform, @@ -66,6 +77,7 @@ private: std::unique_ptr shape; std::unique_ptr motionState; std::unique_ptr body; + std::unique_ptr triangleMesh; }; BodyRecord* FindBodyRecord(const std::string& name, std::string& error); diff --git a/src/services/impl/script_engine_service.cpp b/src/services/impl/script_engine_service.cpp index 9dc87bf..6353259 100644 --- a/src/services/impl/script_engine_service.cpp +++ b/src/services/impl/script_engine_service.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace { namespace mx = MaterialX; @@ -34,6 +35,18 @@ struct MaterialXSurfaceParameters { bool hasMetallic = false; }; +std::array TransformPoint(const std::array& matrix, + const std::array& point) { + const float x = point[0]; + const float y = point[1]; + const float z = point[2]; + return { + matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12], + matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13], + matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14], + }; +} + std::filesystem::path ResolveMaterialXPath(const std::filesystem::path& path, const std::filesystem::path& scriptDirectory) { if (path.empty()) { @@ -474,11 +487,13 @@ void ScriptEngineService::RegisterBindings(lua_State* L) { bind("load_mesh_from_pk3", &ScriptEngineService::LoadMeshFromArchive); bind("physics_create_box", &ScriptEngineService::PhysicsCreateBox); bind("physics_create_sphere", &ScriptEngineService::PhysicsCreateSphere); + bind("physics_create_static_mesh", &ScriptEngineService::PhysicsCreateStaticMesh); bind("physics_remove_body", &ScriptEngineService::PhysicsRemoveBody); bind("physics_set_transform", &ScriptEngineService::PhysicsSetTransform); bind("physics_apply_force", &ScriptEngineService::PhysicsApplyForce); bind("physics_apply_impulse", &ScriptEngineService::PhysicsApplyImpulse); bind("physics_set_linear_velocity", &ScriptEngineService::PhysicsSetLinearVelocity); + bind("physics_get_linear_velocity", &ScriptEngineService::PhysicsGetLinearVelocity); bind("physics_set_gravity", &ScriptEngineService::PhysicsSetGravity); bind("physics_step_simulation", &ScriptEngineService::PhysicsStepSimulation); bind("physics_get_transform", &ScriptEngineService::PhysicsGetTransform); @@ -660,6 +675,94 @@ int ScriptEngineService::PhysicsCreateSphere(lua_State* L) { return 1; } +int ScriptEngineService::PhysicsCreateStaticMesh(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsCreateStaticMesh", + "name=" + std::string(name)); + } + + if (!lua_istable(L, 2) || !lua_istable(L, 3)) { + luaL_error(L, "physics_create_static_mesh expects vertex and index tables"); + } + + std::array transformMatrix = lua::IdentityMatrix(); + if (lua_gettop(L) >= 4) { + if (!lua_istable(L, 4)) { + luaL_error(L, "physics_create_static_mesh expects a transform matrix table"); + } + transformMatrix = lua::ReadMatrix(L, 4); + } + + const int verticesIndex = lua_absindex(L, 2); + const int indicesIndex = lua_absindex(L, 3); + const size_t vertexCount = lua_rawlen(L, verticesIndex); + const size_t indexCount = lua_rawlen(L, indicesIndex); + + std::vector> vertices; + vertices.reserve(vertexCount); + + for (size_t vertexIndex = 1; vertexIndex <= vertexCount; ++vertexIndex) { + lua_rawgeti(L, verticesIndex, static_cast(vertexIndex)); + if (!lua_istable(L, -1)) { + luaL_error(L, "physics_create_static_mesh vertices must be tables"); + } + + std::array position; + lua_getfield(L, -1, "position"); + if (lua_istable(L, -1)) { + position = lua::ReadVector3(L, -1); + lua_pop(L, 1); + } else { + lua_pop(L, 1); + position = lua::ReadVector3(L, -1); + } + lua_pop(L, 1); + + vertices.push_back(TransformPoint(transformMatrix, position)); + } + + std::vector indices; + indices.reserve(indexCount); + for (size_t index = 1; index <= indexCount; ++index) { + lua_rawgeti(L, indicesIndex, static_cast(index)); + if (!lua_isinteger(L, -1) && !lua_isnumber(L, -1)) { + luaL_error(L, "physics_create_static_mesh indices must be numbers"); + } + lua_Integer rawIndex = lua_tointeger(L, -1); + lua_pop(L, 1); + if (rawIndex <= 0) { + luaL_error(L, "physics_create_static_mesh indices must be 1-based positive integers"); + } + indices.push_back(static_cast(rawIndex - 1)); + } + + btTransform transform; + transform.setIdentity(); + std::string error; + if (!context->physicsBridgeService->AddTriangleMeshRigidBody( + name, + vertices, + indices, + transform, + error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + int ScriptEngineService::PhysicsRemoveBody(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); auto logger = context ? context->logger : nullptr; @@ -829,6 +932,39 @@ int ScriptEngineService::PhysicsSetLinearVelocity(lua_State* L) { return 1; } +int ScriptEngineService::PhysicsGetLinearVelocity(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->physicsBridgeService) { + lua_pushnil(L); + lua_pushstring(L, "Physics service not available"); + return 2; + } + + const char* name = luaL_checkstring(L, 1); + if (logger) { + logger->Trace("ScriptEngineService", "PhysicsGetLinearVelocity", + "name=" + std::string(name)); + } + + btVector3 velocity; + std::string error; + if (!context->physicsBridgeService->GetLinearVelocity(name, velocity, error)) { + lua_pushnil(L); + lua_pushstring(L, error.c_str()); + return 2; + } + + lua_newtable(L); + lua_pushnumber(L, velocity.getX()); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, velocity.getY()); + lua_rawseti(L, -2, 2); + lua_pushnumber(L, velocity.getZ()); + lua_rawseti(L, -2, 3); + return 1; +} + int ScriptEngineService::PhysicsSetGravity(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); auto logger = context ? context->logger : nullptr; diff --git a/src/services/impl/script_engine_service.hpp b/src/services/impl/script_engine_service.hpp index 14022fb..ef8328e 100644 --- a/src/services/impl/script_engine_service.hpp +++ b/src/services/impl/script_engine_service.hpp @@ -64,11 +64,13 @@ private: static int LoadMeshFromArchive(lua_State* L); static int PhysicsCreateBox(lua_State* L); static int PhysicsCreateSphere(lua_State* L); + static int PhysicsCreateStaticMesh(lua_State* L); static int PhysicsRemoveBody(lua_State* L); static int PhysicsSetTransform(lua_State* L); static int PhysicsApplyForce(lua_State* L); static int PhysicsApplyImpulse(lua_State* L); static int PhysicsSetLinearVelocity(lua_State* L); + static int PhysicsGetLinearVelocity(lua_State* L); static int PhysicsSetGravity(lua_State* L); static int PhysicsStepSimulation(lua_State* L); static int PhysicsGetTransform(lua_State* L); diff --git a/src/services/interfaces/i_physics_bridge_service.hpp b/src/services/interfaces/i_physics_bridge_service.hpp index 58c6ef8..a2b408d 100644 --- a/src/services/interfaces/i_physics_bridge_service.hpp +++ b/src/services/interfaces/i_physics_bridge_service.hpp @@ -1,7 +1,10 @@ #pragma once +#include #include +#include #include +#include class btVector3; class btTransform; @@ -28,6 +31,11 @@ public: float mass, const btTransform& transform, std::string& error) = 0; + virtual bool AddTriangleMeshRigidBody(const std::string& name, + const std::vector>& vertices, + const std::vector& indices, + const btTransform& transform, + std::string& error) = 0; virtual bool RemoveRigidBody(const std::string& name, std::string& error) = 0; virtual bool SetRigidBodyTransform(const std::string& name, @@ -42,6 +50,9 @@ public: virtual bool SetLinearVelocity(const std::string& name, const btVector3& velocity, std::string& error) = 0; + virtual bool GetLinearVelocity(const std::string& name, + btVector3& outVelocity, + std::string& error) const = 0; virtual int StepSimulation(float deltaTime, int maxSubSteps = 10) = 0; virtual bool GetRigidBodyTransform(const std::string& name, btTransform& outTransform,