From d46105e8ed960f58df741d3d0ff65dff12d016ef Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Wed, 18 Mar 2026 10:32:00 +0000 Subject: [PATCH] feat(gameengine): TAA, mipmaps with anisotropic filtering, LOD bias - postfx.taa: temporal anti-aliasing with Halton jitter, neighborhood clamping, Karis tonemap for stable history, configurable blend_factor from JSON - Texture loader: auto-generate full mipmap chain via SDL_GenerateMipmapsForGPUTexture - 16x anisotropic filtering on all textures - LOD bias 0.5 to reduce moire patterns on high-frequency textures at distance - TAA shader: 3x3 neighborhood clamp with expanded bbox to reduce flicker - Ping-pong history buffers for temporal accumulation - Sub-pixel jitter via Halton(2,3) sequence, 16-frame cycle Co-Authored-By: Claude Opus 4.6 (1M context) --- gameengine/CMakeLists.txt | 1 + .../seed/shaders/spirv/postfx_taa.frag.glsl | 67 +++++++ .../seed/shaders/spirv/postfx_taa.frag.spv | Bin 0 -> 4784 bytes .../packages/seed/workflows/frame_tick.json | 15 ++ .../packages/seed/workflows/seed_game.json | 26 +++ .../graphics/workflow_texture_load_step.cpp | 23 ++- .../rendering/workflow_postfx_taa_step.cpp | 165 ++++++++++++++++++ .../impl/workflow/workflow_registrar.cpp | 2 + .../rendering/workflow_postfx_taa_step.hpp | 19 ++ 9 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 gameengine/packages/seed/shaders/spirv/postfx_taa.frag.glsl create mode 100644 gameengine/packages/seed/shaders/spirv/postfx_taa.frag.spv create mode 100644 gameengine/src/services/impl/workflow/rendering/workflow_postfx_taa_step.cpp create mode 100644 gameengine/src/services/interfaces/workflow/rendering/workflow_postfx_taa_step.hpp diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index 1fafa3b38..dc2aa0739 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -299,6 +299,7 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/rendering/workflow_postfx_composite_step.cpp src/services/impl/workflow/rendering/workflow_postfx_setup_step.cpp src/services/impl/workflow/rendering/workflow_postfx_ssao_step.cpp + src/services/impl/workflow/rendering/workflow_postfx_taa_step.cpp src/services/impl/workflow/rendering/workflow_render_grid_draw_step.cpp src/services/impl/workflow/rendering/workflow_render_grid_setup_step.cpp src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp diff --git a/gameengine/packages/seed/shaders/spirv/postfx_taa.frag.glsl b/gameengine/packages/seed/shaders/spirv/postfx_taa.frag.glsl new file mode 100644 index 000000000..d503e3908 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/postfx_taa.frag.glsl @@ -0,0 +1,67 @@ +#version 450 + +// Temporal Anti-Aliasing resolve +// Blends current frame with history using neighborhood clamping (UE4/Karis style) + +layout(set = 2, binding = 0) uniform sampler2D currentFrame; +layout(set = 2, binding = 1) uniform sampler2D historyFrame; + +layout(set = 3, binding = 0) uniform TAAParams { + vec4 u_params; // x = blend factor (0.05 = 95% history), y = 1/width, z = 1/height, w = frame count +}; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_color; + +// Tonemap for stable neighborhood clamping (Karis 2014) +vec3 tonemap(vec3 c) { + return c / (1.0 + max(c.r, max(c.g, c.b))); +} + +vec3 untonemap(vec3 c) { + return c / (1.0 - max(c.r, max(c.g, c.b))); +} + +void main() { + vec2 texelSize = u_params.yz; + float blendFactor = u_params.x; + float frameCount = u_params.w; + + vec3 current = texture(currentFrame, v_uv).rgb; + + // First frame: no history, just output current + if (frameCount < 1.5) { + o_color = vec4(current, 1.0); + return; + } + + // Sample 3x3 neighborhood of current frame for clamping + vec3 nMin = current; + vec3 nMax = current; + + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + if (x == 0 && y == 0) continue; + vec3 s = texture(currentFrame, v_uv + vec2(float(x), float(y)) * texelSize).rgb; + nMin = min(nMin, s); + nMax = max(nMax, s); + } + } + + // Slightly expand the bounding box to reduce flickering + vec3 nCenter = (nMin + nMax) * 0.5; + vec3 nExtent = (nMax - nMin) * 0.5; + nMin = nCenter - nExtent * 1.25; + nMax = nCenter + nExtent * 1.25; + + // Sample history and clamp to neighborhood (prevents ghosting) + vec3 history = texture(historyFrame, v_uv).rgb; + vec3 historyTM = tonemap(history); + vec3 clampedTM = clamp(historyTM, tonemap(nMin), tonemap(nMax)); + vec3 clamped = untonemap(clampedTM); + + // Blend: low factor = more history = smoother but more ghosting + vec3 result = mix(clamped, current, blendFactor); + + o_color = vec4(result, 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/postfx_taa.frag.spv b/gameengine/packages/seed/shaders/spirv/postfx_taa.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..ebf87da6b2e379d2c0f5b1cde21f225d9108b40e GIT binary patch literal 4784 zcmZ9O2X~c45QbM$K*55bSTTqN8+sJ68<7%JlqlE>3E`q>3Z@`fj$p;!vG58 zx6;$Oe9DTY$D9Ntt|9G!?yB0&^i64~ciSjqcKko4DNVy3tgNhbFIaYcrJC22rlS|m zn9<(Z-`O*O-OQhy&(NafL09`7U!$>`QY-f2?n>{HIh|dDef?E#Cfri$RA%m$50@AV|6Yo77eWk?2P&rb@g>uYd5A2aPQf*85+O1iaS@L$Ztp% zT9xfSv<~^3(eBHfb?Dxitfb<48`Cr5*((QYYimsF!B;ICU^}buH>bCTak18>^!_lv z@N8^rZ9rG2RmDwdBf6`*vuAl_$^S8%wuxy>o9plw!#UNOU)K4zg8M52L*2}_jlFqJ z-kZ5ZYx_*DDg2Ge^O{tL_Xjs1`aO%%FV=5O{od7yh>zGaDY5f0f4l4#NB%gfSZ5O5 z?5DjuRx`QkYgYwrg`eN2Bc_c0I?S&6n4IELy+s=R{WR%;k(d4sD*?Nf|qTIoEmux&?{e z;u8@c^L?p}je4hLY`<9RENo}BCzF|r)@Qx)3A{IvXW%f-`e)`KIoBulynXb0kIi+j z`o*5Voc-;!07+>}>v&eW5X-!a(5}gKnlp*7a+J@c8)y?17whmSq~fN4S->-kMX>@W5{_}wb;%mRDQ+Ujse9bQ~u_vUve z>WTgK1h$^vpAxUD!+wKGzu%w|`>iRl-S+@8&l$~8P8yf-pP{@ z@3D9F6lUl-pMuEGC)4|KYUboUN6+L`PQ%&jo5Aflt*VD>`JP(Odv$vDndAA?a4lyx zIQq}Rj+{Bz`poeR#rnM8^APX0*!9gvyvrA|C)eMO7$ZNQua5ga7m-(d%6+tf^+)gX zuq@+uyhjv^acS*x_4@EjG^kz7+YBT34d2 zss4>UEkj?8_~^d|tzT_j-~DdH8XxA_^=8g*fb$Yxg;>j6ZTtFOMc?Jwr?ziDdI0gU z?;zS-b@Uy=wnp?_fh`w(#n&R%GFLltufsNXeO~iQY`J(R+}A3^eMQXmnKS0~oOc7Z zx%#{(H=%vpx4s(@Is0q77xyKWcRk+k)kt~2*MPm>vEEy-U2h-p-m6;?x$xhH?OMZs zJGNXq>pU8L2V#!AIdXR*zR$Pkn6=o($*;-WT~#|Jeetf{jlCMt7w_6V*z$^xciX${ zIs3iTZ{LyVdy$R;--m6?7`*!LN6bGEF;;)vXMIuU0c`Ja;0LkGIuC)ZGX*hLzje&f zSDuAuVja)@VZ>RlW(ML%GUvUDdn;#t+*|LM*muRVRY$!?u|1op_ZYTZ)YB(t{*`F+ zeOF>%k7IWf_z7(H;2AluClO=h@6X)RnSToHn%AM_qUN*M=8M?&3H%baF_HfYw%p4|^m!HAJbiJ-a#7y>_D_Vc__yJokeq(>cmh%bjCu})u2KRI3Jm2^&{sr57 zjY98$_->Cu#~OFZy#Jex$KEaDaXH4>?4J3E z*(1kHC}Q@+mXDadbIe{v%s$xierK(_FXFzZAm-~2-$d;2+22^-FZ1?8+`ln#UM<-2 zA#3v-Cc(MK7UUoHE-vTfo$)E>v?JI#?Tn5&9e^z#`#Uh>a!v=q$w$n=Ii{SGdzFuv zLvl 1) { maxDim >>= 1; numLevels++; } + + // Create GPU texture with mip chain SDL_GPUTextureCreateInfo tex_info = {}; tex_info.type = SDL_GPU_TEXTURETYPE_2D; tex_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; tex_info.width = static_cast(w); tex_info.height = static_cast(h); tex_info.layer_count_or_depth = 1; - tex_info.num_levels = 1; - tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; + tex_info.num_levels = numLevels; + tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | + (numLevels > 1 ? SDL_GPU_TEXTUREUSAGE_COLOR_TARGET : 0); SDL_GPUTexture* texture = SDL_CreateGPUTexture(device, &tex_info); if (!texture) { @@ -105,6 +111,12 @@ void WorkflowTextureLoadStep::Execute(const WorkflowStepDefinition& step, Workfl SDL_UploadToGPUTexture(copy_pass, &src, &dst, false); SDL_EndGPUCopyPass(copy_pass); + + // Generate mipmaps from the uploaded base level + if (numLevels > 1) { + SDL_GenerateMipmapsForGPUTexture(cmd, texture); + } + SDL_SubmitGPUCommandBuffer(cmd); SDL_ReleaseGPUTransferBuffer(device, transfer); @@ -116,6 +128,11 @@ void WorkflowTextureLoadStep::Execute(const WorkflowStepDefinition& step, Workfl samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_REPEAT; samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_REPEAT; samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_REPEAT; + samp_info.enable_anisotropy = true; + samp_info.max_anisotropy = 16.0f; + samp_info.mip_lod_bias = 0.5f; // bias toward higher mip = less aliasing at distance + samp_info.min_lod = 0.0f; + samp_info.max_lod = static_cast(numLevels); SDL_GPUSampler* sampler = SDL_CreateGPUSampler(device, &samp_info); if (!sampler) { diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_postfx_taa_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_postfx_taa_step.cpp new file mode 100644 index 000000000..dcd860c6d --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_postfx_taa_step.cpp @@ -0,0 +1,165 @@ +#include "services/interfaces/workflow/rendering/workflow_postfx_taa_step.hpp" +#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp" + +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +// Halton sequence for sub-pixel jitter (low discrepancy, covers pixel well) +static float halton(int index, int base) { + float f = 1.0f; + float r = 0.0f; + int i = index; + while (i > 0) { + f /= static_cast(base); + r += f * (i % base); + i /= base; + } + return r; +} + +WorkflowPostfxTaaStep::WorkflowPostfxTaaStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowPostfxTaaStep::GetPluginId() const { + return "postfx.taa"; +} + +void WorkflowPostfxTaaStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + if (context.GetBool("frame_skip", false)) return; + + auto* cmd = context.Get("gpu_command_buffer", nullptr); + auto* device = context.Get("gpu_device", nullptr); + auto* hdrTex = context.Get("postfx_hdr_texture", nullptr); + if (!cmd || !device || !hdrTex) return; + + WorkflowStepParameterResolver params; + auto getNum = [&](const char* name, float def) -> float { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; + + float blendFactor = getNum("blend_factor", 0.05f); + + uint32_t w = context.Get("postfx_hdr_width", 0u); + uint32_t h = context.Get("postfx_hdr_height", 0u); + if (w == 0 || h == 0) return; + + // Track frame count for jitter sequence + double frameCount = context.Get("taa_frame_count", 0.0); + frameCount += 1.0; + context.Set("taa_frame_count", frameCount); + int frameIdx = static_cast(frameCount); + + // --- Apply jitter to projection matrix for NEXT frame --- + // This ensures the current frame was rendered with jitter applied last frame + auto projMatrix = context.Get("render.proj_matrix", glm::mat4(1.0f)); + float jitterX = (halton(frameIdx % 16 + 1, 2) - 0.5f) / static_cast(w); + float jitterY = (halton(frameIdx % 16 + 1, 3) - 0.5f) / static_cast(h); + glm::mat4 jitteredProj = projMatrix; + jitteredProj[2][0] += jitterX * 2.0f; + jitteredProj[2][1] += jitterY * 2.0f; + context.Set("render.proj_matrix", jitteredProj); + + // --- Create or get TAA pipeline --- + auto* taaPipeline = context.Get("postfx_taa_pipeline", nullptr); + if (!taaPipeline) { + // Need TAA shader - look for compiled shader in context + auto* taaShader = context.Get("taa_fragment_shader", nullptr); + auto* fullscreenVert = context.Get("postfx_vertex_shader", nullptr); + if (!taaShader || !fullscreenVert) { + // Shaders not compiled yet - skip TAA this frame + return; + } + + SDL_GPUGraphicsPipelineCreateInfo pipelineInfo = {}; + pipelineInfo.vertex_shader = fullscreenVert; + pipelineInfo.fragment_shader = taaShader; + pipelineInfo.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + + SDL_GPUColorTargetDescription colorDesc = {}; + colorDesc.format = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT; + pipelineInfo.target_info.num_color_targets = 1; + pipelineInfo.target_info.color_target_descriptions = &colorDesc; + + taaPipeline = SDL_CreateGPUGraphicsPipeline(device, &pipelineInfo); + if (taaPipeline) { + context.Set("postfx_taa_pipeline", taaPipeline); + } else { + return; + } + } + + // --- Create or get history textures (ping-pong) --- + auto* historyA = context.Get("taa_history_a", nullptr); + auto* historyB = context.Get("taa_history_b", nullptr); + auto taaW = context.Get("taa_width", 0u); + auto taaH = context.Get("taa_height", 0u); + + if (!historyA || !historyB || taaW != w || taaH != h) { + if (historyA) SDL_ReleaseGPUTexture(device, historyA); + if (historyB) SDL_ReleaseGPUTexture(device, historyB); + + SDL_GPUTextureCreateInfo texInfo = {}; + texInfo.type = SDL_GPU_TEXTURETYPE_2D; + texInfo.format = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT; + texInfo.width = w; + texInfo.height = h; + texInfo.layer_count_or_depth = 1; + texInfo.num_levels = 1; + texInfo.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; + + historyA = SDL_CreateGPUTexture(device, &texInfo); + historyB = SDL_CreateGPUTexture(device, &texInfo); + context.Set("taa_history_a", historyA); + context.Set("taa_history_b", historyB); + context.Set("taa_width", w); + context.Set("taa_height", h); + context.Set("taa_ping", true); + } + + // Ping-pong: read from one history, write to the other + bool ping = context.GetBool("taa_ping", true); + SDL_GPUTexture* historyRead = ping ? historyA : historyB; + SDL_GPUTexture* historyWrite = ping ? historyB : historyA; + context.Set("taa_ping", !ping); + + auto* sampler = context.Get("postfx_linear_sampler", nullptr); + if (!sampler) return; + + // --- TAA resolve pass: current HDR + history → historyWrite --- + SDL_GPUColorTargetInfo colorTarget = {}; + colorTarget.texture = historyWrite; + colorTarget.load_op = SDL_GPU_LOADOP_DONT_CARE; + colorTarget.store_op = SDL_GPU_STOREOP_STORE; + + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorTarget, 1, nullptr); + if (!pass) return; + + SDL_BindGPUGraphicsPipeline(pass, taaPipeline); + + SDL_GPUTextureSamplerBinding bindings[2] = {}; + bindings[0].texture = hdrTex; + bindings[0].sampler = sampler; + bindings[1].texture = historyRead; + bindings[1].sampler = sampler; + SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2); + + struct { float params[4]; } uniforms; + uniforms.params[0] = blendFactor; + uniforms.params[1] = 1.0f / static_cast(w); + uniforms.params[2] = 1.0f / static_cast(h); + uniforms.params[3] = static_cast(frameCount); + SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms, sizeof(uniforms)); + + SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); + SDL_EndGPURenderPass(pass); + + // Replace the HDR texture with the TAA result for downstream post-FX + context.Set("postfx_hdr_texture", historyWrite); +} + +} // 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 5066cc021..6d2a7c638 100644 --- a/gameengine/src/services/impl/workflow/workflow_registrar.cpp +++ b/gameengine/src/services/impl/workflow/workflow_registrar.cpp @@ -43,6 +43,7 @@ #include "services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp" #include "services/interfaces/workflow/rendering/workflow_map_load_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_map_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_postfx_taa_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_textured_box_step.hpp" #include "services/interfaces/workflow/rendering/workflow_shadow_setup_step.hpp" #include "services/interfaces/workflow/rendering/workflow_shadow_pass_step.hpp" @@ -280,6 +281,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_postfx_taa_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_postfx_taa_step.hpp new file mode 100644 index 000000000..6776ce5c0 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_postfx_taa_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 WorkflowPostfxTaaStep : public IWorkflowStep { +public: + explicit WorkflowPostfxTaaStep(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