From 77012adbc6a4ad642677f05a58b5bc8a673007c7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 05:55:03 -0800 Subject: [PATCH] Add alpha-tested foliage shadows: per-batch texture binding and shadow map receiving Shadow casting: foliage batches now bind their actual texture in the shadow pass with alpha testing, producing leaf-shaped shadows instead of solid cards. Uses a per-frame resettable descriptor pool for texture sets. Shadow receiving: foliage fragments now sample the shadow map with PCF instead of using a flat constant darkening. --- assets/shaders/m2.frag.glsl | 34 +++++----- assets/shaders/m2.frag.spv | Bin 15880 -> 17124 bytes include/rendering/m2_renderer.hpp | 2 + src/rendering/m2_renderer.cpp | 100 +++++++++++++++++++++++++++--- 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/assets/shaders/m2.frag.glsl b/assets/shaders/m2.frag.glsl index 9e12129e..9814a99b 100644 --- a/assets/shaders/m2.frag.glsl +++ b/assets/shaders/m2.frag.glsl @@ -125,32 +125,26 @@ void main() { } else { vec3 viewDir = normalize(viewPos.xyz - FragPos); - // Foliage: no specular, no shadow map — both flicker on swaying thin cards float spec = 0.0; float shadow = 1.0; - if (isFoliage) { - // Use a fixed gentle shadow from the shadow system strength - if (shadowParams.x > 0.5) { - shadow = mix(1.0, 0.75, shadowParams.y); - } - } else { + if (!isFoliage) { vec3 halfDir = normalize(ldir + viewDir); spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity; + } - if (shadowParams.x > 0.5) { - float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir))); - vec3 biasedPos = FragPos + norm * normalOffset; - vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0); - vec3 proj = lsPos.xyz / lsPos.w; - proj.xy = proj.xy * 0.5 + 0.5; - if (proj.x >= 0.0 && proj.x <= 1.0 && - proj.y >= 0.0 && proj.y <= 1.0 && - proj.z >= 0.0 && proj.z <= 1.0) { - float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005); - shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias)); - } - shadow = mix(1.0, shadow, shadowParams.y); + if (shadowParams.x > 0.5) { + float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir))); + vec3 biasedPos = FragPos + norm * normalOffset; + vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0); + vec3 proj = lsPos.xyz / lsPos.w; + proj.xy = proj.xy * 0.5 + 0.5; + if (proj.x >= 0.0 && proj.x <= 1.0 && + proj.y >= 0.0 && proj.y <= 1.0 && + proj.z >= 0.0 && proj.z <= 1.0) { + float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005); + shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias)); } + shadow = mix(1.0, shadow, shadowParams.y); } // Leaf subsurface scattering (foliage only) — uses stable normal, no FragPos dependency diff --git a/assets/shaders/m2.frag.spv b/assets/shaders/m2.frag.spv index 273a749132f4c0c50af75d340fe53639795250da..fef212fa22072a2521581ff036519e1b709a0af1 100644 GIT binary patch literal 17124 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 literal 15880 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 diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 265aeddc..02411660 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -326,6 +326,8 @@ private: VkDescriptorSet shadowParamsSet_ = VK_NULL_HANDLE; ::VkBuffer shadowParamsUBO_ = VK_NULL_HANDLE; VmaAllocation shadowParamsAlloc_ = VK_NULL_HANDLE; + // Per-frame pool for foliage shadow texture descriptor sets + VkDescriptorPool shadowTexPool_ = VK_NULL_HANDLE; // Particle pipelines VkPipeline particlePipeline_ = VK_NULL_HANDLE; // M2 emitter particles diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 773379fb..0c3d9b17 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -698,6 +698,7 @@ void M2Renderer::shutdown() { // Destroy shadow resources destroyPipeline(shadowPipeline_); if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; } + if (shadowTexPool_) { vkDestroyDescriptorPool(device, shadowTexPool_, nullptr); shadowTexPool_ = VK_NULL_HANDLE; } if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; } if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; } if (shadowParamsUBO_) { vmaDestroyBuffer(alloc, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; } @@ -2542,6 +2543,24 @@ bool M2Renderer::initializeShadow(VkRenderPass shadowRenderPass) { writes[1].pBufferInfo = &bufInfo; vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + // Per-frame pool for foliage shadow texture sets (reset each frame) + { + VkDescriptorPoolSize texPoolSizes[2]{}; + texPoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + texPoolSizes[0].descriptorCount = 256; + texPoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + texPoolSizes[1].descriptorCount = 256; + VkDescriptorPoolCreateInfo texPoolCI{}; + texPoolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + texPoolCI.maxSets = 256; + texPoolCI.poolSizeCount = 2; + texPoolCI.pPoolSizes = texPoolSizes; + if (vkCreateDescriptorPool(device, &texPoolCI, nullptr, &shadowTexPool_) != VK_SUCCESS) { + LOG_ERROR("M2Renderer: failed to create shadow texture pool"); + return false; + } + } + // Create shadow pipeline layout: set 1 = shadowParamsLayout_, push constants = 128 bytes VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; @@ -2613,23 +2632,74 @@ void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMa if (instances.empty() || models.empty()) return; struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; }; + struct ShadowParamsUBO { + int32_t useBones = 0; + int32_t useTexture = 0; + int32_t alphaTest = 0; + int32_t foliageSway = 0; + float windTime = 0.0f; + float foliageMotionDamp = 1.0f; + }; const float shadowRadiusSq = shadowRadius * shadowRadius; + // Reset per-frame texture descriptor pool for foliage alpha-test sets + if (shadowTexPool_) { + vkResetDescriptorPool(vkCtx_->getDevice(), shadowTexPool_, 0); + } + // Cache: texture imageView -> allocated descriptor set (avoids duplicates within frame) + std::unordered_map texSetCache; + + auto getTexDescSet = [&](VkTexture* tex) -> VkDescriptorSet { + VkImageView iv = tex->getImageView(); + auto cacheIt = texSetCache.find(iv); + if (cacheIt != texSetCache.end()) return cacheIt->second; + + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorSetAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + ai.descriptorPool = shadowTexPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &shadowParamsLayout_; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set) != VK_SUCCESS) { + return shadowParamsSet_; // fallback to white texture + } + VkDescriptorImageInfo imgInfo{}; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imgInfo.imageView = iv; + imgInfo.sampler = tex->getSampler(); + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = shadowParamsUBO_; + bufInfo.offset = 0; + bufInfo.range = sizeof(ShadowParamsUBO); + VkWriteDescriptorSet writes[2]{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = set; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = set; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 2, writes, 0, nullptr); + texSetCache[iv] = set; + return set; + }; + // Helper lambda to draw instances with a given foliageSway setting auto drawPass = [&](bool foliagePass) { - // Update ShadowParams UBO for this pass - struct ShadowParamsUBO { - int32_t useBones = 0; - int32_t useTexture = 0; - int32_t alphaTest = 0; - int32_t foliageSway = 0; - float windTime = 0.0f; - float foliageMotionDamp = 1.0f; - }; ShadowParamsUBO params{}; params.foliageSway = foliagePass ? 1 : 0; params.windTime = globalTime; params.foliageMotionDamp = 1.0f; + // For foliage pass: enable texture+alphaTest in UBO (per-batch textures bound below) + if (foliagePass) { + params.useTexture = 1; + params.alphaTest = 1; + } VmaAllocationInfo allocInfo{}; vmaGetAllocationInfo(vkCtx_->getAllocator(), shadowParamsAlloc_, &allocInfo); @@ -2670,6 +2740,16 @@ void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMa for (const auto& batch : model.batches) { if (batch.submeshLevel > 0) continue; + // For foliage: bind per-batch texture for alpha-tested shadows + if (foliagePass && batch.hasAlpha && batch.texture) { + VkDescriptorSet texSet = getTexDescSet(batch.texture); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, + 0, 1, &texSet, 0, nullptr); + } else if (foliagePass) { + // Non-alpha batch: rebind default set (white texture, alpha test passes) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, + 0, 1, &shadowParamsSet_, 0, nullptr); + } vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); } } @@ -2677,7 +2757,7 @@ void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMa // Pass 1: non-foliage (no wind displacement) drawPass(false); - // Pass 2: foliage (wind displacement enabled) + // Pass 2: foliage (wind displacement enabled, per-batch alpha-tested textures) drawPass(true); }