mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 00:20:16 +00:00
feat: add FXAA post-process anti-aliasing, combinable with MSAA
This commit is contained in:
parent
819a690c33
commit
6e95709b68
5 changed files with 495 additions and 2 deletions
132
assets/shaders/fxaa.frag.glsl
Normal file
132
assets/shaders/fxaa.frag.glsl
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#version 450
|
||||
|
||||
// FXAA 3.11 — Fast Approximate Anti-Aliasing post-process pass.
|
||||
// Reads the resolved scene color and outputs a smoothed result.
|
||||
// Push constant: rcpFrame = vec2(1/width, 1/height).
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D uScene;
|
||||
|
||||
layout(location = 0) in vec2 TexCoord;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(push_constant) uniform PC {
|
||||
vec2 rcpFrame;
|
||||
} pc;
|
||||
|
||||
// Quality tuning
|
||||
#define FXAA_EDGE_THRESHOLD (1.0/8.0) // minimum edge contrast to process
|
||||
#define FXAA_EDGE_THRESHOLD_MIN (1.0/24.0) // ignore very dark regions
|
||||
#define FXAA_SEARCH_STEPS 12
|
||||
#define FXAA_SEARCH_THRESHOLD (1.0/4.0)
|
||||
#define FXAA_SUBPIX 0.75
|
||||
#define FXAA_SUBPIX_TRIM (1.0/4.0)
|
||||
#define FXAA_SUBPIX_TRIM_SCALE (1.0/(1.0 - FXAA_SUBPIX_TRIM))
|
||||
#define FXAA_SUBPIX_CAP (3.0/4.0)
|
||||
|
||||
float luma(vec3 c) {
|
||||
return dot(c, vec3(0.299, 0.587, 0.114));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = TexCoord;
|
||||
vec2 rcp = pc.rcpFrame;
|
||||
|
||||
// --- Centre and cardinal neighbours ---
|
||||
vec3 rgbM = texture(uScene, uv).rgb;
|
||||
vec3 rgbN = texture(uScene, uv + vec2( 0.0, -1.0) * rcp).rgb;
|
||||
vec3 rgbS = texture(uScene, uv + vec2( 0.0, 1.0) * rcp).rgb;
|
||||
vec3 rgbE = texture(uScene, uv + vec2( 1.0, 0.0) * rcp).rgb;
|
||||
vec3 rgbW = texture(uScene, uv + vec2(-1.0, 0.0) * rcp).rgb;
|
||||
|
||||
float lumaN = luma(rgbN);
|
||||
float lumaS = luma(rgbS);
|
||||
float lumaE = luma(rgbE);
|
||||
float lumaW = luma(rgbW);
|
||||
float lumaM = luma(rgbM);
|
||||
|
||||
float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW)));
|
||||
float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW)));
|
||||
float range = lumaMax - lumaMin;
|
||||
|
||||
// Early exit on smooth regions
|
||||
if (range < max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD)) {
|
||||
outColor = vec4(rgbM, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Diagonal neighbours ---
|
||||
vec3 rgbNW = texture(uScene, uv + vec2(-1.0, -1.0) * rcp).rgb;
|
||||
vec3 rgbNE = texture(uScene, uv + vec2( 1.0, -1.0) * rcp).rgb;
|
||||
vec3 rgbSW = texture(uScene, uv + vec2(-1.0, 1.0) * rcp).rgb;
|
||||
vec3 rgbSE = texture(uScene, uv + vec2( 1.0, 1.0) * rcp).rgb;
|
||||
|
||||
float lumaNW = luma(rgbNW);
|
||||
float lumaNE = luma(rgbNE);
|
||||
float lumaSW = luma(rgbSW);
|
||||
float lumaSE = luma(rgbSE);
|
||||
|
||||
// --- Sub-pixel blend factor ---
|
||||
float lumaL = (lumaN + lumaS + lumaE + lumaW) * 0.25;
|
||||
float rangeL = abs(lumaL - lumaM);
|
||||
float blendL = max(0.0, (rangeL / range) - FXAA_SUBPIX_TRIM) * FXAA_SUBPIX_TRIM_SCALE;
|
||||
blendL = min(FXAA_SUBPIX_CAP, blendL) * FXAA_SUBPIX;
|
||||
|
||||
// --- Edge orientation (horizontal vs. vertical) ---
|
||||
float edgeHorz =
|
||||
abs(-2.0*lumaW + lumaNW + lumaSW)
|
||||
+ 2.0*abs(-2.0*lumaM + lumaN + lumaS)
|
||||
+ abs(-2.0*lumaE + lumaNE + lumaSE);
|
||||
float edgeVert =
|
||||
abs(-2.0*lumaS + lumaSW + lumaSE)
|
||||
+ 2.0*abs(-2.0*lumaM + lumaW + lumaE)
|
||||
+ abs(-2.0*lumaN + lumaNW + lumaNE);
|
||||
|
||||
bool horzSpan = (edgeHorz >= edgeVert);
|
||||
float lengthSign = horzSpan ? rcp.y : rcp.x;
|
||||
|
||||
float luma1 = horzSpan ? lumaN : lumaW;
|
||||
float luma2 = horzSpan ? lumaS : lumaE;
|
||||
float grad1 = abs(luma1 - lumaM);
|
||||
float grad2 = abs(luma2 - lumaM);
|
||||
lengthSign = (grad1 >= grad2) ? -lengthSign : lengthSign;
|
||||
|
||||
// --- Edge search ---
|
||||
vec2 posB = uv;
|
||||
vec2 offNP = horzSpan ? vec2(rcp.x, 0.0) : vec2(0.0, rcp.y);
|
||||
if (!horzSpan) posB.x += lengthSign * 0.5;
|
||||
if ( horzSpan) posB.y += lengthSign * 0.5;
|
||||
|
||||
float lumaMLSS = lumaM - (luma1 + luma2) * 0.5;
|
||||
float gradientScaled = max(grad1, grad2) * 0.25;
|
||||
|
||||
vec2 posN = posB - offNP;
|
||||
vec2 posP = posB + offNP;
|
||||
bool done1 = false, done2 = false;
|
||||
float lumaEnd1 = 0.0, lumaEnd2 = 0.0;
|
||||
|
||||
for (int i = 0; i < FXAA_SEARCH_STEPS; ++i) {
|
||||
if (!done1) lumaEnd1 = luma(texture(uScene, posN).rgb) - lumaMLSS;
|
||||
if (!done2) lumaEnd2 = luma(texture(uScene, posP).rgb) - lumaMLSS;
|
||||
done1 = done1 || (abs(lumaEnd1) >= gradientScaled * FXAA_SEARCH_THRESHOLD);
|
||||
done2 = done2 || (abs(lumaEnd2) >= gradientScaled * FXAA_SEARCH_THRESHOLD);
|
||||
if (done1 && done2) break;
|
||||
if (!done1) posN -= offNP;
|
||||
if (!done2) posP += offNP;
|
||||
}
|
||||
|
||||
float dstN = horzSpan ? (uv.x - posN.x) : (uv.y - posN.y);
|
||||
float dstP = horzSpan ? (posP.x - uv.x) : (posP.y - uv.y);
|
||||
bool dirN = (dstN < dstP);
|
||||
float lumaEndFinal = dirN ? lumaEnd1 : lumaEnd2;
|
||||
|
||||
float spanLength = dstN + dstP;
|
||||
float pixelOffset = (dirN ? dstN : dstP) / spanLength;
|
||||
bool goodSpan = ((lumaEndFinal < 0.0) != (lumaMLSS < 0.0));
|
||||
float pixelOffsetFinal = max(goodSpan ? pixelOffset : 0.0, blendL);
|
||||
|
||||
vec2 finalUV = uv;
|
||||
if ( horzSpan) finalUV.y += pixelOffsetFinal * lengthSign;
|
||||
if (!horzSpan) finalUV.x += pixelOffsetFinal * lengthSign;
|
||||
|
||||
outColor = vec4(texture(uScene, finalUV).rgb, 1.0);
|
||||
}
|
||||
|
|
@ -271,6 +271,10 @@ public:
|
|||
float getShadowDistance() const { return shadowDistance_; }
|
||||
void setMsaaSamples(VkSampleCountFlagBits samples);
|
||||
|
||||
// FXAA post-process anti-aliasing (combinable with MSAA)
|
||||
void setFXAAEnabled(bool enabled);
|
||||
bool isFXAAEnabled() const { return fxaa_.enabled; }
|
||||
|
||||
// FSR (FidelityFX Super Resolution) upscaling
|
||||
void setFSREnabled(bool enabled);
|
||||
bool isFSREnabled() const { return fsr_.enabled; }
|
||||
|
|
@ -398,6 +402,31 @@ private:
|
|||
void destroyFSRResources();
|
||||
void renderFSRUpscale();
|
||||
|
||||
// FXAA post-process state
|
||||
struct FXAAState {
|
||||
bool enabled = false;
|
||||
bool needsRecreate = false;
|
||||
|
||||
// Off-screen scene target (same resolution as swapchain — no scaling)
|
||||
AllocatedImage sceneColor{}; // 1x resolved color target
|
||||
AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count)
|
||||
AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x)
|
||||
AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve)
|
||||
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
|
||||
VkSampler sceneSampler = VK_NULL_HANDLE;
|
||||
|
||||
// FXAA fullscreen pipeline
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE;
|
||||
VkDescriptorPool descPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descSet = VK_NULL_HANDLE;
|
||||
};
|
||||
FXAAState fxaa_;
|
||||
bool initFXAAResources();
|
||||
void destroyFXAAResources();
|
||||
void renderFXAAPass();
|
||||
|
||||
// FSR 2.2 temporal upscaling state
|
||||
struct FSR2State {
|
||||
bool enabled = false;
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ private:
|
|||
float pendingLeftBarOffsetY = 0.0f; // Vertical offset from screen center
|
||||
int pendingGroundClutterDensity = 100;
|
||||
int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x
|
||||
bool pendingFXAA = false; // FXAA post-process (combinable with MSAA)
|
||||
bool pendingNormalMapping = true; // on by default
|
||||
float pendingNormalMapStrength = 0.8f; // 0.0-2.0
|
||||
bool pendingPOM = true; // on by default
|
||||
|
|
@ -238,6 +239,7 @@ private:
|
|||
bool minimapSettingsApplied_ = false;
|
||||
bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers
|
||||
bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer
|
||||
bool fxaaSettingsApplied_ = false; // True once saved FXAA setting applied to renderer
|
||||
bool waterRefractionApplied_ = false;
|
||||
bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied
|
||||
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@ void Renderer::shutdown() {
|
|||
|
||||
destroyFSRResources();
|
||||
destroyFSR2Resources();
|
||||
destroyFXAAResources();
|
||||
destroyPerFrameResources();
|
||||
|
||||
zoneManager.reset();
|
||||
|
|
@ -960,8 +961,9 @@ void Renderer::applyMsaaChange() {
|
|||
VkDevice device = vkCtx->getDevice();
|
||||
if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; }
|
||||
if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame()
|
||||
if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame()
|
||||
if (fsr2_.sceneFramebuffer) destroyFSR2Resources();
|
||||
if (fxaa_.sceneFramebuffer) destroyFXAAResources(); // Will be lazily recreated in beginFrame()
|
||||
|
||||
// Reinitialize ImGui Vulkan backend with new MSAA sample count
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
|
|
@ -1017,6 +1019,19 @@ void Renderer::beginFrame() {
|
|||
}
|
||||
}
|
||||
|
||||
// FXAA resource management (disabled when FSR2 is active — FSR2 has its own AA)
|
||||
if (fxaa_.needsRecreate && fxaa_.sceneFramebuffer) {
|
||||
destroyFXAAResources();
|
||||
fxaa_.needsRecreate = false;
|
||||
if (!fxaa_.enabled) LOG_INFO("FXAA: disabled");
|
||||
}
|
||||
if (fxaa_.enabled && !fsr2_.enabled && !fsr_.enabled && !fxaa_.sceneFramebuffer) {
|
||||
if (!initFXAAResources()) {
|
||||
LOG_ERROR("FXAA: initialization failed, disabling");
|
||||
fxaa_.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle swapchain recreation if needed
|
||||
if (vkCtx->isSwapchainDirty()) {
|
||||
vkCtx->recreateSwapchain(window->getWidth(), window->getHeight());
|
||||
|
|
@ -1033,6 +1048,11 @@ void Renderer::beginFrame() {
|
|||
destroyFSR2Resources();
|
||||
initFSR2Resources();
|
||||
}
|
||||
// Recreate FXAA resources for new swapchain dimensions
|
||||
if (fxaa_.enabled && !fsr2_.enabled && !fsr_.enabled) {
|
||||
destroyFXAAResources();
|
||||
initFXAAResources();
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire swapchain image and begin command buffer
|
||||
|
|
@ -1122,6 +1142,9 @@ void Renderer::beginFrame() {
|
|||
} else if (fsr_.enabled && fsr_.sceneFramebuffer) {
|
||||
rpInfo.framebuffer = fsr_.sceneFramebuffer;
|
||||
renderExtent = { fsr_.internalWidth, fsr_.internalHeight };
|
||||
} else if (fxaa_.enabled && fxaa_.sceneFramebuffer) {
|
||||
rpInfo.framebuffer = fxaa_.sceneFramebuffer;
|
||||
renderExtent = vkCtx->getSwapchainExtent(); // native resolution — no downscaling
|
||||
} else {
|
||||
rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex];
|
||||
renderExtent = vkCtx->getSwapchainExtent();
|
||||
|
|
@ -1298,10 +1321,50 @@ void Renderer::endFrame() {
|
|||
|
||||
// Draw FSR upscale fullscreen quad
|
||||
renderFSRUpscale();
|
||||
|
||||
} else if (fxaa_.enabled && fxaa_.sceneFramebuffer) {
|
||||
// End the off-screen scene render pass
|
||||
vkCmdEndRenderPass(currentCmd);
|
||||
|
||||
// Transition resolved scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY
|
||||
transitionImageLayout(currentCmd, fxaa_.sceneColor.image,
|
||||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
|
||||
// Begin swapchain render pass (1x — no MSAA on the output pass)
|
||||
VkRenderPassBeginInfo rpInfo{};
|
||||
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||
rpInfo.renderPass = vkCtx->getImGuiRenderPass();
|
||||
rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex];
|
||||
rpInfo.renderArea.offset = {0, 0};
|
||||
rpInfo.renderArea.extent = vkCtx->getSwapchainExtent();
|
||||
// The swapchain render pass always has 2 attachments when MSAA is off;
|
||||
// FXAA output goes to the non-MSAA swapchain directly.
|
||||
VkClearValue fxaaClear[2]{};
|
||||
fxaaClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
||||
fxaaClear[1].depthStencil = {1.0f, 0};
|
||||
rpInfo.clearValueCount = 2;
|
||||
rpInfo.pClearValues = fxaaClear;
|
||||
|
||||
vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
VkViewport vp{};
|
||||
vp.width = static_cast<float>(ext.width);
|
||||
vp.height = static_cast<float>(ext.height);
|
||||
vp.maxDepth = 1.0f;
|
||||
vkCmdSetViewport(currentCmd, 0, 1, &vp);
|
||||
VkRect2D sc{};
|
||||
sc.extent = ext;
|
||||
vkCmdSetScissor(currentCmd, 0, 1, &sc);
|
||||
|
||||
// Draw FXAA pass
|
||||
renderFXAAPass();
|
||||
}
|
||||
|
||||
// ImGui rendering — must respect subpass contents mode
|
||||
if (!fsr_.enabled && !fsr2_.enabled && parallelRecordingEnabled_) {
|
||||
if (!fsr_.enabled && !fsr2_.enabled && !fxaa_.enabled && parallelRecordingEnabled_) {
|
||||
// Scene pass was begun with VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS,
|
||||
// so ImGui must be recorded into a secondary command buffer.
|
||||
VkCommandBuffer imguiCmd = beginSecondary(SEC_IMGUI);
|
||||
|
|
@ -4698,6 +4761,247 @@ void Renderer::setAmdFsr3FramegenEnabled(bool enabled) {
|
|||
|
||||
// ========================= End FSR 2.2 =========================
|
||||
|
||||
// ========================= FXAA Post-Process =========================
|
||||
|
||||
bool Renderer::initFXAAResources() {
|
||||
if (!vkCtx) return false;
|
||||
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
VkSampleCountFlagBits msaa = vkCtx->getMsaaSamples();
|
||||
bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT);
|
||||
bool useDepthResolve = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE);
|
||||
|
||||
LOG_INFO("FXAA: initializing at ", ext.width, "x", ext.height,
|
||||
" (MSAA=", static_cast<int>(msaa), "x)");
|
||||
|
||||
VkFormat colorFmt = vkCtx->getSwapchainFormat();
|
||||
VkFormat depthFmt = vkCtx->getDepthFormat();
|
||||
|
||||
// sceneColor: 1x resolved color target — FXAA reads from here
|
||||
fxaa_.sceneColor = createImage(device, alloc, ext.width, ext.height,
|
||||
colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
if (!fxaa_.sceneColor.image) {
|
||||
LOG_ERROR("FXAA: failed to create scene color image");
|
||||
return false;
|
||||
}
|
||||
|
||||
// sceneDepth: depth buffer at current MSAA sample count
|
||||
fxaa_.sceneDepth = createImage(device, alloc, ext.width, ext.height,
|
||||
depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa);
|
||||
if (!fxaa_.sceneDepth.image) {
|
||||
LOG_ERROR("FXAA: failed to create scene depth image");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useMsaa) {
|
||||
fxaa_.sceneMsaaColor = createImage(device, alloc, ext.width, ext.height,
|
||||
colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa);
|
||||
if (!fxaa_.sceneMsaaColor.image) {
|
||||
LOG_ERROR("FXAA: failed to create MSAA color image");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
if (useDepthResolve) {
|
||||
fxaa_.sceneDepthResolve = createImage(device, alloc, ext.width, ext.height,
|
||||
depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
|
||||
if (!fxaa_.sceneDepthResolve.image) {
|
||||
LOG_ERROR("FXAA: failed to create depth resolve image");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Framebuffer — same attachment layout as main render pass
|
||||
VkImageView fbAttachments[4]{};
|
||||
uint32_t fbCount;
|
||||
if (useMsaa) {
|
||||
fbAttachments[0] = fxaa_.sceneMsaaColor.imageView;
|
||||
fbAttachments[1] = fxaa_.sceneDepth.imageView;
|
||||
fbAttachments[2] = fxaa_.sceneColor.imageView; // resolve target
|
||||
fbCount = 3;
|
||||
if (useDepthResolve) {
|
||||
fbAttachments[3] = fxaa_.sceneDepthResolve.imageView;
|
||||
fbCount = 4;
|
||||
}
|
||||
} else {
|
||||
fbAttachments[0] = fxaa_.sceneColor.imageView;
|
||||
fbAttachments[1] = fxaa_.sceneDepth.imageView;
|
||||
fbCount = 2;
|
||||
}
|
||||
|
||||
VkFramebufferCreateInfo fbInfo{};
|
||||
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||
fbInfo.renderPass = vkCtx->getImGuiRenderPass();
|
||||
fbInfo.attachmentCount = fbCount;
|
||||
fbInfo.pAttachments = fbAttachments;
|
||||
fbInfo.width = ext.width;
|
||||
fbInfo.height = ext.height;
|
||||
fbInfo.layers = 1;
|
||||
if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fxaa_.sceneFramebuffer) != VK_SUCCESS) {
|
||||
LOG_ERROR("FXAA: failed to create scene framebuffer");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sampler
|
||||
VkSamplerCreateInfo samplerInfo{};
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
samplerInfo.minFilter = VK_FILTER_LINEAR;
|
||||
samplerInfo.magFilter = VK_FILTER_LINEAR;
|
||||
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &fxaa_.sceneSampler) != VK_SUCCESS) {
|
||||
LOG_ERROR("FXAA: failed to create sampler");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Descriptor set layout: binding 0 = combined image sampler
|
||||
VkDescriptorSetLayoutBinding binding{};
|
||||
binding.binding = 0;
|
||||
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
binding.descriptorCount = 1;
|
||||
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
VkDescriptorSetLayoutCreateInfo layoutInfo{};
|
||||
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
layoutInfo.bindingCount = 1;
|
||||
layoutInfo.pBindings = &binding;
|
||||
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout);
|
||||
|
||||
VkDescriptorPoolSize poolSize{};
|
||||
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
poolSize.descriptorCount = 1;
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.maxSets = 1;
|
||||
poolInfo.poolSizeCount = 1;
|
||||
poolInfo.pPoolSizes = &poolSize;
|
||||
vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool);
|
||||
|
||||
VkDescriptorSetAllocateInfo dsAllocInfo{};
|
||||
dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
dsAllocInfo.descriptorPool = fxaa_.descPool;
|
||||
dsAllocInfo.descriptorSetCount = 1;
|
||||
dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout;
|
||||
vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet);
|
||||
|
||||
// Bind the resolved 1x sceneColor
|
||||
VkDescriptorImageInfo imgInfo{};
|
||||
imgInfo.sampler = fxaa_.sceneSampler;
|
||||
imgInfo.imageView = fxaa_.sceneColor.imageView;
|
||||
imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkWriteDescriptorSet write{};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = fxaa_.descSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||
|
||||
// Pipeline layout — push constant holds vec2 rcpFrame
|
||||
VkPushConstantRange pc{};
|
||||
pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
pc.offset = 0;
|
||||
pc.size = 8; // vec2
|
||||
VkPipelineLayoutCreateInfo plCI{};
|
||||
plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
plCI.setLayoutCount = 1;
|
||||
plCI.pSetLayouts = &fxaa_.descSetLayout;
|
||||
plCI.pushConstantRangeCount = 1;
|
||||
plCI.pPushConstantRanges = &pc;
|
||||
vkCreatePipelineLayout(device, &plCI, nullptr, &fxaa_.pipelineLayout);
|
||||
|
||||
// FXAA pipeline — fullscreen triangle into the swapchain render pass
|
||||
// Uses VK_SAMPLE_COUNT_1_BIT: it always runs after MSAA resolve.
|
||||
VkShaderModule vertMod, fragMod;
|
||||
if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") ||
|
||||
!fragMod.loadFromFile(device, "assets/shaders/fxaa.frag.spv")) {
|
||||
LOG_ERROR("FXAA: failed to load shaders");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
fxaa_.pipeline = PipelineBuilder()
|
||||
.setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({}, {})
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
||||
.setMultisample(VK_SAMPLE_COUNT_1_BIT) // swapchain pass is always 1x
|
||||
.setLayout(fxaa_.pipelineLayout)
|
||||
.setRenderPass(vkCtx->getImGuiRenderPass())
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device);
|
||||
|
||||
vertMod.destroy();
|
||||
fragMod.destroy();
|
||||
|
||||
if (!fxaa_.pipeline) {
|
||||
LOG_ERROR("FXAA: failed to create pipeline");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("FXAA: initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::destroyFXAAResources() {
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
vkDeviceWaitIdle(device);
|
||||
|
||||
if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; }
|
||||
if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; }
|
||||
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; }
|
||||
if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; }
|
||||
if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; }
|
||||
if (fxaa_.sceneSampler) { vkDestroySampler(device, fxaa_.sceneSampler, nullptr); fxaa_.sceneSampler = VK_NULL_HANDLE; }
|
||||
destroyImage(device, alloc, fxaa_.sceneDepthResolve);
|
||||
destroyImage(device, alloc, fxaa_.sceneMsaaColor);
|
||||
destroyImage(device, alloc, fxaa_.sceneDepth);
|
||||
destroyImage(device, alloc, fxaa_.sceneColor);
|
||||
}
|
||||
|
||||
void Renderer::renderFXAAPass() {
|
||||
if (!fxaa_.pipeline || currentCmd == VK_NULL_HANDLE) return;
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
|
||||
vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline);
|
||||
vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr);
|
||||
|
||||
// Push rcpFrame = vec2(1/width, 1/height)
|
||||
float rcpFrame[2] = {
|
||||
1.0f / static_cast<float>(ext.width),
|
||||
1.0f / static_cast<float>(ext.height)
|
||||
};
|
||||
vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout,
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 8, rcpFrame);
|
||||
|
||||
vkCmdDraw(currentCmd, 3, 1, 0, 0); // fullscreen triangle
|
||||
}
|
||||
|
||||
void Renderer::setFXAAEnabled(bool enabled) {
|
||||
if (fxaa_.enabled == enabled) return;
|
||||
fxaa_.enabled = enabled;
|
||||
if (!enabled) {
|
||||
fxaa_.needsRecreate = true; // defer destruction to next beginFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= End FXAA =========================
|
||||
|
||||
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||
(void)world;
|
||||
|
||||
|
|
|
|||
|
|
@ -512,6 +512,15 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
msaaSettingsApplied_ = true;
|
||||
}
|
||||
|
||||
// Apply saved FXAA setting once when renderer is available
|
||||
if (!fxaaSettingsApplied_) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) {
|
||||
renderer->setFXAAEnabled(pendingFXAA);
|
||||
fxaaSettingsApplied_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply saved water refraction setting once when renderer is available
|
||||
if (!waterRefractionApplied_) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
|
|
@ -13969,6 +13978,21 @@ void GameScreen::renderSettingsWindow() {
|
|||
updateGraphicsPresetFromCurrentSettings();
|
||||
saveSettings();
|
||||
}
|
||||
// FXAA — post-process, combinable with any MSAA level (disabled with FSR2)
|
||||
if (fsr2Active) {
|
||||
ImGui::BeginDisabled();
|
||||
bool fxaaOff = false;
|
||||
ImGui::Checkbox("FXAA (disabled with FSR3)", &fxaaOff);
|
||||
ImGui::EndDisabled();
|
||||
} else {
|
||||
if (ImGui::Checkbox("FXAA (post-process)", &pendingFXAA)) {
|
||||
if (renderer) renderer->setFXAAEnabled(pendingFXAA);
|
||||
updateGraphicsPresetFromCurrentSettings();
|
||||
saveSettings();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("FXAA smooths jagged edges as a post-process pass.\nCan be combined with MSAA for extra quality.");
|
||||
}
|
||||
}
|
||||
// FSR Upscaling
|
||||
{
|
||||
|
|
@ -16474,6 +16498,7 @@ void GameScreen::saveSettings() {
|
|||
out << "shadow_distance=" << pendingShadowDistance << "\n";
|
||||
out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n";
|
||||
out << "antialiasing=" << pendingAntiAliasing << "\n";
|
||||
out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n";
|
||||
out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n";
|
||||
out << "normal_map_strength=" << pendingNormalMapStrength << "\n";
|
||||
out << "pom=" << (pendingPOM ? 1 : 0) << "\n";
|
||||
|
|
@ -16608,6 +16633,7 @@ void GameScreen::loadSettings() {
|
|||
else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f);
|
||||
else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0);
|
||||
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
|
||||
else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0);
|
||||
else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0);
|
||||
else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f);
|
||||
else if (key == "pom") pendingPOM = (std::stoi(val) != 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue