mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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:
@@ -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
|
||||
|
||||
67
gameengine/packages/seed/shaders/spirv/postfx_taa.frag.glsl
Normal file
67
gameengine/packages/seed/shaders/spirv/postfx_taa.frag.glsl
Normal 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);
|
||||
}
|
||||
BIN
gameengine/packages/seed/shaders/spirv/postfx_taa.frag.spv
Normal file
BIN
gameengine/packages/seed/shaders/spirv/postfx_taa.frag.spv
Normal file
Binary file not shown.
@@ -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 }]
|
||||
}
|
||||
|
||||
@@ -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 }]
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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_));
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user