From 487631458ab249dcbaef668d6dca4a3380b862d6 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Wed, 18 Mar 2026 08:54:04 +0000 Subject: [PATCH] refactor(gameengine): extract spotlight.update step, distance fog, volumetric improvements - Extract spotlight logic from render.prepare into dedicated spotlight.update step - render.prepare now only handles camera, shadow, and lighting uniforms - spotlight.update runs per-frame after render.prepare, reads spotlight.state from context - Aim distance configurable via JSON (aim_distance parameter) - Camera-local offset for spotlight origin (matches viewmodel position) - Direction computed from torch position toward camera aim point (natural beam alignment) - Add distance fog to whole room (exponential, dark blue-grey) - Volumetric beam: 48 steps, UE4 interleaved gradient noise, cubic cone falloff - Fog density increased for visible beam effect Co-Authored-By: Claude Opus 4.6 (1M context) --- gameengine/CMakeLists.txt | 1 + .../seed/shaders/spirv/textured.frag.glsl | 38 +++++---- .../seed/shaders/spirv/textured.frag.spv | Bin 17328 -> 18132 bytes .../packages/seed/workflows/frame_tick.json | 12 +++ .../packages/seed/workflows/seed_game.json | 8 +- .../workflow_render_prepare_step.cpp | 41 +--------- .../workflow_spotlight_update_step.cpp | 72 ++++++++++++++++++ .../impl/workflow/workflow_registrar.cpp | 2 + .../workflow_spotlight_update_step.hpp | 19 +++++ 9 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 gameengine/src/services/impl/workflow/rendering/workflow_spotlight_update_step.cpp create mode 100644 gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_update_step.hpp diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index 3341868bd..3ba2ec308 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -303,6 +303,7 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/rendering/workflow_shadow_pass_step.cpp src/services/impl/workflow/rendering/workflow_shadow_setup_step.cpp src/services/impl/workflow/rendering/workflow_spotlight_setup_step.cpp + src/services/impl/workflow/rendering/workflow_spotlight_update_step.cpp src/services/impl/workflow/graphics/workflow_gpu_pipeline_create_step.cpp src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp src/services/impl/workflow/graphics/workflow_graphics_buffer_create_index_step.cpp diff --git a/gameengine/packages/seed/shaders/spirv/textured.frag.glsl b/gameengine/packages/seed/shaders/spirv/textured.frag.glsl index c03ff13e9..cd2639769 100644 --- a/gameengine/packages/seed/shaders/spirv/textured.frag.glsl +++ b/gameengine/packages/seed/shaders/spirv/textured.frag.glsl @@ -83,36 +83,39 @@ float SpotlightAtten(vec3 lightToFrag, vec3 spotDir, float cosInner, float cosOu // Simulates light scattering through dust particles in the air vec3 VolumetricBeam(vec3 camPos, vec3 fragPos, vec3 flashPos, vec3 flashDir, float cosInner, float cosOuter, vec3 flashColor, float flashRange) { - const int NUM_STEPS = 16; - const float FOG_DENSITY = 0.007; // dust density in the air + const int NUM_STEPS = 48; + const float FOG_DENSITY = 0.05; vec3 rayDir = fragPos - camPos; float rayLen = length(rayDir); + vec3 rayNorm = rayDir / rayLen; float stepSize = rayLen / float(NUM_STEPS); - // Dithered start offset to reduce banding artifacts - float dither = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453); + // Interleaved gradient noise - much smoother than sin-based dithering + // (same technique used by UE4/UE5 volumetric fog) + vec2 fc = gl_FragCoord.xy; + float dither = fract(52.9829189 * fract(0.06711056 * fc.x + 0.00583715 * fc.y)); + + // Mie-like forward scattering (compute once, constant along ray) + float viewDot = max(dot(rayNorm, flashDir), 0.0); + float scatter = mix(1.0, 2.5, viewDot * viewDot); vec3 accumulated = vec3(0.0); for (int i = 0; i < NUM_STEPS; ++i) { float t = (float(i) + dither) * stepSize; - vec3 samplePos = camPos + normalize(rayDir) * t; + vec3 samplePos = camPos + rayNorm * t; - // Check if this point in space is inside the spotlight cone vec3 toSample = samplePos - flashPos; float dist = length(toSample); - // Distance attenuation + // Smooth distance falloff float distAtten = clamp(1.0 - dist / flashRange, 0.0, 1.0); distAtten *= distAtten; - // Cone attenuation - float cosAngle = dot(normalize(toSample), flashDir); + // Cone with smooth cubic falloff at edges + float cosAngle = dot(toSample / dist, flashDir); float coneAtten = clamp((cosAngle - cosOuter) / max(cosInner - cosOuter, 0.001), 0.0, 1.0); - - // Mie-like forward scattering: brighter when looking into the beam - float viewDot = max(dot(normalize(rayDir), flashDir), 0.0); - float scatter = mix(1.0, 3.0, viewDot * viewDot); + coneAtten *= coneAtten; // smoother edge accumulated += flashColor * distAtten * coneAtten * scatter * FOG_DENSITY * stepSize; } @@ -190,7 +193,14 @@ void main() { // === Ambient === vec3 ambient = u_ambient.rgb * albedo; - // Combine surface + volumetric fog (fog is additive, not affected by surface) + // Surface color vec3 color = (Lo + ambient) * exposure + volumetric; + + // === Distance fog (whole room) === + float dist = length(v_worldPos - v_cameraPos); + float fogFactor = 1.0 - exp(-dist * 0.06); // exponential fog density + vec3 fogColor = vec3(0.02, 0.02, 0.03); // dark blue-grey fog + color = mix(color, fogColor, fogFactor); + o_color = vec4(color, 1.0); } diff --git a/gameengine/packages/seed/shaders/spirv/textured.frag.spv b/gameengine/packages/seed/shaders/spirv/textured.frag.spv index 7e0d9cad05c73feacb0c83a54d273f96c79d00a3..3e366a769adee9a575975628fde0b42871efa2c8 100644 GIT binary patch literal 18132 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 literal 17328 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"lighting.directional"); if (lighting) { @@ -93,40 +92,6 @@ void WorkflowRenderPrepareStep::Execute(const WorkflowStepDefinition& step, Work fu.light_color[3] = lighting->value("exposure", 1.0f); } - // --- Spotlight / flashlight (reads from context, set by spotlight.setup step) --- - const auto* spot = context.TryGet("spotlight.state"); - if (spot) { - std::string attach = spot->value("attach", "camera"); - glm::vec3 spotPos, spotDir; - auto offset = spot->value("offset", std::vector{0,0,0}); - glm::vec3 off(offset.size()>0?offset[0]:0, offset.size()>1?offset[1]:0, offset.size()>2?offset[2]:0); - - if (attach == "camera") { - spotPos = cameraPos + off; - spotDir = -glm::vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]); - } else { - auto p = spot->value("position", std::vector{0,0,0}); - auto d = spot->value("direction", std::vector{0,0,-1}); - spotPos = glm::vec3(p[0], p[1], p[2]) + off; - spotDir = glm::normalize(glm::vec3(d[0], d[1], d[2])); - } - - fu.flash_pos[0] = spotPos.x; - fu.flash_pos[1] = spotPos.y; - fu.flash_pos[2] = spotPos.z; - fu.flash_pos[3] = std::cos(glm::radians(spot->value("inner_cone", 12.0f))); - fu.flash_dir[0] = spotDir.x; - fu.flash_dir[1] = spotDir.y; - fu.flash_dir[2] = spotDir.z; - fu.flash_dir[3] = std::cos(glm::radians(spot->value("outer_cone", 25.0f))); - auto col = spot->value("color", std::vector{1,1,1}); - float spotIntensity = spot->value("intensity", 2.5f); - fu.flash_color[0] = (col.size() > 0 ? col[0] : 1.0f) * spotIntensity; - fu.flash_color[1] = (col.size() > 1 ? col[1] : 1.0f) * spotIntensity; - fu.flash_color[2] = (col.size() > 2 ? col[2] : 1.0f) * spotIntensity; - fu.flash_color[3] = spot->value("range", 20.0f); - } - context.Set("render.frag_uniforms", fu); if (logger_) { diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_spotlight_update_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_spotlight_update_step.cpp new file mode 100644 index 000000000..5363a02f6 --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_spotlight_update_step.cpp @@ -0,0 +1,72 @@ +#include "services/interfaces/workflow/rendering/workflow_spotlight_update_step.hpp" +#include "services/interfaces/workflow/rendering/rendering_types.hpp" + +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowSpotlightUpdateStep::WorkflowSpotlightUpdateStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowSpotlightUpdateStep::GetPluginId() const { + return "spotlight.update"; +} + +void WorkflowSpotlightUpdateStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + const auto* spot = context.TryGet("spotlight.state"); + if (!spot) return; + + auto fu = context.Get("render.frag_uniforms", rendering::FragmentUniformData{}); + + std::string attach = spot->value("attach", "camera"); + auto offset = spot->value("offset", std::vector{0, 0, 0}); + glm::vec3 off(offset.size() > 0 ? offset[0] : 0, + offset.size() > 1 ? offset[1] : 0, + offset.size() > 2 ? offset[2] : 0); + + glm::vec3 spotPos, spotDir; + + if (attach == "camera") { + auto viewMatrix = context.Get("render.view_matrix", glm::mat4(1.0f)); + auto cameraPos = context.Get("render.camera_pos", glm::vec3(0.0f)); + + glm::vec3 camRight = glm::vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]); + glm::vec3 camUp = glm::vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]); + glm::vec3 camFwd = -glm::vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]); + + spotPos = cameraPos + camRight * off.x + camUp * off.y + camFwd * (-off.z); + + // Aim toward a far point on camera center axis (natural torch aim) + float aimDist = spot->value("aim_distance", 50.0f); + glm::vec3 aimTarget = cameraPos + camFwd * aimDist; + spotDir = glm::normalize(aimTarget - spotPos); + } else { + auto p = spot->value("position", std::vector{0, 0, 0}); + auto d = spot->value("direction", std::vector{0, 0, -1}); + spotPos = glm::vec3(p[0], p[1], p[2]) + off; + spotDir = glm::normalize(glm::vec3(d[0], d[1], d[2])); + } + + fu.flash_pos[0] = spotPos.x; + fu.flash_pos[1] = spotPos.y; + fu.flash_pos[2] = spotPos.z; + fu.flash_pos[3] = std::cos(glm::radians(spot->value("inner_cone", 12.0f))); + fu.flash_dir[0] = spotDir.x; + fu.flash_dir[1] = spotDir.y; + fu.flash_dir[2] = spotDir.z; + fu.flash_dir[3] = std::cos(glm::radians(spot->value("outer_cone", 25.0f))); + + auto col = spot->value("color", std::vector{1, 1, 1}); + float intensity = spot->value("intensity", 2.5f); + fu.flash_color[0] = (col.size() > 0 ? col[0] : 1.0f) * intensity; + fu.flash_color[1] = (col.size() > 1 ? col[1] : 1.0f) * intensity; + fu.flash_color[2] = (col.size() > 2 ? col[2] : 1.0f) * intensity; + fu.flash_color[3] = spot->value("range", 20.0f); + + context.Set("render.frag_uniforms", fu); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/workflow_registrar.cpp b/gameengine/src/services/impl/workflow/workflow_registrar.cpp index ecad7b543..864eddc3e 100644 --- a/gameengine/src/services/impl/workflow/workflow_registrar.cpp +++ b/gameengine/src/services/impl/workflow/workflow_registrar.cpp @@ -37,6 +37,7 @@ #include "services/interfaces/workflow/rendering/workflow_draw_textured_step.hpp" #include "services/interfaces/workflow/rendering/workflow_lighting_setup_step.hpp" #include "services/interfaces/workflow/rendering/workflow_spotlight_setup_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_spotlight_update_step.hpp" #include "services/interfaces/workflow/rendering/workflow_model_load_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp" #include "services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp" @@ -271,6 +272,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_)); diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_update_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_update_step.hpp new file mode 100644 index 000000000..054118e29 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_update_step.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowSpotlightUpdateStep : public IWorkflowStep { +public: + explicit WorkflowSpotlightUpdateStep(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