The matrix-transformed normal could be near-zero if the M2 instance
has a degenerate scale; glm::normalize then returns NaN that
contaminates the slope check (NaN < 0.35 is false → no early-out)
and bestNormalZ goes NaN, breaking the walkable-floor heuristic.
Length-check the transformed normal and fall back to the (0,0,1)
flat default — same pattern as the WMO renderer.
Same boundary-rejection pattern as createInstance. NaN in either
function would corrupt the spatial grid (stale cells pointing at
NaN-bounded instances) and the GPU model-matrix UBO.
The editor's rebuildObjects path was destroying every cached model and
re-uploading it on every (debounced) change. Added M2Renderer::clearInstances
that drops only the instance list while keeping models loaded. Editor's
clearObjects switches to clearInstances (M2) + clearInstances (WMO),
and persistent path->modelId maps survive across rebuilds. clearTerrain
fully evicts when loading a new zone.
setGhostPreview reused modelId 59999 for every preview, but loadModel
returns true without doing anything when the ID is already cached. So
selecting a new NPC kept the old ghost model in GPU memory and createInstance
used the stale model. Added M2Renderer::unloadModel public API and call it
from clearGhostPreview.
Re-allocate megaBoneSet_[0..1] in M2Renderer::clear() after vkResetDescriptorPool invalidates all
sets from boneDescPool_. Stale handles were bound to command buffers during rendering, causing
cascading validation errors. Also add ImGui::Dummy() after SetCursorScreenPos in the shaman totem
bar to satisfy ImGui's window boundary extension assertion.
M2Renderer::clear() reset descriptor pools but left the texture cache
and failed-texture tracking intact. On re-login the stale cache filled
the budget, failedTextureRetryAt_ blocked reloads, and the client
entered an infinite model-load loop. Match the cleanup already done in
shutdown() and CharacterRenderer::clear().