From 3f408341e13ee8aa90ef0bb6d4061d8f4c27682e Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH] fix(rendering): correct alpha test on opaque batches and hair transparency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - alphaTestPipeline_ uses blendDisabled() so surviving pixels are fully opaque (was blendAlpha, causing hair to blend with background) - Remove alphaCutout from alphaTest condition — opaque materials like capes no longer alpha-test just because their texture has an alpha channel - Two-pass batch rendering: opaque (blendMode 0) draws first to establish depth, then alpha-key/blend draws on top --- assets/shaders/character.frag.spv | Bin 16048 -> 17632 bytes src/rendering/character_renderer.cpp | 91 +++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/assets/shaders/character.frag.spv b/assets/shaders/character.frag.spv index 99c68edf24ddec7f733c836d9232807899737158..f970ac7cfaa372c9f83b94572e6aea8b0bcdf821 100644 GIT binary patch literal 17632 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 bitanAccum(vertCount, glm::vec3(0.0f)); // Copy base vertex data + size_t numBones = model.bones.size(); + int outOfRangeCount = 0, ge128Count = 0, nonzeroWeightOOR = 0; for (size_t i = 0; i < vertCount; i++) { const auto& src = model.vertices[i]; auto& dst = gpuVerts[i]; @@ -1490,6 +1492,22 @@ void CharacterRenderer::setupModelBuffers(M2ModelGPU& gpuModel) { dst.normal = src.normal; dst.texCoords = src.texCoords[0]; // Use first UV set dst.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); // default + + // Diagnostic: check bone indices + for (int j = 0; j < 4; j++) { + uint8_t bi = src.boneIndices[j]; + uint8_t bw = src.boneWeights[j]; + if (bi >= numBones) { + outOfRangeCount++; + if (bw > 0) nonzeroWeightOOR++; + } + if (bi >= 128) ge128Count++; + } + } + if (outOfRangeCount > 0 || ge128Count > 0) { + LOG_WARNING("VERTEX DIAG: model bones=", numBones, " verts=", vertCount, + " outOfRange=", outOfRangeCount, " (nonzeroWeight=", nonzeroWeightOOR, ")", + " ge128=", ge128Count); } // Accumulate tangent/bitangent per triangle @@ -1959,6 +1977,19 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) { const auto& gsd = model.globalSequenceDurations; + // One-time diagnostic: check bone ordering (parents must precede children) + static bool checkedBoneOrder = false; + if (!checkedBoneOrder) { + checkedBoneOrder = true; + for (size_t i = 0; i < numBones; i++) { + const auto& bone = model.bones[i]; + if (bone.parentBone >= 0 && static_cast(bone.parentBone) >= i) { + LOG_WARNING("Bone ", i, " references parent ", bone.parentBone, + " which comes AFTER it — will use stale matrix!"); + } + } + } + for (size_t i = 0; i < numBones; i++) { const auto& bone = model.bones[i]; @@ -1973,6 +2004,26 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) { } else { instance.boneMatrices[i] = localTransform; } + + // Diagnostic: detect bones with extreme translation + float tx = std::abs(instance.boneMatrices[i][3][0]); + float ty = std::abs(instance.boneMatrices[i][3][1]); + float tz = std::abs(instance.boneMatrices[i][3][2]); + static int diagFrames = 0; + if (diagFrames < 3 && (tx > 50.0f || ty > 50.0f || tz > 50.0f)) { + LOG_WARNING("BONE DIAG: bone[", i, "] keyBone=", bone.keyBoneId, + " flags=0x", std::hex, bone.flags, std::dec, + " parent=", bone.parentBone, + " pivot=(", bone.pivot.x, ",", bone.pivot.y, ",", bone.pivot.z, ")", + " mat_t=(", instance.boneMatrices[i][3][0], ",", + instance.boneMatrices[i][3][1], ",", instance.boneMatrices[i][3][2], ")", + " local_t=(", localTransform[3][0], ",", localTransform[3][1], ",", + localTransform[3][2], ")", + " animTime=", instance.animationTime, + " gsTime=", instance.globalSequenceTime, + " seqIdx=", instance.currentSequenceIndex); + } + if (i == numBones - 1) diagFrames++; } } @@ -2297,8 +2348,39 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, return whiteTexture_.get(); }; - // Draw batches (submeshes) with per-batch textures + // One-time batch diagnostic for first character instance + static bool batchDiagDone = false; + if (!batchDiagDone && !instance.hasOverrideModelMatrix) { + batchDiagDone = true; + for (const auto& b : gpuModel.data.batches) { + uint16_t bm = 0, mf = 0; + if (b.materialIndex < gpuModel.data.materials.size()) { + bm = gpuModel.data.materials[b.materialIndex].blendMode; + mf = gpuModel.data.materials[b.materialIndex].flags; + } + uint16_t bg = static_cast(b.submeshId / 100); + bool active = instance.activeGeosets.empty() || + instance.activeGeosets.count(b.submeshId); + LOG_WARNING("BATCH DIAG: submesh=", b.submeshId, " group=", bg, + " blend=", bm, " matFlags=0x", std::hex, mf, std::dec, + " texIdx=", b.textureIndex, " matIdx=", b.materialIndex, + " active=", active); + } + } + + // Draw batches in two passes: opaque (blendMode 0) first, then + // alpha-key/blend after. This ensures capes and body parts write + // depth before hair overlay, preventing hair→cape z-fight. + auto getBatchBlendMode = [&](const pipeline::M2Batch& b) -> uint16_t { + if (b.materialIndex < gpuModel.data.materials.size()) + return gpuModel.data.materials[b.materialIndex].blendMode; + return 0; + }; + for (int pass = 0; pass < 2; pass++) { for (const auto& batch : gpuModel.data.batches) { + uint16_t bm = getBatchBlendMode(batch); + if (pass == 0 && bm != 0) continue; // pass 0: opaque only + if (pass == 1 && bm == 0) continue; // pass 1: non-opaque only if (applyGeosetFilter) { if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { continue; @@ -2449,7 +2531,7 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, // Create per-batch material UBO CharMaterialUBO matData{}; matData.opacity = instance.opacity; - matData.alphaTest = (blendNeedsCutout || alphaCutout) ? 1 : 0; + matData.alphaTest = blendNeedsCutout ? 1 : 0; matData.colorKeyBlack = (blendNeedsCutout || colorKeyBlack) ? 1 : 0; matData.unlit = unlit ? 1 : 0; matData.emissiveBoost = emissiveBoost; @@ -2509,6 +2591,7 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); } + } // end pass loop } else { // Draw entire model with first texture VkTexture* texPtr = !gpuModel.textureIds.empty() ? gpuModel.textureIds[0] : whiteTexture_.get(); @@ -3425,7 +3508,7 @@ void CharacterRenderer::recreatePipelines() { " pipelineLayout=", (void*)pipelineLayout_); opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); - alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); + alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false);