67 KiB
Renderer Notes
Воспроизводимый renderer baseline для текущей цели M00, regression manifest, метрики и правила сравнения описаны в docs/RENDER_BASELINE.md. Baseline фиксирует текущее поведение и известные расхождения, но не заявляет parity с WoW 3.3.5a.
Парные checkpoint PNG можно автоматически проверить через src/tools/compare_render_checkpoints.gd; reference-снимки оригинального клиента остаются вне Git, а автоматический tolerance не заменяет human fidelity approval.
Paired run 2026-07-11 подтвердил крупный coordinate/placement gap: некоторые server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC. До исправления этого расхождения perceptual metrics измеряют также несовпадение композиции, а не только материалы и свет.
Цель renderer-работы в этом проекте: добиться ощущения производительности оригинального клиента WoW 3.3.5a в Godot, без видимых фризов при переходе ADT -> ADT и без постоянного отката видимых участков к низкому качеству.
Этот документ фиксирует текущее состояние рендера, сделанные оптимизации и практические правила дальнейшей работы.
Главный принцип
Оригинальный клиент WoW выживал на слабых CPU за счет агрессивного сокращения CPU work:
- мало draw calls;
- крупные батчи terrain;
- instancing/батчинг повторяющихся props;
- ленивое и заранее подготовленное подключение деталей;
- отдельные LOD/visibility решения для terrain, M2 и WMO;
- заранее подготовленные mipmaps и компактные texture formats.
В Godot мы идем тем же путем: runtime не должен строить тяжелую геометрию, декодировать много текстур или инстанцировать большие сцены в момент, когда игрок пересекает границу ADT.
Основные файлы
src/render/world_render_facade.gd- стабильная граница runtime/tools для focus и metrics.src/scenes/streaming/streaming_world_loader.gd- внутренний runtime streamer мира.src/render/wmo/wmo_placement_resolver.gd- value-only WMO path, identity and transform rules.src/render/wmo/wmo_placement_registry.gd- WMO placement-key to tile/global reference sets; Nodes and render jobs remain in the streamer.src/render/wmo/wmo_render_build_step_planner.gd- mesh-first lightweight WMO group operation and cursor planning without Nodes or Resources.src/render/wmo/wmo_render_build_queue.gd/wmo_render_build_job.gd- typed pending group jobs, FIFO placement keys and strong root/resource references without engine destruction.src/render/wmo/wmo_render_resource_cache_state.gd- validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O.src/render/wmo/wmo_scene_resource_cache_state.gd- validated cached-WMO PackedScenes, negative cache and pending.tscnpaths without file/I/O/Node ownership.src/scenes/streaming/eastern_kingdoms_streaming.tscn- текущая сцена для проверки Azeroth/Eastern Kingdoms.addons/mpq_extractor/loaders/adt_builder.gd- сборка ADT terrain, control splat материалов и MH2O liquids.addons/mpq_extractor/loaders/m2_builder.gd- сборка статического M2 mesh/material.addons/mpq_extractor/loaders/wmo_builder.gd- сборка WMO group meshes, doodads и MLIQ liquids.addons/mpq_extractor/loaders/wow_liquid_material.gd- общий shader material для ADT/WMO жидкостей.src/scenes/sky/wow_sky_controller.gd- DBC-based outdoor lighting/sky controller.src/scenes/player/third_person_wow_controller.gd- тестовый third-person controller.src/native/src/adt_loader.cpp- ADT/MH2O parsing.src/native/src/blp_loader.cpp- BLP decoding.src/native/src/m2_loader.cpp/.h- M2/SKIN parsing.src/native/src/wmo_loader.cpp- WMO/MLIQ parsing.
Runtime Streaming
StreamingWorldLoader строит мир вокруг focus/camera/player. Карта берется из WDT, поэтому для Azeroth загружается реальный список ADT tiles, например:
StreamingWorld: loaded 687 ADT tiles from WDT for Azeroth
Важные runtime очереди:
tile- загрузка/парсинг ADT tile.terrainup- upgrade terrain до полного качества.terrainctl- подключение control splat terrain cache.terrainsplat- legacy/high quality splat path, сейчас не основной путь.detail- запуск деталей tile.m2task- background grouping M2 placements.m2mesh- загрузка/финализация M2 mesh resource.m2build- создание MultiMesh групп.wmobuild- подключение WMO instances.wmogroups- подключение render-cache групп WMO.lod+/lod-- создание/удаление tile LOD.chunk+/chunk-- legacy chunk mesh ops.
Фриз-профили пишутся строками вида:
HITCH 22.75ms refresh=false queues[...] terrainup=21.99 terrainq=0.28 m2build=0.08
Главный смысл этих логов: если hitch больше порога, смотреть самый большой named section. Мы уже находили так bottlenecks:
details=4000-5800ms- тяжелая синхронная сборка деталей.m2build=20-75ms- слишком крупные MultiMesh записи за один frame.wmobuild=300-1100ms- тяжелое инстанцирование WMO.tscn.terrainsplat=150-220ms- runtime high-quality splat generation.terrainup=20-45ms- слишком дорогой upgrade terrain за frame.
Quality Presets And Budgets
Главные настройки находятся в streaming_world_loader.gd:
quality_preset:Performance,Balanced,High,Custom.tiles_per_tickmax_concurrent_tile_taskstile_finalize_ops_per_tickterrain_upgrade_finalize_ops_per_tickterrain_control_splat_cache_finalize_ops_per_tickm2_build_groups_per_tickm2_multimesh_batch_sizem2_mesh_finalize_ops_per_tickwmo_build_instances_per_tickwmo_render_group_ops_per_tick
Практическое правило: если новая оптимизация возвращает hitch, сначала уменьшать per-frame budgets, а не отключать качество целиком. Видимое качество должно приходить заранее, вне поля зрения игрока.
Terrain Rendering
Старый подход
Изначально terrain мог быть слишком мелко разбитым:
- много chunk/node операций;
- отдельные материалы/меши;
- слабый контроль над моментом upgrade;
- низкое качество или мыло при возврате в уже посещенный участок.
Это приводило к CPU overhead и видимому popping.
Текущий подход
Основной путь качества сейчас - ControlSplatADTTile:
- geometry хранится как готовый tile mesh;
- texture set собирается в
Texture2DArray; - alpha maps собираются в один atlas;
- layer ids хранятся в layer index map;
- material подключается через
ADTBuilder.apply_control_splat_material; - runtime должен только загрузить cache resource и применить material, а не пересобирать splat texture на лету.
Кеш:
res://data/cache/terrain_control_splat_v3/<Map>/<Map>_<x>_<y>.res
Формат:
ControlSplatADTTile.FORMAT_VERSION = 3
StreamingWorldLoader.REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION = 3
Команда bake:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/build_adt_control_splat_cache.gd -- --map Azeroth --jobs auto --texture-size 256 --lod 0 --force
Важно: --force пересобирает cache. Не запускать его случайно, если задача не про bake.
Texture Quality
Что было сделано:
- перешли от baked albedo как основного visible quality к control splat;
- оставили baked/streaming terrain как fallback и warm path;
- добавили/используем full quality terrain радиус;
- добавили retention/cache для full-quality meshes, чтобы возврат в tile не сбрасывал видимый участок обратно в низкое качество;
- добавили логи
TERRAIN_QUALITY, чтобы понимать, когда видимый набор tile действительно готов.
Ключевые настройки:
terrain_full_quality_enabledterrain_full_quality_radius_tilesterrain_control_splat_quality_enabledterrain_control_splat_primaryterrain_control_splat_radius_tilesmax_active_terrain_control_splat_tilesterrain_quality_mesh_cache_limitprewarm_tile_marginretain_tile_marginfull_lod_prewarm_tilesboundary_prefetch_threshold
Правило для High: игрок не должен видеть low-res terrain под ногами или прямо перед собой. Low quality допустим только далеко или вне видимости.
Texture Seams
Была проблема видимых стыков и неправильного повторения texture pattern между чанками. Исправление в shader control splat:
- terrain texture sample теперь использует continuous
chunk_uv, а неlocal_uv; - alpha atlas sample clamp'ится внутри 64x64 chunk alpha cell;
- это уменьшает видимые seams на границах MCNK.
Файл: addons/mpq_extractor/loaders/adt_builder.gd, _get_control_splat_shader().
ADT Baking
Были ускорены bake scripts. Главный рабочий bake для качества terrain:
src/tools/build_adt_control_splat_cache.gd
Что важно:
- параллельные workers;
- cache resource на ADT tile;
- не пересобирать в runtime;
- incomplete tile считается ошибкой worker и выводится warning;
- cache лежит в
data/cacheи не должен попадать в git.
Пример успешного результата:
Starting parallel ADT control splat cache: jobs=15
Parallel control splat cache finished. jobs=15 failed_workers=0 elapsed=...
Если один worker падает:
WARNING: Control splat produced incomplete tile: ...
failed_workers=1
Такой tile надо перезапечь отдельно или проверить исходный ADT.
M2 Rendering
Цель: тысячи props не должны быть тысячами независимых Node3D/MeshInstance3D.
Текущий подход:
- ADT
MDDFplacements группируются по M2 path; - для каждого M2 path создается один или несколько
MultiMeshInstance3D; - transforms пишутся батчами через
m2_multimesh_batch_size; - mesh/resource finalization ограничен
m2_mesh_finalize_ops_per_tick; - build групп ограничен
m2_build_groups_per_tick.
Это убрало большие m2build фризы, где раньше один frame мог тратить 20-75ms только на M2.
Кеш:
res://data/cache/m2_glb
Скрипты:
src/tools/bake_m2_cache.gd
src/tools/collect_world_m2_list.gd
src/tools/m2_to_gltf.py
Что улучшали:
- M2 parser читает дополнительные material/texture/batch данные;
- BLP alpha fixes улучшили прозрачность листвы/декора;
M2Builderулучшен для texture/material setup;- runtime M2 строится через MultiMesh, а не как дерево отдельных props.
Ограничения:
- M2 animation/skinning пока не полноценные;
- particles/ribbons не реализованы;
- WoW material flags не полностью совпадают;
- billboards/leaf bones пока не как в клиенте.
M2 Animation Research
Проверка по reference/whoa, reference/open-realm и reference/wow.export показала, что анимации M2 нельзя надежно тащить как обычный Godot/glTF skeletal mesh.
Оригинальная схема M2:
- для каждой кости считается матрица через pivot transform: translation/rotation/scale вокруг
bone.pivot; - parent chain считается вручную;
- каждый skin section использует свой bone palette через
bone_combo_index; - vertex bone indices интерпретируются относительно palette текущего skin section.
Именно так делают:
reference/whoa/src/model/CM2Model.cpp- расчетm_boneMatrices;reference/whoa/src/model/CM2SceneRender.cpp- upload bone palette изboneCombos;reference/open-realm/games/world-of-warcraft/renderer/m2/r_m2.c-M2_CalculateBoneMatricesиM2_UploadBatchBones;reference/wow.export/src/js/3D/renderers/M2RendererGL.js- ручной pivot-based runtime skinning.
Вывод: старый экспериментальный путь через cached GLB ломал композитные doodad M2, потому что анимировал node с bind-pivot translation напрямую. Конвертер переведен на схему pivot node -> animated bone node, аналогичную wow.export modelsExportWithBonePrefix.
Runtime принимает для M2-анимаций только GLB с меткой:
asset.extras.openwc_m2_anim_schema = pivot_prefix_v1
Это защищает мир от старых битых GLB cache. Точечно пересобраны тестовые animated M2:
data/cache/m2_glb/GryphonRoost01.glb
data/cache/m2_glb/Fish.glb
data/cache/m2_glb/eagle.glb
Ограничение runtime:
enable_m2_animated_instances = true
m2_animated_max_primitives = 3
m2_animated_denylist_patterns = ["gryphonroost"]
m2_animated_allowlist_patterns = ["creature/fish/", "creature/eagle/", "world/critter/"]
GryphonRoost stays out of the animated GLB path. The cached GLB has Stand
tracks, but the current converter/runtime breaks its composite skin/material layout.
Use the native M2 runtime path for this class of doodad; the old GLB animation
path remains disabled for gryphonroost.
Native M2 animation first pass for composite doodads:
M2Loader.load_m2_animated()parses M2 bones, Stand track keys,.skinbone lookup/palette data, and expanded skinned surfaces.M2NativeAnimatedBuilderbuilds a regularArrayMeshand keeps the original per-surface skin data.M2NativeAnimatorevaluates the selected Stand sequence and applies WoW-style bone matrices:T(pivot + translation) * R * S * T(-pivot) * parent.- Vertex influences are resolved through
.skinlocal bone indices and the M2boneCombospalette, matching the original section/batch renderer model used by WoW/whoa. StreamingWorldLoaderroutes onlygryphonroostthrough this native path; simple critters still use the existing GLB allowlist and most M2 world props stay on static MultiMesh.- Current implementation rebuilds the animated
ArrayMeshon CPU. This is correct enough for the problematic rare doodads and gives us the same data layout that can later move to shader/GPU skinning.
Причина: композитные doodad вроде GryphonRoost снова ломают визуал при GLB-анимации. До M2-native renderer все world placement M2 должны оставаться статическими. GLB-анимация оставлена только как вручную включаемый debug experiment через allowlist.
Дополнительно static M2 cache lookup ищет both original-case and lowercase paths. Это важно для ADT paths вроде world/GENERIC/..., потому что запеченный nested cache лежит lowercase. Static path также не использует GLB с pivot_prefix_v1, чтобы animated test cache не мог подменить корректный .tscn.
Полноценный M2-native animation renderer/cache все еще остается правильным долгосрочным решением для 1:1 поведения, particles/ribbons и производительности на большом количестве animated props.
Blizzlike Shader Research
Идея для следующего visual fidelity слоя: уходить от Godot StandardMaterial3D для WoW assets к собственным WoW-style ShaderMaterial.
Что видно по whoa:
- M2 использует маленькие shader permutations вроде
Diffuse_T1,Diffuse_T2,Diffuse_Env,Combiners_Mod,Combiners_Add,Combiners_Mod2x; - material state задается отдельно: blend mode, alpha ref, culling, depth test/write;
- light model старый и ручной: ambient + diffuse + emissive + fog constants, а не PBR;
- texture stages учитывают wrap/clamp, texture transform matrices и env/sphere map.
Практический вывод для проекта:
- terrain уже ближе к WoW, потому что использует
ShaderMaterialи ручной light/fog; - M2/WMO пока используют
StandardMaterial3D, поэтому выглядят не совсем blizzlike; - следующий шаг -
wow_m2_material/wow_wmo_materialс WoW blend modes, alpha ref, combiners и ручным fog/light.
M2 ShaderMaterial First Pass
Implemented file: addons/mpq_extractor/loaders/wow_m2_material.gd.
M2Builder._build_material() now returns a WoW-style ShaderMaterial instead of StandardMaterial3D. The same path is reused by M2NativeAnimatedBuilder, so static M2 batches and the native animated gryphonroost path share material behavior.
Current M2 shader layer:
- maps M2 blend modes 0..6 to Godot shader variants: opaque, alpha-key, alpha blend, additive, mod and mod2x approximation;
- uses alpha reference values observed in
reference/whoa/src/model/CM2SceneRender.cpp: 0 for opaque, about 0.878 for alpha-key, 1/255 for blended passes; - applies M2 render flags for unlit (
0x01), two-sided/culling (0x04), depth test (0x08) and depth write (0x10); - keeps BLP mipmap generation and anisotropic sampler hints;
- respects M2 texture wrap flags by compiling repeat/clamp shader variants;
M2Loaderexposes UV2,texture_coord_combos,texture_combiner_combos,m2_flags, batchshader_id,texture_countand texture-coordinate combo indices;M2Loaderalso exposesm2_colors,texture_weights,texture_transforms,texture_weight_combosandtexture_transform_combos;M2Buildernow resolves the first two texture stages from texture combos and passes combiner state intoWowM2Material;WowM2Materialhas a first-pass implementation for combiner ops: opaque, mod, decal/fade, add, mod2x, mod2xNA/addNA and a cheap env-map UV approximation;- static M2 color/alpha tracks and texture weight tracks are applied to
mesh_color, matching thewhoarender path where batch alpha is multiplied by color alpha and texture weight; - static texture transform translation/scale is applied per texture stage through shader UV transform uniforms;
- animated texture transform translation tracks are approximated as GPU UV scroll (
translation_speed * TIME) per texture stage, without per-frame CPU material updates; - native animated M2 rebuild path preserves UV2 and the same material combiner state.
- uses a simple old-client-style ambient + diffuse light model and disables PBR/specular behavior.
Known limits:
- only the first two texture stages are represented;
- texture transform translation animation is supported as linear UV scroll; rotation/scale texture animation and time-varying color/weight tracks are still future work;
- exact env/sphere map math, model emissive tracks and full WoW render-state parity are still future work;
- WMO has a matching first-pass shader layer, but still needs full material flag parity.
WMO ShaderMaterial First Pass
Implemented file: addons/mpq_extractor/loaders/wow_wmo_material.gd.
WMOBuilder._build_material() now returns a WoW-style ShaderMaterial instead of StandardMaterial3D. WMO group meshes already carry Mesh.ARRAY_COLOR from group vertex colors, so the shader treats WMO as mostly baked-lit geometry:
- blend modes 0..2 map to opaque, alpha-key and alpha blend variants;
- alpha-key uses discard instead of transparent sorting;
- texture repeat stays enabled for WMO wall/floor tiling;
- BLP mipmap generation and anisotropic sampler hints are preserved;
- shader is
unshadedand multiplies texture color by WMO vertex color with a small ambient floor; - DBC zone light is applied as a soft tint only, so baked WMO vertex colors and texture albedo stay readable;
color2is passed as a secondary/detail tint, andtexture2is used as a fallback detail texture whentexture1is absent;WMOLoaderexposes MOMTdiffuse_color,emissive_color,texture1,texture2,flags2andcolor2;WMOBuilderloadstexture1as a conservative secondary detail/multiply layer and passes MOMT diffuse/emissive colors into the shader;- culling is disabled like the previous WMO material path, which avoids missing interior/backface geometry.
Known limits:
- deeper MOMT shader semantics, exact secondary texture mode and
texture2usage are still approximate; - WMO fog/light constants are not wired into this material yet;
- this first pass prioritizes matching old-client baked lighting behavior over physical lighting.
WMO Rendering
Цель: WMO не должны фризить сцену при догрузке, особенно большие здания/города.
Что сделано:
WMOLoaderчитает root/group WMO данные и MLIQ;WMOBuilderстроит group meshes;- runtime registry дедуплицирует WMO по
unique_id; - WMO на границе ADT не дублируются;
WmoPlacementRegistryowns only placement reference sets; the streamer keeps the parallel key-to-Node map and performs final-release cancellation/free;WmoRenderBuildStepPlannerselects one mesh or MultiMesh group and its next cursors; the streamer still materializes Nodes and consumes the frame permit;WmoRenderBuildQueueowns pending typed jobs/FIFO keys and releases references on cancel/reset; the streamer still validates and frees engine objects;- большие WMO не должны инстанцироваться как тяжелые
.tscnво время движения; - добавлен lightweight render-cache path для WMO groups;
wmo_render_group_ops_per_tickограничивает подключение WMO groups по кадрам;wmo_max_runtime_scene_mbзащищает от тяжелого runtime.tscninstantiate.
Кеши:
res://data/cache/wmo_tscn
res://data/cache/wmo_render_v1
Скрипты:
src/tools/bake_wmo_cache.gd
src/tools/build_wmo_streaming_cache.gd
Что улучшали по качеству:
- включен texture repeat для WMO materials;
- улучшены BLP alpha/texture decode проблемы;
- WMO liquid mesh получает нормали/UV;
- WMO liquids используют общий
WowLiquidMaterial.
Ограничения:
- portal/room based visibility WMO пока не повторяет клиент;
- occluders не являются полноценной заменой WoW portal culling;
- часть WMO material flags еще требует сверки с клиентом.
Liquids
Open-realm не дал готовую реализацию water shader: в его docs liquid rendering отмечен как future pass. Поэтому добавлен собственный легкий Godot shader.
Файл:
addons/mpq_extractor/loaders/wow_liquid_material.gd
Подключение:
- ADT
MH2OчерезADTBuilder.build_tile_water_scene; - WMO
MLIQчерезWMOBuilder._build_group_liquid. - Для cached terrain tiles (
streaming_tile/baked_tile) вода не хранится в terrain cache:StreamingWorldLoaderзапускает отдельную background ADT load task только дляMH2O, затем на main thread прикрепляетWaternode к уже созданному tile root.
Что делает shader:
- один общий
ShaderMaterialcache для ADT/WMO; - world-space animated ripples;
- fresnel highlight;
- отдельные цвета/параметры для water, magma, slime;
- без screen texture/refraction, чтобы не раздувать GPU cost;
- shadows у liquid mesh выключены.
Почему так:
- дешево;
- не грузит дополнительные textures;
- не создает D3D12 descriptor pressure;
- дает рабочую визуальную воду уже сейчас.
Ограничения:
- нет настоящего depth fade;
- нет shoreline foam/blending;
- LiquidType.dbc texture selection пока не используется;
- waterfalls/flowing water требуют отдельного M2/material path.
Shared WoW Shader Globals
Current terrain/M2/WMO/liquid shaders are intentionally unshaded: Godot lighting is not a good match for old WoW material rules, M2 blend modes, WMO baked vertex colors and control-splat terrain. To keep them visually tied to the active zone without updating every material instance, WowSkyController owns a shared global shader state:
wow_ambient_color
wow_light_color
wow_light_dir
wow_fog_color
wow_fog_range
wow_fog_density
wow_sun_elevation
These parameters are registered through RenderingServer.global_shader_parameter_add and updated from Light.dbc / LightParams.dbc sampling in src/scenes/sky/wow_sky_controller.gd.
Consumers:
addons/mpq_extractor/loaders/adt_builder.gd: control-splat, fallback layered terrain and single-texture terrain.addons/mpq_extractor/loaders/wow_m2_material.gd: M2 combiner material uses zone ambient/diffuse light and distance fog.addons/mpq_extractor/loaders/wow_wmo_material.gd: WMO material keeps baked vertex lighting, but tints it by zone light and applies distance fog.addons/mpq_extractor/loaders/wow_liquid_material.gd: liquids use the shared fog color/range/density.
The fog distance is computed from view-space position passed from vertex() to fragment(). This avoids per-camera material updates and keeps shader code compatible with Godot 4.6's spatial shader parser.
Sky And Lighting
Добавлен WowSkyController:
src/scenes/sky/wow_sky_controller.gd
Что он делает:
- читает DBC данные для light profiles;
- определяет текущую area/zone по позиции;
- выбирает lighting/fog profile;
- логирует
SKY_LIGHT; - поддерживает LightSkybox model loading;
- двигает skybox model за камерой/player.
Пример лога:
SKY_LIGHT time=13.00 map=0 area=279:... zone=36:... profiles=[...] skybox=0 fog=614..31556 density=0.500
SKYBOX_MODEL id=... path=...
Ограничения:
- зона/skybox transitions еще требуют ручной сверки с клиентом;
- не все outdoor skybox models используются на всех локациях;
- lighting пока приближение, а не 1:1 с fixed-function клиентом.
Shadows
Цель по производительности сейчас важнее полного совпадения dynamic shadows.
Текущее правило:
- terrain shadows выключены;
- M2 shadows выключены;
- WMO shadows выключены;
- liquid shadows выключены.
Настройки:
terrain_cast_shadows
m2_cast_shadows
wmo_cast_shadows
Почему:
- old WoW 3.3.5a использовал очень дешевую модель теней;
- полноценные Godot realtime shadows на большом мире быстро создают лишний GPU/CPU cost;
- сначала нужна стабильная streaming performance, затем можно добавлять selective blob/projected shadows.
Для будущего: если брать идеи Cataclysm, делать не full dynamic shadows everywhere, а ограниченные character/near-object shadows с жестким distance budget.
Third Person Controller
Добавлен тестовый контроллер для оценки рендера "с земли":
src/scenes/player/third_person_wow_controller.gd
Зачем:
- свободная камера плохо показывает реальные проблемы игрока;
- нужен WoW-like темп перемещения;
- sprint нужен для стресс-теста ADT boundary streaming.
Модель сейчас не важна, используется простая capsule.
Texture/BLP Work
Что сделано:
- BLP loader улучшен для alpha handling;
- texture mipmaps генерируются для runtime ImageTexture там, где texture создается из decoded BLP;
- для WMO/M2 материалов включается anisotropic/mipmap filtering там, где texture подключается;
- включен repeat для WMO материалов.
Важная проблема Godot/D3D12:
Cannot create uniform set because there's not enough room in the RESOURCES descriptor heap.
Please increase rendering/rendering_device/d3d12/max_resource_descriptors.
Это возникает, когда renderer создает слишком много уникальных texture/uniform resources. Наши меры:
- кешировать materials/textures;
- не создавать уникальный heavy shader/material на каждый chunk;
- использовать
Texture2DArrayдля terrain control splat; - ограничивать активные high-quality terrain tiles;
- избегать screen/refraction textures для water.
Если ошибка возвращается, увеличивать project setting rendering/rendering_device/d3d12/max_resource_descriptors и одновременно искать, где плодятся уникальные материалы. В project.godot это должно быть записано внутри секции [rendering] как rendering_device/d3d12/max_resource_descriptors; вариант с повторным префиксом rendering/rendering_device/... внутри секции игнорируется.
Caches
Активные cache directories:
res://data/cache/baked_terrain_v2
res://data/cache/baked_terrain_stream_v1
res://data/cache/terrain_control_splat_v3
res://data/cache/terrain_splat_v1
res://data/cache/m2_glb
res://data/cache/wmo_tscn
res://data/cache/wmo_render_v1
Правила:
- cache не коммитить;
- при изменении resource format bump'ать
FORMAT_VERSION; - при изменении shader/material без изменения cached resource иногда достаточно restart scene;
- при изменении baked geometry/resource payload нужен rebake;
- WMO render cache нужен для больших WMO, иначе будут
wmobuildфризы.
Версии:
Baked terrain required: 5
Streaming terrain required: 2
Splat terrain required: 1
Control splat required: 3
WMO streaming resource: 2
Debug Logs
Важные логи:
HITCH ...
PERF ...
TERRAIN_QUALITY ...
SKY_LIGHT ...
SKYBOX_MODEL ...
Как читать HITCH:
- если большой
terrainup- full-quality terrain подключается слишком тяжело; - если большой
terrainctl- control splat cache финализация слишком дорогая; - если большой
terrainsplat- runtime splat path не должен быть включен для обычной игры; - если большой
m2mesh- mesh resource finalization надо сильнее дробить; - если большой
m2build- уменьшитьm2_build_groups_per_tickилиm2_multimesh_batch_size; - если большой
wmobuild- использовать render cache, уменьшитьwmo_build_instances_per_tick; - если большой
wmogroups- уменьшитьwmo_render_group_ops_per_tick.
Reference Findings
По open-realm:
- полезен как документация форматов/пайплайна;
- terrain/ADT/WMO/M2 идеи можно сверять;
- полноценный liquid rendering там не реализован;
- skybox/liquid не стоит напрямую переносить как готовый код.
По WoW 3.3.5a:
- старый клиент не рендерил все как modern physically based renderer;
- он агрессивно экономил CPU;
- основная цель для нас - frame pacing и отсутствие hitch, а не дорогие эффекты.
Commands
Headless smoke:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --quit
Renderer material regression smoke:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/verify_render_materials.gd
ADT M2 placement probe:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/verify_adt_m2_placements.gd -- --map Azeroth --uid 11785 --path-contains waterfall
M2 uniqueId dedupe smoke:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/verify_m2_unique_dedupe.gd
Renderer checkpoint capture:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --path . --script res://src/tools/capture_render_checkpoints.gd -- --wait 8 --output user://render_checkpoints
Headless checkpoint dry-run:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/capture_render_checkpoints.gd -- --dry-run --wait 0.1
Run scene headless:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . src/scenes/streaming/eastern_kingdoms_streaming.tscn --quit
Build native GDExtension:
cd src\native
.\build.bat Release
Build ADT control splat cache:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/build_adt_control_splat_cache.gd -- --map Azeroth --jobs auto --texture-size 256 --lod 0 --force
Build WMO render cache:
$exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
& $exe --headless --path . --script res://src/tools/build_wmo_streaming_cache.gd -- --map Azeroth
Current Known Gaps
- M2 animation/skinning.
- M2 particles/ribbons.
- Full WoW material flag parity. Current first pass covers common M2 combiners and WMO shader ids 0, 3, 5, 6, 8, 9, 12, 13, 15, 16, 17 approximately.
- True WMO portal/room culling.
- Selective cheap dynamic shadows.
- Liquid depth fade/shore blending.
- LiquidType.dbc based water textures.
- Better skybox transition validation per zone.
- Automated renderer screenshot scenes. A cheap material regression smoke exists at
src/tools/verify_render_materials.gd.
2026-07-04 WMO Shader Pass
- WMO materials now consume shared
wow_light_dirandwow_sun_elevationshader globals. - Cached WMO meshes are refreshed at attach time, so already baked render-cache meshes can receive the latest WMO material shader without immediately rebuilding the whole WMO cache.
- The WMO shader now uses normals for a cheap WoW-style ambient + directional lighting term instead of relying only on texture * vertex color.
- Added approximate pixel shader branches for common WMO modes: diffuse, env, env metal, two-layer diffuse, terrain-style two-layer, diffuse emissive and mod2x variants.
- Still missing for true parity: second/third WMO vertex color streams, extra WMO texture coordinate sets, third+ texture samplers, exact env-map coordinate generation and portal/room visibility.
2026-07-04 M2 Shader Mapping Pass
- M2 material build now maps raw skin
shaderIDthrough the same vertex/pixel shader selection rules used bywow.export. WowM2Materialreceivesvertex_shader_idandpixel_shader_iduniforms instead of relying only on generic two-stage ops.- Vertex shader ids 0..18 now drive UV source selection, UV2 use, environment coordinates and edge-fade modes.
- Pixel shader ids 0..29 have first-pass two-texture combiner behavior for the common opaque/mod/add/mod2x/alpha cases.
- M2 materials now load and bind up to four texture combo entries. This enables first-pass support for glow/specular/crossfade/guild/const combiner cases that need
tex3ortex4. - Texture weight combos are exposed to the shader as
tex_sample_alpha, matching the common WoW combiner weight slots. - Native animated M2 surfaces use the same
_build_material()path, so this also applies to runtime-animated M2 objects. - Debug metadata is stored on materials: raw
wow_m2_shader_id, mappedwow_m2_vertex_shader_id, and mappedwow_m2_pixel_shader_id. - Transparent M2 passes such as waterfalls now avoid depth writes/prepass (
depth_draw_never). This prevents layered alpha planes from self-occluding into white/solid webbing while keeping cutout foliage on the opaque-depth path. - Scrolling alpha-blend M2 models with
waterfallin the path get a conservative soft-alpha profile: lower opacity, stronger alpha falloff and UV-edge fade. This hides the rectangular card edges without changing normal foliage/cutout props. .skintexture unitpriorityis exposed aspriority_planefor future transparent-pass ordering. This needs the native GDExtension DLL to be rebuilt when the editor is not holding it open.- Waterfalls seen near terrain cliffs are usually WMO embedded doodads rather than ADT
MDDFplacements. WMO doodad transforms now use the converted MODD quaternion directly, without the old extra+90 degM2 yaw offset. - WMO render/scene caches now carry version guards for this transform path. Rebuild
wmo_render_v1/wmo_tscnafter changing WMO doodad placement code; old caches are ignored by runtime. - Elwynn/Thunder Falls style outdoor waterfalls are ADT
MDDFM2 placements. Regular ADT M2 doodads use the native cached Godot-space Euler directly viaBasis.from_euler(rot); do not apply a global yaw/order remap, because it flips ordinary props such as wagons, stalls, barrels, fences and trees.ElwynnCliffRock01/02are a model-specific exception: these are open-backed shell meshes, so they use a narrowYZXplacement correction withrot.y + 180 degto push the hollow side into the terrain. The waterfall transform correction is also filename-allowlisted to the actual sheet M2s (NewWaterfall,ElwynnTallWaterfall01). Sheet M2s keep the calibrated world-space+90 degGodot-Y yaw to anchor the top to the upper pool, plus model-specific twist where needed. Tall waterfall sheets twist around their local fall line with an origin offset so top/bottom anchors stay fixed instead of rotating around the model pivot;ElwynnTallWaterfall01uses the opposite twist sign fromNewWaterfallto face the correct side. Do not apply the raw-M2/WoWee render-space basis here; that double-converts M2 vertices. Do not cull boundary waterfall placements byfloor(world_pos / TILE_SIZE): tiles can carry a small cross-border placement that is still the active streamed instance at low M2 radius. Cross-tile dedupe usesMDDF.uniqueId, not geometry ownership. - Still missing for true parity: exact shader array entries beyond the common WotLK set, particles/ribbons, full edge-scan tuning, exact guild/color constants and specialized blend-state edge cases.
2026-07-07 World Shader Light Stabilization
- DBC sky colors remain the source for
Environmentand sky rendering, but material shader globals now receive desaturated/neutralized ambient, diffuse and fog colors. This prevents Elwynn daylight profiles from tinting all ADT/M2/WMO albedo cyan or green. - ADT terrain shaders now transform normals with
MODEL_NORMAL_MATRIXbefore comparing them against the world-spacewow_light_dir. Using fragmentNORMALdirectly made terrain lighting depend on the local surface basis and produced blue/green patches on slopes and roads. - ADT, M2 and WMO material fog clamps
wow_fog_densitybefore mixing intoALBEDO; fog should add distance haze, not repaint nearby assets. - M2/WMO shaders now reduce zone-color strength and clamp final albedo/specular/emissive contribution. Additive/parity edge cases still need exact WoW pass ordering later, but ordinary Elwynn props should no longer produce overexposed or acid-green surfaces.
- Human building WMO materials such as
HumanTwoStoryandGoldshireInncarry strong paintedMOMT.diffColorvalues, including green trim colors. Textured WMO surfaces now use this as a soft hue tint instead of multiplying texture albedo by the raw RGB value; untextured WMO surfaces still use a muted material paint color. - WMO window materials such as
MM_ELWYNN_WND_EXT__01.BLPcarry highMOMT.sidnColor/emissive values. Emissive is texture-masked and capped for window paths so it brightens the pane pattern instead of flooding the whole quad white. - WMO window atlas textures use a stable albedo sampler (
filter_linear,repeat_disable) instead of generated-mipmap anisotropic sampling. These textures pack several window variants into one BLP; generated mipmaps can bleed between atlas cells and make the upper-house window shimmer/change along diagonal texture features while the camera moves. - WMO window atlas materials use
cull_backeven when they are opaqueblendMode == 0. The human-house exterior window texture is a real WMO material (MM_ELWYNN_WND_EXT__01.BLP,flags=0x11), not a transparent glass pass, and two-sided rendering can make nearby interior/exterior pane faces compete as the view angle changes. - WMO material flag
0x01is treated as unlit in the WMO shader. Elwynn window materials use this flag together with emissive color, so they should not receive ordinary per-normal directional lighting across their triangulated panes. - WMO alpha discard is now restricted to
blendMode == 1alpha-key materials.blendMode == 2is true alpha blend; treating it as alpha-key caused windows/glass and other translucent WMO surfaces to become cut-out. - WMO window/glass texture paths (
/windows/,/glass/,window,glass, exact_wnd_in the file name) are metadata only for opaque materials. Do not use a broadwnd_substring: Stormwind texture names such asMM_STRMWND_WALL_03.BLPare ordinary wall textures, not windows. Window metadata does not make an opaque WMO material transparent and does not apply glass tint, because many house facades use window-named atlas textures for walls and trim too. The separate glass shader variant and glass tint are only used when the WMO material is alsoblendMode == 2. In the Godot path, glass and other alpha-blend WMO surfaces avoid depth writes; depth-writing glass caused moving-camera flicker/z-fighting on facade window overlays. Glass WMO surfaces also use back-face culling instead ofcull_disabled, because two-sided transparent facade panes can show the quad's internal triangle split as a moving diagonal seam. - Runtime WMO cache material refresh is versioned separately from WMO geometry cache. Shader-only WMO material fixes should bump
WMO_MATERIAL_REFRESH_VERSIONinstead of forcing a fullwmo_render_v1rebake.
2026-07-07 M2 Billboard Pass
- Native
M2Loadernow readsM2CompBone.flagsfor billboard bits0x8/0x10/0x20/0x40on the static path, not only on the animated path. - Billboard metadata is resolved per drawn vertex through the skin section bone palette and vertex bone indices. This avoids treating an entire mixed surface as billboard just because the palette contains a billboard bone.
M2Builderstores billboard pivot/mode inMesh.ARRAY_CUSTOM0as RGBA float data.CUSTOM0.xyzis the Godot-space bone pivot;CUSTOM0.wis the billboard mode: spherical, lock X, lock Y, or lock Z.WowM2Materialapplies camera-facing billboarding in the vertex shader for marked vertices. This works with grouped ADT M2MultiMeshbatches because the shader uses each instance transform.- Runtime M2 cache refresh now has
M2_MATERIAL_REFRESH_VERSION. Old cached.tscn/.glbmeshes are replaced in memory from raw.m2only when the raw model actually needs custom billboard attributes or UV-rotation data. Do not rebuild every stale cached M2 during streaming: creating fresh materials for ordinary props can exhaust the D3D12 resource descriptor heap. - Verified with
ElwynnGrass1.m2: native data exposes a billboard batch, the built mesh containsCUSTOM0, and the M2 shader compiles with the billboard uniform enabled. - Remaining parity work: many Elwynn tree canopy M2s do not carry billboard bone flags, so their flat-looking leaves are a separate material/geometry parity issue, not this bone-billboard path.
2026-07-07 M2 UV Transform Pass
- Native
M2Loadernow decodesM2TextureTransform.rotationTrackas raw texture-space quaternions, separate from Godot-space bone quaternions. - Texture transform dictionaries now expose static UV rotation matrices and approximate Z-rotation speeds, in addition to existing translation, translation speed and scale.
WowM2Materialapplies UV rotation and scale around WoW's texture center(0.5, 0.5), then applies translation/scroll. This is closer to the client path shown byCM2Model::AnimateTextureTransformsMT.- All four M2 texture stages receive UV rotation and rotation-speed uniforms, matching the existing four-stage texture binding path.
M2_MATERIAL_REFRESH_VERSIONandM2Builder.MATERIAL_FORMAT_VERSIONare bumped to2, so cached M2 meshes are rebuilt in memory when raw.m2data is available.- Verified with
Spells/ArcaneForceShield_Blue.m2: native data exposes a non-identity UV rotation, the built material receives the stage rotation uniform, and the shader compiles.
2026-07-08 Renderer Material Regression Smoke
- Added
src/tools/verify_render_materials.gdas a cheap headless guard for shader/material regressions. - The script checks the D3D12 descriptor heap project setting, opaque WMO window atlas handling, ordinary Stormwind wall detection, WMO glass depth/culling state, M2 alpha depth state, and M2 billboard/UV-rotation shader paths.
- This is not a visual screenshot regression suite; it catches the material-state mistakes that caused the waterfall/window fixes to regress.
2026-07-08 ADT M2 UniqueId Probe
- Native
ADTLoadernow exposesMDDF.uniqueIdasunique_idon M2 placement dictionaries, matching the existing WMOMODF.unique_idfield. - Added
src/tools/verify_adt_m2_placements.gdto find raw ADT M2 placements byunique_idand path substring. The default check targets the Elwynn waterfall placementuid=11785. - Runtime M2 cross-tile dedupe is enabled through an active
MDDF.uniqueIdregistry. It filters duplicate placements before the M2 grouping worker runs, then releases the reservation when the owning tile unloads or leaves the M2 detail radius. - Candidate tiles that skipped a duplicate uid are re-queued when the active owner releases it. This avoids the old
floor(world_pos / TILE_SIZE)ownership trap where boundary props could disappear. BakedADTTile.FORMAT_VERSIONis now5andStreamingADTTile.FORMAT_VERSIONis now2, so old ADT caches without M2unique_idare ignored until rebaked.- Added
src/tools/verify_m2_unique_dedupe.gdas a headless smoke for reserve, skip and release behavior.
2026-07-08 Renderer Checkpoint Capture
- Added
src/tools/find_adt_placements.gdas a diagnostic placement search helper for M2/WMO paths. - Added
src/tools/capture_render_checkpoints.gdto load the Eastern Kingdoms streaming scene, move a dedicated camera to fixed regression viewpoints and save PNGs underuser://render_checkpoints. - Current checkpoints cover the Elwynn waterfall from front/side and Goldshire Inn/Blacksmith windows. Headless runs are dry-run only because the headless display server cannot produce viewport PNGs.
2026-07-13 Renderer Shutdown Ownership
StreamingWorldLoaderdrains its asynchronous work before teardown, leaves parented streamed nodes to SceneTree ownership, then explicitly frees detached M2/WMO prototype nodes and clears resource caches.ADTBuilderexplicitly frees a temporaryWaterroot when an ADT liquid payload produces no child geometry. Dry tiles must not leave empty nodes in ObjectDB.src/tools/verify_render_runtime_cache_shutdown.gdregression-checks detached prototype and resource-cache release plus empty-liquid-root ownership. The unified render-baseline runner executes it before capture.- A focused 8-second GUI checkpoint and a seven-checkpoint single-pass GUI run finish without Node, renderer resource or RID leak diagnostics. A timing-dependent anonymous zero-reference
RefCountedObjectDB warning can remain at engine teardown; it is tracked separately and is not suppressed. - This is shutdown-only ownership behavior and does not alter checkpoint rendering, cache formats or the
Blizzlike335compatibility profile.
2026-07-13 Explicit Streaming Focus
StreamingWorldLoaderconsumes an immutableStreamingFocuscontaining a typedGodotWorldPosition; runtime streaming no longer discovers the activeCamera3Dfrom the viewport or child nodes.- Eastern Kingdoms and Kalimdor runtime scenes explicitly use the player as the streaming source. Checkpoint capture and terrain/occluder probes explicitly use their dedicated camera through the same public refresh path.
- The editor viewport camera remains behind a dedicated editor-only adapter.
camera_pathis retained solely for optional automatic overview positioning. - Streaming radii, LOD decisions, cache formats and renderer quality profiles are unchanged by this migration.
2026-07-13 Coordinate Conversion Boundary Gate
- Sky/area lookup, streaming tile ownership, player spawn/terrain sampling and
terrain diagnostics now cross world-coordinate spaces through typed
CoordinateMappermethods. - Renderer distance, LOD, ray and model-local M2/WMO calculations remain in their existing Godot/local spaces; they are not axis conversions.
- The repository gate rejects new manual world-center, WoW/Godot and Godot-position-to-ADT formulas. The unified renderer baseline runs this gate.
- Native ADT/WDT parsers keep one shared world conversion in
wow_chunk_reader.h; the independent M00 calibration formula remains a test oracle. - Coordinate contract version 2, renderer radii, cache formats and visual quality profiles are unchanged.
2026-07-13 Golden Server Spawn Renderer Evidence
- The pinned AzerothCore Human Warrior start at server position
(-8949.95, -132.493, 83.5312)maps throughCoordinateMapperto Godot(17199.159666667, 83.5312, 26016.616666667), ADT(32,48)and MCNK(3,12). server_spawn_render_manifest.jsondrives the ordinary Eastern Kingdoms checkpoint capture pipeline at that location; no live database or production spawn entity is introduced.- A renderer-native emissive marker starts at the exact mapped origin and keeps depth testing enabled so GUI evidence exposes its relationship to terrain.
verify_server_spawn_renderer.gdrejects drift between the pinned fixture, manifest target/player coordinates and expected ADT ownership. The unified renderer baseline runs this contract check before capture.- This closes a coordinate/renderer integration criterion only. It does not claim TrinityCore row equivalence or visual parity with the original client.
2026-07-15 WorldRenderFacade First Seam
- Runtime streaming scenes, checkpoint capture and renderer probes now submit
typed focus through
WorldRenderFacade; those callers no longer call the monolithic streamer's focus methods or configure its focus source directly. - Baseline capture reads queue/cache/activity counts through
renderer_metrics_snapshot(). The facade returns a deep copy, so diagnostics cannot mutate renderer-owned state. StreamingWorldLoaderremains the internal implementation and still owns all queues, tasks, caches, nodes and RIDs. The facade adds no parallel state or replacement scheduler.- This package does not alter targets, budgets, cache versions, placement, visibility, materials or animation. Environment, entity visuals and ground query remain explicit follow-up facade contracts in M03.
2026-07-15 StreamingTargetPlanner Extraction
- Runtime and editor wanted/retained ADT set calculation now lives in the
scene-free
StreamingTargetPlanner. StreamingTargetPolicypreserves existing chunk/tile radii, warm/prewarm/ retain margins and the clamped boundary-prefetch threshold.StreamingTargetPlanfreezes its focus/wanted/retained result; the streamer still owns available paths, load-queue distance sorting, loaded-tile LOD/detail state, async work, caches, nodes and RIDs.- Asset-free fixtures cover center, edge/corner prefetch, catalog filtering, clamp and editor behavior plus bounded High-like iteration timing.
- Cache versions, quality profiles, queue priority and visible rendering rules are unchanged by the extraction.
2026-07-16 RenderBudgetScheduler Extraction
- The scene-free
RenderBudgetSchedulernow owns the per-frame counters for 16 named renderer operation lanes. It owns no queue, task, cache, Node or RID. StreamingWorldLoadersnapshots its existing exported limits once per process frame and asks for a boolean permit immediately before each historically budgeted operation.- Chunk removals and creations intentionally share one
chunk_geometrylane; the existing removal-first drain order and combined limit are preserved. - Loader teardown cancels permit issuance before waiting for asynchronous work. Existing worker/result staleness checks and resource cleanup remain unchanged.
- Asset-free contracts cover exact bounds, independent/shared lanes, frame reset, invalid inputs, terminal cancellation, detached diagnostics and hot-path timing.
- Defaults, quality presets, queue order, cache versions and visible rendering rules are unchanged. Asset-backed p95/p99 comparison remains required.
2026-07-16 Streamer Internal Access Gate
verify_renderer_internal_access.gdderives private queue, task, cache and tile-state field names fromStreamingWorldLoaderdeclarations and rejects external member/reflection access in gameplay and GodotEditorPluginpackage sources.- The gate follows the implementation inventory instead of duplicating a manual list, so a newly named private renderer field is covered automatically when it matches those ownership categories.
- Scene composition may still reference the loader script and the facade's implementation path; those are composition details, not mutable queue access.
- Renderer diagnostic probes are added explicitly as their facade contracts land.
- This is a source-only boundary check and changes no renderer behavior or fidelity.
2026-07-16 Rendered Ground Query Facade
WorldRenderFacade.sample_ground_height()acceptsGodotWorldPositionand returns renderer-owned immutableRenderedGroundSamplefrom already loaded quality/tile-LOD render meshes, avoiding a render-to-gameplay dependency.- Its factories load the predeclared Script path instead of relying on a warm global-class cache; a dedicated cold-start verifier covers this bootstrap path.
renderer_ground_query_snapshot()preserves the terrain probe's tile readiness, queue position, mesh bounds and nearby seam-fallback diagnostics as a detached Dictionary; callers receive no queue, tile-state, Mesh or Node reference.probe_render_terrain_height.gdnow uses this facade snapshot and no longer reads_tile_states,_tile_load_queue,_tile_loading_tasksor_available_tiles.- Gameplay continues using the independently composed M02
TerrainQuery/AdtTerrainQuery. Loaded render meshes are diagnostic and are not authoritative movement collision. - The ray height, 3x3 tile search and
2/5/10/20/40-unit nearby fallback are preserved. No terrain geometry, cache, streaming or visual behavior changed.
2026-07-16 Terrain Chunk LOD Planner Extraction
TerrainChunkLodPlannernow owns the pure mapping from populated parsed ADT chunk indices to desired terrain LOD 0/1/2.TerrainChunkLodPolicysnapshots the existing enabled flag, three raw chunk radii and chunk size once per streaming-target application.- Horizontal XZ distance to the chunk center, inclusive squared thresholds, sparse-index omission and historical negative-radius squaring are unchanged.
StreamingWorldLoaderstill owns tile/chunk state, queues, operation budgets, creation/removal, Mesh/Node/RID finalization and tile-LOD fallback.- Cache formats, quality presets and visible behavior are unchanged. The scene-free contract is formula evidence only; asset-backed p95/p99 and visual acceptance remain required.
2026-07-16 Terrain Chunk Geometry Queue Planner Extraction
TerrainChunkGeometryQueuePlannernow compares current and desired chunk state and returns fresh chunk create/remove request arrays.- LOD mismatches and absent chunks retain the existing request shape; creates remain sorted by horizontal distance from chunk origin to streaming focus.
- Desired tile LOD still removes every current chunk, while sparse parsed chunk records retain the existing tile-origin distance fallback.
StreamingWorldLoaderstill owns queue storage/drains, removal-first execution, the sharedchunk_geometrybudget lane and every Mesh/Node/RID side effect.- Cache formats, quality profiles and visual rules are unchanged. Asset-backed p95/p99 and visual acceptance remain required.
2026-07-16 M2 Unique Placement Registry Extraction
M2UniquePlacementRegistrynow owns positive ADTMDDF.uniqueIdreservations across streamed tiles using the existinguid:<decimal>keys.- Missing/zero/negative IDs remain untracked; duplicates inside one reservation call are suppressed and another tile records one skipped key until owner release.
- Owner release still lets the loader find eligible waiting tiles and enqueue the existing detail retry path. First ownership remains tile-processing-order based.
StreamingWorldLoaderstill owns tile state, retry queues, grouping/build tasks, M2 caches, MultiMesh/Node/Mesh/material/RID creation and all operation budgets.- M2/ADT cache formats, transforms, profiles and visible rules are unchanged. Asset-backed cross-boundary p95/p99 and visual acceptance remain required.
2026-07-16 M2 Placement Transform Resolver Extraction
M2PlacementTransformResolvernow owns the pure ADT M2 basis and local origin-compensation formulas used by worker grouping, placeholders and direct instance creation.- Regular models retain
Basis.from_euler;ElwynnCliffRock01/02retain their narrow shell correction;NewWaterfallandElwynnTallWaterfall01retain calibrated world yaw, local fall-axis twist and tall-sheet anchor compensation. - Path separator/case/MDX handling, constants and caller-specific final scale clamps are unchanged. The resolver owns no Node, task, cache or RID.
- Cache formats, placement positions and visible rules are unchanged by the extraction. Asset-backed visual recheck and general placement parity remain gaps.
2026-07-16 M2 Placement Grouper Extraction
M2PlacementGroupernow owns pure worker-side validation, historical path normalization and ordered tile-local transform grouping of ADT M2 placements.- Invalid records, backslash replacement, lowercase
.mdx/.mdlconversion, first-seen ordering and the0.0001scale clamp are unchanged. - The loader retains tasks, mutex/result queues, stale-result checks, tile/build state, caches and all Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, batching, profiles and visible rules are unchanged. Asset-backed traversal p95/p99, visual recheck and spatial-cell grouping remain pending.
2026-07-17 M2 Build Batch Planner Extraction
M2BuildBatchPlannernow owns pure static/animated batch-limit selection, remaining-count capping and next-offset/group-completion calculation.- Existing animated/static limits still clamp to at least one; resource readiness,
queue rotation, serial progression and
M2_BUILDpermit consumption are unchanged. - The loader retains build jobs/queues, tile cancellation, animation/mesh caches, retries and all Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, quality profiles, batching output and visible rules are unchanged. Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
2026-07-17 WMO Placement Resolver Extraction
WmoPlacementResolvernow owns lowercase/slash cache-key normalization, positive MODFuid:<decimal>identity with the legacy tile/index fallback and worldTransform3Dcomposition.- Lightweight render roots, cached scenes and live prototypes now share the same position/Euler/unclamped-scale formula.
- The loader retains registry refs, jobs/queues, cache/load state, resource fallback, cancellation and all Node/Mesh/MultiMesh/material/RID ownership.
- Cache formats, placement values, profiles and visible rules are unchanged. Asset-backed placement/p95/p99 and general WMO parity remain pending.
2026-07-17 WMO Render Resource Cache State Extraction
WmoRenderResourceCacheStatenow owns validated lightweight-WMO render Resources, negative entries and normalized-path to pending-cache-path records.StreamingWorldLoaderstill constructs cache paths, callsResourceLoader, polls requests and validatesWMOStreamingResourcescript identity plusFORMAT_VERSIONbefore completing cache state.- Map reset and orderly request draining clear pending/negative state while retaining accepted Resources; final runtime cache release clears all state.
- Missing render-cache files still are not negatively cached, preserving retry and cached-scene/live fallback behavior. Formats, profiles and visuals are unchanged.
- Asset-backed corrupt-cache, traversal/leak p95/p99 and paired fidelity evidence remain pending.
2026-07-17 WMO Scene Resource Cache State Extraction
WmoSceneResourceCacheStatenow owns validated cached-WMO PackedScenes, negative entries and normalized-path to pending-.tscnrecords.StreamingWorldLoaderstill checks file existence andwmo_max_runtime_scene_mb, callsResourceLoader, instantiates a validation probe, checks WMOBuilder cache metadata and frees the probe before adoption.- Missing files, oversize scenes, request errors, load failures and stale scenes retain their prior negative-cache and live-prototype fallback behavior.
- Map reset clears pending/negative state while retaining accepted scenes; final shutdown releases all scene state. Formats, profiles and visuals are unchanged.
- Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity evidence remain pending.
2026-07-16 World Environment Snapshot Facade
WorldEnvironmentSnapshotcarries one immutable finite time-of-day value, normalized to[0, 24)hours; invalid non-finite input is explicit.WorldRenderFacade.apply_environment_snapshot()delegates the snapshot to the scene-ownedWowSkyController. The facade owns noEnvironment, sun, skybox, shader global or DBC sampling state.- Checkpoint capture now submits its manifest time through the facade instead of
locating
WowSkyControllerand mutatinguse_system_time,time_speedandfixed_time_hoursdirectly. - Applying a snapshot performs the same three fixed-clock assignments as the old capture path. Light/Area/Skybox DBC lookup, interpolation, fog, sun and shader behavior remain unchanged; weather and indoor environment state are follow-up work.
2026-07-16 World Entity Presentation Facade
EntityPresentationSnapshotcontract version 1 combines sessionEntityId, typed Godot world position, visual PackedScene path, finite yaw/scale and visibility.WorldRenderFacade.present_entity()andremove_entity()delegate to an explicit scene-ownedWorldEntityPresenter; both runtime scenes compose that service.- The presenter owns only visual subtrees. Same-path snapshots update transform/ visibility, changed paths replace after successful instantiation, and removal detaches plus queues the owned root for deletion.
- The local player, authoritative state, GUID mapping and asset/display selection remain outside this service. Runtime emits no entity commands yet, so baseline output is unchanged.
- PackedScene loading is currently synchronous on create/replace and is a documented prototype limitation, not an accepted network-scale frame-critical path.
2026-07-16 Terrain Quality Mesh Cache Extraction
TerrainQualityMeshCachenow owns full-quality terrain revisit Mesh references, tile-key LRU order and source admission outsideStreamingWorldLoader.- The historical rules are unchanged: successful restore touches newest; store
trims oldest to
terrain_quality_mesh_cache_limit; zero/negative capacity keeps no entries; control-splat/splat sources are never retained in this revisit cache. - The loader keeps three narrow adapters: restore into tile state, store after a full-quality result and clear during existing world reset/shutdown.
- ADT parsing, quality tasks/results, tile state, cache format versions, material/ Node/RID finalization, budgets and visible terrain behavior remain loader-owned.
Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
High quality must appear before the player arrives, not after crossing the ADT boundary.