From 2858c68e892bb40bfbc7b3735b4e3569e0d42d01 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 12 Jul 2026 16:12:31 -0700 Subject: [PATCH] fix(character): render ghost opacity through blended pipeline Route faded character instances through a dedicated translucent character pipeline so dead and ghost characters fade as a whole model instead of only showing translucent hair. Opaque body batches previously wrote the shader alpha from instance opacity, but the opaque pipeline had blending disabled, so the body stayed fully solid. Hair went through the alpha-test path, where reduced alpha affected coverage, making only hair appear faded. Add a translucent pipeline with alpha blending and depth writes enabled. This keeps normal self-occlusion for faded characters while allowing the whole model to respect instance opacity. Reuse the existing per-batch alpha-test flag so cutout materials such as hair keep crisp edges while fading with the rest of the body. Also make the whole-model fallback path bind an explicit opaque or translucent pipeline instead of inheriting the previously bound pipeline. Create, recreate, validate, and destroy the new pipeline alongside the other character pipelines. Build passes. --- include/rendering/character_renderer.hpp | 4 ++++ src/rendering/character_renderer.cpp | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 6988664a..a829713c 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -317,6 +317,10 @@ private: VkPipeline alphaTestPipeline_ = VK_NULL_HANDLE; VkPipeline alphaPipeline_ = VK_NULL_HANDLE; VkPipeline additivePipeline_ = VK_NULL_HANDLE; + // Whole-instance fades (ghost form, spawn fade-in): alpha blend with depth + // write kept on, so the faded model still self-occludes instead of showing + // backfaces and under-armor skin through the body. + VkPipeline translucentPipeline_ = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; // Descriptor set layouts diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 9fa03688..be1a3a22 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -402,6 +402,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true, true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); + translucentPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); // Clean up shader modules charVert.destroy(); @@ -473,6 +474,7 @@ void CharacterRenderer::shutdown() { destroyPipeline(alphaTestPipeline_); destroyPipeline(alphaPipeline_); destroyPipeline(additivePipeline_); + destroyPipeline(translucentPipeline_); if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } @@ -2722,6 +2724,14 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, // declare Mod/alpha blending, which would composite that black // background as an opaque quad — force additive so only the light adds. desiredPipeline = additivePipeline_; + } else if (instance.opacity < 0.999f) { + // Whole-instance fade (ghost form, spawn fade-in): the opaque and + // alpha-test pipelines have blending disabled, so the shader's + // texColor.a * opacity output is discarded and only hair (via + // alpha-to-coverage) ever looked translucent. Route every batch + // through the blend pipeline; the per-batch alphaTest UBO flag + // still handles cutout materials in the shader. + desiredPipeline = translucentPipeline_; } else if (hairMaterial) { desiredPipeline = alphaTestPipeline_; } else { @@ -2849,6 +2859,15 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (pomQuality_ == 0) pomSamples2 = 16; else if (pomQuality_ == 2) pomSamples2 = 64; + // Whole-model fallback inherits whatever pipeline was bound last; + // pick it explicitly so instance fades blend here too. + VkPipeline fallbackPipeline = (instance.opacity < 0.999f) + ? translucentPipeline_ : opaquePipeline_; + if (fallbackPipeline != currentPipeline) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fallbackPipeline); + currentPipeline = fallbackPipeline; + } + CharMaterialUBO matData{}; matData.opacity = instance.opacity; matData.alphaTest = 0; @@ -3774,6 +3793,7 @@ void CharacterRenderer::recreatePipelines() { if (alphaTestPipeline_) { vkDestroyPipeline(device, alphaTestPipeline_, nullptr); alphaTestPipeline_ = VK_NULL_HANDLE; } if (alphaPipeline_) { vkDestroyPipeline(device, alphaPipeline_, nullptr); alphaPipeline_ = VK_NULL_HANDLE; } if (additivePipeline_) { vkDestroyPipeline(device, additivePipeline_, nullptr); additivePipeline_ = VK_NULL_HANDLE; } + if (translucentPipeline_) { vkDestroyPipeline(device, translucentPipeline_, nullptr); translucentPipeline_ = VK_NULL_HANDLE; } // --- Load shaders --- rendering::VkShaderModule charVert, charFrag; @@ -3830,11 +3850,13 @@ void CharacterRenderer::recreatePipelines() { alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true, true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); + translucentPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); charVert.destroy(); charFrag.destroy(); - if (!opaquePipeline_ || !alphaTestPipeline_ || !alphaPipeline_ || !additivePipeline_) { + if (!opaquePipeline_ || !alphaTestPipeline_ || !alphaPipeline_ || !additivePipeline_ || + !translucentPipeline_) { LOG_ERROR("CharacterRenderer::recreatePipelines FAILED: opaque=", (void*)opaquePipeline_, " alphaTest=", (void*)alphaTestPipeline_, " alpha=", (void*)alphaPipeline_,