From 10ebc18471587f22ee20007d8eb2dedc2a827d81 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 12 Jul 2026 13:55:32 -0700 Subject: [PATCH] fix(render): stop the character alpha heuristics erasing the backdrop's buildings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stormwind's walls, trim and roofs are DXT5 with an unused alpha channel — mean alpha 17/255, every texel below the cutoff. The renderer infers an alpha cutout for an opaque batch whenever its texture "has alpha", a rescue for character textures, so all of Stormwind was being discarded and the sky showed through the buildings with speckle where a stray texel cleared the threshold. Mark backdrops as scene models and take them at their word: only an alpha-key material (blendMode 1) cuts out. The hair-geoset and skin-composite guesses are skipped for them too — a scene's submesh ids are all 0, which the hair guess reads as a hair geoset for every batch. --- include/rendering/character_renderer.hpp | 15 ++++++----- src/rendering/character_preview.cpp | 2 +- src/rendering/character_renderer.cpp | 32 ++++++++++++++++-------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 034c3b5e..6988664a 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -128,8 +128,8 @@ public: /** Remove all enchant visuals from the weapon at the given attachment point. */ void detachWeaponEffects(uint32_t charInstanceId, uint32_t attachmentId); - /** Exempt an instance from distance/frustum culling (scene backdrops). */ - void setInstanceIgnoreCulling(uint32_t instanceId, bool ignore); + /** Mark an instance as a scene backdrop: no culling, no character material heuristics. */ + void setInstanceSceneModel(uint32_t instanceId, bool isScene); /** Get the world-space transform of an attachment point on an instance. */ @@ -233,10 +233,13 @@ private: // needs its animation advanced even though its transform comes from the parent. bool isEffectModel = false; - // Scene backdrops (the character-select glue screens) keep their authored - // coordinates, so their origin can sit hundreds of units from the geometry - // and from the camera. Distance culling would drop them on that basis alone. - bool ignoreCulling = false; + // A scene rather than a character: the glue-screen backdrops. Two things + // follow. Their origin can sit hundreds of units from their geometry, so + // culling on it would drop them. And the material heuristics below exist to + // rescue character textures — applied to a scene they erase it, because + // Stormwind's walls are DXT5 with an unused alpha channel that the opaque + // batches must ignore, exactly as the blend mode says. + bool isSceneModel = false; // Bone update throttling for characters outside normal gameplay range. diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 53b1d912..023b8989 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -1259,7 +1259,7 @@ void CharacterPreview::loadRacialBackdrop(game::Race race) { backdropInstanceId_ = charRenderer_->createInstance(PREVIEW_BACKDROP_MODEL_ID, glm::vec3(0.0f)); if (backdropInstanceId_ == 0) return; - charRenderer_->setInstanceIgnoreCulling(backdropInstanceId_, true); + charRenderer_->setInstanceSceneModel(backdropInstanceId_, true); // loadCharacter() has already framed the camera on the character: it sits on // +Y at (0, distance, centerZ). Keep that framing distance and height, but move diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 67548368..49e0bcdc 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1876,7 +1876,7 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) { if (inst.hasOverrideModelMatrix && !inst.isEffectModel) continue; float distSq = glm::distance2(inst.position, cameraPos); - if (distSq >= animUpdateRadiusSq && !inst.ignoreCulling) continue; + if (distSq >= animUpdateRadiusSq && !inst.isSceneModel) continue; // Advance global sequence timer (accumulates independently of animation wrapping) inst.globalSequenceTime += deltaTime * 1000.0f; @@ -2416,7 +2416,7 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (!instance.visible) continue; // Character instance culling: test both distance and frustum visibility - if (!instance.hasOverrideModelMatrix && !instance.ignoreCulling) { + if (!instance.hasOverrideModelMatrix && !instance.isSceneModel) { glm::vec3 toInst = instance.position - camPos; float distSq = glm::dot(toInst, toInst); @@ -2631,8 +2631,11 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const bool hairTexture = batchUsesTextureType(gpuModel, batch, 6); const bool hairGeoset = (submeshGroup == 1) || (submeshGroup == 0 && batch.submeshId > 0 && batch.submeshId <= 99); - const bool hairMaterial = hairTexture || - (hairGeoset && (blendMode != 0 || batch.textureCount > 1)); + // Scene models have no hair, and their submesh ids are all 0, which + // would otherwise satisfy the hair-geoset guess for every batch. + const bool hairMaterial = !instance.isSceneModel && + (hairTexture || + (hairGeoset && (blendMode != 0 || batch.textureCount > 1))); // Attached weapon models can include additive FX/card batches that // appear as detached flat quads for some swords. Keep core geometry @@ -2643,7 +2646,7 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, } // For body/equipment parts with white/fallback texture, use skin (type 1) texture. - if (texPtr == whiteTexture_.get()) { + if (texPtr == whiteTexture_.get() && !instance.isSceneModel) { uint16_t group = batchGroup; bool isSkinGroup = (group == 0 || group == 3 || group == 4 || group == 5 || group == 8 || group == 9 || group == 13); @@ -2685,10 +2688,17 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, colorKeyBlack = pit->second.colorKeyBlack; } } - const bool blendNeedsCutout = (blendMode == 1) || - (blendMode == 0 && alphaCutout) || - (blendMode >= 2 && !alphaCutout) || - hairMaterial; + // A scene means what its materials say. Stormwind's walls are DXT5 with + // an unused alpha channel — every texel below the 0.5 cutoff — so + // inferring a cutout from "the texture has alpha" discards the whole + // building and leaves the sky showing through it. Only an alpha-key + // material (blendMode 1) cuts out here. + const bool blendNeedsCutout = instance.isSceneModel + ? (blendMode == 1) + : ((blendMode == 1) || + (blendMode == 0 && alphaCutout) || + (blendMode >= 2 && !alphaCutout) || + hairMaterial); // Enchant glows emit their own light; scene lighting must not tint them. const bool unlit = ((materialFlags & 0x01) != 0) || (blendMode >= 3) || instance.isEffectModel; @@ -3649,9 +3659,9 @@ bool CharacterRenderer::attachWeaponEffect(uint32_t charInstanceId, uint32_t att return true; } -void CharacterRenderer::setInstanceIgnoreCulling(uint32_t instanceId, bool ignore) { +void CharacterRenderer::setInstanceSceneModel(uint32_t instanceId, bool isScene) { auto it = instances.find(instanceId); - if (it != instances.end()) it->second.ignoreCulling = ignore; + if (it != instances.end()) it->second.isSceneModel = isScene; } void CharacterRenderer::detachWeaponEffects(uint32_t charInstanceId, uint32_t attachmentId) {