fix(render): stop the character alpha heuristics erasing the backdrop's buildings

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.
This commit is contained in:
Kelsi
2026-07-12 13:55:32 -07:00
parent 36d2705c2f
commit 10ebc18471
3 changed files with 31 additions and 18 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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) {