From e34ece227ee139dd6e25eb7be99654b650e395d1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 18 Jul 2026 21:14:06 -0700 Subject: [PATCH] feat: smooth foliage rendering with alpha-to-coverage Enable MSAA alpha-to-coverage on the M2 cutout pipeline with screen-space sharpened alpha replacing the hard discard, preserve mip alpha so distant canopies keep their density, and add hemispheric sky ambient so tree crowns shade with depth. --- assets/shaders/m2.frag.glsl | 39 ++++++++++++++++++------- assets/shaders/m2.frag.spv | Bin 12604 -> 13256 bytes src/rendering/m2_renderer.cpp | 14 ++++++--- src/rendering/m2_renderer_instance.cpp | 14 ++++++--- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/assets/shaders/m2.frag.glsl b/assets/shaders/m2.frag.glsl index ec334eeb..725ceb06 100644 --- a/assets/shaders/m2.frag.glsl +++ b/assets/shaders/m2.frag.glsl @@ -107,8 +107,21 @@ void main() { } else if (alphaTest != 0) { alphaCutoff = 0.4; } - if (alphaTest != 0 && texColor.a < alphaCutoff) { - discard; + // Mip-alpha preservation: alpha mips average downward, thinning distant + // canopies to skeletons. Boost alpha with mip level so perceived leaf + // density stays constant with distance. + if (isFoliage && hasTexture != 0) { + float mip = textureQueryLod(uTexture, TexCoord).x; + texColor.a *= 1.0 + clamp(mip, 0.0, 4.0) * 0.18; + } + if (alphaTest != 0) { + // Screen-space sharpened alpha: rescale so the cutoff maps to the + // texel boundary. With MSAA + alpha-to-coverage on the cutout + // pipeline this dithers the edge band across samples, smoothing + // leaf silhouettes instead of the old hard binary discard. + float aGrad = fwidth(texColor.a); + texColor.a = clamp((texColor.a - alphaCutoff) / max(aGrad, 0.001) * 0.5 + 0.5, 0.0, 1.0); + if (texColor.a < 1.0 / 255.0) discard; } if (colorKeyBlack != 0) { float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114)); @@ -187,7 +200,14 @@ void main() { sss = sssAmount * vec3(1.0, 0.9, 0.5) * lightColor.rgb; } - result = ambientColor.rgb * texColor.rgb + // Sky-bounce ambient for foliage: upward-facing leaves catch more + // ambient than the canopy underside, giving the crown depth instead + // of a uniformly-lit blob. + vec3 ambientTerm = ambientColor.rgb; + if (isFoliage) { + ambientTerm *= 0.82 + 0.30 * clamp(norm.z, 0.0, 1.0); + } + result = ambientTerm * texColor.rgb + shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb) + sss; @@ -210,15 +230,12 @@ void main() { result = mix(fogColor.rgb, result, fogFactor); float outAlpha = texColor.a * vFadeAlpha; - // Cutout materials should not remain partially transparent after discard, - // otherwise foliage cards look view-dependent. - if (alphaTest != 0 || colorKeyBlack != 0) { + // Cutout materials output the sharpened coverage alpha computed above — + // alpha-to-coverage turns it into per-sample coverage for smooth edges. + // Color-key-only materials have no meaningful texture alpha; keep them + // opaque after the discard. + if (colorKeyBlack != 0 && alphaTest == 0) { outAlpha = vFadeAlpha; } - // Foliage cutout should stay opaque after alpha discard to avoid - // view-angle translucency artifacts. - if (alphaTest == 2 || alphaTest == 3) { - outAlpha = 1.0 * vFadeAlpha; - } outColor = vec4(result, outAlpha); } diff --git a/assets/shaders/m2.frag.spv b/assets/shaders/m2.frag.spv index 3fe2466f50b862a8fc727a559d0d87421c79ff2d..29875e9c4a20387eb433a51a2c9907de94cfa5c3 100644 GIT binary patch literal 13256 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 literal 12604 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 VkPipeline { - return PipelineBuilder() + VkPipelineCreateFlags flags = 0, VkPipeline basePipeline = VK_NULL_HANDLE, + bool alphaToCoverage = false) -> VkPipeline { + auto builder = PipelineBuilder() .setShaders(m2Vert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), m2Frag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({m2Binding}, m2Attrs) @@ -741,7 +742,11 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setDepthTest(!skyMode_, skyMode_ ? false : depthWrite, skyMode_ ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) - .setMultisample(vkCtx_->getMsaaSamples()) + .setMultisample(vkCtx_->getMsaaSamples()); + // MSAA alpha-to-coverage dithers the shader's sharpened cutout alpha + // across samples for smooth foliage/leaf silhouettes. + if (alphaToCoverage) builder.setAlphaToCoverage(true); + return builder .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -753,7 +758,8 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true, VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT); alphaTestPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), true, - VK_PIPELINE_CREATE_DERIVATIVE_BIT, opaquePipeline_); + VK_PIPELINE_CREATE_DERIVATIVE_BIT, opaquePipeline_, + /*alphaToCoverage=*/true); alphaPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), false, VK_PIPELINE_CREATE_DERIVATIVE_BIT, opaquePipeline_); additivePipeline_ = buildM2Pipeline(PipelineBuilder::blendAdditive(), false, diff --git a/src/rendering/m2_renderer_instance.cpp b/src/rendering/m2_renderer_instance.cpp index 1409cc37..88e6296c 100644 --- a/src/rendering/m2_renderer_instance.cpp +++ b/src/rendering/m2_renderer_instance.cpp @@ -1226,8 +1226,9 @@ void M2Renderer::recreatePipelines() { // Pipeline derivatives — opaque is the base, others derive from it for shared state optimization auto buildM2Pipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite, - VkPipelineCreateFlags flags = 0, VkPipeline basePipeline = VK_NULL_HANDLE) -> VkPipeline { - return PipelineBuilder() + VkPipelineCreateFlags flags = 0, VkPipeline basePipeline = VK_NULL_HANDLE, + bool alphaToCoverage = false) -> VkPipeline { + auto builder = PipelineBuilder() .setShaders(m2Vert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), m2Frag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({m2Binding}, m2Attrs) @@ -1236,7 +1237,11 @@ void M2Renderer::recreatePipelines() { .setDepthTest(!skyMode_, skyMode_ ? false : depthWrite, skyMode_ ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) - .setMultisample(vkCtx_->getMsaaSamples()) + .setMultisample(vkCtx_->getMsaaSamples()); + // MSAA alpha-to-coverage dithers the shader's sharpened cutout alpha + // across samples for smooth foliage/leaf silhouettes. + if (alphaToCoverage) builder.setAlphaToCoverage(true); + return builder .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -1248,7 +1253,8 @@ void M2Renderer::recreatePipelines() { opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true, VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT); alphaTestPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), true, - VK_PIPELINE_CREATE_DERIVATIVE_BIT, opaquePipeline_); + VK_PIPELINE_CREATE_DERIVATIVE_BIT, opaquePipeline_, + /*alphaToCoverage=*/true); alphaPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), false, VK_PIPELINE_CREATE_DERIVATIVE_BIT, opaquePipeline_); additivePipeline_ = buildM2Pipeline(PipelineBuilder::blendAdditive(), false,