diff --git a/config/quake3_runtime.json b/config/quake3_runtime.json index 42ee707..9679b78 100644 --- a/config/quake3_runtime.json +++ b/config/quake3_runtime.json @@ -13,6 +13,29 @@ "bgfx": { "renderer": "vulkan" }, + "materialx": { + "enabled": true, + "parameters_enabled": true, + "document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx", + "shader_key": "pbr", + "material": "Tiled_Wood", + "library_path": "MaterialX/libraries", + "library_folders": [ + "stdlib", + "pbrlib", + "lights", + "bxdf", + "cmlib", + "nprlib", + "targets" + ], + "use_constant_color": false, + "constant_color": [ + 1.0, + 1.0, + 1.0 + ] + }, "mouse_grab": { "enabled": true, "grab_on_click": true, @@ -68,7 +91,7 @@ "quake3": { "pk3_path": "/home/rewrich/Documents/GitHub/q3/pak0.pk3", "map_path": "q3dm1", - "scale": 0.01, + "scale": 0.03, "rotate_x_degrees": -90.0, "offset": [0.0, 0.0, 0.0], "shader_key": "pbr", diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index a83b869..5ac9add 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -257,6 +257,16 @@ local controls = { } local last_frame_time = nil + +local function get_time_seconds() + if type(time_get_seconds) == "function" then + local ok, value = pcall(time_get_seconds) + if ok and type(value) == "number" then + return value + end + end + return os.clock() +end local movement_log_cooldown = 0.0 local world_up = {0.0, 1.0, 0.0} local room = { @@ -1040,7 +1050,7 @@ end local function build_view_state(aspect) - local now = os.clock() + local now = get_time_seconds() local dt = 0.0 if last_frame_time then dt = now - last_frame_time diff --git a/scripts/quake3_arena.lua b/scripts/quake3_arena.lua index 78e2bdf..291c0ae 100644 --- a/scripts/quake3_arena.lua +++ b/scripts/quake3_arena.lua @@ -99,6 +99,17 @@ local function transform_point(matrix, point) } end +local function transform_direction(matrix, direction) + local x = direction[1] + local y = direction[2] + local z = direction[3] + return { + matrix[1] * x + matrix[5] * y + matrix[9] * z, + matrix[2] * x + matrix[6] * y + matrix[10] * z, + matrix[3] * x + matrix[7] * y + matrix[11] * z, + } +end + local function compute_bounds(vertices, matrix) if type(vertices) ~= "table" then return nil @@ -145,6 +156,10 @@ local function normalize(vec) return {x / len, y / len, z / len} end +local function scale_vec3(vec, scale) + return {vec[1] * scale, vec[2] * scale, vec[3] * scale} +end + local function cross(a, b) return { a[2] * b[3] - a[3] * b[2], @@ -213,6 +228,7 @@ local function build_map_model_matrix() end local map_model_matrix = build_map_model_matrix() +local map_up = normalize(transform_direction(map_model_matrix, {0.0, 0.0, 1.0})) if type(load_mesh_from_pk3) ~= "function" then error("load_mesh_from_pk3() is unavailable; rebuild with libzip support") @@ -259,7 +275,7 @@ local function compute_spawn_position(bounds) } end -local function is_position_far_from_bounds(position, bounds) +local function is_position_far_from_bounds(position, bounds, margin_scale) if not bounds then return false end @@ -268,7 +284,8 @@ local function is_position_far_from_bounds(position, bounds) 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 + local scale = resolve_number(margin_scale, 0.5) + local margin = math.max(extent_x, extent_y, extent_z) * scale if position[1] < min_bounds[1] - margin or position[1] > max_bounds[1] + margin then return true end @@ -282,17 +299,25 @@ local function is_position_far_from_bounds(position, bounds) end local camera_position = resolve_vec3(camera_config.position, {0.0, 48.0, 0.0}) -local spawn_position = compute_spawn_position(map_bounds) +local default_spawn_position = compute_spawn_position(map_bounds) +local spawn_position = resolve_vec3(quake3_config.spawn_position, camera_position) 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]} +if auto_spawn and is_position_far_from_bounds(camera_position, map_bounds, quake3_config.bounds_margin_scale) then + camera_position = {default_spawn_position[1], default_spawn_position[2], default_spawn_position[3]} + spawn_position = resolve_vec3(quake3_config.spawn_position, camera_position) log_debug("Camera spawn adjusted to map bounds (%.2f, %.2f, %.2f)", camera_position[1], camera_position[2], camera_position[3]) end +local respawn_config = resolve_table(quake3_config.respawn) +local respawn_enabled = resolve_boolean(respawn_config.enabled, true) +local respawn_margin_scale = resolve_number( + respawn_config.margin_scale, + resolve_number(quake3_config.bounds_margin_scale, 0.5)) + local camera = { position = camera_position, yaw = math.rad(resolve_number(camera_config.yaw_degrees, 0.0)), @@ -322,6 +347,15 @@ if physics_enabled and not physics_available then log_debug("Physics disabled: required bindings are unavailable") end +local align_gravity_to_map = resolve_boolean(physics_config.align_gravity_to_map, true) +local default_gravity = {0.0, -9.8, 0.0} +if align_gravity_to_map then + default_gravity = scale_vec3(map_up, -9.8) + log_debug("Gravity aligned to map up=(%.2f, %.2f, %.2f) gravity=(%.2f, %.2f, %.2f)", + map_up[1], map_up[2], map_up[3], + default_gravity[1], default_gravity[2], default_gravity[3]) +end + local physics_state = { enabled = physics_enabled and physics_available, ready = false, @@ -331,7 +365,7 @@ local physics_state = { 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}), + gravity = resolve_vec3(physics_config.gravity, default_gravity), 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, @@ -340,14 +374,24 @@ local physics_state = { } physics_state.spawn_position = { - camera.position[1], - camera.position[2] - physics_state.eye_height, - camera.position[3], + spawn_position[1], + spawn_position[2] - physics_state.eye_height, + spawn_position[3], } local last_frame_time = nil local world_up = {0.0, 1.0, 0.0} +local function get_time_seconds() + if type(time_get_seconds) == "function" then + local ok, value = pcall(time_get_seconds) + if ok and type(value) == "number" then + return value + end + end + return os.clock() +end + local function update_camera_angles() local look_delta_x = 0.0 local look_delta_y = 0.0 @@ -502,6 +546,45 @@ local function sync_camera_from_physics() end end +local function reset_player_to_spawn(reason) + camera.position[1] = spawn_position[1] + camera.position[2] = spawn_position[2] + camera.position[3] = spawn_position[3] + + if physics_state.enabled and physics_state.ready and type(physics_set_transform) == "function" then + local rotation = {0.0, 0.0, 0.0, 1.0} + local reset_position = { + spawn_position[1], + spawn_position[2] - physics_state.eye_height, + spawn_position[3], + } + local ok, err = physics_set_transform( + physics_state.player_body_name, + reset_position, + rotation) + if not ok then + log_debug("Physics respawn 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 + + log_debug("Respawned player (%s) at (%.2f, %.2f, %.2f)", + tostring(reason or "unknown"), + spawn_position[1], + spawn_position[2], + spawn_position[3]) +end + +local function check_respawn(position) + if not respawn_enabled then + return + end + if is_position_far_from_bounds(position, map_bounds, respawn_margin_scale) then + reset_player_to_spawn("out_of_bounds") + end +end + local shader_variants_module = require("shader_variants") local shader_variants = shader_variants_module.build_cube_variants(config, log_debug) @@ -523,7 +606,7 @@ function get_shader_paths() end local function build_view_state(aspect) - local now = os.clock() + local now = get_time_seconds() local dt = 0.0 if last_frame_time then dt = now - last_frame_time @@ -566,15 +649,18 @@ local function build_view_state(aspect) end if physics_state.noclip then update_free_fly(dt, forward_flat, right) + check_respawn(camera.position) 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() + check_respawn(camera.position) end else update_free_fly(dt, forward_flat, right) + check_respawn(camera.position) end local center = { camera.position[1] + forward[1], diff --git a/src/services/impl/bgfx_graphics_backend.cpp b/src/services/impl/bgfx_graphics_backend.cpp index fd3a38e..7b0fd08 100644 --- a/src/services/impl/bgfx_graphics_backend.cpp +++ b/src/services/impl/bgfx_graphics_backend.cpp @@ -670,10 +670,17 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label, bool isVertex) const { shaderc::Compiler compiler; shaderc::CompileOptions options; - options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1); options.SetAutoBindUniforms(true); options.SetAutoMapLocations(true); + if (logger_) { + logger_->Trace("BgfxGraphicsBackend", "CreateShader", + "label=" + label + + ", renderer=" + RendererTypeName(bgfx::getRendererType()) + + ", sourceLength=" + std::to_string(source.size())); + } + shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader; auto result = compiler.CompileGlslToSpv(source, kind, label.c_str(), options); diff --git a/src/services/impl/bgfx_gui_service.cpp b/src/services/impl/bgfx_gui_service.cpp index 015e53f..bfae446 100644 --- a/src/services/impl/bgfx_gui_service.cpp +++ b/src/services/impl/bgfx_gui_service.cpp @@ -25,6 +25,29 @@ namespace { constexpr uint64_t kGuiSamplerFlags = BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; +const char* RendererTypeName(bgfx::RendererType::Enum type) { + switch (type) { + case bgfx::RendererType::Vulkan: + return "Vulkan"; + case bgfx::RendererType::OpenGL: + return "OpenGL"; + case bgfx::RendererType::OpenGLES: + return "OpenGLES"; + case bgfx::RendererType::Direct3D11: + return "Direct3D11"; + case bgfx::RendererType::Direct3D12: + return "Direct3D12"; + case bgfx::RendererType::Metal: + return "Metal"; + case bgfx::RendererType::Noop: + return "Noop"; + case bgfx::RendererType::Count: + return "Auto"; + default: + return "Unknown"; + } +} + const char* kGuiVertexSource = R"( #version 450 @@ -35,7 +58,9 @@ layout(location = 2) in vec2 inTexCoord; layout(location = 0) out vec4 fragColor; layout(location = 1) out vec2 fragTexCoord; -uniform mat4 u_modelViewProj; +layout(std140) uniform GuiUniforms { + mat4 u_modelViewProj; +}; void main() { fragColor = inColor; @@ -832,13 +857,15 @@ bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label, bool isVertex) const { shaderc::Compiler compiler; shaderc::CompileOptions options; - options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1); options.SetAutoBindUniforms(true); options.SetAutoMapLocations(true); if (logger_) { logger_->Trace("BgfxGuiService", "CreateShader", - "label=" + label + ", sourceLength=" + std::to_string(source.size())); + "label=" + label + + ", renderer=" + std::string(RendererTypeName(bgfx::getRendererType())) + + ", sourceLength=" + std::to_string(source.size())); } shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader; diff --git a/src/services/impl/mesh_service.cpp b/src/services/impl/mesh_service.cpp index 6a50d1c..df8cd05 100644 --- a/src/services/impl/mesh_service.cpp +++ b/src/services/impl/mesh_service.cpp @@ -286,6 +286,7 @@ bool BuildPayloadFromBspBuffer(const std::vector& buffer, outPayload.positions.resize(vertexCount); outPayload.normals.resize(vertexCount); outPayload.colors.resize(vertexCount); + outPayload.texcoords.resize(vertexCount); outPayload.indices.clear(); for (size_t i = 0; i < vertexCount; ++i) { @@ -297,6 +298,7 @@ bool BuildPayloadFromBspBuffer(const std::vector& buffer, static_cast(vertex.color[1]) / 255.0f, static_cast(vertex.color[2]) / 255.0f }; + outPayload.texcoords[i] = {vertex.texCoord[0], vertex.texCoord[1]}; } size_t trianglesBuilt = 0; @@ -344,6 +346,7 @@ bool BuildPayloadFromBspBuffer(const std::vector& buffer, ", meshVertCount=" + std::to_string(meshVertCount) + ", faceCount=" + std::to_string(faceCount) + ", trianglesBuilt=" + std::to_string(trianglesBuilt) + + ", texcoordCount=" + std::to_string(outPayload.texcoords.size()) + ", trianglesSkipped=" + std::to_string(trianglesSkipped)); } diff --git a/src/services/impl/script_engine_service.cpp b/src/services/impl/script_engine_service.cpp index 6353259..ce10a82 100644 --- a/src/services/impl/script_engine_service.cpp +++ b/src/services/impl/script_engine_service.cpp @@ -520,6 +520,7 @@ void ScriptEngineService::RegisterBindings(lua_State* L) { bind("config_get_json", &ScriptEngineService::ConfigGetJson); bind("config_get_table", &ScriptEngineService::ConfigGetTable); bind("materialx_get_surface_parameters", &ScriptEngineService::MaterialXGetSurfaceParameters); + bind("time_get_seconds", &ScriptEngineService::TimeGetSeconds); bind("window_get_size", &ScriptEngineService::WindowGetSize); bind("window_set_title", &ScriptEngineService::WindowSetTitle); bind("window_is_minimized", &ScriptEngineService::WindowIsMinimized); @@ -1320,6 +1321,23 @@ int ScriptEngineService::ConfigGetTable(lua_State* L) { return 1; } +int ScriptEngineService::TimeGetSeconds(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + const Uint64 counter = SDL_GetPerformanceCounter(); + const Uint64 frequency = SDL_GetPerformanceFrequency(); + double seconds = 0.0; + if (frequency > 0) { + seconds = static_cast(counter) / static_cast(frequency); + } + if (logger) { + logger->Trace("ScriptEngineService", "TimeGetSeconds", + "seconds=" + std::to_string(seconds)); + } + lua_pushnumber(L, seconds); + return 1; +} + int ScriptEngineService::MaterialXGetSurfaceParameters(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 ef8328e..5aec40d 100644 --- a/src/services/impl/script_engine_service.hpp +++ b/src/services/impl/script_engine_service.hpp @@ -97,6 +97,7 @@ private: static int ConfigGetJson(lua_State* L); static int ConfigGetTable(lua_State* L); static int MaterialXGetSurfaceParameters(lua_State* L); + static int TimeGetSeconds(lua_State* L); static int WindowGetSize(lua_State* L); static int WindowSetTitle(lua_State* L); static int WindowIsMinimized(lua_State* L);