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) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 10:32:00 +00:00
parent 915e18d67b
commit d46105e8ed
9 changed files with 315 additions and 3 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -440,6 +440,16 @@
"typeVersion": 1,
"position": [1200, 0]
},
{
"id": "postfx_taa",
"name": "TAA Resolve",
"type": "postfx.taa",
"typeVersion": 1,
"position": [1250, 0],
"parameters": {
"blend_factor": 0.02
}
},
{
"id": "postfx_ssao",
"name": "SSAO Pass",
@@ -631,6 +641,11 @@
}
},
"end_scene": {
"main": {
"0": [{ "node": "postfx_taa", "type": "main", "index": 0 }]
}
},
"postfx_taa": {
"main": {
"0": [{ "node": "postfx_ssao", "type": "main", "index": 0 }]
}

View File

@@ -115,6 +115,11 @@
"name": "postfx_bloom_blur_frag_path",
"type": "string",
"defaultValue": "packages/seed/shaders/msl/postfx_bloom_blur.frag.metal"
},
"postfx_taa_frag_path": {
"name": "postfx_taa_frag_path",
"type": "string",
"defaultValue": "packages/seed/shaders/spirv/postfx_taa.frag.spv"
}
},
"nodes": [
@@ -1056,6 +1061,22 @@
"shader_path": "postfx_bloom_blur_frag_path"
}
},
{
"id": "compile_taa_frag",
"name": "Compile TAA Fragment",
"type": "graphics.gpu.shader.compile",
"typeVersion": 1,
"position": [1315, 200],
"parameters": {
"stage": "fragment",
"output_key": "taa_fragment_shader",
"num_uniform_buffers": 1,
"num_samplers": 2
},
"inputs": {
"shader_path": "postfx_taa_frag_path"
}
},
{
"id": "create_ssao_pipeline",
"name": "Create SSAO Pipeline",
@@ -1452,6 +1473,11 @@
}
},
"compile_postfx_bloom_blur_frag": {
"main": {
"0": [{ "node": "compile_taa_frag", "type": "main", "index": 0 }]
}
},
"compile_taa_frag": {
"main": {
"0": [{ "node": "create_ssao_pipeline", "type": "main", "index": 0 }]
}

View File

@@ -53,15 +53,21 @@ void WorkflowTextureLoadStep::Execute(const WorkflowStepDefinition& step, Workfl
throw std::runtime_error("texture.load: GPU device not found in context");
}
// Create GPU texture
// Calculate mip levels: floor(log2(max(w,h))) + 1
int maxDim = std::max(w, h);
Uint32 numLevels = 1;
while (maxDim > 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<Uint32>(w);
tex_info.height = static_cast<Uint32>(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<float>(numLevels);
SDL_GPUSampler* sampler = SDL_CreateGPUSampler(device, &samp_info);
if (!sampler) {

View File

@@ -0,0 +1,165 @@
#include "services/interfaces/workflow/rendering/workflow_postfx_taa_step.hpp"
#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp"
#include <SDL3/SDL_gpu.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
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<float>(base);
r += f * (i % base);
i /= base;
}
return r;
}
WorkflowPostfxTaaStep::WorkflowPostfxTaaStep(std::shared_ptr<ILogger> 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<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
auto* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
auto* hdrTex = context.Get<SDL_GPUTexture*>("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<float>(p->numberValue) : def;
};
float blendFactor = getNum("blend_factor", 0.05f);
uint32_t w = context.Get<uint32_t>("postfx_hdr_width", 0u);
uint32_t h = context.Get<uint32_t>("postfx_hdr_height", 0u);
if (w == 0 || h == 0) return;
// Track frame count for jitter sequence
double frameCount = context.Get<double>("taa_frame_count", 0.0);
frameCount += 1.0;
context.Set<double>("taa_frame_count", frameCount);
int frameIdx = static_cast<int>(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<glm::mat4>("render.proj_matrix", glm::mat4(1.0f));
float jitterX = (halton(frameIdx % 16 + 1, 2) - 0.5f) / static_cast<float>(w);
float jitterY = (halton(frameIdx % 16 + 1, 3) - 0.5f) / static_cast<float>(h);
glm::mat4 jitteredProj = projMatrix;
jitteredProj[2][0] += jitterX * 2.0f;
jitteredProj[2][1] += jitterY * 2.0f;
context.Set<glm::mat4>("render.proj_matrix", jitteredProj);
// --- Create or get TAA pipeline ---
auto* taaPipeline = context.Get<SDL_GPUGraphicsPipeline*>("postfx_taa_pipeline", nullptr);
if (!taaPipeline) {
// Need TAA shader - look for compiled shader in context
auto* taaShader = context.Get<SDL_GPUShader*>("taa_fragment_shader", nullptr);
auto* fullscreenVert = context.Get<SDL_GPUShader*>("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<SDL_GPUGraphicsPipeline*>("postfx_taa_pipeline", taaPipeline);
} else {
return;
}
}
// --- Create or get history textures (ping-pong) ---
auto* historyA = context.Get<SDL_GPUTexture*>("taa_history_a", nullptr);
auto* historyB = context.Get<SDL_GPUTexture*>("taa_history_b", nullptr);
auto taaW = context.Get<uint32_t>("taa_width", 0u);
auto taaH = context.Get<uint32_t>("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<SDL_GPUTexture*>("taa_history_a", historyA);
context.Set<SDL_GPUTexture*>("taa_history_b", historyB);
context.Set<uint32_t>("taa_width", w);
context.Set<uint32_t>("taa_height", h);
context.Set<bool>("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<bool>("taa_ping", !ping);
auto* sampler = context.Get<SDL_GPUSampler*>("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<float>(w);
uniforms.params[2] = 1.0f / static_cast<float>(h);
uniforms.params[3] = static_cast<float>(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<SDL_GPUTexture*>("postfx_hdr_texture", historyWrite);
}
} // namespace sdl3cpp::services::impl

View File

@@ -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<IWorkflowStepRegistry> reg
registry->RegisterStep(std::make_shared<WorkflowGeometryCreateFlashlightStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowMapLoadStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowDrawMapStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowPostfxTaaStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowShadowSetupStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowShadowPassStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowRenderPrepareStep>(logger_));

View File

@@ -0,0 +1,19 @@
#pragma once
#include "services/interfaces/i_workflow_step.hpp"
#include "services/interfaces/i_logger.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowPostfxTaaStep : public IWorkflowStep {
public:
explicit WorkflowPostfxTaaStep(std::shared_ptr<ILogger> logger);
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl