From 4905d85667206c271557a74d413c77c340730e28 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 2 May 2026 21:26:20 +0100 Subject: [PATCH] Add Quake 3 menu and weapon workflow steps --- gameengine/CMakeLists.txt | 4 + .../quake3/shaders/msl/bsp.frag.metal | 31 +- .../quake3/shaders/msl/overlay.frag.metal | 13 + .../quake3/shaders/msl/overlay.vert.metal | 19 ++ .../packages/quake3/workflows/q3_frame.json | 36 ++- .../packages/quake3/workflows/q3_game.json | 6 +- .../workflow_gpu_pipeline_create_step.cpp | 11 + .../quake3/workflow_q3_menu_update_step.cpp | 48 +++ .../quake3/workflow_q3_overlay_draw_step.cpp | 296 ++++++++++++++++++ .../quake3/workflow_q3_weapon_update_step.cpp | 97 ++++++ .../workflow_bsp_build_geometry_step.cpp | 1 + .../workflow_bsp_entity_update_step.cpp | 30 +- .../rendering/workflow_bsp_load_step.cpp | 15 +- .../workflow_bsp_portal_view_step.cpp | 215 +++++++++++++ .../rendering/workflow_draw_map_step.cpp | 38 ++- .../workflow_input_poll_step.cpp | 31 ++ .../workflow_physics_fps_move_step.cpp | 5 + .../impl/workflow/workflow_registrar.cpp | 10 + .../quake3/workflow_q3_menu_update_step.hpp | 20 ++ .../quake3/workflow_q3_overlay_draw_step.hpp | 46 +++ .../quake3/workflow_q3_weapon_update_step.hpp | 20 ++ .../workflow_bsp_portal_view_step.hpp | 20 ++ 22 files changed, 990 insertions(+), 22 deletions(-) create mode 100644 gameengine/packages/quake3/shaders/msl/overlay.frag.metal create mode 100644 gameengine/packages/quake3/shaders/msl/overlay.vert.metal create mode 100644 gameengine/src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp create mode 100644 gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp create mode 100644 gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp create mode 100644 gameengine/src/services/impl/workflow/rendering/workflow_bsp_portal_view_step.cpp create mode 100644 gameengine/src/services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp create mode 100644 gameengine/src/services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp create mode 100644 gameengine/src/services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp create mode 100644 gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index 216723e60..96c03d659 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -283,6 +283,9 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/geometry/workflow_geometry_create_cube_step.cpp src/services/impl/workflow/geometry/workflow_geometry_create_plane_step.cpp src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp + src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp + src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp + src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp src/services/impl/workflow/rendering/workflow_bsp_build_collision_step.cpp src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp @@ -290,6 +293,7 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/rendering/workflow_bsp_lightmap_atlas_step.cpp src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp src/services/impl/workflow/rendering/workflow_bsp_parse_spawn_step.cpp + src/services/impl/workflow/rendering/workflow_bsp_portal_view_step.cpp src/services/impl/workflow/rendering/workflow_bsp_upload_geometry_step.cpp src/services/impl/workflow/rendering/workflow_draw_map_step.cpp src/services/impl/workflow/rendering/workflow_draw_textured_box_step.cpp diff --git a/gameengine/packages/quake3/shaders/msl/bsp.frag.metal b/gameengine/packages/quake3/shaders/msl/bsp.frag.metal index b2d416d42..ae4c30937 100644 --- a/gameengine/packages/quake3/shaders/msl/bsp.frag.metal +++ b/gameengine/packages/quake3/shaders/msl/bsp.frag.metal @@ -28,13 +28,13 @@ fragment float4 main0( sampler shadowSampler [[sampler(1)]], texture2d lightmapTex [[texture(2)]], sampler lightmapSampler [[sampler(2)]], + texture2d portalTex [[texture(3)]], + sampler portalSampler [[sampler(3)]], constant PBRUniforms& pbr [[buffer(0)]]) { (void)shadowMap; (void)shadowSampler; (void)in.worldNormal; - (void)in.worldPos; - (void)in.cameraPos; float3 albedo = albedoTex.sample(albedoSampler, in.uv).rgb; float3 lightmap = lightmapTex.sample(lightmapSampler, in.lightmapUv).rgb; @@ -42,5 +42,32 @@ fragment float4 main0( float3 ambient = pbr.u_ambient.rgb * albedo; float exposure = (pbr.u_lightColor.a > 0.0) ? pbr.u_lightColor.a : 1.0; + if (pbr.u_material.w > 0.5) { + float time = pbr.u_material.y; + float3 viewDir = normalize(in.cameraPos - in.worldPos); + float3 n = normalize(in.worldNormal); + float3 reflected = reflect(-viewDir, n); + float fresnel = pow(1.0 - saturate(dot(viewDir, n)), 3.0); + + float2 reflectUv = reflected.xz * 0.32 + float2(0.5, 0.5); + reflectUv += float2(sin(time * 1.7 + in.worldPos.y * 0.35), + cos(time * 1.3 + in.worldPos.x * 0.28)) * 0.035; + + float3 portalBase = albedoTex.sample(albedoSampler, reflectUv).rgb; + float2 portalUv = fract(in.uv); + portalUv.y = 1.0 - portalUv.y; + float2 centered = portalUv - float2(0.5, 0.5); + float radial = length(centered); + float centerMask = 1.0 - smoothstep(0.34, 0.49, radial); + float3 destination = portalTex.sample(portalSampler, portalUv).rgb; + float pulse = 0.5 + 0.5 * sin(time * 3.0 + length(in.worldPos.xz) * 0.45); + float3 glow = float3(0.18, 0.42, 0.95) * (0.35 + 0.35 * pulse); + float3 reflectedColor = mix(portalBase, glow, 0.18 + 0.32 * fresnel); + float3 color = mix(reflectedColor, destination, centerMask * 0.88); + color += glow * (1.0 - centerMask) * 0.45; + float alpha = mix(0.18 + 0.24 * fresnel, 0.92, centerMask); + return float4((color * 1.08 + ambient * 0.08) * exposure, alpha); + } + return float4((albedo * lightmap * overbright + ambient) * exposure, 1.0); } diff --git a/gameengine/packages/quake3/shaders/msl/overlay.frag.metal b/gameengine/packages/quake3/shaders/msl/overlay.frag.metal new file mode 100644 index 000000000..463833f51 --- /dev/null +++ b/gameengine/packages/quake3/shaders/msl/overlay.frag.metal @@ -0,0 +1,13 @@ +#include +using namespace metal; + +struct FragmentInput { + float4 position [[position]]; + float2 uv; +}; + +fragment float4 main0(FragmentInput in [[stage_in]], + texture2d overlayTex [[texture(0)]], + sampler overlaySampler [[sampler(0)]]) { + return overlayTex.sample(overlaySampler, in.uv); +} diff --git a/gameengine/packages/quake3/shaders/msl/overlay.vert.metal b/gameengine/packages/quake3/shaders/msl/overlay.vert.metal new file mode 100644 index 000000000..a00e1a1e1 --- /dev/null +++ b/gameengine/packages/quake3/shaders/msl/overlay.vert.metal @@ -0,0 +1,19 @@ +#include +using namespace metal; + +struct VertexInput { + float3 position [[attribute(0)]]; + float2 uv [[attribute(1)]]; +}; + +struct VertexOutput { + float4 position [[position]]; + float2 uv; +}; + +vertex VertexOutput main0(VertexInput in [[stage_in]]) { + VertexOutput out; + out.position = float4(in.position, 1.0); + out.uv = in.uv; + return out; +} diff --git a/gameengine/packages/quake3/workflows/q3_frame.json b/gameengine/packages/quake3/workflows/q3_frame.json index c6e595d4d..acb62aa02 100644 --- a/gameengine/packages/quake3/workflows/q3_frame.json +++ b/gameengine/packages/quake3/workflows/q3_frame.json @@ -8,6 +8,12 @@ "typeVersion": 1, "position": [0, 0] }, + { + "id": "q3_menu", + "type": "q3.menu.update", + "typeVersion": 1, + "position": [100, 0] + }, { "id": "physics_move", "type": "physics.fps.move", @@ -58,12 +64,24 @@ "far": 500.0 } }, + { + "id": "q3_weapon", + "type": "q3.weapon.update", + "typeVersion": 1, + "position": [690, 0] + }, { "id": "render_prepare", "type": "render.prepare", "typeVersion": 1, "position": [750, 0] }, + { + "id": "portal_view", + "type": "bsp.portal_view", + "typeVersion": 1, + "position": [775, 0] + }, { "id": "frame_begin", "type": "frame.gpu.begin", @@ -99,6 +117,12 @@ "typeVersion": 1, "position": [1225, 0] }, + { + "id": "q3_overlay", + "type": "q3.overlay.draw", + "typeVersion": 1, + "position": [1235, 0] + }, { "id": "postfx_taa", "type": "postfx.taa", @@ -132,17 +156,21 @@ } ], "connections": { - "input_poll": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } }, + "input_poll": { "main": { "0": [{ "node": "q3_menu", "type": "main", "index": 0 }] } }, + "q3_menu": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } }, "physics_move": { "main": { "0": [{ "node": "physics_step", "type": "main", "index": 0 }] } }, "physics_step": { "main": { "0": [{ "node": "sync_transforms", "type": "main", "index": 0 }] } }, "sync_transforms": { "main": { "0": [{ "node": "bsp_entities_update", "type": "main", "index": 0 }] } }, "bsp_entities_update": { "main": { "0": [{ "node": "camera_update", "type": "main", "index": 0 }] } }, - "camera_update": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } }, - "render_prepare": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } }, + "camera_update": { "main": { "0": [{ "node": "q3_weapon", "type": "main", "index": 0 }] } }, + "q3_weapon": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } }, + "render_prepare": { "main": { "0": [{ "node": "portal_view", "type": "main", "index": 0 }] } }, + "portal_view": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } }, "frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } }, "draw_map": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } }, "end_scene": { "main": { "0": [{ "node": "overlay_fps", "type": "main", "index": 0 }] } }, - "overlay_fps": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } }, + "overlay_fps": { "main": { "0": [{ "node": "q3_overlay", "type": "main", "index": 0 }] } }, + "q3_overlay": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } }, "postfx_taa": { "main": { "0": [{ "node": "postfx_ssao", "type": "main", "index": 0 }] } }, "postfx_ssao": { "main": { "0": [{ "node": "bloom_extract", "type": "main", "index": 0 }] } }, "bloom_extract": { "main": { "0": [{ "node": "bloom_blur", "type": "main", "index": 0 }] } }, diff --git a/gameengine/packages/quake3/workflows/q3_game.json b/gameengine/packages/quake3/workflows/q3_game.json index 38416448d..c3c2f061c 100644 --- a/gameengine/packages/quake3/workflows/q3_game.json +++ b/gameengine/packages/quake3/workflows/q3_game.json @@ -37,15 +37,15 @@ "parameters": { "stage": "vertex", "output_key": "bsp_vertex_shader", "num_uniform_buffers": 1, "num_samplers": 0 }, "inputs": { "shader_path": "shader_bsp_vert_path" } }, { "id": "compile_bsp_frag", "type": "graphics.gpu.shader.compile", "typeVersion": 1, "position": [1400, 0], - "parameters": { "stage": "fragment", "output_key": "bsp_fragment_shader", "num_uniform_buffers": 1, "num_samplers": 3 }, + "parameters": { "stage": "fragment", "output_key": "bsp_fragment_shader", "num_uniform_buffers": 1, "num_samplers": 4 }, "inputs": { "shader_path": "shader_bsp_frag_path" } }, { "id": "create_bsp_pipeline", "type": "graphics.gpu.pipeline.create", "typeVersion": 1, "position": [1500, 0], - "parameters": { "vertex_shader_key": "bsp_vertex_shader", "fragment_shader_key": "bsp_fragment_shader", "vertex_format": "position_uv_lmuv_normal", "pipeline_key": "gpu_pipeline_bsp" } }, + "parameters": { "vertex_shader_key": "bsp_vertex_shader", "fragment_shader_key": "bsp_fragment_shader", "vertex_format": "position_uv_lmuv_normal", "pipeline_key": "gpu_pipeline_bsp", "alpha_blend": 1 } }, { "id": "tex_walls", "name": "Load Texture", "type": "texture.load", "typeVersion": 1, "position": [1600, 0], "parameters": { "inputs": { "image_path": "tex_walls_path" }, "outputs": { "texture": "walls_texture" } } }, { "id": "physics_world", "type": "physics.world.create", "typeVersion": 1, "position": [0, 200] }, { "id": "load_bsp", "name": "Load Q3 BSP", "type": "bsp.load", "typeVersion": 1, "position": [200, 200], - "parameters": { "pk3_path": "${env:QUAKE3_PAK0}", "map_name": "q3dm7", "scale": 0.03125 } }, + "parameters": { "pk3_path": "${env:QUAKE3_PAK0}", "map_name": "${env:QUAKE3_MAP}", "scale": 0.03125 } }, { "id": "bsp_lightmap", "name": "BSP Lightmap Atlas", "type": "bsp.lightmap_atlas", "typeVersion": 1, "position": [400, 200] }, { "id": "bsp_geometry", "name": "BSP Build Geometry", "type": "bsp.build_geometry", "typeVersion": 1, "position": [600, 200], "parameters": { "patch_tess_level": 4 } }, diff --git a/gameengine/src/services/impl/workflow/graphics/workflow_gpu_pipeline_create_step.cpp b/gameengine/src/services/impl/workflow/graphics/workflow_gpu_pipeline_create_step.cpp index e12043908..03b8ee2a6 100644 --- a/gameengine/src/services/impl/workflow/graphics/workflow_gpu_pipeline_create_step.cpp +++ b/gameengine/src/services/impl/workflow/graphics/workflow_gpu_pipeline_create_step.cpp @@ -42,6 +42,7 @@ void WorkflowGpuPipelineCreateStep::Execute(const WorkflowStepDefinition& step, const bool release_shaders = static_cast(getNum("release_shaders", 1)) != 0; const std::string color_format_str = getStr("color_format", "swapchain"); const bool has_depth = static_cast(getNum("has_depth", 1)) != 0; + const bool alpha_blend = static_cast(getNum("alpha_blend", 0)) != 0; // Get GPU device SDL_GPUDevice* device = context.Get("gpu_device", nullptr); @@ -155,6 +156,16 @@ void WorkflowGpuPipelineCreateStep::Execute(const WorkflowStepDefinition& step, color_target.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; } } + + if (alpha_blend) { + color_target.blend_state.enable_blend = true; + color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; + color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ZERO; + color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + } } // Build pipeline create info diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp new file mode 100644 index 000000000..5ba5a3dcc --- /dev/null +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp @@ -0,0 +1,48 @@ +#include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp" + +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowQ3MenuUpdateStep::WorkflowQ3MenuUpdateStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowQ3MenuUpdateStep::GetPluginId() const { + return "q3.menu.update"; +} + +void WorkflowQ3MenuUpdateStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + bool open = context.GetBool("q3.menu_open", true); + if (context.GetBool("input_key_escape_pressed", false)) { + open = !open; + } + context.Set("q3.menu_open", open); + + auto maps = context.Get("q3.maps", nlohmann::json::array()); + if (!maps.is_array() || maps.empty()) { + maps = nlohmann::json::array({"q3dm7"}); + } + + int selected = context.Get("q3.menu_selected_map", 0); + selected = std::clamp(selected, 0, static_cast(maps.size()) - 1); + + if (open) { + if (context.GetBool("input_key_up_pressed", false)) { + selected = (selected + static_cast(maps.size()) - 1) % static_cast(maps.size()); + } + if (context.GetBool("input_key_down_pressed", false)) { + selected = (selected + 1) % static_cast(maps.size()); + } + if (context.GetBool("input_key_enter_pressed", false)) { + const std::string map = maps[selected].get(); + context.Set("q3.pending_map", map); + if (logger_) logger_->Info("q3.menu.update: selected map " + map + " (restart with QUAKE3_MAP=" + map + ")"); + } + } + + context.Set("q3.menu_selected_map", selected); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp new file mode 100644 index 000000000..6a17f9770 --- /dev/null +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp @@ -0,0 +1,296 @@ +#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +namespace { + +std::vector LoadBinary(const char* path) { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f.is_open()) return {}; + auto size = f.tellg(); + std::vector data(static_cast(size)); + f.seekg(0); + f.read(reinterpret_cast(data.data()), size); + return data; +} + +void Text(SDL_Renderer* r, float x, float y, const char* text, SDL_Color color) { + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); + SDL_RenderDebugText(r, x, y, text); +} + +} // namespace + +WorkflowQ3OverlayDrawStep::WorkflowQ3OverlayDrawStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +WorkflowQ3OverlayDrawStep::~WorkflowQ3OverlayDrawStep() { + if (renderer_) SDL_DestroyRenderer(renderer_); + if (surface_) SDL_DestroySurface(surface_); + if (device_) { + if (sampler_) SDL_ReleaseGPUSampler(device_, sampler_); + if (vtx_buf_) SDL_ReleaseGPUBuffer(device_, vtx_buf_); + if (transfer_) SDL_ReleaseGPUTransferBuffer(device_, transfer_); + if (tex_) SDL_ReleaseGPUTexture(device_, tex_); + if (pipeline_) SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_); + } +} + +std::string WorkflowQ3OverlayDrawStep::GetPluginId() const { + return "q3.overlay.draw"; +} + +void WorkflowQ3OverlayDrawStep::TryInit(SDL_GPUDevice* device, SDL_Window* window) { + if (disabled_ || ready_) return; + device_ = device; + + const char* driver = SDL_GetGPUDeviceDriver(device); + const std::string driverName = driver ? driver : ""; + SDL_GPUShaderFormat shaderFormat = SDL_GPU_SHADERFORMAT_INVALID; + std::vector vert; + std::vector frag; + const char* entry = "main"; + if (driverName == "metal") { + shaderFormat = SDL_GPU_SHADERFORMAT_MSL; + vert = LoadBinary("packages/quake3/shaders/msl/overlay.vert.metal"); + frag = LoadBinary("packages/quake3/shaders/msl/overlay.frag.metal"); + entry = "main0"; + } else if (driverName == "vulkan") { + shaderFormat = SDL_GPU_SHADERFORMAT_SPIRV; + vert = LoadBinary("packages/quake3/shaders/spirv/overlay.vert.spv"); + frag = LoadBinary("packages/quake3/shaders/spirv/overlay.frag.spv"); + } else { + disabled_ = true; + return; + } + if (vert.empty() || frag.empty()) { + disabled_ = true; + return; + } + + SDL_GPUShaderCreateInfo vsi = {}; + vsi.code = vert.data(); + vsi.code_size = vert.size(); + vsi.entrypoint = entry; + vsi.format = shaderFormat; + vsi.stage = SDL_GPU_SHADERSTAGE_VERTEX; + SDL_GPUShaderCreateInfo fsi = {}; + fsi.code = frag.data(); + fsi.code_size = frag.size(); + fsi.entrypoint = entry; + fsi.format = shaderFormat; + fsi.stage = SDL_GPU_SHADERSTAGE_FRAGMENT; + fsi.num_samplers = 1; + auto* vs = SDL_CreateGPUShader(device, &vsi); + auto* fs = SDL_CreateGPUShader(device, &fsi); + if (!vs || !fs) { + if (vs) SDL_ReleaseGPUShader(device, vs); + if (fs) SDL_ReleaseGPUShader(device, fs); + disabled_ = true; + return; + } + + SDL_GPUVertexBufferDescription vbd = {}; + vbd.slot = 0; + vbd.pitch = sizeof(float) * 5; + vbd.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; + SDL_GPUVertexAttribute attrs[2] = {}; + attrs[0] = {0, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, 0}; + attrs[1] = {1, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, sizeof(float) * 3}; + SDL_GPUVertexInputState vis = {}; + vis.vertex_buffer_descriptions = &vbd; + vis.num_vertex_buffers = 1; + vis.vertex_attributes = attrs; + vis.num_vertex_attributes = 2; + + SDL_GPUColorTargetDescription ctd = {}; + ctd.format = window ? SDL_GetGPUSwapchainTextureFormat(device, window) : SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; + ctd.blend_state.enable_blend = true; + ctd.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + ctd.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + ctd.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; + ctd.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + ctd.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ZERO; + ctd.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + + SDL_GPUGraphicsPipelineCreateInfo pci = {}; + pci.vertex_shader = vs; + pci.fragment_shader = fs; + pci.vertex_input_state = vis; + pci.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + pci.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; + pci.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; + pci.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; + pci.depth_stencil_state.enable_depth_test = false; + pci.depth_stencil_state.enable_depth_write = false; + pci.target_info.num_color_targets = 1; + pci.target_info.color_target_descriptions = &ctd; + pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &pci); + SDL_ReleaseGPUShader(device, vs); + SDL_ReleaseGPUShader(device, fs); + if (!pipeline_) { + disabled_ = true; + return; + } + + SDL_GPUTextureCreateInfo tci = {}; + tci.type = SDL_GPU_TEXTURETYPE_2D; + tci.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; + tci.width = kW; + tci.height = kH; + tci.layer_count_or_depth = 1; + tci.num_levels = 1; + tci.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; + tex_ = SDL_CreateGPUTexture(device, &tci); + SDL_GPUTransferBufferCreateInfo tbci = {}; + tbci.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbci.size = kW * kH * 4; + transfer_ = SDL_CreateGPUTransferBuffer(device, &tbci); + SDL_GPUBufferCreateInfo bci = {}; + bci.usage = SDL_GPU_BUFFERUSAGE_VERTEX; + bci.size = 6u * 5u * static_cast(sizeof(float)); + vtx_buf_ = SDL_CreateGPUBuffer(device, &bci); + SDL_GPUSamplerCreateInfo sci = {}; + sci.min_filter = SDL_GPU_FILTER_NEAREST; + sci.mag_filter = SDL_GPU_FILTER_NEAREST; + sci.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; + sci.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + sci.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + sampler_ = SDL_CreateGPUSampler(device, &sci); + surface_ = SDL_CreateSurface(kW, kH, SDL_PIXELFORMAT_RGBA32); + renderer_ = surface_ ? SDL_CreateSoftwareRenderer(surface_) : nullptr; + ready_ = tex_ && transfer_ && vtx_buf_ && sampler_ && surface_ && renderer_; +} + +void WorkflowQ3OverlayDrawStep::DrawSurface(WorkflowContext& context, uint32_t frameW, uint32_t frameH) { + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); + SDL_RenderClear(renderer_); + + SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); + SDL_FRect hudBg{12, static_cast(kH - 44), 230, 28}; + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 150); + SDL_RenderFillRect(renderer_, &hudBg); + + const std::string weapon = context.Get("q3.current_weapon", "weapon_machinegun"); + const int shots = context.Get("q3.shots_fired", 0); + std::string hud = "WEAPON " + weapon.substr(7) + " SHOTS " + std::to_string(shots); + Text(renderer_, 20, static_cast(kH - 36), hud.c_str(), SDL_Color{255, 216, 64, 255}); + Text(renderer_, static_cast(kW / 2 - 4), static_cast(kH / 2 - 4), "+", SDL_Color{255, 255, 255, 220}); + + if (context.GetBool("q3.menu_open", false)) { + SDL_FRect panel{120, 42, 400, 250}; + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 210); + SDL_RenderFillRect(renderer_, &panel); + SDL_SetRenderDrawColor(renderer_, 40, 120, 220, 255); + SDL_RenderRect(renderer_, &panel); + Text(renderer_, 170, 62, "QUAKE III ARENA", SDL_Color{255, 216, 64, 255}); + Text(renderer_, 160, 88, "SKIRMISH / MAP SELECTION", SDL_Color{180, 220, 255, 255}); + + auto maps = context.Get("q3.maps", nlohmann::json::array()); + int selected = context.Get("q3.menu_selected_map", 0); + for (int i = 0; i < 8 && i < static_cast(maps.size()); ++i) { + int idx = (selected / 8) * 8 + i; + if (idx >= static_cast(maps.size())) break; + std::string line = (idx == selected ? "> " : " ") + maps[idx].get(); + Text(renderer_, 176, 122 + i * 16, line.c_str(), + idx == selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{140, 190, 240, 255}); + } + Text(renderer_, 154, 266, "UP/DOWN SELECT ENTER SET MAP ESC RESUME Q QUIT", SDL_Color{180, 180, 180, 255}); + auto pending = context.Get("q3.pending_map", ""); + if (!pending.empty()) { + std::string msg = "NEXT START: QUAKE3_MAP=" + pending; + Text(renderer_, 160, 246, msg.c_str(), SDL_Color{255, 170, 80, 255}); + } + } + + SDL_RenderPresent(renderer_); +} + +void WorkflowQ3OverlayDrawStep::Render(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchainTex, + SDL_GPUDevice* device, uint32_t frameW, uint32_t frameH) { + void* mapped = SDL_MapGPUTransferBuffer(device, transfer_, false); + if (!mapped) return; + std::memcpy(mapped, surface_->pixels, kW * kH * 4); + SDL_UnmapGPUTransferBuffer(device, transfer_); + + auto* copy = SDL_BeginGPUCopyPass(cmd); + if (copy) { + SDL_GPUTextureTransferInfo src = {}; + src.transfer_buffer = transfer_; + src.pixels_per_row = kW; + src.rows_per_layer = kH; + SDL_GPUTextureRegion dst = {}; + dst.texture = tex_; + dst.w = kW; + dst.h = kH; + dst.d = 1; + SDL_UploadToGPUTexture(copy, &src, &dst, false); + SDL_EndGPUCopyPass(copy); + } + + if (!vbuf_uploaded_) { + const float verts[6][5] = { + {-1, 1, 0, 0, 0}, { 1, 1, 0, 1, 0}, { 1, -1, 0, 1, 1}, + {-1, 1, 0, 0, 0}, { 1, -1, 0, 1, 1}, {-1, -1, 0, 0, 1}, + }; + const uint32_t size = sizeof(verts); + SDL_GPUTransferBufferCreateInfo tb = {}; + tb.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tb.size = size; + auto* tmp = SDL_CreateGPUTransferBuffer(device, &tb); + if (tmp) { + void* ptr = SDL_MapGPUTransferBuffer(device, tmp, false); + if (ptr) { + std::memcpy(ptr, verts, size); + SDL_UnmapGPUTransferBuffer(device, tmp); + } + auto* cp = SDL_BeginGPUCopyPass(cmd); + if (cp) { + SDL_GPUTransferBufferLocation src = {tmp, 0}; + SDL_GPUBufferRegion dst = {vtx_buf_, 0, size}; + SDL_UploadToGPUBuffer(cp, &src, &dst, false); + SDL_EndGPUCopyPass(cp); + } + SDL_ReleaseGPUTransferBuffer(device, tmp); + } + vbuf_uploaded_ = true; + } + + SDL_GPUColorTargetInfo target = {}; + target.texture = swapchainTex; + target.load_op = SDL_GPU_LOADOP_LOAD; + target.store_op = SDL_GPU_STOREOP_STORE; + auto* pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr); + if (!pass) return; + SDL_BindGPUGraphicsPipeline(pass, pipeline_); + SDL_GPUBufferBinding vb = {vtx_buf_, 0}; + SDL_BindGPUVertexBuffers(pass, 0, &vb, 1); + SDL_GPUTextureSamplerBinding ts = {tex_, sampler_}; + SDL_BindGPUFragmentSamplers(pass, 0, &ts, 1); + SDL_DrawGPUPrimitives(pass, 6, 1, 0, 0); + SDL_EndGPURenderPass(pass); +} + +void WorkflowQ3OverlayDrawStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + if (context.GetBool("frame_skip", false)) return; + auto* cmd = context.Get("gpu_command_buffer", nullptr); + auto* swapchain = context.Get("gpu_swapchain_texture", nullptr); + auto* device = context.Get("gpu_device", nullptr); + if (!cmd || !swapchain || !device) return; + if (!ready_) TryInit(device, context.Get("sdl_window", nullptr)); + if (!ready_) return; + const auto fw = context.Get("frame_width", 1280u); + const auto fh = context.Get("frame_height", 960u); + DrawSurface(context, fw, fh); + Render(cmd, swapchain, device, fw, fh); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp new file mode 100644 index 000000000..21216918e --- /dev/null +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp @@ -0,0 +1,97 @@ +#include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp" + +#include +#include +#include + +#include +#include + +namespace sdl3cpp::services::impl { + +namespace { + +const std::array kWeapons = { + "weapon_gauntlet", + "weapon_machinegun", + "weapon_shotgun", + "weapon_grenadelauncher", + "weapon_rocketlauncher", + "weapon_lightning", + "weapon_railgun", + "weapon_plasmagun", + "weapon_bfg" +}; + +bool HasWeapon(const nlohmann::json& inventory, const std::string& weapon) { + return weapon == "weapon_gauntlet" || + weapon == "weapon_machinegun" || + inventory.value(weapon, false); +} + +} // namespace + +WorkflowQ3WeaponUpdateStep::WorkflowQ3WeaponUpdateStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowQ3WeaponUpdateStep::GetPluginId() const { + return "q3.weapon.update"; +} + +void WorkflowQ3WeaponUpdateStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + auto inventory = context.Get("q3.inventory", nlohmann::json::object()); + inventory["weapon_gauntlet"] = true; + inventory["weapon_machinegun"] = true; + + std::string current = context.Get("q3.current_weapon", "weapon_machinegun"); + for (size_t i = 0; i < kWeapons.size(); ++i) { + if (!context.GetBool("input_key_" + std::to_string(i + 1), false)) continue; + const std::string requested = kWeapons[i]; + if (HasWeapon(inventory, requested)) { + current = requested; + context.Set("q3.current_weapon", current); + } + } + + const bool fireHeld = context.GetBool("input_mouse_left", false); + const bool firePressed = context.GetBool("input_mouse_left_pressed", false); + const uint32_t frame = static_cast(context.GetDouble("loop.iteration", 0.0)); + uint32_t lastFire = context.Get("q3.weapon_last_fire_frame", 0u); + const uint32_t interval = current == "weapon_machinegun" ? 8u : + current == "weapon_lightning" ? 3u : + current == "weapon_gauntlet" ? 18u : + 28u; + + bool wantsFire = firePressed || (fireHeld && current == "weapon_machinegun") || (fireHeld && current == "weapon_lightning"); + if (!context.GetBool("q3.menu_open", false) && wantsFire && (lastFire == 0u || frame >= lastFire + interval)) { + lastFire = frame == 0u ? 1u : frame; + context.Set("q3.weapon_last_fire_frame", lastFire); + context.Set("q3.weapon_flash_until_frame", lastFire + 4u); + context.Set("q3.shots_fired", context.Get("q3.shots_fired", 0) + 1); + + auto* world = context.Get("physics_world", nullptr); + auto cameraState = context.Get("camera.state", nlohmann::json::object()); + if (world && cameraState.contains("position") && cameraState.contains("front")) { + const auto pos = cameraState["position"]; + const auto front = cameraState["front"]; + btVector3 from(pos[0].get(), pos[1].get(), pos[2].get()); + btVector3 dir(front[0].get(), front[1].get(), front[2].get()); + btVector3 to = from + dir.normalized() * 120.0f; + btCollisionWorld::ClosestRayResultCallback hit(from, to); + world->rayTest(from, to, hit); + context.Set("q3.last_shot_hit", hit.hasHit()); + if (hit.hasHit()) { + context.Set("q3.last_shot_position", nlohmann::json::array({ + hit.m_hitPointWorld.x(), hit.m_hitPointWorld.y(), hit.m_hitPointWorld.z() + })); + } + } + + if (logger_) logger_->Info("q3.weapon.update: fired " + current); + } + + context.Set("q3.inventory", inventory); + context.Set("q3.current_weapon", current); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp index 63a29a8f6..d920e8465 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp @@ -316,6 +316,7 @@ void WorkflowBspBuildGeometryStep::Execute(const WorkflowStepDefinition& step, W mapNodes.push_back({ {"name", "bsp_" + map_name}, {"texture_index", texIdx}, + {"texture_name", (texIdx >= 0 && texIdx < numTextures) ? std::string(bspTextures[texIdx].name) : std::string{}}, {"index_offset", indexOffset}, {"index_count", group.indices.size()} }); diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp index 6de6c25c9..2f6e384c6 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp @@ -28,19 +28,27 @@ bool ReadVec3(const nlohmann::json& value, btVector3& out) { return true; } -bool InBounds(const btVector3& p, const nlohmann::json& bounds, float pad) { +bool ReadBounds(const nlohmann::json& bounds, btVector3& mn, btVector3& mx) { if (!bounds.is_object() || !bounds.contains("min") || !bounds.contains("max")) return false; - btVector3 mn, mx; - if (!ReadVec3(bounds["min"], mn) || !ReadVec3(bounds["max"], mx)) return false; - return p.x() >= mn.x() - pad && p.x() <= mx.x() + pad && - p.y() >= mn.y() - pad && p.y() <= mx.y() + pad && - p.z() >= mn.z() - pad && p.z() <= mx.z() + pad; + return ReadVec3(bounds["min"], mn) && ReadVec3(bounds["max"], mx); +} + +bool AabbIntersectsBounds(const btVector3& bodyMin, + const btVector3& bodyMax, + const nlohmann::json& bounds, + float pad) { + btVector3 triggerMin, triggerMax; + if (!ReadBounds(bounds, triggerMin, triggerMax)) return false; + return bodyMax.x() >= triggerMin.x() - pad && bodyMin.x() <= triggerMax.x() + pad && + bodyMax.y() >= triggerMin.y() - pad && bodyMin.y() <= triggerMax.y() + pad && + bodyMax.z() >= triggerMin.z() - pad && bodyMin.z() <= triggerMax.z() + pad; } void TeleportBody(btRigidBody* body, const btVector3& dest) { btTransform xform; xform.setIdentity(); xform.setOrigin(dest); + body->setCenterOfMassTransform(xform); body->setWorldTransform(xform); if (body->getMotionState()) body->getMotionState()->setWorldTransform(xform); body->setLinearVelocity(btVector3(0, 0, 0)); @@ -81,6 +89,9 @@ void WorkflowBspEntityUpdateStep::Execute(const WorkflowStepDefinition& step, Wo btTransform xform; body->getMotionState()->getWorldTransform(xform); const btVector3 playerPos = xform.getOrigin(); + btVector3 playerAabbMin; + btVector3 playerAabbMax; + body->getCollisionShape()->getAabb(xform, playerAabbMin, playerAabbMax); const uint32_t frame = static_cast(context.GetDouble("loop.iteration", 0.0)); auto collected = context.Get("q3.collected", nlohmann::json::object()); @@ -111,7 +122,10 @@ void WorkflowBspEntityUpdateStep::Execute(const WorkflowStepDefinition& step, Wo } if (classname != "trigger_push" && classname != "trigger_teleport") continue; - if (!ent.contains("bounds") || !InBounds(playerPos, ent["bounds"], 0.15f)) continue; + if (!ent.contains("bounds") || + !AabbIntersectsBounds(playerAabbMin, playerAabbMax, ent["bounds"], 0.15f)) { + continue; + } const uint32_t lastFrame = cooldowns.value(id, 0u); const uint32_t cooldownFrames = classname == "trigger_teleport" ? 45u : 15u; @@ -124,6 +138,8 @@ void WorkflowBspEntityUpdateStep::Execute(const WorkflowStepDefinition& step, Wo if (classname == "trigger_teleport") { target += btVector3(0, 1.0f, 0); TeleportBody(body, target); + playerAabbMin = target - btVector3(0.3f, 0.8f, 0.3f); + playerAabbMax = target + btVector3(0.3f, 0.8f, 0.3f); if (logger_) logger_->Info("bsp.entities.update: teleported player via " + id); } else { body->setLinearVelocity(JumpPadVelocity(playerPos, target)); diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp index 098e62608..412edf166 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp @@ -38,7 +38,8 @@ void WorkflowBspLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowCo }; const std::string pk3_path = getStr("pk3_path", ""); - const std::string map_name = getStr("map_name", "q3dm17"); + std::string map_name = getStr("map_name", "q3dm17"); + if (map_name.empty()) map_name = "q3dm7"; const float scale = getNum("scale", 1.0f / 32.0f); if (pk3_path.empty()) { @@ -52,6 +53,18 @@ void WorkflowBspLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowCo throw std::runtime_error("bsp.load: Failed to open pk3: " + pk3_path); } + nlohmann::json maps = nlohmann::json::array(); + const zip_int64_t entries = zip_get_num_entries(archive, 0); + for (zip_uint64_t i = 0; i < static_cast(entries); ++i) { + const char* name = zip_get_name(archive, i, 0); + if (!name) continue; + std::string entry(name); + if (entry.rfind("maps/", 0) != 0 || entry.size() <= 9) continue; + if (entry.substr(entry.size() - 4) != ".bsp") continue; + maps.push_back(entry.substr(5, entry.size() - 9)); + } + context.Set("q3.maps", maps); + std::string bsp_entry = "maps/" + map_name + ".bsp"; zip_stat_t st; if (zip_stat(archive, bsp_entry.c_str(), 0, &st) != 0) { diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_bsp_portal_view_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_portal_view_step.cpp new file mode 100644 index 000000000..f379f8ac5 --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_bsp_portal_view_step.cpp @@ -0,0 +1,215 @@ +#include "services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp" +#include "services/interfaces/workflow/rendering/rendering_types.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +namespace { + +bool ReadVec3(const nlohmann::json& value, glm::vec3& out) { + if (!value.is_array() || value.size() != 3) return false; + out = glm::vec3(value[0].get(), value[1].get(), value[2].get()); + return true; +} + +bool FindPortalDestination(const nlohmann::json& entities, glm::vec3& out) { + if (!entities.is_array()) return false; + for (const auto& ent : entities) { + if (ent.value("classname", std::string{}) != "trigger_teleport") continue; + if (ent.contains("target_position") && ReadVec3(ent["target_position"], out)) return true; + } + return false; +} + +} // namespace + +WorkflowBspPortalViewStep::WorkflowBspPortalViewStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowBspPortalViewStep::GetPluginId() const { + return "bsp.portal_view"; +} + +void WorkflowBspPortalViewStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + if (context.GetBool("frame_skip", false)) return; + + SDL_GPUDevice* device = context.Get("gpu_device", nullptr); + SDL_Window* window = context.Get("sdl_window", nullptr); + auto* pipeline = context.Get("gpu_pipeline_bsp", nullptr); + auto* lmTex = context.Get("bsp_lightmap_atlas_gpu", nullptr); + auto* lmSamp = context.Get("bsp_lightmap_atlas_sampler", nullptr); + const auto* mapNodes = context.TryGet("map.nodes"); + const auto* entities = context.TryGet("bsp.entities"); + + if (!device || !window || !pipeline || !lmTex || !lmSamp || + !mapNodes || !mapNodes->is_array() || mapNodes->empty() || !entities) { + return; + } + + glm::vec3 dest; + if (!FindPortalDestination(*entities, dest)) return; + dest += glm::vec3(0.0f, 1.4f, 0.0f); + + constexpr uint32_t kPortalSize = 512; + auto* portalTex = context.Get("bsp_portal_view_texture", nullptr); + auto* portalDepth = context.Get("bsp_portal_view_depth", nullptr); + auto* portalSampler = context.Get("bsp_portal_view_sampler", nullptr); + + if (!portalTex) { + SDL_GPUTextureCreateInfo ti = {}; + ti.type = SDL_GPU_TEXTURETYPE_2D; + ti.format = SDL_GetGPUSwapchainTextureFormat(device, window); + ti.width = kPortalSize; + ti.height = kPortalSize; + ti.layer_count_or_depth = 1; + ti.num_levels = 1; + ti.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; + portalTex = SDL_CreateGPUTexture(device, &ti); + if (!portalTex) return; + context.Set("bsp_portal_view_texture", portalTex); + } + + if (!portalDepth) { + SDL_GPUTextureCreateInfo di = {}; + di.type = SDL_GPU_TEXTURETYPE_2D; + di.format = SDL_GPU_TEXTUREFORMAT_D32_FLOAT; + di.width = kPortalSize; + di.height = kPortalSize; + di.layer_count_or_depth = 1; + di.num_levels = 1; + di.usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET; + portalDepth = SDL_CreateGPUTexture(device, &di); + if (!portalDepth) return; + context.Set("bsp_portal_view_depth", portalDepth); + } + + if (!portalSampler) { + SDL_GPUSamplerCreateInfo si = {}; + si.min_filter = SDL_GPU_FILTER_LINEAR; + si.mag_filter = SDL_GPU_FILTER_LINEAR; + si.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; + si.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + si.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + portalSampler = SDL_CreateGPUSampler(device, &si); + if (!portalSampler) return; + context.Set("bsp_portal_view_sampler", portalSampler); + } + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device); + if (!cmd) return; + + SDL_GPUColorTargetInfo colorTarget = {}; + colorTarget.texture = portalTex; + colorTarget.clear_color = {0.02f, 0.03f, 0.05f, 1.0f}; + colorTarget.load_op = SDL_GPU_LOADOP_CLEAR; + colorTarget.store_op = SDL_GPU_STOREOP_STORE; + + SDL_GPUDepthStencilTargetInfo depthTarget = {}; + depthTarget.texture = portalDepth; + depthTarget.clear_depth = 1.0f; + depthTarget.load_op = SDL_GPU_LOADOP_CLEAR; + depthTarget.store_op = SDL_GPU_STOREOP_DONT_CARE; + + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorTarget, 1, &depthTarget); + if (!pass) { + SDL_CancelGPUCommandBuffer(cmd); + return; + } + + const float yaw = context.Get("camera_yaw", 0.0f); + const float pitch = context.Get("camera_pitch", 0.0f); + glm::vec3 front; + front.x = std::cos(pitch) * (-std::sin(yaw)); + front.y = std::sin(pitch); + front.z = std::cos(pitch) * (-std::cos(yaw)); + front = glm::normalize(front); + + glm::mat4 view = glm::lookAt(dest, dest + front, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 500.0f); + glm::mat4 model = glm::mat4(1.0f); + glm::mat4 mvp = proj * view * model; + + rendering::VertexUniformData vu = {}; + std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16); + std::memcpy(vu.model_mat, glm::value_ptr(model), sizeof(float) * 16); + vu.normal[1] = 1.0f; + vu.uv_scale[0] = 1.0f; + vu.uv_scale[1] = 1.0f; + vu.camera_pos[0] = dest.x; + vu.camera_pos[1] = dest.y; + vu.camera_pos[2] = dest.z; + auto shadowVP = context.Get("render.shadow_vp", glm::mat4(1.0f)); + std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16); + + auto fu = context.Get("render.frag_uniforms", rendering::FragmentUniformData{}); + fu.material[0] = 0.7f; + fu.material[1] = static_cast(context.GetDouble("frame.elapsed", 0.0)); + fu.material[2] = 2.0f; + fu.material[3] = 0.0f; + + const auto& firstNode = (*mapNodes)[0]; + std::string meshName = firstNode["name"]; + auto* vb = context.Get("plane_" + meshName + "_vb", nullptr); + auto* ib = context.Get("plane_" + meshName + "_ib", nullptr); + if (!vb || !ib) { + SDL_EndGPURenderPass(pass); + SDL_SubmitGPUCommandBuffer(cmd); + return; + } + + SDL_BindGPUGraphicsPipeline(pass, pipeline); + SDL_GPUBufferBinding vbBind = {}; + vbBind.buffer = vb; + SDL_BindGPUVertexBuffers(pass, 0, &vbBind, 1); + SDL_GPUBufferBinding ibBind = {}; + ibBind.buffer = ib; + SDL_BindGPUIndexBuffer(pass, &ibBind, SDL_GPU_INDEXELEMENTSIZE_32BIT); + SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu)); + SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu)); + + for (const auto& node : *mapNodes) { + const int texIdx = node.value("texture_index", -1); + SDL_GPUTexture* albedoTex = nullptr; + SDL_GPUSampler* albedoSamp = nullptr; + if (texIdx >= 0) { + const std::string texKey = "bsp_tex_" + std::to_string(texIdx); + albedoTex = context.Get(texKey + "_gpu", nullptr); + albedoSamp = context.Get(texKey + "_sampler", nullptr); + } + if (!albedoTex || !albedoSamp) continue; + + SDL_GPUTextureSamplerBinding bindings[4] = {}; + bindings[0].texture = albedoTex; + bindings[0].sampler = albedoSamp; + bindings[1].texture = albedoTex; + bindings[1].sampler = albedoSamp; + bindings[2].texture = lmTex; + bindings[2].sampler = lmSamp; + bindings[3].texture = albedoTex; + bindings[3].sampler = albedoSamp; + SDL_BindGPUFragmentSamplers(pass, 0, bindings, 4); + + SDL_DrawGPUIndexedPrimitives(pass, + node["index_count"].get(), + 1, + node.value("index_offset", 0u), + 0, + 0); + } + + SDL_EndGPURenderPass(pass); + SDL_SubmitGPUCommandBuffer(cmd); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp index 89bfda582..2686ba5f9 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_draw_map_step.cpp @@ -7,10 +7,29 @@ #include #include #include +#include +#include #include namespace sdl3cpp::services::impl { +namespace { + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return value; +} + +bool IsPortalTexture(const std::string& textureName) { + const std::string lower = ToLower(textureName); + return lower.find("portal_sfx") != std::string::npos || + lower.find("mapobjects/portal") != std::string::npos; +} + +} // namespace + WorkflowDrawMapStep::WorkflowDrawMapStep(std::shared_ptr logger) : logger_(std::move(logger)) {} @@ -89,6 +108,8 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo // BSP lightmap atlas (shared across all groups) auto* lm_tex = context.Get("bsp_lightmap_atlas_gpu", nullptr); auto* lm_samp = context.Get("bsp_lightmap_atlas_sampler", nullptr); + auto* portal_tex = context.Get("bsp_portal_view_texture", nullptr); + auto* portal_samp = context.Get("bsp_portal_view_sampler", nullptr); if (isBsp) { // BSP mode: single VB + single IB, per-group draw calls @@ -104,18 +125,19 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo SDL_GPUBufferBinding ibBind = {}; ibBind.buffer = ib; SDL_BindGPUIndexBuffer(pass, &ibBind, SDL_GPU_INDEXELEMENTSIZE_32BIT); - // Push uniforms once (same for all groups) + // Vertex uniforms are shared by all BSP texture groups. SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu)); - SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu)); // Default texture fallback auto* defaultTex = context.Get(defaultTexture + "_gpu", nullptr); auto* defaultSamp = context.Get(defaultTexture + "_sampler", nullptr); + const float elapsed = static_cast(context.GetDouble("frame.elapsed", 0.0)); for (const auto& node : *mapNodes) { uint32_t indexCount = node["index_count"]; uint32_t indexOffset = node.value("index_offset", 0u); int texIdx = node.value("texture_index", -1); + std::string textureName = node.value("texture_name", std::string{}); // Look up per-texture albedo SDL_GPUTexture* albedoTex = nullptr; @@ -133,16 +155,22 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo } if (!albedoTex || !albedoSamp) continue; - // Bind 3 samplers: albedo, shadow, lightmap - SDL_GPUTextureSamplerBinding bindings[3] = {}; + // Bind 4 samplers: albedo, shadow, lightmap, portal destination. + SDL_GPUTextureSamplerBinding bindings[4] = {}; bindings[0].texture = albedoTex; bindings[0].sampler = albedoSamp; bindings[1].texture = shadow_tex ? shadow_tex : albedoTex; bindings[1].sampler = shadow_samp ? shadow_samp : albedoSamp; bindings[2].texture = lm_tex ? lm_tex : albedoTex; bindings[2].sampler = lm_samp ? lm_samp : albedoSamp; - SDL_BindGPUFragmentSamplers(pass, 0, bindings, 3); + bindings[3].texture = portal_tex ? portal_tex : albedoTex; + bindings[3].sampler = portal_samp ? portal_samp : albedoSamp; + SDL_BindGPUFragmentSamplers(pass, 0, bindings, 4); + auto groupFu = fu; + groupFu.material[1] = elapsed; + groupFu.material[3] = IsPortalTexture(textureName) ? 1.0f : 0.0f; + SDL_PushGPUFragmentUniformData(cmd, 0, &groupFu, sizeof(groupFu)); SDL_DrawGPUIndexedPrimitives(pass, indexCount, 1, indexOffset, 0, 0); } } else { diff --git a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp index a9ccebfbc..619e8c9c8 100644 --- a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp +++ b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp @@ -19,6 +19,11 @@ void WorkflowInputPollStep::Execute( const WorkflowStepDefinition& step, WorkflowContext& context) { float mouseRelX = 0.0f, mouseRelY = 0.0f; + bool keyEscapePressed = false; + bool keyEnterPressed = false; + bool keyUpPressed = false; + bool keyDownPressed = false; + bool mouseLeftPressed = false; SDL_Event event; while (SDL_PollEvent(&event)) { @@ -28,9 +33,20 @@ void WorkflowInputPollStep::Execute( break; case SDL_EVENT_KEY_DOWN: if (event.key.key == SDLK_ESCAPE) { + keyEscapePressed = true; + } else if (event.key.key == SDLK_RETURN) { + keyEnterPressed = true; + } else if (event.key.key == SDLK_UP) { + keyUpPressed = true; + } else if (event.key.key == SDLK_DOWN) { + keyDownPressed = true; + } else if (event.key.key == SDLK_Q) { context.Set("game_running", false); } break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + if (event.button.button == SDL_BUTTON_LEFT) mouseLeftPressed = true; + break; case SDL_EVENT_MOUSE_MOTION: mouseRelX += event.motion.xrel; mouseRelY += event.motion.yrel; @@ -41,6 +57,11 @@ void WorkflowInputPollStep::Execute( // Store accumulated mouse motion for this frame context.Set("input_mouse_rel_x", mouseRelX); context.Set("input_mouse_rel_y", mouseRelY); + context.Set("input_key_escape_pressed", keyEscapePressed); + context.Set("input_key_enter_pressed", keyEnterPressed); + context.Set("input_key_up_pressed", keyUpPressed); + context.Set("input_key_down_pressed", keyDownPressed); + context.Set("input_mouse_left_pressed", mouseLeftPressed); // Read keyboard state (snapshot, not event-based) const bool* keyState = SDL_GetKeyboardState(nullptr); @@ -52,6 +73,16 @@ void WorkflowInputPollStep::Execute( context.Set("input_key_space", keyState[SDL_SCANCODE_SPACE]); context.Set("input_key_shift", keyState[SDL_SCANCODE_LSHIFT]); context.Set("input_key_ctrl", keyState[SDL_SCANCODE_LCTRL]); + context.Set("input_mouse_left", (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK) != 0); + context.Set("input_key_1", keyState[SDL_SCANCODE_1]); + context.Set("input_key_2", keyState[SDL_SCANCODE_2]); + context.Set("input_key_3", keyState[SDL_SCANCODE_3]); + context.Set("input_key_4", keyState[SDL_SCANCODE_4]); + context.Set("input_key_5", keyState[SDL_SCANCODE_5]); + context.Set("input_key_6", keyState[SDL_SCANCODE_6]); + context.Set("input_key_7", keyState[SDL_SCANCODE_7]); + context.Set("input_key_8", keyState[SDL_SCANCODE_8]); + context.Set("input_key_9", keyState[SDL_SCANCODE_9]); } } diff --git a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp index d1eb8d6ec..549fe2cf7 100644 --- a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp +++ b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp @@ -26,6 +26,11 @@ void WorkflowPhysicsFpsMoveStep::Execute( auto* body = context.Get("physics_body_" + playerName, nullptr); if (!body) return; + if (context.GetBool("q3.menu_open", false)) { + btVector3 vel = body->getLinearVelocity(); + body->setLinearVelocity(btVector3(0, vel.y(), 0)); + return; + } // Read parameters from workflow JSON WorkflowStepParameterResolver paramResolver; diff --git a/gameengine/src/services/impl/workflow/workflow_registrar.cpp b/gameengine/src/services/impl/workflow/workflow_registrar.cpp index 92205fb92..e22e6bc80 100644 --- a/gameengine/src/services/impl/workflow/workflow_registrar.cpp +++ b/gameengine/src/services/impl/workflow/workflow_registrar.cpp @@ -48,6 +48,7 @@ #include "services/interfaces/workflow/rendering/workflow_bsp_lightmap_atlas_step.hpp" #include "services/interfaces/workflow/rendering/workflow_bsp_parse_spawn_step.hpp" #include "services/interfaces/workflow/rendering/workflow_bsp_entity_update_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp" #include "services/interfaces/workflow/rendering/workflow_bsp_build_geometry_step.hpp" #include "services/interfaces/workflow/rendering/workflow_bsp_extract_textures_step.hpp" #include "services/interfaces/workflow/rendering/workflow_bsp_upload_geometry_step.hpp" @@ -112,6 +113,11 @@ // Camera (service-dependent) #include "services/interfaces/workflow/workflow_generic_steps/workflow_camera_build_view_state_step.hpp" +// Quake 3 +#include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp" +#include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp" +#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp" + // Audio (service-dependent, registered with nullptr) #include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_pause_step.hpp" #include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_play_step.hpp" @@ -297,6 +303,7 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr reg registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); @@ -314,6 +321,9 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr reg registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); count += 18; // ── Texture ─────────────────────────────────────────────── diff --git a/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp new file mode 100644 index 000000000..58ae882ed --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowQ3MenuUpdateStep final : public IWorkflowStep { +public: + explicit WorkflowQ3MenuUpdateStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp new file mode 100644 index 000000000..01e6a1272 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" +#include "services/interfaces/workflow_context.hpp" + +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +class WorkflowQ3OverlayDrawStep final : public IWorkflowStep { +public: + explicit WorkflowQ3OverlayDrawStep(std::shared_ptr logger); + ~WorkflowQ3OverlayDrawStep(); + + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + void TryInit(SDL_GPUDevice* device, SDL_Window* window); + void DrawSurface(WorkflowContext& context, uint32_t frameW, uint32_t frameH); + void Render(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchainTex, + SDL_GPUDevice* device, uint32_t frameW, uint32_t frameH); + + std::shared_ptr logger_; + bool ready_ = false; + bool disabled_ = false; + bool vbuf_uploaded_ = false; + SDL_GPUDevice* device_ = nullptr; + SDL_GPUGraphicsPipeline* pipeline_ = nullptr; + SDL_GPUTexture* tex_ = nullptr; + SDL_GPUTransferBuffer* transfer_ = nullptr; + SDL_GPUBuffer* vtx_buf_ = nullptr; + SDL_GPUSampler* sampler_ = nullptr; + SDL_Surface* surface_ = nullptr; + SDL_Renderer* renderer_ = nullptr; + + static constexpr int kW = 640; + static constexpr int kH = 360; +}; + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp new file mode 100644 index 000000000..369692247 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowQ3WeaponUpdateStep final : public IWorkflowStep { +public: + explicit WorkflowQ3WeaponUpdateStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp new file mode 100644 index 000000000..6abd290cd --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowBspPortalViewStep final : public IWorkflowStep { +public: + explicit WorkflowBspPortalViewStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl