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 7e0d9cad0..3e366a769 100644 Binary files a/gameengine/packages/seed/shaders/spirv/textured.frag.spv and b/gameengine/packages/seed/shaders/spirv/textured.frag.spv differ diff --git a/gameengine/packages/seed/workflows/frame_tick.json b/gameengine/packages/seed/workflows/frame_tick.json index 8dd63254f..ea41c70df 100644 --- a/gameengine/packages/seed/workflows/frame_tick.json +++ b/gameengine/packages/seed/workflows/frame_tick.json @@ -75,6 +75,13 @@ "typeVersion": 1, "position": [750, 0] }, + { + "id": "spotlight_update", + "name": "Update Spotlight", + "type": "spotlight.update", + "typeVersion": 1, + "position": [760, 0] + }, { "id": "frame_begin", "name": "Begin Offscreen Frame", @@ -472,6 +479,11 @@ } }, "render_prepare": { + "main": { + "0": [{ "node": "spotlight_update", "type": "main", "index": 0 }] + } + }, + "spotlight_update": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } diff --git a/gameengine/packages/seed/workflows/seed_game.json b/gameengine/packages/seed/workflows/seed_game.json index cf03e696d..2aaeb2e7b 100644 --- a/gameengine/packages/seed/workflows/seed_game.json +++ b/gameengine/packages/seed/workflows/seed_game.json @@ -862,14 +862,16 @@ "position": [1125, 200], "parameters": { "attach": "camera", - "inner_cone": 6, - "outer_cone": 16, + "inner_cone": 3, + "outer_cone": 12, "intensity": 3.0, "range": 25, "color_r": 1.0, "color_g": 0.95, "color_b": 0.85, - "offset_y": -0.15 + "offset_x": 0.15, + "offset_y": -0.1, + "offset_z": -0.1 } }, { diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp index b7f3574ea..5c59ad680 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp @@ -52,17 +52,16 @@ void WorkflowRenderPrepareStep::Execute(const WorkflowStepDefinition& step, Work // --- Fragment uniforms from lighting --- rendering::FragmentUniformData fu = {}; - // Defaults: downward white light, subtle ambient, full exposure fu.light_dir[1] = -1.0f; fu.light_color[0] = 1.0f; fu.light_color[1] = 1.0f; fu.light_color[2] = 1.0f; - fu.light_color[3] = 1.0f; // exposure + fu.light_color[3] = 1.0f; fu.ambient[0] = 0.2f; fu.ambient[1] = 0.2f; fu.ambient[2] = 0.2f; - fu.material[0] = 0.8f; // roughness default - fu.material[1] = 0.0f; // metallic default + fu.material[0] = 0.8f; + fu.material[1] = 0.0f; const auto* lighting = context.TryGet("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