Files
open-wc/src/scenes/streaming/streaming_world_loader.gd
T
sindoring 7815385b3b feat(M01): add explicit streaming focus
Work-Package: M01-RND-STREAMING-FOCUS-001

Agent: sindo-main-codex
2026-07-13 15:41:16 +04:00

5561 lines
191 KiB
GDScript

@tool
## Streams Azeroth terrain around an explicit [StreamingFocus] with chunk-based LODs.
extends Node3D
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
const BAKED_TILE_SCRIPT := preload("res://src/resources/baked_adt_tile.gd")
const STREAMING_TILE_SCRIPT := preload("res://src/resources/streaming_adt_tile.gd")
const SPLAT_TILE_SCRIPT := preload("res://src/resources/splat_adt_tile.gd")
const CONTROL_SPLAT_TILE_SCRIPT := preload("res://src/resources/control_splat_adt_tile.gd")
const WMO_STREAMING_SCRIPT := preload("res://src/resources/wmo_streaming_resource.gd")
const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd")
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
const M2_MATERIAL_REFRESH_VERSION := 2
const WMO_MATERIAL_REFRESH_VERSION := 10
const TILE_SIZE := 533.33333
const CHUNK_SIZE := TILE_SIZE / 16.0
const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI
const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
const QUALITY_CUSTOM := "Custom"
const QUALITY_PERFORMANCE := "Performance"
const QUALITY_BALANCED := "Balanced"
const QUALITY_HIGH := "High"
@export var extracted_dir: String = "res://data/extracted"
@export var map_name: String = "Azeroth"
## Optional scene adapter source. Any Node3D may provide the runtime focus.
@export var streaming_focus_source_path: NodePath
## Camera used only by the optional automatic overview positioning feature.
@export var camera_path: NodePath
@export_enum("Custom", "Performance", "Balanced", "High") var quality_preset: String = QUALITY_CUSTOM
## Seconds between streaming updates (lower = more responsive, higher = cheaper)
@export var update_interval: float = 0.25
## ADT tiles parsed per update tick (increase for faster initial load, decrease to reduce hitching)
@export var tiles_per_tick: int = 2
## Maximum number of ADT parse tasks running on worker threads at once
@export var max_concurrent_tile_tasks: int = 2
## Chunk mesh nodes created OR removed per update tick
@export var chunk_ops_per_tick: int = 24
## Heavy tile LOD creations per update tick
@export var tile_lod_ops_per_tick: int = 1
## Heavy tile LOD removals per update tick.
@export var tile_lod_remove_ops_per_tick: int = 2
## Loaded tile resources finalized on the main thread per frame.
@export var tile_finalize_ops_per_tick: int = 1
## Full-quality baked terrain tile upgrades finalized per frame.
@export var terrain_upgrade_finalize_ops_per_tick: int = 1
## Full-quality terrain tile resources loaded in parallel.
@export var max_concurrent_terrain_upgrade_tasks: int = 1
## M2/WMO detail batches attached to loaded tiles per frame.
@export var detail_asset_ops_per_tick: int = 1
## M2 MultiMesh groups materialized per frame after background grouping.
@export var m2_build_groups_per_tick: int = 2
## Maximum instances written into a single M2 MultiMesh per frame.
@export var m2_multimesh_batch_size: int = 128
## Experimental GLB M2 animation is allowed only for simple one-primitive creatures.
## Complex doodads (for example GryphonRoost) need a native M2 animator because GLB
## conversion currently breaks their skin/material fidelity.
@export var enable_m2_animated_instances: bool = true
## Reject complex animated M2 GLBs until the skinning converter is robust for composite doodads.
@export var m2_animated_max_primitives: int = 3
@export var m2_animated_denylist_patterns: PackedStringArray = PackedStringArray(["gryphonroost"])
@export var m2_animated_allowlist_patterns: PackedStringArray = PackedStringArray([
"creature/fish/",
"creature/eagle/",
"world/critter/",
])
## Maximum animated M2 scene instances attached per build step. Keep low to avoid hitches.
@export var m2_animated_instances_per_tick: int = 8
## Loaded animated M2 scene resources finalized per frame.
@export var m2_animation_finalize_ops_per_tick: int = 1
## Loaded M2 scene resources converted to meshes per frame.
@export var m2_mesh_finalize_ops_per_tick: int = 1
## WMO instances attached per frame.
@export var wmo_build_instances_per_tick: int = 1
## WMO render-cache mesh groups attached per frame.
@export var wmo_render_group_ops_per_tick: int = 24
## Raw ADT water parse tasks running in parallel for cached terrain tiles.
@export var max_concurrent_water_tasks: int = 1
## Cached-tile water roots attached per frame.
@export var water_finalize_ops_per_tick: int = 1
## Maximum cached WMO .tscn size instantiated while streaming. Larger WMOs need a lightweight render cache.
@export var wmo_max_runtime_scene_mb: float = 8.0
## Minimum camera movement before recomputing streaming targets.
@export var streaming_update_distance: float = CHUNK_SIZE * 2.0
## Recently built tile meshes kept for quick revisits.
@export var cached_tile_mesh_limit: int = 24
## Full-quality terrain meshes kept after an ADT is unloaded, so revisits do not flash back to low quality.
@export var terrain_quality_mesh_cache_limit: int = 24
@export var lod0_radius_chunks: int = 6
@export var lod1_radius_chunks: int = 12
@export var lod2_radius_chunks: int = 24
@export var lod2_tile_radius: int = 3
@export var warm_tile_margin: int = 1
## Extra ADT ring loaded and GPU-warmed outside the visible terrain radius.
@export var prewarm_tile_margin: int = 1
@export var prewarm_tile_lod_instances: bool = true
## Extra ADT rings retained after leaving the prewarm radius to avoid boundary churn.
@export var retain_tile_margin: int = 2
## Pre-create hidden full terrain LOD this many ADT widths before it becomes visible.
@export var full_lod_prewarm_tiles: float = 1.0
## Start loading the next ADT-centered ring before crossing a tile boundary.
@export_range(0.0, 0.49, 0.01) var boundary_prefetch_threshold: float = 0.35
@export var auto_position_camera: bool = true
@export var editor_preview_enabled: bool = true
@export var editor_preview_center_x: int = 32
@export var editor_preview_center_y: int = 48
@export var editor_preview_tile_radius: int = 1
@export var editor_follow_view_camera: bool = true
@export var editor_persist_generated_nodes: bool = false
@export var enable_occlusion_culling: bool = true
@export var use_rendering_server_for_terrain: bool = true
@export var use_baked_tile_cache: bool = true
@export var baked_tile_cache_dir: String = "res://data/cache/baked_terrain_v2"
@export var streaming_tile_cache_dir: String = "res://data/cache/baked_terrain_stream_v1"
@export var terrain_control_splat_cache_dir: String = "res://data/cache/terrain_control_splat_v3"
@export var terrain_splat_cache_dir: String = "res://data/cache/terrain_splat_v1"
@export var terrain_full_quality_enabled: bool = true
@export var terrain_full_quality_radius_tiles: int = 1
@export var terrain_control_splat_quality_enabled: bool = true
@export var terrain_control_splat_primary: bool = true
@export var terrain_control_splat_hide_fallback: bool = false
@export var terrain_control_splat_hide_fallback_radius_tiles: int = 0
@export var terrain_control_splat_radius_tiles: int = 0
@export var max_active_terrain_control_splat_tiles: int = 5
@export var terrain_control_splat_cache_finalize_ops_per_tick: int = 1
@export var terrain_splat_quality_enabled: bool = true
@export var terrain_splat_radius_tiles: int = 0
@export var terrain_splat_lod: int = 0
@export var terrain_splat_runtime_build_enabled: bool = false
@export var max_active_terrain_splat_tiles: int = 5
@export var terrain_splat_cache_finalize_ops_per_tick: int = 1
@export var max_concurrent_terrain_splat_tasks: int = 1
@export var terrain_splat_builds_per_tick: int = 1
@export var use_chunk_terrain_lods: bool = false
@export var enable_water: bool = false
@export var enable_wmo: bool = true
@export var enable_m2_assets: bool = true
@export var enable_m2_placeholders: bool = false
@export var m2_placeholder_only_trees: bool = true
## Dynamic doodad render radius in ADT tiles. Negative keeps all loaded M2s active.
@export var m2_tile_radius: int = 1
## Dynamic WMO render radius in ADT tiles. Negative keeps all loaded WMOs active.
@export var wmo_tile_radius: int = 3
## Godot visibility range applied to instantiated M2 MultiMesh nodes. <= 0 disables it.
@export var m2_visibility_range: float = 700.0
## Godot visibility range applied to instantiated WMO trees. <= 0 disables it.
@export var wmo_visibility_range: float = 1800.0
@export var wmo_render_cache_dir: String = "res://data/cache/wmo_render_v1"
@export var m2_cache_dir: String = "res://data/cache/m2_glb"
@export var wmo_cache_dir: String = "res://data/cache/wmo_tscn"
@export var terrain_cast_shadows: bool = false
@export var m2_cast_shadows: bool = false
@export var wmo_cast_shadows: bool = false
@export var editor_reload_now: bool = false
@export var debug_streaming: bool = false
@export var runtime_stats_enabled: bool = false
@export var runtime_stats_interval: float = 2.0
@export var terrain_quality_log_enabled: bool = true
@export var terrain_quality_log_interval: float = 1.0
@export var hitch_profiler_enabled: bool = false
@export var hitch_profiler_threshold_ms: float = 20.0
var _builder
var _map_dir := ""
var _abs_map_dir := ""
var _wdt_info: Dictionary = {}
var _available_tiles: Dictionary = {}
var _tile_states: Dictionary = {}
var _tile_load_queue: Array = []
var _chunk_remove_queue: Array = []
var _chunk_create_queue: Array = []
var _tile_lod_remove_queue: Array = []
var _tile_lod_create_queue: Array = []
var _detail_asset_queue: Array = []
var _detail_asset_queued: Dictionary = {}
var _m2_group_tasks: Dictionary = {}
var _m2_group_result_mutex := Mutex.new()
var _m2_group_result_queue: Array = []
var _m2_build_jobs: Dictionary = {}
var _m2_build_queue: Array = []
var _m2_unique_registry: Dictionary = {}
var _m2_mesh_cache: Dictionary = {}
var _m2_mesh_load_requests: Dictionary = {}
var _m2_mesh_finalize_queue: Array = []
var _m2_runtime_rebuild_required_cache: Dictionary = {}
var _m2_animation_load_requests: Dictionary = {}
var _m2_animation_finalize_queue: Array = []
var _wmo_build_jobs: Dictionary = {}
var _wmo_build_queue: Array = []
var _tile_loading_tasks: Dictionary = {}
var _terrain_upgrade_tasks: Dictionary = {}
var _terrain_control_splat_cache_tasks: Dictionary = {}
var _terrain_splat_cache_tasks: Dictionary = {}
var _terrain_splat_tasks: Dictionary = {}
var _terrain_splat_result_mutex := Mutex.new()
var _terrain_splat_result_queue: Array = []
var _water_load_queue: Array = []
var _water_load_queued: Dictionary = {}
var _water_load_tasks: Dictionary = {}
var _water_result_mutex := Mutex.new()
var _water_result_queue: Array = []
var _tile_result_mutex := Mutex.new()
var _tile_result_queue: Array = []
var _shared_tex_cache: Dictionary = {}
var _terrain_root: Node3D
var _update_accum := 0.0
var _tile_min := Vector2i(63, 63)
var _tile_max := Vector2i(0, 0)
var _camera_initialized := false
var _editor_signature := ""
var _streaming_focus: StreamingFocus
var _missing_focus_source_reported := false
var _last_focus_pos := Vector3.ZERO
var _dbg_chunks_created := 0
var _dbg_chunks_null := 0
var _dbg_last_report := 0.0
var _runtime_stats_accum := 0.0
var _terrain_quality_log_accum := 0.0
var _shutting_down := false
var _last_refresh_focus_pos := Vector3.ZERO
var _has_refresh_focus := false
var _tile_mesh_cache: Dictionary = {}
var _tile_mesh_cache_order: Array = []
var _terrain_quality_mesh_cache: Dictionary = {}
var _terrain_quality_mesh_cache_order: Array = []
var _wmo_prototype_cache: Dictionary = {}
var _wmo_render_cache: Dictionary = {}
var _wmo_render_missing_cache: Dictionary = {}
var _wmo_render_load_requests: Dictionary = {}
var _wmo_render_build_jobs: Dictionary = {}
var _wmo_render_build_queue: Array = []
var _wmo_scene_resource_cache: Dictionary = {}
var _wmo_scene_cache_missing: Dictionary = {}
var _wmo_scene_load_requests: Dictionary = {}
var _wmo_missing_cache: Dictionary = {}
var _m2_scene_cache: Dictionary = {}
var _m2_animated_scene_cache: Dictionary = {}
var _m2_static_animation_cache: Dictionary = {}
var _m2_missing_cache: Dictionary = {}
var _world_wmo_root: Node3D
var _wmo_registry: Dictionary = {}
func _ready() -> void:
_apply_quality_preset()
if not ClassDB.class_exists("ADTLoader"):
push_error("ADTLoader not found. Rebuild GDExtension first.")
return
if enable_occlusion_culling:
get_viewport().set_use_occlusion_culling(true)
_builder = ADT_BUILDER_SCRIPT.new()
_map_dir = extracted_dir.path_join("World/Maps/%s" % map_name)
_abs_map_dir = ProjectSettings.globalize_path(_map_dir)
var existing_terrain := get_node_or_null("Terrain")
if existing_terrain is Node3D:
_terrain_root = existing_terrain
else:
_terrain_root = Node3D.new()
_terrain_root.name = "Terrain"
add_child(_terrain_root)
_reset_terrain_root_children()
_ensure_world_wmo_root()
_set_editor_owner_recursive(_terrain_root)
_scan_available_tiles()
if _available_tiles.is_empty() and not _wdt_has_global_wmo():
push_error("No ADT tiles found in %s" % _map_dir)
return
if _can_use_baked_tile_cache():
var baked_dir := ProjectSettings.globalize_path(baked_tile_cache_dir.path_join(map_name))
if not DirAccess.dir_exists_absolute(baked_dir):
push_warning("Baked terrain cache not found: %s. Runtime will fall back to raw ADT builds." % baked_dir)
if terrain_control_splat_quality_enabled:
var control_splat_dir := ProjectSettings.globalize_path(terrain_control_splat_cache_dir.path_join(map_name))
if not DirAccess.dir_exists_absolute(control_splat_dir):
push_warning("Control splat terrain cache not found: %s. High terrain quality will use baked atlas fallback." % control_splat_dir)
if terrain_splat_quality_enabled:
var splat_dir := ProjectSettings.globalize_path(terrain_splat_cache_dir.path_join(map_name))
if not DirAccess.dir_exists_absolute(splat_dir):
push_warning("Terrain splat cache not found: %s. High terrain quality will use baked atlas fallback." % splat_dir)
if enable_wmo:
_place_global_wmo()
if Engine.is_editor_hint():
_tick_editor_streaming()
elif auto_position_camera:
_position_camera_over_world()
if not Engine.is_editor_hint():
_capture_streaming_focus_from_source()
set_process(true)
call_deferred("_refresh_streaming_targets_after_ready")
func _process(delta: float) -> void:
if _available_tiles.is_empty():
return
var profile_enabled := hitch_profiler_enabled and not Engine.is_editor_hint()
var profile_start := Time.get_ticks_usec() if profile_enabled else 0
var section_start := profile_start
var timings: Array[String] = []
# Queues are always drained — works in both editor and game.
_drain_tile_load_results()
_profile_section(timings, "finalize", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_terrain_upgrade_results()
_profile_section(timings, "terrainup", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_terrain_control_splat_cache_results()
_profile_section(timings, "terrainctl", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_terrain_splat_cache_results()
_profile_section(timings, "terrainsplatcache", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_terrain_splat_results()
_profile_section(timings, "terrainsplat", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_process_water_load_queue()
_drain_water_load_results()
_profile_section(timings, "water", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_process_queues()
_profile_section(timings, "terrainq", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_m2_group_results()
_profile_section(timings, "m2results", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_m2_animation_loads()
_profile_section(timings, "m2anim", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_drain_m2_mesh_loads()
_profile_section(timings, "m2mesh", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_process_m2_build_jobs()
_profile_section(timings, "m2build", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_process_wmo_build_jobs()
_profile_section(timings, "wmobuild", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_process_wmo_render_build_jobs()
_profile_section(timings, "wmogroups", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_process_detail_asset_queue()
_profile_section(timings, "details", section_start, profile_enabled)
section_start = Time.get_ticks_usec() if profile_enabled else 0
_tick_runtime_stats(delta)
_tick_terrain_quality_log(delta)
_update_accum += delta
var did_refresh := false
if _update_accum < update_interval:
_log_hitch_profile(profile_start, timings, did_refresh, profile_enabled)
return
_update_accum = 0.0
if Engine.is_editor_hint():
_tick_editor_streaming()
else:
refresh_streaming_focus(false)
did_refresh = true
_profile_section(timings, "refresh", section_start, profile_enabled)
_log_hitch_profile(profile_start, timings, did_refresh, profile_enabled)
func _exit_tree() -> void:
_shutting_down = true
_wait_for_tile_tasks()
_clear_streamed_world()
_release_runtime_caches_for_shutdown()
## Frees detached prototype nodes and releases render resources owned by this
## loader after all asynchronous work has completed. Map refreshes retain these
## caches; only scene shutdown destroys them.
func _release_runtime_caches_for_shutdown() -> void:
_free_detached_node_cache(_m2_scene_cache)
_free_detached_node_cache(_m2_animated_scene_cache)
_free_detached_node_cache(_wmo_prototype_cache)
_m2_mesh_cache.clear()
_m2_static_animation_cache.clear()
_m2_missing_cache.clear()
_wmo_render_cache.clear()
_wmo_scene_resource_cache.clear()
_wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear()
_wmo_missing_cache.clear()
_shared_tex_cache.clear()
func _free_detached_node_cache(node_cache: Dictionary) -> void:
for cached_value in node_cache.values():
if not (cached_value is Node):
continue
var cached_node := cached_value as Node
if not is_instance_valid(cached_node):
continue
if cached_node.is_inside_tree():
cached_node.queue_free()
else:
cached_node.free()
node_cache.clear()
func _apply_quality_preset() -> void:
match quality_preset:
QUALITY_PERFORMANCE:
update_interval = 0.12
tiles_per_tick = 1
max_concurrent_tile_tasks = 1
chunk_ops_per_tick = 16
tile_lod_ops_per_tick = 1
tile_lod_remove_ops_per_tick = 1
tile_finalize_ops_per_tick = 1
terrain_upgrade_finalize_ops_per_tick = 1
max_concurrent_terrain_upgrade_tasks = 1
detail_asset_ops_per_tick = 1
m2_build_groups_per_tick = 1
m2_multimesh_batch_size = 32
m2_animation_finalize_ops_per_tick = 1
m2_mesh_finalize_ops_per_tick = 1
wmo_build_instances_per_tick = 1
wmo_render_group_ops_per_tick = 16
terrain_quality_mesh_cache_limit = 0
lod0_radius_chunks = 6
lod1_radius_chunks = 12
lod2_radius_chunks = 24
lod2_tile_radius = 3
warm_tile_margin = 1
prewarm_tile_margin = 2
retain_tile_margin = 3
full_lod_prewarm_tiles = 1.0
boundary_prefetch_threshold = 0.4
terrain_full_quality_enabled = false
terrain_full_quality_radius_tiles = 0
terrain_control_splat_quality_enabled = false
terrain_control_splat_primary = false
terrain_control_splat_hide_fallback = false
terrain_control_splat_hide_fallback_radius_tiles = 0
terrain_control_splat_radius_tiles = 0
max_active_terrain_control_splat_tiles = 0
terrain_control_splat_cache_finalize_ops_per_tick = 1
terrain_splat_quality_enabled = false
terrain_splat_radius_tiles = 0
terrain_splat_lod = 3
terrain_splat_runtime_build_enabled = false
max_active_terrain_splat_tiles = 0
terrain_splat_cache_finalize_ops_per_tick = 1
max_concurrent_terrain_splat_tasks = 1
terrain_splat_builds_per_tick = 1
m2_tile_radius = 1
wmo_tile_radius = 3
m2_visibility_range = 700.0
wmo_visibility_range = 1800.0
m2_cast_shadows = false
wmo_cast_shadows = false
terrain_cast_shadows = false
QUALITY_BALANCED:
update_interval = 0.10
tiles_per_tick = 1
max_concurrent_tile_tasks = 1
chunk_ops_per_tick = 20
tile_lod_ops_per_tick = 1
tile_lod_remove_ops_per_tick = 1
tile_finalize_ops_per_tick = 1
terrain_upgrade_finalize_ops_per_tick = 1
max_concurrent_terrain_upgrade_tasks = 1
detail_asset_ops_per_tick = 1
m2_build_groups_per_tick = 1
m2_multimesh_batch_size = 32
m2_animation_finalize_ops_per_tick = 1
m2_mesh_finalize_ops_per_tick = 1
wmo_build_instances_per_tick = 1
wmo_render_group_ops_per_tick = 20
terrain_quality_mesh_cache_limit = 24
lod0_radius_chunks = 8
lod1_radius_chunks = 16
lod2_radius_chunks = 32
lod2_tile_radius = 4
warm_tile_margin = 1
prewarm_tile_margin = 2
retain_tile_margin = 3
full_lod_prewarm_tiles = 1.0
boundary_prefetch_threshold = 0.4
terrain_full_quality_enabled = true
terrain_full_quality_radius_tiles = 1
terrain_control_splat_quality_enabled = true
terrain_control_splat_primary = true
terrain_control_splat_hide_fallback = false
terrain_control_splat_hide_fallback_radius_tiles = 0
terrain_control_splat_radius_tiles = 0
max_active_terrain_control_splat_tiles = 1
terrain_control_splat_cache_finalize_ops_per_tick = 1
terrain_splat_quality_enabled = false
terrain_splat_radius_tiles = 0
terrain_splat_lod = 0
terrain_splat_runtime_build_enabled = false
max_active_terrain_splat_tiles = 1
terrain_splat_cache_finalize_ops_per_tick = 1
max_concurrent_terrain_splat_tasks = 1
terrain_splat_builds_per_tick = 1
m2_tile_radius = 2
wmo_tile_radius = 4
m2_visibility_range = 1100.0
wmo_visibility_range = 2600.0
m2_cast_shadows = false
wmo_cast_shadows = false
terrain_cast_shadows = false
QUALITY_HIGH:
update_interval = 0.10
tiles_per_tick = 1
max_concurrent_tile_tasks = 1
chunk_ops_per_tick = 24
tile_lod_ops_per_tick = 1
tile_lod_remove_ops_per_tick = 1
tile_finalize_ops_per_tick = 1
terrain_upgrade_finalize_ops_per_tick = 1
max_concurrent_terrain_upgrade_tasks = 1
detail_asset_ops_per_tick = 1
m2_build_groups_per_tick = 1
m2_multimesh_batch_size = 64
m2_animation_finalize_ops_per_tick = 1
m2_mesh_finalize_ops_per_tick = 1
wmo_build_instances_per_tick = 1
wmo_render_group_ops_per_tick = 16
terrain_quality_mesh_cache_limit = 48
lod0_radius_chunks = 10
lod1_radius_chunks = 20
lod2_radius_chunks = 40
lod2_tile_radius = 5
warm_tile_margin = 1
prewarm_tile_margin = 3
retain_tile_margin = 4
full_lod_prewarm_tiles = 1.5
boundary_prefetch_threshold = 0.42
terrain_full_quality_enabled = true
terrain_full_quality_radius_tiles = 5
terrain_control_splat_quality_enabled = true
terrain_control_splat_primary = true
terrain_control_splat_hide_fallback = true
terrain_control_splat_hide_fallback_radius_tiles = 0
terrain_control_splat_radius_tiles = 5
max_active_terrain_control_splat_tiles = 121
terrain_control_splat_cache_finalize_ops_per_tick = 8
terrain_splat_quality_enabled = false
terrain_splat_radius_tiles = 0
terrain_splat_lod = 0
terrain_splat_runtime_build_enabled = false
max_active_terrain_splat_tiles = 5
terrain_splat_cache_finalize_ops_per_tick = 1
max_concurrent_terrain_splat_tasks = 1
terrain_splat_builds_per_tick = 1
m2_tile_radius = 3
wmo_tile_radius = 5
m2_visibility_range = 1600.0
wmo_visibility_range = 3600.0
m2_cast_shadows = false
wmo_cast_shadows = false
terrain_cast_shadows = false
func _tick_editor_streaming() -> void:
if editor_reload_now:
editor_reload_now = false
_clear_streamed_world()
_editor_signature = ""
if not editor_preview_enabled:
if not _editor_signature.is_empty():
_clear_streamed_world()
_terrain_root.position = Vector3.ZERO
_editor_signature = ""
return
var sig := _make_editor_signature()
if sig != _editor_signature:
_editor_signature = sig
_clear_streamed_world()
_reset_refresh_focus()
# Offset terrain_root so preview center sits near Godot origin.
var focus := _tile_center_to_world(editor_preview_center_x, editor_preview_center_y)
_terrain_root.position = -focus
var focus_world := _tile_center_to_world(editor_preview_center_x, editor_preview_center_y)
if editor_follow_view_camera:
var editor_camera := _get_editor_view_camera()
if editor_camera:
focus_world += editor_camera.global_position
_set_streaming_focus_from_vector3(focus_world)
_refresh_editor_streaming_targets_at(_streaming_focus_to_vector3(), false)
func _scan_available_tiles() -> void:
_available_tiles.clear()
_wdt_info = {}
if _load_tiles_from_wdt():
print("StreamingWorld: loaded %d ADT tiles from WDT for %s" % [_available_tiles.size(), map_name])
return
push_warning("StreamingWorld: falling back to directory scan for %s (no WDT)" % map_name)
_load_tiles_from_directory()
print("StreamingWorld: found %d ADT tiles by directory scan for %s" % [_available_tiles.size(), map_name])
func _load_tiles_from_wdt() -> bool:
if not ClassDB.class_exists("WDTLoader"):
return false
var wdt_path_abs := _abs_map_dir.path_join("%s.wdt" % map_name)
if not FileAccess.file_exists(wdt_path_abs):
return false
var loader = ClassDB.instantiate("WDTLoader")
if loader == null:
return false
var data: Dictionary = loader.call("load_wdt", wdt_path_abs)
if data.is_empty():
push_warning("StreamingWorld: WDTLoader returned empty data for %s" % wdt_path_abs)
return false
var tiles: Array = data.get("tiles", [])
for tile_variant in tiles:
if not (tile_variant is Dictionary):
continue
var entry: Dictionary = tile_variant
var tx: int = int(entry.get("x", -1))
var ty: int = int(entry.get("y", -1))
if tx < 0 or tx > 63 or ty < 0 or ty > 63:
continue
var adt_file := "%s_%d_%d.adt" % [map_name, tx, ty]
var adt_abs := _abs_map_dir.path_join(adt_file)
# WDT flags the tile as present, but the ADT may still be missing in
# a partial extraction. Skip so downstream code never chases a ghost.
if not FileAccess.file_exists(adt_abs):
continue
var key := _tile_key(tx, ty)
_available_tiles[key] = adt_abs
_tile_min.x = min(_tile_min.x, tx)
_tile_min.y = min(_tile_min.y, ty)
_tile_max.x = max(_tile_max.x, tx)
_tile_max.y = max(_tile_max.y, ty)
_wdt_info = data
return not _available_tiles.is_empty()
func _load_tiles_from_directory() -> void:
var dir := DirAccess.open(_map_dir)
if dir == null:
push_error("Cannot open map directory: %s" % _map_dir)
return
for file_name in dir.get_files():
if not file_name.ends_with(".adt"):
continue
var stem := file_name.trim_suffix(".adt")
var parts := stem.split("_")
if parts.size() != 3 or parts[0] != map_name:
continue
if not parts[1].is_valid_int() or not parts[2].is_valid_int():
continue
var tx := parts[1].to_int()
var ty := parts[2].to_int()
var key := _tile_key(tx, ty)
_available_tiles[key] = _abs_map_dir.path_join(file_name)
_tile_min.x = min(_tile_min.x, tx)
_tile_min.y = min(_tile_min.y, ty)
_tile_max.x = max(_tile_max.x, tx)
_tile_max.y = max(_tile_max.y, ty)
## Replaces the current streaming point without requiring a Node or Camera3D.
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
_streaming_focus = streaming_focus
## Samples the configured scene source, then refreshes streaming from the typed focus.
## Returns false when no valid focus is available; existing streamed content is retained.
func refresh_streaming_focus(force: bool = false) -> bool:
_capture_streaming_focus_from_source()
if _streaming_focus == null or _streaming_focus.world_position == null:
return false
_terrain_root.position = Vector3.ZERO
_refresh_streaming_targets_at(_streaming_focus_to_vector3(), force)
return true
func _refresh_streaming_targets_after_ready() -> void:
_has_refresh_focus = false
refresh_streaming_focus(false)
## Core streaming update for runtime and editor focus positions.
func _refresh_editor_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
if not force and not _should_refresh_focus(focus_pos):
return
var focus_tile := _world_to_tile(focus_pos)
var total_tile_radius: int = maxi(0, editor_preview_tile_radius)
var wanted_tiles: Dictionary = {}
for ty in range(focus_tile.y - total_tile_radius, focus_tile.y + total_tile_radius + 1):
for tx in range(focus_tile.x - total_tile_radius, focus_tile.x + total_tile_radius + 1):
var key := _tile_key(tx, ty)
if _available_tiles.has(key):
wanted_tiles[key] = true
_apply_streaming_target(wanted_tiles, wanted_tiles, focus_pos)
_note_refresh_focus(focus_pos)
if force or debug_streaming:
print("StreamingWorld(editor): focus_tile=%s wanted=%d queued=%d" % [
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
if not force and not _should_refresh_focus(focus_pos):
return
var focus_tile := _world_to_tile(focus_pos)
# Chunk-LOD radius → tile radius (ceiling), plus margin for pre-warming.
var focus_tiles := _predictive_focus_tiles(focus_pos)
var total_tile_radius: int = _runtime_load_tile_radius()
var retain_tile_radius: int = _runtime_retain_tile_radius()
var retained_tiles: Dictionary = {}
for center in focus_tiles:
for ty in range(center.y - retain_tile_radius, center.y + retain_tile_radius + 1):
for tx in range(center.x - retain_tile_radius, center.x + retain_tile_radius + 1):
var retained_key := _tile_key(tx, ty)
if _available_tiles.has(retained_key):
retained_tiles[retained_key] = true
var wanted_tiles: Dictionary = {}
for center in focus_tiles:
for ty in range(center.y - total_tile_radius, center.y + total_tile_radius + 1):
for tx in range(center.x - total_tile_radius, center.x + total_tile_radius + 1):
var key := _tile_key(tx, ty)
if _available_tiles.has(key):
wanted_tiles[key] = true
_apply_streaming_target(wanted_tiles, retained_tiles, focus_pos)
_note_refresh_focus(focus_pos)
if force or debug_streaming:
print("StreamingWorld: focus_tile=%s wanted=%d queued=%d" % [
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionary, focus_pos: Vector3) -> void:
_last_focus_pos = focus_pos
var filtered_load_queue: Array = []
for request in _tile_load_queue:
if wanted_tiles.has(request["key"]) and not _tile_states.has(request["key"]):
filtered_load_queue.append(request)
_tile_load_queue = filtered_load_queue
for key in wanted_tiles.keys():
if not _tile_states.has(key) and not _is_tile_queued(key):
var parts: PackedStringArray = key.split("_")
var tx: int = int(parts[0])
var ty: int = int(parts[1])
var tcx: float = (tx + 0.5) * TILE_SIZE
var tcz: float = (ty + 0.5) * TILE_SIZE
var dx: float = tcx - focus_pos.x
var dz: float = tcz - focus_pos.z
_tile_load_queue.append({
"key": key,
"tx": tx,
"ty": ty,
"path": _available_tiles[key],
"dist_sq": dx * dx + dz * dz,
})
# Nearest tiles first
_tile_load_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a["dist_sq"] < b["dist_sq"])
for key in _tile_states.keys():
var state: Dictionary = _tile_states[key]
var wanted := wanted_tiles.has(key)
var retained := retained_tiles.has(key)
state["wanted"] = wanted
state["retained"] = retained
if wanted:
state["desired_lods"] = _compute_desired_lods(state, focus_pos)
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
elif retained:
state["desired_lods"] = {}
var current_tile_lod: int = int(state.get("tile_lod", -1))
state["desired_tile_lod"] = current_tile_lod if current_tile_lod >= 0 else -1
state["desired_tile_lod_visible"] = false
else:
state["desired_lods"] = {}
state["desired_tile_lod"] = -1
state["desired_tile_lod_visible"] = false
state = _sync_terrain_quality(state, focus_pos)
state = _sync_detail_assets(state, focus_pos)
_tile_states[key] = state
_prune_active_terrain_control_splat_tiles(focus_pos)
_prune_active_terrain_splat_tiles(focus_pos)
_rebuild_chunk_queues()
func _chunk_origin_for(state: Dictionary, chunk_id: int) -> Vector3:
var chunks: Array = state["data"].get("chunks", [])
if chunk_id >= 0 and chunk_id < chunks.size():
var c: Dictionary = chunks[chunk_id]
if not c.is_empty():
return c.get("origin", Vector3.ZERO)
return state.get("origin", Vector3.ZERO)
func _rebuild_chunk_queues() -> void:
_chunk_remove_queue.clear()
_chunk_create_queue.clear()
_tile_lod_remove_queue.clear()
_tile_lod_create_queue.clear()
for key in _tile_states.keys():
var state: Dictionary = _tile_states[key]
var current_chunks: Dictionary = state["chunks"]
var desired_lods: Dictionary = state["desired_lods"]
var current_tile_lod: int = int(state.get("tile_lod", -1))
var desired_tile_lod: int = int(state.get("desired_tile_lod", -1))
var current_tile_lod_visible: bool = bool(state.get("tile_lod_visible", false))
var desired_tile_lod_visible: bool = bool(state.get("desired_tile_lod_visible", false))
if desired_tile_lod >= 0:
for chunk_id in current_chunks.keys():
_chunk_remove_queue.append({"tile": key, "chunk": chunk_id})
if current_tile_lod != desired_tile_lod or current_tile_lod_visible != desired_tile_lod_visible:
_tile_lod_create_queue.append({
"tile": key,
"lod": desired_tile_lod,
"visible": desired_tile_lod_visible,
"dist_sq": _tile_dist_sq(state, _last_focus_pos),
})
elif _should_prepare_full_tile_lod(state, _last_focus_pos):
_tile_lod_create_queue.append({
"tile": key,
"lod": 0,
"visible": false,
"prepare": true,
"dist_sq": _tile_dist_sq(state, _last_focus_pos),
})
continue
if current_tile_lod >= 0 and _is_tile_chunk_set_ready(state):
_tile_lod_remove_queue.append({"tile": key})
for chunk_id in current_chunks.keys():
if not desired_lods.has(chunk_id):
_chunk_remove_queue.append({"tile": key, "chunk": chunk_id})
continue
if current_chunks[chunk_id]["lod"] != desired_lods[chunk_id]:
var lod_change_origin: Vector3 = _chunk_origin_for(state, chunk_id)
var dx2: float = lod_change_origin.x - _last_focus_pos.x
var dz2: float = lod_change_origin.z - _last_focus_pos.z
_chunk_create_queue.append({
"tile": key,
"chunk": chunk_id,
"lod": desired_lods[chunk_id],
"dist_sq": dx2 * dx2 + dz2 * dz2,
})
for chunk_id in desired_lods.keys():
if current_chunks.has(chunk_id):
continue
var chunk_origin: Vector3 = _chunk_origin_for(state, chunk_id)
var dx: float = chunk_origin.x - _last_focus_pos.x
var dz: float = chunk_origin.z - _last_focus_pos.z
_chunk_create_queue.append({
"tile": key,
"chunk": chunk_id,
"lod": desired_lods[chunk_id],
"dist_sq": dx * dx + dz * dz,
})
# Sort after fully rebuilding so nearest chunks are processed first
_chunk_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a["dist_sq"] < b["dist_sq"])
_tile_lod_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a["dist_sq"] < b["dist_sq"])
func _process_queues() -> void:
var ops_left: int = chunk_ops_per_tick
var tile_lod_create_left: int = tile_lod_ops_per_tick
var tile_lod_remove_left: int = tile_lod_remove_ops_per_tick
# Free stale terrain first so GPU descriptors/memory are reclaimed before new creates.
while ops_left > 0 and not _chunk_remove_queue.is_empty():
_remove_chunk(_chunk_remove_queue.pop_back())
ops_left -= 1
while tile_lod_remove_left > 0 and not _tile_lod_remove_queue.is_empty():
_remove_tile_lod(_tile_lod_remove_queue.pop_back())
tile_lod_remove_left -= 1
# Parse N ADT files this tick — the main throttle knob
var loads_left: int = tiles_per_tick
while loads_left > 0 and not _tile_load_queue.is_empty() and _tile_loading_tasks.size() < max_concurrent_tile_tasks:
_start_tile_load_async(_tile_load_queue.pop_front())
loads_left -= 1
while tile_lod_create_left > 0 and not _tile_lod_create_queue.is_empty():
_create_tile_lod(_tile_lod_create_queue.pop_front())
tile_lod_create_left -= 1
# Create chunk meshes — sorted nearest-first so visible detail appears first
while ops_left > 0 and not _chunk_create_queue.is_empty():
_create_chunk(_chunk_create_queue.pop_front())
ops_left -= 1
# Debug summary every 2 seconds
_dbg_last_report += get_process_delta_time()
if debug_streaming and _dbg_last_report >= 2.0:
_dbg_last_report = 0.0
print("DBG QUEUES tiles_load=%d tile_lod_create=%d tile_lod_remove=%d chunk_create=%d chunk_remove=%d created=%d null=%d states=%d" % [
_tile_load_queue.size(), _tile_lod_create_queue.size(), _tile_lod_remove_queue.size(),
_chunk_create_queue.size(), _chunk_remove_queue.size(),
_dbg_chunks_created, _dbg_chunks_null, _tile_states.size()])
func _profile_section(timings: Array[String], label: String, start_usec: int, enabled: bool) -> void:
if not enabled:
return
var elapsed_ms := float(Time.get_ticks_usec() - start_usec) / 1000.0
if elapsed_ms >= 0.05:
timings.append("%s=%.2f" % [label, elapsed_ms])
func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bool, enabled: bool) -> void:
if not enabled:
return
var total_ms := float(Time.get_ticks_usec() - start_usec) / 1000.0
if total_ms < maxf(1.0, hitch_profiler_threshold_ms):
return
print("HITCH %.2fms refresh=%s queues[tile=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d detail=%d m2task=%d m2anim=%d m2mesh=%d m2build=%d wmobuild=%d wmogroups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] %s" % [
total_ms,
str(did_refresh),
_tile_load_queue.size(),
_tile_loading_tasks.size(),
_terrain_upgrade_tasks.size(),
_terrain_control_splat_cache_tasks.size(),
_terrain_splat_queue_size(),
_water_load_queue.size() + _water_load_tasks.size(),
_detail_asset_queue.size(),
_m2_group_tasks.size(),
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
_m2_build_queue.size(),
_wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
_wmo_render_build_queue.size(),
_tile_lod_create_queue.size(),
_tile_lod_remove_queue.size(),
_chunk_create_queue.size(),
_chunk_remove_queue.size(),
" ".join(timings),
])
## Read-only machine-readable state used by renderer baseline tooling.
## Keep this free of queue mutation so checkpoint capture cannot alter runtime behavior.
func render_baseline_snapshot() -> Dictionary:
return {
"tiles": _tile_states.size(),
"loading": _tile_loading_tasks.size(),
"queues": {
"tile": _tile_load_queue.size(),
"terrain_upgrade": _terrain_upgrade_tasks.size(),
"terrain_control": _terrain_control_splat_cache_tasks.size(),
"terrain_splat": _terrain_splat_queue_size(),
"water": _water_load_queue.size() + _water_load_tasks.size(),
"detail": _detail_asset_queue.size(),
"m2_task": _m2_group_tasks.size(),
"m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
"m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
"m2_build": _m2_build_queue.size(),
"wmo_build": _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
"wmo_groups": _wmo_render_build_queue.size(),
"lod_create": _tile_lod_create_queue.size(),
"lod_remove": _tile_lod_remove_queue.size(),
"chunk_create": _chunk_create_queue.size(),
"chunk_remove": _chunk_remove_queue.size(),
},
"cache_entries": _tile_mesh_cache.size(),
"m2_active_unique_ids": _m2_unique_registry.size(),
"wmo_instances": _wmo_registry.size(),
}
func _tick_runtime_stats(delta: float) -> void:
if not runtime_stats_enabled or Engine.is_editor_hint():
return
_runtime_stats_accum += delta
if _runtime_stats_accum < maxf(runtime_stats_interval, 0.25):
return
_runtime_stats_accum = 0.0
var wanted_tiles := 0
var retained_tiles := 0
var chunk_nodes := 0
var chunk_rids := 0
var tile_lod_nodes := 0
var tile_lod_rids := 0
var tile_lod_hidden := 0
var tile_lod_prepared := 0
var m2_tiles := 0
var m2_multimeshes := 0
var m2_instances := 0
var terrain_baked_full := 0
var terrain_control_splat := 0
var terrain_splat := 0
for state_variant in _tile_states.values():
if not (state_variant is Dictionary):
continue
var state: Dictionary = state_variant
if bool(state.get("wanted", false)):
wanted_tiles += 1
if bool(state.get("retained", false)):
retained_tiles += 1
var chunks: Dictionary = state.get("chunks", {})
for entry_variant in chunks.values():
if not (entry_variant is Dictionary):
continue
var entry: Dictionary = entry_variant
if bool(entry.get("empty", false)):
continue
var chunk_rid: RID = entry.get("rid", RID())
if chunk_rid.is_valid():
chunk_rids += 1
elif entry.get("node", null) != null:
chunk_nodes += 1
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
if tile_lod_rid.is_valid():
tile_lod_rids += 1
elif state.get("tile_lod_node", null) != null:
tile_lod_nodes += 1
if int(state.get("tile_lod", -1)) >= 0 and not bool(state.get("tile_lod_visible", true)):
tile_lod_hidden += 1
var prepared_tile_lod_rid: RID = state.get("prepared_tile_lod_rid", RID())
if prepared_tile_lod_rid.is_valid():
tile_lod_prepared += 1
var terrain_source := String(state.get("quality_terrain_source", ""))
if terrain_source == "baked_full":
terrain_baked_full += 1
elif terrain_source == "control_splat_cache":
terrain_control_splat += 1
elif terrain_source == "splat_cache" or terrain_source == "splat":
terrain_splat += 1
var root: Node = state.get("root", null)
if root == null or not is_instance_valid(root):
continue
var m2_root := root.get_node_or_null("M2s")
if m2_root == null:
continue
m2_tiles += 1
var m2_counts := {"multimeshes": 0, "instances": 0}
_accumulate_multimesh_stats(m2_root, m2_counts)
m2_multimeshes += int(m2_counts["multimeshes"])
m2_instances += int(m2_counts["instances"])
print("PERF fps=%.1f tiles=%d wanted=%d retained=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d src[baked=%d ctl=%d splat=%d] cache=%d lod_rid=%d lod_node=%d lod_hidden=%d lod_prepared=%d chunk_rid=%d chunk_node=%d queues[tile=%d detail=%d m2_task=%d m2_anim=%d m2_mesh=%d m2_build=%d wmo_build=%d wmo_groups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] m2_tiles=%d m2_mm=%d m2_inst=%d wmo=%d" % [
Engine.get_frames_per_second(),
_tile_states.size(),
wanted_tiles,
retained_tiles,
_tile_loading_tasks.size(),
_terrain_upgrade_tasks.size(),
_terrain_control_splat_cache_tasks.size(),
_terrain_splat_queue_size(),
_water_load_queue.size() + _water_load_tasks.size(),
terrain_baked_full,
terrain_control_splat,
terrain_splat,
_tile_mesh_cache.size(),
tile_lod_rids,
tile_lod_nodes,
tile_lod_hidden,
tile_lod_prepared,
chunk_rids,
chunk_nodes,
_tile_load_queue.size(),
_detail_asset_queue.size(),
_m2_group_tasks.size(),
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
_m2_build_queue.size(),
_wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
_wmo_render_build_queue.size(),
_tile_lod_create_queue.size(),
_tile_lod_remove_queue.size(),
_chunk_create_queue.size(),
_chunk_remove_queue.size(),
m2_tiles,
m2_multimeshes,
m2_instances,
_wmo_registry.size(),
])
func _tick_terrain_quality_log(delta: float) -> void:
if not terrain_quality_log_enabled or Engine.is_editor_hint():
return
_terrain_quality_log_accum += delta
if _terrain_quality_log_accum < maxf(terrain_quality_log_interval, 0.25):
return
_terrain_quality_log_accum = 0.0
var target := 0
var ready := 0
var pending := 0
var fallback_baked := 0
var fallback_stream := 0
var hidden_wait := 0
var missing_cache := 0
var stale_or_other := 0
var focus_state_source := "none"
var focus_tile := _world_to_tile(_last_focus_pos)
for state_variant in _tile_states.values():
if not (state_variant is Dictionary):
continue
var state: Dictionary = state_variant
if not bool(state.get("wanted", false)):
continue
if not terrain_control_splat_quality_enabled:
continue
if not _tile_within_control_splat_terrain_radius(state, _last_focus_pos):
continue
target += 1
var key := String(state.get("key", ""))
var source := String(state.get("quality_terrain_source", ""))
var is_focus := int(state.get("tx", -9999)) == focus_tile.x and int(state.get("ty", -9999)) == focus_tile.y
if is_focus:
focus_state_source = source if not source.is_empty() else "stream"
if source == "control_splat_cache":
ready += 1
continue
if not key.is_empty() and _terrain_control_splat_cache_tasks.has(key):
pending += 1
continue
var cache_path := _get_terrain_control_splat_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0)))
if not ResourceLoader.exists(cache_path):
missing_cache += 1
continue
var desired_lod := int(state.get("desired_tile_lod", -1))
if desired_lod < 0:
hidden_wait += 1
elif source == "baked_full":
fallback_baked += 1
else:
var tile_lod := int(state.get("tile_lod", -1))
if tile_lod >= 0:
fallback_stream += 1
else:
stale_or_other += 1
var done := ready >= target and pending == 0 and missing_cache == 0 and hidden_wait == 0 and fallback_baked == 0 and fallback_stream == 0
print("TERRAIN_QUALITY target=%d ready=%d pending=%d fallback[baked=%d stream=%d hidden=%d other=%d] missing_cache=%d queue=%d focus=%d_%d:%s done=%s" % [
target,
ready,
pending,
fallback_baked,
fallback_stream,
hidden_wait,
stale_or_other,
missing_cache,
_terrain_control_splat_cache_tasks.size(),
focus_tile.x,
focus_tile.y,
focus_state_source,
str(done),
])
func _accumulate_multimesh_stats(node: Node, counts: Dictionary) -> void:
if node is MultiMeshInstance3D:
var mmi := node as MultiMeshInstance3D
if mmi.multimesh:
counts["multimeshes"] = int(counts["multimeshes"]) + 1
counts["instances"] = int(counts["instances"]) + mmi.multimesh.instance_count
for child in node.get_children():
_accumulate_multimesh_stats(child, counts)
func _start_tile_load_async(request: Dictionary) -> void:
var key: String = request["key"]
if _tile_loading_tasks.has(key):
return
var stream_res_path := _get_streaming_tile_resource_path(int(request["tx"]), int(request["ty"]))
if _can_use_baked_tile_cache() and ResourceLoader.exists(stream_res_path):
var err := ResourceLoader.load_threaded_request(
stream_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK:
_tile_loading_tasks[key] = {
"mode": "stream",
"path": stream_res_path,
"request": request,
}
return
var baked_res_path := _get_baked_tile_resource_path(int(request["tx"]), int(request["ty"]))
if _can_use_baked_tile_cache() and ResourceLoader.exists(baked_res_path):
var err := ResourceLoader.load_threaded_request(
baked_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK:
_tile_loading_tasks[key] = {
"mode": "baked",
"path": baked_res_path,
"request": request,
}
return
_start_raw_tile_load_async(request)
func _start_raw_tile_load_async(request: Dictionary) -> void:
var key: String = request["key"]
if _tile_loading_tasks.has(key):
return
var task_id: int = WorkerThreadPool.add_task(_load_tile_task.bind(request))
_tile_loading_tasks[key] = {
"mode": "raw",
"task_id": task_id,
"request": request,
}
func _load_tile_task(request: Dictionary) -> void:
var data: Dictionary = {}
var loader = ClassDB.instantiate("ADTLoader")
if loader:
data = loader.call("load_adt", request["path"])
_tile_result_mutex.lock()
_tile_result_queue.append({
"key": request["key"],
"data": data,
})
_tile_result_mutex.unlock()
func _drain_tile_load_results() -> void:
var results: Array = []
var finalize_budget: int = maxi(1, tile_finalize_ops_per_tick)
_tile_result_mutex.lock()
while finalize_budget > 0 and not _tile_result_queue.is_empty():
results.append(_tile_result_queue.pop_front())
finalize_budget -= 1
_tile_result_mutex.unlock()
for result in results:
var key: String = result["key"]
if not _tile_loading_tasks.has(key):
continue
var pending: Dictionary = _tile_loading_tasks[key]
if pending.get("mode", "raw") != "raw":
continue
WorkerThreadPool.wait_for_task_completion(int(pending["task_id"]))
_tile_loading_tasks.erase(key)
if _shutting_down:
continue
var request: Dictionary = pending["request"]
if not _should_accept_loaded_tile(request):
continue
_finalize_loaded_tile(request, result["data"])
var baked_ready: Array = []
if finalize_budget <= 0:
return
for key in _tile_loading_tasks.keys():
if baked_ready.size() >= finalize_budget:
break
var pending: Dictionary = _tile_loading_tasks[key]
if pending.get("mode", "raw") != "baked" and pending.get("mode", "raw") != "stream":
continue
var status := ResourceLoader.load_threaded_get_status(pending["path"])
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
baked_ready.append(key)
for key in baked_ready:
if not _tile_loading_tasks.has(key):
continue
var pending: Dictionary = _tile_loading_tasks[key]
_tile_loading_tasks.erase(key)
var request: Dictionary = pending["request"]
if _shutting_down:
continue
var status := ResourceLoader.load_threaded_get_status(pending["path"])
if status != ResourceLoader.THREAD_LOAD_LOADED:
_start_raw_tile_load_async(request)
continue
var loaded_tile: Resource = ResourceLoader.load_threaded_get(pending["path"])
if not _should_accept_loaded_tile(request):
continue
if pending.get("mode", "raw") == "stream":
if loaded_tile == null or loaded_tile.get_script() != STREAMING_TILE_SCRIPT:
push_warning("Invalid streaming tile resource: %s" % pending["path"])
_start_raw_tile_load_async(request)
continue
_finalize_loaded_streaming_tile(request, loaded_tile)
continue
if loaded_tile == null or loaded_tile.get_script() != BAKED_TILE_SCRIPT:
push_warning("Invalid baked tile resource: %s" % pending["path"])
_start_raw_tile_load_async(request)
continue
_finalize_loaded_baked_tile(request, loaded_tile)
func _sync_terrain_quality(state: Dictionary, focus_pos: Vector3) -> Dictionary:
state = _drop_splat_quality_if_inactive(state, focus_pos)
if not terrain_full_quality_enabled:
return state
if state.get("stream_tile", null) == null:
return state
if not bool(state.get("wanted", false)):
return state
if not _tile_within_full_terrain_radius(state, focus_pos):
return state
var key := String(state.get("key", ""))
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
var quality_source := String(state.get("quality_terrain_source", ""))
var control_splat_requested := false
if terrain_control_splat_quality_enabled and _tile_within_control_splat_terrain_radius(state, focus_pos):
if quality_source == "control_splat_cache":
return state
if key.is_empty():
return state
if not _can_activate_terrain_control_splat(key, state, focus_pos):
if quality_mesh != null:
return state
return state
if terrain_control_splat_primary and _tile_is_focus_tile(state, focus_pos):
var immediate_state := _load_terrain_control_splat_immediate(state, focus_pos)
if String(immediate_state.get("quality_terrain_source", "")) == "control_splat_cache":
return immediate_state
control_splat_requested = _request_terrain_control_splat_cache(state)
if control_splat_requested:
return state
if state.get("baked_tile", null) != null:
return state
if terrain_splat_quality_enabled and _tile_within_splat_terrain_radius(state, focus_pos):
if quality_source == "splat_cache" or quality_source == "splat":
return state
if key.is_empty():
return state
if not _can_activate_terrain_splat(key, state, focus_pos):
if quality_mesh != null:
return state
return state
if _request_terrain_splat_cache(state):
return state
if terrain_splat_runtime_build_enabled:
if _terrain_splat_tasks.has(key):
return state
if _terrain_splat_tasks.size() >= maxi(1, max_concurrent_terrain_splat_tasks):
return state
var task_id: int = WorkerThreadPool.add_task(_load_terrain_splat_task.bind(key, String(state.get("path", ""))))
_terrain_splat_tasks[key] = task_id
return state
if quality_mesh != null:
return state
if terrain_control_splat_primary and control_splat_requested:
return state
if terrain_control_splat_primary and terrain_control_splat_quality_enabled and _tile_within_control_splat_terrain_radius(state, focus_pos):
var control_path := _get_terrain_control_splat_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0)))
if ResourceLoader.exists(control_path):
return state
if key.is_empty() or _terrain_upgrade_tasks.has(key):
return state
if _terrain_upgrade_tasks.size() >= maxi(1, max_concurrent_terrain_upgrade_tasks):
return state
var baked_path := _get_baked_tile_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0)))
if not ResourceLoader.exists(baked_path):
return state
var err := ResourceLoader.load_threaded_request(
baked_path,
"",
false,
ResourceLoader.CACHE_MODE_IGNORE)
if err == OK or err == ERR_BUSY:
_terrain_upgrade_tasks[key] = {
"path": baked_path,
"tx": int(state.get("tx", 0)),
"ty": int(state.get("ty", 0)),
}
return state
func _drop_splat_quality_if_inactive(state: Dictionary, focus_pos: Vector3) -> Dictionary:
var quality_source := String(state.get("quality_terrain_source", ""))
if quality_source == "control_splat_cache":
if bool(state.get("wanted", false)) and _tile_within_control_splat_terrain_radius(state, focus_pos):
return state
return _drop_splat_quality(state)
if quality_source != "splat_cache" and quality_source != "splat":
return state
if bool(state.get("wanted", false)) and _tile_within_splat_terrain_radius(state, focus_pos):
return state
return _drop_splat_quality(state)
func _drop_splat_quality(state: Dictionary) -> Dictionary:
state.erase("quality_terrain_mesh")
state.erase("quality_terrain_source")
var key := String(state.get("key", ""))
if not key.is_empty():
state = _restore_quality_terrain_mesh(key, state)
_remove_tile_mesh_cache_entries(key)
return state
func _can_activate_terrain_splat(key: String, state: Dictionary, focus_pos: Vector3) -> bool:
var limit := maxi(0, max_active_terrain_splat_tiles)
if limit <= 0:
return false
if _tile_is_focus_tile(state, focus_pos):
return true
if _terrain_splat_cache_tasks.has(key) or _terrain_splat_tasks.has(key):
return true
return _active_terrain_splat_count(focus_pos) < limit
func _can_activate_terrain_control_splat(key: String, state: Dictionary, focus_pos: Vector3) -> bool:
var limit := maxi(0, max_active_terrain_control_splat_tiles)
if limit <= 0:
return false
if _tile_is_focus_tile(state, focus_pos):
return true
if _terrain_control_splat_cache_tasks.has(key):
return true
return _active_terrain_control_splat_count(focus_pos) < limit
func _active_terrain_control_splat_count(focus_pos: Vector3) -> int:
var count := _terrain_control_splat_cache_tasks.size()
for state_variant in _tile_states.values():
if not (state_variant is Dictionary):
continue
var state: Dictionary = state_variant
if not bool(state.get("wanted", false)):
continue
if String(state.get("quality_terrain_source", "")) != "control_splat_cache":
continue
if _tile_within_control_splat_terrain_radius(state, focus_pos):
count += 1
return count
func _active_terrain_splat_count(focus_pos: Vector3) -> int:
var count := _terrain_splat_cache_tasks.size() + _terrain_splat_tasks.size()
for state_variant in _tile_states.values():
if not (state_variant is Dictionary):
continue
var state: Dictionary = state_variant
if not bool(state.get("wanted", false)):
continue
var source := String(state.get("quality_terrain_source", ""))
if source != "splat_cache" and source != "splat":
continue
if _tile_within_splat_terrain_radius(state, focus_pos):
count += 1
return count
func _prune_active_terrain_control_splat_tiles(focus_pos: Vector3) -> void:
var limit := maxi(0, max_active_terrain_control_splat_tiles)
if limit <= 0:
for key_variant in _tile_states.keys():
var key := String(key_variant)
var state: Dictionary = _tile_states[key]
if String(state.get("quality_terrain_source", "")) == "control_splat_cache":
state = _drop_splat_quality(state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_tile_states[key] = state
return
var active: Array = []
for key_variant in _tile_states.keys():
var key := String(key_variant)
var state: Dictionary = _tile_states[key]
if String(state.get("quality_terrain_source", "")) != "control_splat_cache":
continue
if not bool(state.get("wanted", false)) or not _tile_within_control_splat_terrain_radius(state, focus_pos):
state = _drop_splat_quality(state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_tile_states[key] = state
continue
active.append({
"key": key,
"dist_sq": _tile_dist_sq(state, focus_pos),
"focus": _tile_is_focus_tile(state, focus_pos),
})
if active.size() <= limit:
return
active.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
if bool(a["focus"]) != bool(b["focus"]):
return bool(a["focus"])
return float(a["dist_sq"]) < float(b["dist_sq"]))
var keep := {}
for i in range(mini(limit, active.size())):
keep[String(active[i]["key"])] = true
for row in active:
var key := String(row["key"])
if keep.has(key) or not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
state = _drop_splat_quality(state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_tile_states[key] = state
func _prune_active_terrain_splat_tiles(focus_pos: Vector3) -> void:
var limit := maxi(0, max_active_terrain_splat_tiles)
if limit <= 0:
for key_variant in _tile_states.keys():
var key := String(key_variant)
var state: Dictionary = _tile_states[key]
var source := String(state.get("quality_terrain_source", ""))
if source == "splat_cache" or source == "splat":
state = _drop_splat_quality(state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_tile_states[key] = state
return
var active: Array = []
for key_variant in _tile_states.keys():
var key := String(key_variant)
var state: Dictionary = _tile_states[key]
var source := String(state.get("quality_terrain_source", ""))
if source != "splat_cache" and source != "splat":
continue
if not bool(state.get("wanted", false)) or not _tile_within_splat_terrain_radius(state, focus_pos):
state = _drop_splat_quality(state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_tile_states[key] = state
continue
active.append({
"key": key,
"dist_sq": _tile_dist_sq(state, focus_pos),
"focus": _tile_is_focus_tile(state, focus_pos),
})
if active.size() <= limit:
return
active.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
if bool(a["focus"]) != bool(b["focus"]):
return bool(a["focus"])
return float(a["dist_sq"]) < float(b["dist_sq"]))
var keep := {}
for i in range(mini(limit, active.size())):
keep[String(active[i]["key"])] = true
for row in active:
var key := String(row["key"])
if keep.has(key):
continue
if not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
state = _drop_splat_quality(state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_tile_states[key] = state
func _tile_is_focus_tile(state: Dictionary, focus_pos: Vector3) -> bool:
var focus_tile := _world_to_tile(focus_pos)
return int(state.get("tx", -9999)) == focus_tile.x and int(state.get("ty", -9999)) == focus_tile.y
func _request_terrain_control_splat_cache(state: Dictionary) -> bool:
var key := String(state.get("key", ""))
if key.is_empty():
return false
if _terrain_control_splat_cache_tasks.has(key):
return true
var cache_path := _get_terrain_control_splat_resource_path(
int(state.get("tx", 0)),
int(state.get("ty", 0)))
if not ResourceLoader.exists(cache_path):
return false
var err := ResourceLoader.load_threaded_request(
cache_path,
"",
false,
ResourceLoader.CACHE_MODE_IGNORE)
if err == OK or err == ERR_BUSY:
_terrain_control_splat_cache_tasks[key] = {
"path": cache_path,
"tx": int(state.get("tx", 0)),
"ty": int(state.get("ty", 0)),
}
return true
return false
func _load_terrain_control_splat_immediate(state: Dictionary, focus_pos: Vector3) -> Dictionary:
var key := String(state.get("key", ""))
if key.is_empty():
return state
var cache_path := _get_terrain_control_splat_resource_path(
int(state.get("tx", 0)),
int(state.get("ty", 0)))
if not ResourceLoader.exists(cache_path):
return state
var control_tile: Resource = ResourceLoader.load(cache_path, "", ResourceLoader.CACHE_MODE_IGNORE)
if control_tile == null or control_tile.get_script() != CONTROL_SPLAT_TILE_SCRIPT:
push_warning("Invalid immediate control splat tile resource: %s" % cache_path)
return state
if int(control_tile.get("format_version")) < REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION:
push_warning("Outdated immediate control splat tile resource: %s" % cache_path)
return state
var mesh: Mesh = control_tile.get("terrain_mesh")
if mesh == null:
push_warning("Immediate control splat tile has no terrain mesh: %s" % cache_path)
return state
mesh = mesh.duplicate(true)
if _builder.has_method("apply_control_splat_material"):
mesh = _builder.apply_control_splat_material(
mesh,
control_tile.get("texture_images"),
control_tile.get("alpha_atlas"),
control_tile.get("layer_index_map"))
if mesh == null or mesh.surface_get_material(0) == null:
push_warning("Immediate control splat material failed: %s" % cache_path)
return state
state["quality_terrain_mesh"] = mesh
state["quality_terrain_source"] = "control_splat_cache"
state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
_terrain_control_splat_cache_tasks.erase(key)
var cache_key := _tile_mesh_cache_key(key, 0)
_tile_mesh_cache.erase(cache_key)
_tile_mesh_cache_order.erase(cache_key)
return state
func _request_terrain_splat_cache(state: Dictionary) -> bool:
var key := String(state.get("key", ""))
if key.is_empty():
return false
if _terrain_splat_cache_tasks.has(key):
return true
var cache_path := _get_terrain_splat_resource_path(
int(state.get("tx", 0)),
int(state.get("ty", 0)))
if not ResourceLoader.exists(cache_path):
return false
var err := ResourceLoader.load_threaded_request(
cache_path,
"",
false,
ResourceLoader.CACHE_MODE_IGNORE)
if err == OK or err == ERR_BUSY:
_terrain_splat_cache_tasks[key] = {
"path": cache_path,
"tx": int(state.get("tx", 0)),
"ty": int(state.get("ty", 0)),
}
return true
return false
func _drain_terrain_upgrade_results() -> void:
var ready: Array[String] = []
var budget := maxi(0, terrain_upgrade_finalize_ops_per_tick)
if budget <= 0:
return
for key_variant in _terrain_upgrade_tasks.keys():
if ready.size() >= budget:
break
var key := String(key_variant)
var pending: Dictionary = _terrain_upgrade_tasks[key]
var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", "")))
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
ready.append(key)
var changed := false
for key in ready:
if not _terrain_upgrade_tasks.has(key):
continue
var pending: Dictionary = _terrain_upgrade_tasks[key]
_terrain_upgrade_tasks.erase(key)
var path := String(pending.get("path", ""))
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED:
continue
var baked_tile: Resource = ResourceLoader.load_threaded_get(path)
if baked_tile == null or baked_tile.get_script() != BAKED_TILE_SCRIPT:
continue
if not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
if state.get("stream_tile", null) == null:
continue
if String(state.get("quality_terrain_source", "")) == "control_splat_cache":
continue
var baked_version: int = int(baked_tile.get("format_version"))
if baked_version < REQUIRED_BAKED_TILE_FORMAT_VERSION:
continue
state["baked_tile"] = baked_tile
var full_mesh: Mesh = baked_tile.get("full_mesh")
if full_mesh != null:
full_mesh = full_mesh.duplicate(true)
if _builder.has_method("upgrade_cached_baked_mesh_materials"):
full_mesh = _builder.upgrade_cached_baked_mesh_materials(full_mesh, 1.0, -1.25, 0.35)
state["quality_terrain_mesh"] = full_mesh
state["quality_terrain_source"] = "baked_full"
_store_quality_terrain_mesh(key, full_mesh, "baked_full")
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
_tile_mesh_cache.erase(_tile_mesh_cache_key(key, 0))
_tile_mesh_cache_order.erase(_tile_mesh_cache_key(key, 0))
changed = true
if changed:
_rebuild_chunk_queues()
func _drain_terrain_control_splat_cache_results() -> void:
var ready: Array[String] = []
var budget := maxi(0, terrain_control_splat_cache_finalize_ops_per_tick)
if budget <= 0:
return
for key_variant in _terrain_control_splat_cache_tasks.keys():
if ready.size() >= budget:
break
var key := String(key_variant)
var pending: Dictionary = _terrain_control_splat_cache_tasks[key]
var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", "")))
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
ready.append(key)
var changed := false
for key in ready:
if not _terrain_control_splat_cache_tasks.has(key):
continue
var pending: Dictionary = _terrain_control_splat_cache_tasks[key]
_terrain_control_splat_cache_tasks.erase(key)
var path := String(pending.get("path", ""))
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED:
continue
var control_tile: Resource = ResourceLoader.load_threaded_get(path)
if control_tile == null or control_tile.get_script() != CONTROL_SPLAT_TILE_SCRIPT:
continue
if int(control_tile.get("format_version")) < REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION:
continue
if not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
if not bool(state.get("wanted", false)):
continue
if not _tile_within_control_splat_terrain_radius(state, _last_focus_pos):
continue
if not _can_activate_terrain_control_splat(key, state, _last_focus_pos):
continue
var mesh: Mesh = control_tile.get("terrain_mesh")
if mesh == null:
continue
mesh = mesh.duplicate(true)
if _builder.has_method("apply_control_splat_material"):
mesh = _builder.apply_control_splat_material(
mesh,
control_tile.get("texture_images"),
control_tile.get("alpha_atlas"),
control_tile.get("layer_index_map"))
state["quality_terrain_mesh"] = mesh
state["quality_terrain_source"] = "control_splat_cache"
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
var cache_key := _tile_mesh_cache_key(key, 0)
_tile_mesh_cache.erase(cache_key)
_tile_mesh_cache_order.erase(cache_key)
changed = true
if changed:
_rebuild_chunk_queues()
func _drain_terrain_splat_cache_results() -> void:
var ready: Array[String] = []
var budget := maxi(0, terrain_splat_cache_finalize_ops_per_tick)
if budget <= 0:
return
for key_variant in _terrain_splat_cache_tasks.keys():
if ready.size() >= budget:
break
var key := String(key_variant)
var pending: Dictionary = _terrain_splat_cache_tasks[key]
var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", "")))
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
ready.append(key)
var changed := false
for key in ready:
if not _terrain_splat_cache_tasks.has(key):
continue
var pending: Dictionary = _terrain_splat_cache_tasks[key]
_terrain_splat_cache_tasks.erase(key)
var path := String(pending.get("path", ""))
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED:
continue
var splat_tile: Resource = ResourceLoader.load_threaded_get(path)
if splat_tile == null or splat_tile.get_script() != SPLAT_TILE_SCRIPT:
continue
if int(splat_tile.get("format_version")) < REQUIRED_SPLAT_TILE_FORMAT_VERSION:
continue
if not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
if not bool(state.get("wanted", false)):
continue
if not _tile_within_splat_terrain_radius(state, _last_focus_pos):
continue
if not _can_activate_terrain_splat(key, state, _last_focus_pos):
continue
var mesh: Mesh = splat_tile.get("terrain_mesh")
if mesh == null:
continue
state["quality_terrain_mesh"] = mesh
state["quality_terrain_source"] = "splat_cache"
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
var cache_key := _tile_mesh_cache_key(key, 0)
_tile_mesh_cache.erase(cache_key)
_tile_mesh_cache_order.erase(cache_key)
changed = true
if changed:
_rebuild_chunk_queues()
func _build_high_quality_stream_terrain_mesh(stream_mesh: Mesh, full_mesh: Mesh) -> Mesh:
if stream_mesh == null or full_mesh == null:
return null
var upgraded := stream_mesh.duplicate(true) as ArrayMesh
if upgraded == null:
return null
var surface_count := mini(upgraded.get_surface_count(), full_mesh.get_surface_count())
for surface_idx in surface_count:
var material := full_mesh.surface_get_material(surface_idx)
if material != null:
upgraded.surface_set_material(surface_idx, material)
return upgraded
func _load_terrain_splat_task(key: String, adt_path: String) -> void:
var data: Dictionary = {}
if not adt_path.is_empty():
var loader = ClassDB.instantiate("ADTLoader")
if loader:
data = loader.call("load_adt", adt_path)
_terrain_splat_result_mutex.lock()
_terrain_splat_result_queue.append({
"key": key,
"data": data,
})
_terrain_splat_result_mutex.unlock()
func _drain_terrain_splat_results() -> void:
var results: Array = []
var budget := maxi(0, terrain_splat_builds_per_tick)
if budget <= 0:
return
_terrain_splat_result_mutex.lock()
while budget > 0 and not _terrain_splat_result_queue.is_empty():
results.append(_terrain_splat_result_queue.pop_front())
budget -= 1
_terrain_splat_result_mutex.unlock()
var changed := false
for result_variant in results:
if not (result_variant is Dictionary):
continue
var result: Dictionary = result_variant
var key := String(result.get("key", ""))
if _terrain_splat_tasks.has(key):
WorkerThreadPool.wait_for_task_completion(int(_terrain_splat_tasks[key]))
_terrain_splat_tasks.erase(key)
if not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
if not bool(state.get("wanted", false)):
continue
if not _tile_within_splat_terrain_radius(state, _last_focus_pos):
continue
if not _can_activate_terrain_splat(key, state, _last_focus_pos):
continue
var data: Dictionary = result.get("data", {})
if data.is_empty():
continue
var payload: Dictionary = _builder.build_tile_coarse_render_payload(
data,
_shared_tex_cache,
ProjectSettings.globalize_path(extracted_dir),
maxi(0, terrain_splat_lod))
var mesh: Mesh = payload.get("mesh", null)
if mesh == null:
continue
state["quality_terrain_mesh"] = mesh
state["quality_terrain_source"] = "splat"
state["data"] = data
state["tex_names"] = data.get("textures", PackedStringArray())
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
var cache_key := _tile_mesh_cache_key(key, 0)
_tile_mesh_cache.erase(cache_key)
_tile_mesh_cache_order.erase(cache_key)
changed = true
if changed:
_rebuild_chunk_queues()
func _request_tile_water_load(state: Dictionary) -> void:
if not enable_water:
return
if bool(state.get("water_loaded", false)):
return
var key := String(state.get("key", ""))
var path := String(state.get("path", ""))
if key.is_empty() or path.is_empty():
return
if _water_load_tasks.has(key) or _water_load_queued.has(key):
return
_water_load_queue.append({
"key": key,
"path": path,
})
_water_load_queued[key] = true
func _process_water_load_queue() -> void:
if not enable_water:
return
var limit := maxi(1, max_concurrent_water_tasks)
while _water_load_tasks.size() < limit and not _water_load_queue.is_empty():
var request: Dictionary = _water_load_queue.pop_front()
var key := String(request.get("key", ""))
_water_load_queued.erase(key)
if key.is_empty() or _water_load_tasks.has(key):
continue
if not _tile_states.has(key):
continue
var path := String(request.get("path", ""))
if path.is_empty():
continue
var task_id: int = WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))
_water_load_tasks[key] = task_id
func _load_tile_water_task(key: String, adt_path: String) -> void:
var data: Dictionary = {}
var loader = ClassDB.instantiate("ADTLoader")
if loader:
data = loader.call("load_adt", adt_path)
_water_result_mutex.lock()
_water_result_queue.append({
"key": key,
"path": adt_path,
"data": data,
})
_water_result_mutex.unlock()
func _drain_water_load_results() -> void:
var results: Array = []
var budget := maxi(0, water_finalize_ops_per_tick)
if budget <= 0:
return
_water_result_mutex.lock()
while budget > 0 and not _water_result_queue.is_empty():
results.append(_water_result_queue.pop_front())
budget -= 1
_water_result_mutex.unlock()
for result in results:
var key := String(result.get("key", ""))
if key.is_empty():
continue
if not _water_load_tasks.has(key):
continue
WorkerThreadPool.wait_for_task_completion(int(_water_load_tasks[key]))
_water_load_tasks.erase(key)
if _shutting_down or not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
if String(state.get("path", "")) != String(result.get("path", "")):
continue
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
continue
var data: Dictionary = result.get("data", {})
if not data.is_empty():
var origin: Vector3 = state.get("origin", Vector3.ZERO)
var water_root: Node3D = _builder.build_tile_water_scene(data, origin)
if water_root:
tile_root.add_child(water_root)
_set_editor_owner_recursive(water_root)
state["water_loaded"] = true
_tile_states[key] = state
func _wait_for_tile_tasks() -> void:
for pending in _tile_loading_tasks.values():
if pending.get("mode", "raw") == "raw":
WorkerThreadPool.wait_for_task_completion(int(pending["task_id"]))
elif pending.get("mode", "raw") == "baked" or pending.get("mode", "raw") == "stream":
var path: String = String(pending.get("path", ""))
if not path.is_empty():
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_tile_loading_tasks.clear()
for task_id in _m2_group_tasks.values():
WorkerThreadPool.wait_for_task_completion(int(task_id))
_m2_group_tasks.clear()
_tile_result_mutex.lock()
_tile_result_queue.clear()
_tile_result_mutex.unlock()
_m2_group_result_mutex.lock()
_m2_group_result_queue.clear()
_m2_group_result_mutex.unlock()
for pending in _m2_mesh_load_requests.values():
var path: String = String(pending.get("path", ""))
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.clear()
_m2_runtime_rebuild_required_cache.clear()
for pending in _m2_animation_load_requests.values():
var path: String = String(pending.get("path", ""))
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
for path_variant in _wmo_scene_load_requests.values():
var path := String(path_variant)
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_scene_load_requests.clear()
for path_variant in _wmo_render_load_requests.values():
var path := String(path_variant)
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear()
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear()
for pending in _terrain_upgrade_tasks.values():
var path := String(pending.get("path", ""))
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_upgrade_tasks.clear()
for pending in _terrain_control_splat_cache_tasks.values():
if not (pending is Dictionary):
continue
var path := String(pending.get("path", ""))
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_control_splat_cache_tasks.clear()
for task_id in _terrain_splat_tasks.values():
WorkerThreadPool.wait_for_task_completion(int(task_id))
_terrain_splat_tasks.clear()
for pending in _terrain_splat_cache_tasks.values():
if not (pending is Dictionary):
continue
var path := String(pending.get("path", ""))
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_splat_cache_tasks.clear()
_terrain_splat_result_mutex.lock()
_terrain_splat_result_queue.clear()
_terrain_splat_result_mutex.unlock()
for task_id in _water_load_tasks.values():
WorkerThreadPool.wait_for_task_completion(int(task_id))
_water_load_tasks.clear()
_water_load_queue.clear()
_water_load_queued.clear()
_water_result_mutex.lock()
_water_result_queue.clear()
_water_result_mutex.unlock()
func _should_accept_loaded_tile(request: Dictionary) -> bool:
if _tile_states.has(request["key"]):
return false
if not _available_tiles.has(request["key"]):
return false
return _is_tile_wanted_now(int(request["tx"]), int(request["ty"]))
func _is_tile_wanted_now(tx: int, ty: int) -> bool:
var focus_tile := _world_to_tile(_last_focus_pos)
var radius: int
if Engine.is_editor_hint():
radius = maxi(0, editor_preview_tile_radius)
else:
radius = _runtime_load_tile_radius()
return abs(tx - focus_tile.x) <= radius and abs(ty - focus_tile.y) <= radius
func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
var key: String = request["key"]
if _tile_states.has(key):
return
if data.is_empty() or not data.has("chunks"):
push_warning("Failed to load ADT tile: %s" % request["path"])
return
var tile_root := Node3D.new()
tile_root.name = "Tile_%d_%d" % [request["tx"], request["ty"]]
_terrain_root.add_child(tile_root)
_set_editor_owner_recursive(tile_root)
var tile_origin: Vector3 = _builder.get_tile_origin_for_data(data)
tile_root.position = tile_origin
if enable_water:
var water_root: Node3D = _builder.build_tile_water_scene(data, tile_origin)
if water_root:
tile_root.add_child(water_root)
_set_editor_owner_recursive(water_root)
if enable_m2_placeholders:
_build_tile_m2_placeholders(
tile_root,
tile_origin,
data.get("m2_names", PackedStringArray()),
data.get("m2_placements", []))
var state := {
"key": key,
"tx": request["tx"],
"ty": request["ty"],
"path": request["path"],
"origin": tile_origin,
"data": data,
"baked_tile": null,
"tex_names": data.get("textures", PackedStringArray()),
"chunks": {},
"desired_lods": {},
"tile_lod": -1,
"tile_lod_node": null,
"tile_lod_rid": RID(),
"tile_lod_mesh": null,
"tile_lod_visible": false,
"prepared_tile_lod": -1,
"prepared_tile_lod_rid": RID(),
"prepared_tile_lod_mesh": null,
"desired_tile_lod": -1,
"desired_tile_lod_visible": false,
"wanted": true,
"retained": true,
"root": tile_root,
"water_loaded": true,
"m2_names": data.get("m2_names", PackedStringArray()),
"m2_placements": data.get("m2_placements", []),
"m2_built": false,
"m2_unique_keys": [],
"m2_skipped_unique_keys": [],
"wmo_names": data.get("wmo_names", PackedStringArray()),
"wmo_placements": data.get("wmo_placements", []),
"wmo_refs": [],
}
# Use _last_focus_pos — already set correctly for both editor and game.
state["desired_lods"] = _compute_desired_lods(state, _last_focus_pos)
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
state = _sync_terrain_quality(state, _last_focus_pos)
state = _sync_detail_assets(state, _last_focus_pos)
_tile_states[key] = state
_rebuild_chunk_queues()
if debug_streaming:
print("StreamingWorld: loaded tile %d,%d" % [request["tx"], request["ty"]])
func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> void:
var key: String = request["key"]
if _tile_states.has(key):
return
var baked_version: int = int(baked_tile.get("format_version"))
if baked_version < REQUIRED_BAKED_TILE_FORMAT_VERSION:
push_warning(
"Outdated baked tile cache for %s_%d_%d (format=%d, required=%d). Falling back to raw ADT load." % [
map_name, request["tx"], request["ty"], baked_version, REQUIRED_BAKED_TILE_FORMAT_VERSION
]
)
_start_raw_tile_load_async(request)
return
var tile_root := Node3D.new()
tile_root.name = "Tile_%d_%d" % [request["tx"], request["ty"]]
_terrain_root.add_child(tile_root)
_set_editor_owner_recursive(tile_root)
var tile_origin: Vector3 = baked_tile.get("tile_origin")
tile_root.position = tile_origin
var baked_m2_names_value: Variant = baked_tile.get("m2_names")
var baked_m2_placements_value: Variant = baked_tile.get("m2_placements")
var baked_m2_names: PackedStringArray = baked_m2_names_value if baked_m2_names_value is PackedStringArray else PackedStringArray()
var baked_m2_placements: Array = baked_m2_placements_value if baked_m2_placements_value is Array else []
var baked_wmo_names_value: Variant = baked_tile.get("wmo_names")
var baked_wmo_placements_value: Variant = baked_tile.get("wmo_placements")
var baked_wmo_names: PackedStringArray = baked_wmo_names_value if baked_wmo_names_value is PackedStringArray else PackedStringArray()
var baked_wmo_placements: Array = baked_wmo_placements_value if baked_wmo_placements_value is Array else []
var state := {
"key": key,
"tx": request["tx"],
"ty": request["ty"],
"path": request["path"],
"origin": tile_origin,
"data": {},
"baked_tile": baked_tile,
"tex_names": PackedStringArray(),
"chunks": {},
"desired_lods": {},
"tile_lod": -1,
"tile_lod_node": null,
"tile_lod_rid": RID(),
"tile_lod_mesh": null,
"tile_lod_visible": false,
"prepared_tile_lod": -1,
"prepared_tile_lod_rid": RID(),
"prepared_tile_lod_mesh": null,
"desired_tile_lod": -1,
"desired_tile_lod_visible": false,
"wanted": true,
"retained": true,
"root": tile_root,
"water_loaded": false,
"m2_names": baked_m2_names,
"m2_placements": baked_m2_placements,
"m2_built": false,
"m2_unique_keys": [],
"m2_skipped_unique_keys": [],
"wmo_names": baked_wmo_names,
"wmo_placements": baked_wmo_placements,
"wmo_refs": [],
}
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
_request_tile_water_load(state)
state = _sync_detail_assets(state, _last_focus_pos)
_tile_states[key] = state
_rebuild_chunk_queues()
if debug_streaming:
print("StreamingWorld: loaded baked tile %d,%d" % [request["tx"], request["ty"]])
func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource) -> void:
var key: String = request["key"]
if _tile_states.has(key):
return
var stream_version: int = int(stream_tile.get("format_version"))
if stream_version < STREAMING_TILE_SCRIPT.FORMAT_VERSION:
push_warning(
"Outdated streaming tile cache for %s_%d_%d (format=%d, required=%d). Falling back to baked/raw ADT load." % [
map_name, request["tx"], request["ty"], stream_version, STREAMING_TILE_SCRIPT.FORMAT_VERSION
]
)
_start_raw_tile_load_async(request)
return
var tile_root := Node3D.new()
tile_root.name = "Tile_%d_%d" % [request["tx"], request["ty"]]
_terrain_root.add_child(tile_root)
_set_editor_owner_recursive(tile_root)
var tile_origin: Vector3 = stream_tile.get("tile_origin")
tile_root.position = tile_origin
var stream_m2_names_value: Variant = stream_tile.get("m2_names")
var stream_m2_placements_value: Variant = stream_tile.get("m2_placements")
var stream_m2_names: PackedStringArray = stream_m2_names_value if stream_m2_names_value is PackedStringArray else PackedStringArray()
var stream_m2_placements: Array = stream_m2_placements_value if stream_m2_placements_value is Array else []
var stream_wmo_names_value: Variant = stream_tile.get("wmo_names")
var stream_wmo_placements_value: Variant = stream_tile.get("wmo_placements")
var stream_wmo_names: PackedStringArray = stream_wmo_names_value if stream_wmo_names_value is PackedStringArray else PackedStringArray()
var stream_wmo_placements: Array = stream_wmo_placements_value if stream_wmo_placements_value is Array else []
var state := {
"key": key,
"tx": request["tx"],
"ty": request["ty"],
"path": request["path"],
"origin": tile_origin,
"data": {},
"baked_tile": null,
"stream_tile": stream_tile,
"tex_names": PackedStringArray(),
"chunks": {},
"desired_lods": {},
"tile_lod": -1,
"tile_lod_node": null,
"tile_lod_rid": RID(),
"tile_lod_mesh": null,
"tile_lod_visible": false,
"prepared_tile_lod": -1,
"prepared_tile_lod_rid": RID(),
"prepared_tile_lod_mesh": null,
"desired_tile_lod": -1,
"desired_tile_lod_visible": false,
"wanted": true,
"retained": true,
"root": tile_root,
"water_loaded": false,
"m2_names": stream_m2_names,
"m2_placements": stream_m2_placements,
"m2_built": false,
"m2_unique_keys": [],
"m2_skipped_unique_keys": [],
"wmo_names": stream_wmo_names,
"wmo_placements": stream_wmo_placements,
"wmo_refs": [],
}
state = _restore_quality_terrain_mesh(key, state)
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
_tile_states[key] = state
_request_tile_water_load(state)
state = _sync_terrain_quality(state, _last_focus_pos)
state = _sync_detail_assets(state, _last_focus_pos)
_tile_states[key] = state
_rebuild_chunk_queues()
if debug_streaming:
print("StreamingWorld: loaded streaming tile %d,%d" % [request["tx"], request["ty"]])
func _create_tile_lod(op: Dictionary) -> void:
var key: String = op["tile"]
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
var lod: int = int(op["lod"])
var visible: bool = bool(op.get("visible", true))
var prepare_only: bool = bool(op.get("prepare", false))
if int(state.get("tile_lod", -1)) == lod:
_set_tile_lod_visible(state, visible)
state["tile_lod_visible"] = visible
_tile_states[key] = state
return
if prepare_only:
if int(state.get("prepared_tile_lod", -1)) == lod:
return
var prepared_mesh := _get_tile_lod_mesh_for_state(key, state, lod)
if prepared_mesh == null or not use_rendering_server_for_terrain:
return
var prepared_rid := _create_render_instance(
prepared_mesh,
_tile_local_to_world_transform(state, Vector3.ZERO))
if not prepared_rid.is_valid():
return
RenderingServer.instance_set_visible(prepared_rid, false)
var old_prepared_rid: RID = state.get("prepared_tile_lod_rid", RID())
if old_prepared_rid.is_valid():
_free_render_instance(old_prepared_rid)
state["prepared_tile_lod"] = lod
state["prepared_tile_lod_rid"] = prepared_rid
state["prepared_tile_lod_mesh"] = prepared_mesh
_tile_states[key] = state
return
var old_node: Node = state.get("tile_lod_node", null)
var old_rid: RID = state.get("tile_lod_rid", RID())
var prepared_lod: int = int(state.get("prepared_tile_lod", -1))
var prepared_rid: RID = state.get("prepared_tile_lod_rid", RID())
if prepared_lod == lod and prepared_rid.is_valid():
state["tile_lod"] = lod
state["tile_lod_node"] = null
state["tile_lod_rid"] = prepared_rid
state["tile_lod_mesh"] = state.get("prepared_tile_lod_mesh", null)
state["prepared_tile_lod"] = -1
state["prepared_tile_lod_rid"] = RID()
state["prepared_tile_lod_mesh"] = null
RenderingServer.instance_set_visible(prepared_rid, visible)
if old_rid.is_valid():
_free_render_instance(old_rid)
if old_node:
old_node.queue_free()
state["tile_lod_visible"] = visible
_tile_states[key] = state
return
state["tile_lod"] = lod
state["tile_lod_node"] = null
state["tile_lod_rid"] = RID()
state["tile_lod_mesh"] = null
var mesh := _get_tile_lod_mesh_for_state(key, state, lod)
if use_rendering_server_for_terrain and mesh:
var instance_rid := _create_render_instance(
mesh,
_tile_local_to_world_transform(state, Vector3.ZERO))
if instance_rid.is_valid():
RenderingServer.instance_set_visible(instance_rid, visible)
state["tile_lod_rid"] = instance_rid
state["tile_lod_mesh"] = mesh
if not state["tile_lod_rid"].is_valid() and mesh:
var node := MeshInstance3D.new()
node.name = "TileLOD%d" % lod
node.mesh = mesh
node.visible = visible
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if terrain_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
state["tile_lod_node"] = node
state["tile_lod_mesh"] = mesh
state["root"].add_child(node)
_set_editor_owner_recursive(node)
if old_rid.is_valid():
_free_render_instance(old_rid)
if old_node:
old_node.queue_free()
state["tile_lod_visible"] = visible
_tile_states[key] = state
func _get_tile_lod_mesh_for_state(key: String, state: Dictionary, lod: int) -> Mesh:
if lod <= 0:
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
if quality_mesh != null:
return quality_mesh
var cache_key := _tile_mesh_cache_key(key, lod)
var mesh: Mesh = _get_cached_tile_mesh(cache_key)
if mesh != null:
return mesh
mesh = _get_baked_tile_mesh(state, lod)
if mesh == null:
var payload: Dictionary = _builder.build_tile_coarse_render_payload(
state["data"],
_shared_tex_cache,
ProjectSettings.globalize_path(extracted_dir),
lod)
if not payload.is_empty():
mesh = payload.get("mesh", null)
if mesh:
_store_cached_tile_mesh(cache_key, mesh)
return mesh
func _create_chunk(op: Dictionary) -> void:
var key: String = op["tile"]
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
var chunk_id: int = int(op["chunk"])
var lod: int = int(op["lod"])
var chunks: Array = state["data"].get("chunks", [])
if chunk_id < 0 or chunk_id >= chunks.size():
return
var chunk: Dictionary = chunks[chunk_id]
if chunk.is_empty():
return
var current_chunks: Dictionary = state["chunks"]
if current_chunks.has(chunk_id):
_remove_chunk({"tile": key, "chunk": chunk_id})
current_chunks = state["chunks"]
var instance_rid := RID()
var node: MeshInstance3D = null
var mesh: Mesh = null
if use_rendering_server_for_terrain:
var payload: Dictionary = _builder.build_chunk_render_payload(
chunk,
state["tex_names"],
_shared_tex_cache,
ProjectSettings.globalize_path(extracted_dir),
lod,
state.get("origin", Vector3.ZERO))
if not payload.is_empty():
mesh = payload.get("mesh", null)
instance_rid = _create_render_instance(
mesh,
_tile_local_to_world_transform(state, payload.get("position", Vector3.ZERO)))
if not instance_rid.is_valid():
node = _builder.build_chunk_scene(
chunk,
state["tex_names"],
_shared_tex_cache,
ProjectSettings.globalize_path(extracted_dir),
lod,
state.get("origin", Vector3.ZERO))
if node == null and not instance_rid.is_valid():
_dbg_chunks_null += 1
current_chunks[chunk_id] = {
"node": null,
"rid": RID(),
"mesh": null,
"lod": lod,
"empty": true,
}
state["chunks"] = current_chunks
_tile_states[key] = state
if debug_streaming and _dbg_chunks_null <= 3:
var h: PackedFloat32Array = chunk.get("heights", PackedFloat32Array())
print("DBG NULL chunk tile=%s id=%d lod=%d heights=%d origin=%s" % [
key, chunk_id, lod, h.size(), chunk.get("origin", Vector3.ZERO)])
return
if node:
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if terrain_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
state["root"].add_child(node)
_set_editor_owner_recursive(node)
_dbg_chunks_created += 1
if debug_streaming and _dbg_chunks_created <= 3:
var dbg_origin := node.global_position if node else _tile_local_to_world_transform(
state,
chunk.get("origin", Vector3.ZERO) - state.get("origin", Vector3.ZERO)).origin
print("DBG CHUNK ADDED tile=%s id=%d lod=%d world_pos=%s rs=%s" % [
key, chunk_id, lod, dbg_origin, instance_rid.is_valid()])
current_chunks[chunk_id] = {
"node": node,
"rid": instance_rid,
"mesh": mesh,
"lod": lod,
}
state["chunks"] = current_chunks
_tile_states[key] = state
func _remove_chunk(op: Dictionary) -> void:
var key: String = op["tile"]
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
var chunk_id: int = int(op["chunk"])
var current_chunks: Dictionary = state["chunks"]
if not current_chunks.has(chunk_id):
return
var entry: Dictionary = current_chunks[chunk_id]
var node: Node = entry.get("node", null)
var instance_rid: RID = entry.get("rid", RID())
if instance_rid.is_valid():
_free_render_instance(instance_rid)
if node:
node.queue_free()
current_chunks.erase(chunk_id)
state["chunks"] = current_chunks
if not bool(state.get("retained", false)) and current_chunks.is_empty():
_release_tile_if_unused(key)
else:
_tile_states[key] = state
func _remove_tile_lod(op: Dictionary) -> void:
var key: String = op["tile"]
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
var node: Node = state.get("tile_lod_node", null)
var instance_rid: RID = state.get("tile_lod_rid", RID())
if instance_rid.is_valid():
_free_render_instance(instance_rid)
if node:
node.queue_free()
state["tile_lod_node"] = null
state["tile_lod_rid"] = RID()
state["tile_lod_mesh"] = null
state["tile_lod_visible"] = false
state["tile_lod"] = -1
if not bool(state.get("retained", false)) and state["chunks"].is_empty():
_release_tile_if_unused(key)
else:
_tile_states[key] = state
func _set_tile_lod_visible(state: Dictionary, visible: bool) -> void:
var instance_rid: RID = state.get("tile_lod_rid", RID())
if instance_rid.is_valid():
RenderingServer.instance_set_visible(instance_rid, visible)
var node: Node = state.get("tile_lod_node", null)
if node and is_instance_valid(node):
node.visible = visible
func _release_tile(key: String) -> void:
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
_unregister_tile_wmos(state)
_remove_tile_m2_assets(state)
_terrain_upgrade_tasks.erase(key)
_terrain_control_splat_cache_tasks.erase(key)
_terrain_splat_cache_tasks.erase(key)
_terrain_splat_tasks.erase(key)
_water_load_queued.erase(key)
for i in range(_water_load_queue.size() - 1, -1, -1):
var pending: Dictionary = _water_load_queue[i]
if String(pending.get("key", "")) == key:
_water_load_queue.remove_at(i)
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
if tile_lod_rid.is_valid():
_free_render_instance(tile_lod_rid)
var prepared_tile_lod_rid: RID = state.get("prepared_tile_lod_rid", RID())
if prepared_tile_lod_rid.is_valid():
_free_render_instance(prepared_tile_lod_rid)
for entry in state.get("chunks", {}).values():
var chunk_rid: RID = entry.get("rid", RID())
if chunk_rid.is_valid():
_free_render_instance(chunk_rid)
var tile_root: Node = state.get("root", null)
if tile_root:
tile_root.queue_free()
_remove_tile_mesh_cache_entries(key)
_tile_states.erase(key)
func _clear_streamed_world() -> void:
_tile_load_queue.clear()
_chunk_remove_queue.clear()
_chunk_create_queue.clear()
_tile_lod_remove_queue.clear()
_tile_lod_create_queue.clear()
_terrain_upgrade_tasks.clear()
_terrain_control_splat_cache_tasks.clear()
_terrain_splat_cache_tasks.clear()
_terrain_splat_tasks.clear()
_terrain_splat_result_mutex.lock()
_terrain_splat_result_queue.clear()
_terrain_splat_result_mutex.unlock()
_water_load_queue.clear()
_water_load_queued.clear()
_water_load_tasks.clear()
_water_result_mutex.lock()
_water_result_queue.clear()
_water_result_mutex.unlock()
_detail_asset_queue.clear()
_detail_asset_queued.clear()
for key in _m2_build_jobs.keys():
_cancel_m2_build_job(String(key))
_m2_build_queue.clear()
_m2_unique_registry.clear()
_m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.clear()
_m2_runtime_rebuild_required_cache.clear()
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
_wmo_build_jobs.clear()
_wmo_build_queue.clear()
_wmo_scene_load_requests.clear()
_wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear()
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear()
_tile_result_mutex.lock()
_tile_result_queue.clear()
_tile_result_mutex.unlock()
for key in _tile_states.keys():
var state: Dictionary = _tile_states[key]
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
if tile_lod_rid.is_valid():
_free_render_instance(tile_lod_rid)
var prepared_tile_lod_rid: RID = state.get("prepared_tile_lod_rid", RID())
if prepared_tile_lod_rid.is_valid():
_free_render_instance(prepared_tile_lod_rid)
for entry in state.get("chunks", {}).values():
var chunk_rid: RID = entry.get("rid", RID())
if chunk_rid.is_valid():
_free_render_instance(chunk_rid)
var tile_root: Node = state.get("root", null)
if tile_root:
_queue_free_streamed_node(tile_root)
_tile_states.clear()
_clear_tile_mesh_cache()
_clear_quality_terrain_mesh_cache()
_clear_wmo_registry()
if not _shutting_down:
_ensure_world_wmo_root()
if enable_wmo:
_place_global_wmo()
## During world shutdown, parented nodes are recursively owned by the world
## root. Queueing them separately from `_exit_tree()` can detach them from that
## deletion and leave empty Node objects alive at engine teardown.
func _queue_free_streamed_node(node: Node) -> void:
if node == null or not is_instance_valid(node):
return
if _shutting_down and node.is_inside_tree():
return
node.queue_free()
func _reset_terrain_root_children() -> void:
if _terrain_root == null:
return
for child in _terrain_root.get_children():
_terrain_root.remove_child(child)
child.free()
func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary:
if not use_chunk_terrain_lods:
return {}
var desired := {}
var chunks: Array = state["data"].get("chunks", [])
var lod0_sq: float = (lod0_radius_chunks * CHUNK_SIZE) ** 2
var lod1_sq: float = (lod1_radius_chunks * CHUNK_SIZE) ** 2
var lod2_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2
for chunk_id in range(chunks.size()):
var chunk: Dictionary = chunks[chunk_id]
if chunk.is_empty():
continue
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
var center := origin + Vector3(CHUNK_SIZE * 0.5, 0.0, CHUNK_SIZE * 0.5)
var dx := center.x - cam_pos.x
var dz := center.z - cam_pos.z
var dist_sq := dx * dx + dz * dz
if dist_sq <= lod0_sq: desired[chunk_id] = 0
elif dist_sq <= lod1_sq: desired[chunk_id] = 1
elif dist_sq <= lod2_sq: desired[chunk_id] = 2
return desired
func _runtime_visible_tile_radius() -> int:
var chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin
return maxi(chunk_tile_radius, lod2_tile_radius)
func _runtime_load_tile_radius() -> int:
return _runtime_visible_tile_radius() + maxi(0, prewarm_tile_margin)
func _runtime_retain_tile_radius() -> int:
return _runtime_load_tile_radius() + maxi(0, retain_tile_margin)
func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
var base := _world_to_tile(focus_pos)
var result: Array[Vector2i] = [base]
var threshold: float = clampf(boundary_prefetch_threshold, 0.0, 0.49)
if threshold <= 0.0:
return result
var local_x := fposmod(focus_pos.x, TILE_SIZE) / TILE_SIZE
var local_z := fposmod(focus_pos.z, TILE_SIZE) / TILE_SIZE
var x_offsets: Array[int] = [0]
var z_offsets: Array[int] = [0]
if local_x <= threshold:
x_offsets.append(-1)
elif local_x >= 1.0 - threshold:
x_offsets.append(1)
if local_z <= threshold:
z_offsets.append(-1)
elif local_z >= 1.0 - threshold:
z_offsets.append(1)
for ox in x_offsets:
for oz in z_offsets:
if ox == 0 and oz == 0:
continue
result.append(Vector2i(base.x + ox, base.y + oz))
return result
func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int:
var tile_dist_sq: float = _tile_dist_sq(state, cam_pos)
var chunk_radius_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2
var tile_radius_sq: float = (lod2_tile_radius * TILE_SIZE) ** 2
var prewarm_radius_sq: float = (_runtime_load_tile_radius() * TILE_SIZE) ** 2
if state.get("stream_tile", null) != null:
if _state_has_applicable_quality_terrain(state, cam_pos):
return 0
if _should_hide_terrain_fallback_for_quality(state, cam_pos):
return -1
if state.get("baked_tile", null) != null and _tile_within_full_terrain_radius(state, cam_pos):
return 0
return 3 if tile_dist_sq <= prewarm_radius_sq else -1
if not use_chunk_terrain_lods:
if tile_dist_sq <= chunk_radius_sq:
return 0
if tile_dist_sq <= tile_radius_sq:
return 3
if prewarm_tile_lod_instances and tile_dist_sq <= prewarm_radius_sq:
return 3
return -1
if tile_dist_sq <= chunk_radius_sq:
return -1
if tile_dist_sq <= tile_radius_sq:
return 3
return -1
func _compute_desired_tile_lod_visible(state: Dictionary, cam_pos: Vector3) -> bool:
var tile_dist_sq: float = _tile_dist_sq(state, cam_pos)
var chunk_radius_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2
var tile_radius_sq: float = (lod2_tile_radius * TILE_SIZE) ** 2
return tile_dist_sq <= maxf(chunk_radius_sq, tile_radius_sq)
func _tile_within_full_terrain_radius(state: Dictionary, cam_pos: Vector3) -> bool:
var radius_tiles := maxi(0, terrain_full_quality_radius_tiles)
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
return _tile_dist_sq(state, cam_pos) <= radius_sq
func _tile_within_control_splat_terrain_radius(state: Dictionary, cam_pos: Vector3) -> bool:
var radius_tiles := maxi(0, terrain_control_splat_radius_tiles)
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
return _tile_dist_sq(state, cam_pos) <= radius_sq
func _tile_within_splat_terrain_radius(state: Dictionary, cam_pos: Vector3) -> bool:
var radius_tiles := maxi(0, terrain_splat_radius_tiles)
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
return _tile_dist_sq(state, cam_pos) <= radius_sq
func _should_hide_terrain_fallback_for_quality(state: Dictionary, cam_pos: Vector3) -> bool:
if not terrain_control_splat_hide_fallback:
return false
if not terrain_control_splat_primary or not terrain_control_splat_quality_enabled:
return false
if not bool(state.get("wanted", false)):
return false
var radius_tiles := maxi(0, terrain_control_splat_hide_fallback_radius_tiles)
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
if _tile_dist_sq(state, cam_pos) > radius_sq:
return false
if String(state.get("quality_terrain_source", "")) == "control_splat_cache":
return false
var control_path := _get_terrain_control_splat_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0)))
return ResourceLoader.exists(control_path)
func _should_prepare_full_tile_lod(state: Dictionary, cam_pos: Vector3) -> bool:
if use_chunk_terrain_lods or not prewarm_tile_lod_instances:
return false
if state.get("stream_tile", null) != null:
return false
if int(state.get("tile_lod", -1)) <= 0:
return false
if int(state.get("prepared_tile_lod", -1)) == 0:
return false
var tile_dist_sq: float = _tile_dist_sq(state, cam_pos)
var chunk_radius: float = float(lod2_radius_chunks) * CHUNK_SIZE
var prepare_radius: float = chunk_radius + maxf(0.0, full_lod_prewarm_tiles) * TILE_SIZE
return tile_dist_sq <= prepare_radius * prepare_radius
func _is_tile_chunk_set_ready(state: Dictionary) -> bool:
var desired_lods: Dictionary = state.get("desired_lods", {})
var current_chunks: Dictionary = state.get("chunks", {})
for chunk_id in desired_lods.keys():
if not current_chunks.has(chunk_id):
return false
if int(current_chunks[chunk_id].get("lod", -999)) != int(desired_lods[chunk_id]):
return false
return true
func _tile_dist_sq(state: Dictionary, cam_pos: Vector3) -> float:
var min_x: float = float(state["tx"]) * TILE_SIZE
var max_x: float = min_x + TILE_SIZE
var min_z: float = float(state["ty"]) * TILE_SIZE
var max_z: float = min_z + TILE_SIZE
var dx := 0.0
if cam_pos.x < min_x:
dx = min_x - cam_pos.x
elif cam_pos.x > max_x:
dx = cam_pos.x - max_x
var dz := 0.0
if cam_pos.z < min_z:
dz = min_z - cam_pos.z
elif cam_pos.z > max_z:
dz = cam_pos.z - max_z
return dx * dx + dz * dz
func _is_tile_queued(key: String) -> bool:
if _tile_loading_tasks.has(key):
return true
for request in _tile_load_queue:
if request["key"] == key:
return true
return false
func _capture_streaming_focus_from_source() -> void:
if streaming_focus_source_path == NodePath():
return
var focus_source := get_node_or_null(streaming_focus_source_path) as Node3D
if focus_source == null:
if not _missing_focus_source_reported:
push_warning("Streaming focus source is missing or is not Node3D: %s" % streaming_focus_source_path)
_missing_focus_source_reported = true
return
_missing_focus_source_reported = false
_set_streaming_focus_from_vector3(focus_source.global_position)
func _set_streaming_focus_from_vector3(world_position: Vector3) -> void:
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(
world_position.x,
world_position.y,
world_position.z
)
set_streaming_focus(STREAMING_FOCUS_SCRIPT.new(typed_world_position))
func _streaming_focus_to_vector3() -> Vector3:
var world_position: GodotWorldPosition = _streaming_focus.world_position
return Vector3(world_position.x_units, world_position.y_units, world_position.z_units)
func _get_editor_view_camera() -> Camera3D:
# EditorInterface owns this camera. This method is the explicit editor adapter;
# runtime streaming never discovers a viewport camera.
var editor_viewport := EditorInterface.get_editor_viewport_3d(0)
if editor_viewport == null:
return null
return editor_viewport.get_camera_3d()
func _get_auto_position_camera() -> Camera3D:
if camera_path == NodePath():
return null
return get_node_or_null(camera_path) as Camera3D
func _can_use_baked_tile_cache() -> bool:
return use_baked_tile_cache and not use_chunk_terrain_lods
func _get_baked_tile_resource_path(tx: int, ty: int) -> String:
return baked_tile_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty])
func _get_streaming_tile_resource_path(tx: int, ty: int) -> String:
return streaming_tile_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty])
func _get_terrain_control_splat_resource_path(tx: int, ty: int) -> String:
return terrain_control_splat_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty])
func _get_terrain_splat_resource_path(tx: int, ty: int) -> String:
return terrain_splat_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty])
func _terrain_splat_queue_size() -> int:
return _terrain_splat_cache_tasks.size() + _terrain_splat_tasks.size() + _terrain_splat_result_queue.size()
func _state_has_applicable_quality_terrain(state: Dictionary, cam_pos: Vector3) -> bool:
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
if quality_mesh == null:
return false
var quality_source := String(state.get("quality_terrain_source", ""))
if quality_source == "control_splat_cache":
return _tile_within_control_splat_terrain_radius(state, cam_pos)
if quality_source == "splat_cache" or quality_source == "splat":
return _tile_within_splat_terrain_radius(state, cam_pos)
if quality_source == "baked_full":
return _tile_within_full_terrain_radius(state, cam_pos)
return _tile_within_full_terrain_radius(state, cam_pos)
func _get_baked_tile_mesh(state: Dictionary, lod: int) -> Mesh:
var stream_tile: Resource = state.get("stream_tile", null)
var baked_tile: Resource = state.get("baked_tile", null)
if stream_tile != null:
if lod <= 0:
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
if quality_mesh != null:
return quality_mesh
return stream_tile.get("terrain_mesh")
if baked_tile == null:
return null
if lod <= 0:
return baked_tile.get("full_mesh")
return baked_tile.get("coarse_mesh")
func _should_refresh_focus(focus_pos: Vector3) -> bool:
if not _has_refresh_focus:
return true
var current_tile := _world_to_tile(focus_pos)
var last_tile := _world_to_tile(_last_refresh_focus_pos)
if current_tile != last_tile:
return true
var min_distance := maxf(1.0, streaming_update_distance)
return focus_pos.distance_squared_to(_last_refresh_focus_pos) >= min_distance * min_distance
func _note_refresh_focus(focus_pos: Vector3) -> void:
_last_refresh_focus_pos = focus_pos
_has_refresh_focus = true
func _reset_refresh_focus() -> void:
_last_refresh_focus_pos = Vector3.ZERO
_has_refresh_focus = false
func _tile_mesh_cache_key(tile_key: String, lod: int) -> String:
return "%s|%d" % [tile_key, lod]
func _ensure_world_wmo_root() -> void:
if _world_wmo_root != null and is_instance_valid(_world_wmo_root):
return
_world_wmo_root = Node3D.new()
_world_wmo_root.name = "WMOs"
_terrain_root.add_child(_world_wmo_root)
_set_editor_owner_recursive(_world_wmo_root)
func _wdt_has_global_wmo() -> bool:
return bool(_wdt_info.get("has_global_wmo", false)) and _wdt_info.has("global_wmo_name")
func _place_global_wmo() -> void:
if not _wdt_has_global_wmo():
return
if _wmo_registry.has("global"):
return
_ensure_world_wmo_root()
var rel_path := String(_wdt_info.get("global_wmo_name", "")).replace("\\", "/")
if rel_path.is_empty():
return
var placement: Dictionary = _wdt_info.get("global_wmo_placement", {})
if placement.is_empty():
placement = {
"pos": Vector3.ZERO,
"rot": Vector3.ZERO,
"scale": 1.0,
}
var instance := _instantiate_wmo_world(rel_path, placement)
if instance == null:
push_warning("StreamingWorld: failed to load global WMO: %s" % rel_path)
return
_world_wmo_root.add_child(instance)
_set_editor_owner_recursive(instance)
_wmo_registry["global"] = {
"node": instance,
"refs": {"__global__": true},
}
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
if not bool(state.get("wanted", false)):
_disable_tile_detail_assets(state)
return state
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
return state
if enable_wmo and _tile_within_detail_radius(state, focus_pos, wmo_tile_radius):
var refs: Array = state.get("wmo_refs", [])
var wmo_placements: Array = state.get("wmo_placements", [])
var wmo_key := String(state.get("key", ""))
if refs.is_empty() and not wmo_placements.is_empty() and not _wmo_build_jobs.has(wmo_key):
_enqueue_detail_asset_sync(String(state.get("key", "")))
else:
_unregister_tile_wmos(state)
if enable_m2_assets and _tile_within_detail_radius(state, focus_pos, m2_tile_radius):
var key := String(state.get("key", ""))
var m2_placements: Array = state.get("m2_placements", [])
if (
not bool(state.get("m2_built", false))
and not m2_placements.is_empty()
and not _m2_group_tasks.has(key)
and not _m2_build_jobs.has(key)
):
_enqueue_detail_asset_sync(key)
else:
_remove_tile_m2_assets(state)
state["m2_built"] = false
return state
func _enqueue_detail_asset_sync(key: String) -> void:
if key.is_empty() or _detail_asset_queued.has(key):
return
_detail_asset_queued[key] = true
_detail_asset_queue.append(key)
func _process_detail_asset_queue() -> void:
var ops_left: int = maxi(0, detail_asset_ops_per_tick)
while ops_left > 0 and not _detail_asset_queue.is_empty():
var key: String = String(_detail_asset_queue.pop_front())
_detail_asset_queued.erase(key)
_process_detail_asset_key(key)
ops_left -= 1
func _process_detail_asset_key(key: String) -> void:
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
if not bool(state.get("wanted", false)):
_disable_tile_detail_assets(state)
_tile_states[key] = state
return
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
return
if enable_wmo and _tile_within_detail_radius(state, _last_focus_pos, wmo_tile_radius):
var refs: Array = state.get("wmo_refs", [])
if refs.is_empty():
_register_tile_wmos(
state,
state.get("wmo_names", PackedStringArray()),
state.get("wmo_placements", []))
else:
_unregister_tile_wmos(state)
if enable_m2_assets and _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
if not bool(state.get("m2_built", false)):
var m2_request := _request_tile_m2_assets(
tile_root as Node3D,
String(state.get("key", "")),
state.get("origin", Vector3.ZERO),
state.get("m2_names", PackedStringArray()),
state.get("m2_placements", []))
state["m2_built"] = bool(m2_request.get("built", false))
if m2_request.has("unique_keys"):
state["m2_unique_keys"] = m2_request.get("unique_keys", [])
if m2_request.has("skipped_unique_keys"):
state["m2_skipped_unique_keys"] = m2_request.get("skipped_unique_keys", [])
else:
_remove_tile_m2_assets(state)
state["m2_built"] = false
_tile_states[key] = state
func _disable_tile_detail_assets(state: Dictionary) -> void:
_unregister_tile_wmos(state)
_remove_tile_m2_assets(state)
state["m2_built"] = false
func _tile_within_detail_radius(state: Dictionary, focus_pos: Vector3, radius_tiles: int) -> bool:
if radius_tiles < 0:
return true
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
return _tile_dist_sq(state, focus_pos) <= radius_sq
func _register_tile_wmos(state: Dictionary, wmo_names: PackedStringArray, wmo_placements: Array) -> void:
var refs: Array = state.get("wmo_refs", [])
if wmo_names.is_empty() or wmo_placements.is_empty():
state["wmo_refs"] = refs
return
_ensure_world_wmo_root()
var tile_key: String = state["key"]
if _wmo_build_jobs.has(tile_key):
state["wmo_building"] = true
return
_wmo_build_jobs[tile_key] = {
"names": wmo_names,
"placements": wmo_placements,
"index": 0,
"refs": refs,
}
_wmo_build_queue.append(tile_key)
state["wmo_building"] = true
func _process_wmo_build_jobs() -> void:
_drain_wmo_render_loads()
_drain_wmo_scene_loads()
var ops_left: int = maxi(0, wmo_build_instances_per_tick)
while ops_left > 0 and not _wmo_build_queue.is_empty():
var tile_key: String = String(_wmo_build_queue.front())
if not _wmo_build_jobs.has(tile_key):
_wmo_build_queue.pop_front()
continue
if not _tile_states.has(tile_key):
_wmo_build_jobs.erase(tile_key)
_wmo_build_queue.pop_front()
continue
var state: Dictionary = _tile_states[tile_key]
if not bool(state.get("wanted", false)) or not _tile_within_detail_radius(state, _last_focus_pos, wmo_tile_radius):
_cancel_wmo_build_job(tile_key)
_unregister_tile_wmos(state)
_tile_states[tile_key] = state
continue
var job: Dictionary = _wmo_build_jobs[tile_key]
var wmo_names: PackedStringArray = job.get("names", PackedStringArray())
var wmo_placements: Array = job.get("placements", [])
var refs: Array = job.get("refs", [])
var index: int = int(job.get("index", 0))
if index >= wmo_placements.size():
state["wmo_refs"] = refs
state["wmo_building"] = false
_tile_states[tile_key] = state
_wmo_build_jobs.erase(tile_key)
_wmo_build_queue.pop_front()
continue
var placement_variant = wmo_placements[index]
var advance_job := true
if placement_variant is Dictionary:
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id >= 0 and name_id < wmo_names.size():
var unique_key := _wmo_unique_key(placement, tile_key, index)
if _wmo_registry.has(unique_key):
var entry: Dictionary = _wmo_registry[unique_key]
var entry_refs: Dictionary = entry["refs"]
entry_refs[tile_key] = true
refs.append(unique_key)
else:
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
var render_resource := _get_wmo_render_or_request(rel_path)
if render_resource != null:
var instance := _instantiate_wmo_render_root(rel_path, placement)
if instance != null:
_world_wmo_root.add_child(instance)
_set_editor_owner_recursive(instance)
_wmo_registry[unique_key] = {
"node": instance,
"refs": {tile_key: true},
}
refs.append(unique_key)
_start_wmo_render_build(unique_key, instance, render_resource)
else:
var normalized_rel := _normalize_wmo_rel_path(rel_path)
if _wmo_render_load_requests.has(normalized_rel):
advance_job = false
else:
var scene := _get_wmo_scene_or_request(rel_path)
if scene == null:
if _wmo_scene_load_requests.has(normalized_rel):
advance_job = false
else:
var live_instance := _instantiate_wmo_world(rel_path, placement)
if live_instance != null:
_prepare_runtime_wmo_instance(live_instance)
_world_wmo_root.add_child(live_instance)
_set_editor_owner_recursive(live_instance)
_wmo_registry[unique_key] = {
"node": live_instance,
"refs": {tile_key: true},
}
refs.append(unique_key)
else:
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
if scene_instance != null:
_prepare_runtime_wmo_instance(scene_instance)
_world_wmo_root.add_child(scene_instance)
_set_editor_owner_recursive(scene_instance)
_wmo_registry[unique_key] = {
"node": scene_instance,
"refs": {tile_key: true},
}
refs.append(unique_key)
if advance_job:
job["refs"] = refs
if not advance_job:
_wmo_build_jobs[tile_key] = job
state["wmo_refs"] = refs
state["wmo_building"] = true
_tile_states[tile_key] = state
_wmo_build_queue.pop_front()
_wmo_build_queue.append(tile_key)
ops_left -= 1
continue
job["index"] = index + 1
_wmo_build_jobs[tile_key] = job
state["wmo_refs"] = refs
state["wmo_building"] = true
_tile_states[tile_key] = state
ops_left -= 1
if int(job["index"]) >= wmo_placements.size():
state["wmo_refs"] = refs
state["wmo_building"] = false
_tile_states[tile_key] = state
_wmo_build_jobs.erase(tile_key)
_wmo_build_queue.pop_front()
func _drain_wmo_scene_loads() -> void:
for normalized_rel_variant in _wmo_scene_load_requests.keys():
var normalized_rel := String(normalized_rel_variant)
var path := String(_wmo_scene_load_requests[normalized_rel])
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
_wmo_scene_load_requests.erase(normalized_rel)
if status != ResourceLoader.THREAD_LOAD_LOADED:
_wmo_scene_cache_missing[normalized_rel] = true
continue
var resource: Resource = ResourceLoader.load_threaded_get(path)
if resource is PackedScene and _is_wmo_scene_cache_current(resource as PackedScene):
_wmo_scene_resource_cache[normalized_rel] = resource as PackedScene
else:
_wmo_scene_cache_missing[normalized_rel] = true
func _drain_wmo_render_loads() -> void:
for normalized_rel_variant in _wmo_render_load_requests.keys():
var normalized_rel := String(normalized_rel_variant)
var path := String(_wmo_render_load_requests[normalized_rel])
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
_wmo_render_load_requests.erase(normalized_rel)
if status != ResourceLoader.THREAD_LOAD_LOADED:
_wmo_render_missing_cache[normalized_rel] = true
continue
var resource: Resource = ResourceLoader.load_threaded_get(path)
if (
resource != null
and resource.get_script() == WMO_STREAMING_SCRIPT
and int(resource.get("format_version")) >= WMO_STREAMING_SCRIPT.FORMAT_VERSION
):
_wmo_render_cache[normalized_rel] = resource
else:
_wmo_render_missing_cache[normalized_rel] = true
func _normalize_wmo_rel_path(rel_path: String) -> String:
return rel_path.replace("\\", "/").to_lower()
func _get_wmo_render_or_request(rel_path: String) -> Resource:
var normalized_rel := _normalize_wmo_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _wmo_render_cache.has(normalized_rel):
return _wmo_render_cache[normalized_rel]
if _wmo_render_missing_cache.has(normalized_rel):
return null
if _wmo_render_load_requests.has(normalized_rel):
return null
var cache_path := wmo_render_cache_dir.path_join(normalized_rel.get_basename() + ".res")
if not ResourceLoader.exists(cache_path):
return null
var err := ResourceLoader.load_threaded_request(
cache_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_wmo_render_load_requests[normalized_rel] = cache_path
return null
func _get_wmo_scene_or_request(rel_path: String) -> PackedScene:
var normalized_rel := _normalize_wmo_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _wmo_scene_resource_cache.has(normalized_rel):
return _wmo_scene_resource_cache[normalized_rel]
if _wmo_scene_cache_missing.has(normalized_rel):
return null
if _wmo_scene_load_requests.has(normalized_rel):
return null
var cache_path := wmo_cache_dir.path_join(normalized_rel.get_basename() + ".tscn")
if not ResourceLoader.exists(cache_path):
_wmo_scene_cache_missing[normalized_rel] = true
return null
if wmo_max_runtime_scene_mb > 0.0:
var size_bytes := _get_resource_file_size(cache_path)
var max_bytes := int(wmo_max_runtime_scene_mb * 1024.0 * 1024.0)
if size_bytes > max_bytes:
_wmo_scene_cache_missing[normalized_rel] = true
if debug_streaming:
print("StreamingWorld: skipping large WMO %.2f MB: %s" % [
float(size_bytes) / (1024.0 * 1024.0),
normalized_rel,
])
return null
var err := ResourceLoader.load_threaded_request(
cache_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_wmo_scene_load_requests[normalized_rel] = cache_path
else:
_wmo_scene_cache_missing[normalized_rel] = true
return null
func _instantiate_wmo_render_root(rel_path: String, placement: Dictionary) -> Node3D:
var root := Node3D.new()
root.name = rel_path.get_file().get_basename()
root.position = placement.get("pos", Vector3.ZERO)
root.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
root.scale = Vector3.ONE * scale_value
return root
func _start_wmo_render_build(unique_key: String, root: Node3D, render_resource: Resource) -> void:
if unique_key.is_empty() or root == null or render_resource == null:
return
_wmo_render_build_jobs[unique_key] = {
"root": root,
"resource": render_resource,
"mesh_index": 0,
"multimesh_index": 0,
}
_wmo_render_build_queue.append(unique_key)
func _process_wmo_render_build_jobs() -> void:
var ops_left: int = maxi(0, wmo_render_group_ops_per_tick)
while ops_left > 0 and not _wmo_render_build_queue.is_empty():
var unique_key := String(_wmo_render_build_queue.front())
if not _wmo_render_build_jobs.has(unique_key):
_wmo_render_build_queue.pop_front()
continue
var job: Dictionary = _wmo_render_build_jobs[unique_key]
var root: Node = job.get("root", null)
if root == null or not is_instance_valid(root):
_cancel_wmo_render_build_job(unique_key)
continue
var render_resource: Resource = job.get("resource", null)
if render_resource == null:
_cancel_wmo_render_build_job(unique_key)
continue
var meshes: Array = render_resource.get("meshes")
var mesh_transforms: Array = render_resource.get("mesh_transforms")
var mesh_names: PackedStringArray = render_resource.get("mesh_names")
var mesh_index: int = int(job.get("mesh_index", 0))
if mesh_index < meshes.size():
var mesh := meshes[mesh_index] as Mesh
if mesh != null:
_refresh_cached_wmo_mesh_materials(mesh)
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = mesh_names[mesh_index] if mesh_index < mesh_names.size() else "Group_%d" % mesh_index
mesh_instance.mesh = mesh
if mesh_index < mesh_transforms.size():
mesh_instance.transform = mesh_transforms[mesh_index]
mesh_instance.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if wmo_cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
if wmo_visibility_range > 0.0:
mesh_instance.visibility_range_end = wmo_visibility_range
mesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(mesh_instance)
_set_editor_owner_recursive(mesh_instance)
job["mesh_index"] = mesh_index + 1
_wmo_render_build_jobs[unique_key] = job
ops_left -= 1
continue
var multimeshes: Array = render_resource.get("multimeshes")
var multimesh_transforms: Array = render_resource.get("multimesh_transforms")
var multimesh_names: PackedStringArray = render_resource.get("multimesh_names")
var multimesh_index: int = int(job.get("multimesh_index", 0))
if multimesh_index < multimeshes.size():
var multimesh := multimeshes[multimesh_index] as MultiMesh
if multimesh != null:
_refresh_cached_wmo_mesh_materials(multimesh.mesh)
var multimesh_instance := MultiMeshInstance3D.new()
multimesh_instance.name = (
multimesh_names[multimesh_index]
if multimesh_index < multimesh_names.size()
else "DoodadGroup_%d" % multimesh_index
)
multimesh_instance.multimesh = multimesh
if multimesh_index < multimesh_transforms.size():
multimesh_instance.transform = multimesh_transforms[multimesh_index]
multimesh_instance.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if wmo_cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
if wmo_visibility_range > 0.0:
multimesh_instance.visibility_range_end = wmo_visibility_range
multimesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(multimesh_instance)
_set_editor_owner_recursive(multimesh_instance)
job["multimesh_index"] = multimesh_index + 1
_wmo_render_build_jobs[unique_key] = job
ops_left -= 1
continue
_cancel_wmo_render_build_job(unique_key)
func _cancel_wmo_render_build_job(unique_key: String) -> void:
if _wmo_render_build_jobs.has(unique_key):
_wmo_render_build_jobs.erase(unique_key)
_wmo_render_build_queue.erase(unique_key)
func _get_resource_file_size(path: String) -> int:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
file = FileAccess.open(ProjectSettings.globalize_path(path), FileAccess.READ)
if file == null:
return 0
var length := file.get_length()
file.close()
return int(length)
func _is_wmo_scene_cache_current(scene: PackedScene) -> bool:
if scene == null:
return false
var instance := scene.instantiate() as Node3D
if instance == null:
return false
var current := _is_wmo_node_cache_current(instance)
instance.free()
return current
func _is_wmo_node_cache_current(node: Node) -> bool:
if node == null:
return false
return bool(WMO_BUILDER_SCRIPT.is_scene_cache_current(node))
func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dictionary) -> Node3D:
if scene == null:
return null
var instance := scene.instantiate() as Node3D
if instance == null:
return null
if not _is_wmo_node_cache_current(instance):
instance.free()
return null
instance.name = rel_path.get_file().get_basename()
instance.position = placement.get("pos", Vector3.ZERO)
instance.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
instance.scale = Vector3.ONE * scale_value
return instance
func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
_refresh_cached_wmo_materials_recursive(instance)
if not enable_occlusion_culling:
var occluders := instance.get_node_or_null("Occluders")
if occluders != null:
instance.remove_child(occluders)
occluders.queue_free()
if wmo_cast_shadows:
_apply_shadow_cast_recursive(instance, true)
func _refresh_cached_wmo_materials_recursive(node: Node) -> void:
if node is MeshInstance3D:
_refresh_cached_wmo_mesh_materials((node as MeshInstance3D).mesh)
elif node is MultiMeshInstance3D:
var multimesh := (node as MultiMeshInstance3D).multimesh
if multimesh != null:
_refresh_cached_wmo_mesh_materials(multimesh.mesh)
for child in node.get_children():
_refresh_cached_wmo_materials_recursive(child)
func _refresh_cached_wmo_mesh_materials(mesh: Mesh) -> void:
if mesh == null:
return
if int(mesh.get_meta("wow_wmo_material_refresh_version", 0)) >= WMO_MATERIAL_REFRESH_VERSION:
return
mesh.set_meta("wow_wmo_material_refresh_version", WMO_MATERIAL_REFRESH_VERSION)
if not (mesh is ArrayMesh):
return
var array_mesh := mesh as ArrayMesh
for surface_idx in array_mesh.get_surface_count():
var rebuilt := _rebuild_cached_wmo_material(array_mesh.surface_get_material(surface_idx))
if rebuilt != null:
array_mesh.surface_set_material(surface_idx, rebuilt)
func _rebuild_cached_wmo_material(material: Material) -> Material:
if material == null or not material.has_meta("texture0_path"):
return null
var tex0_path := String(material.get_meta("texture0_path", ""))
var tex1_path := String(material.get_meta("texture1_path", ""))
var tex2_path := String(material.get_meta("texture2_path", ""))
var textures := PackedStringArray()
var tex0_index := -1
var tex1_index := -1
var tex2_index := -1
if not tex0_path.is_empty():
tex0_index = textures.size()
textures.append(tex0_path)
if not tex1_path.is_empty():
tex1_index = textures.size()
textures.append(tex1_path)
if not tex2_path.is_empty():
tex2_index = textures.size()
textures.append(tex2_path)
var diffuse := Color.WHITE
var emissive := Color.BLACK
var secondary := Color.WHITE
if material is ShaderMaterial:
var shader_material := material as ShaderMaterial
var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color")
var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color")
var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color")
if diffuse_value is Color:
diffuse = diffuse_value
if emissive_value is Color:
emissive = emissive_value
if secondary_value is Color:
secondary = secondary_value
var mat_def := {
"texture0": tex0_index,
"texture1": tex1_index,
"texture2": tex2_index,
"flags": int(material.get_meta("wow_flags", 0)),
"shader": int(material.get_meta("wow_shader", 0)),
"blend_mode": int(material.get_meta("wow_blend_mode", 0)),
"diffuse_color": diffuse,
"emissive_color": emissive,
"color2": secondary,
}
return WMO_BUILDER_SCRIPT._build_material(mat_def, textures, extracted_dir)
func _adt_m2_placement_basis(rot: Vector3, rel_path: String = "") -> Basis:
if not _is_waterfall_m2_path(rel_path):
return _adt_m2_regular_placement_basis(rot, rel_path)
# ADTLoader stores MDDF yaw as raw_yaw - 90 degrees for historical Godot
# facing alignment. In our tile-positive ADT world, waterfall models need a
# world-space +90 degree yaw to anchor their top to the source pool. Twist
# the sheet around its own local fall axis afterwards, so the anchor stays in
# place but the alpha plane is not rendered edge-on.
var basis := Basis.from_euler(rot)
return (
Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET)
* basis
* Basis(_waterfall_local_fall_axis(rel_path), _waterfall_sheet_twist(rel_path))
)
func _adt_m2_regular_placement_basis(rot: Vector3, rel_path: String = "") -> Basis:
if not _is_elwynn_cliffrock_m2_path(rel_path):
return Basis.from_euler(rot)
# Elwynn cliff rock M2s are open-backed shell meshes. Plain Godot Euler
# points their hollow side out of the cliff at waterfall placements. Keep the
# correction model-specific so normal props retain the native ADT rotation.
return (
Basis(Vector3.UP, rot.y + ADT_CLIFFROCK_WORLD_YAW_OFFSET)
* Basis(Vector3.BACK, rot.x)
* Basis(Vector3.RIGHT, -rot.z)
)
func _adt_m2_placement_origin_offset(rot: Vector3, rel_path: String, scale_value: float) -> Vector3:
if not _is_waterfall_m2_path(rel_path):
return Vector3.ZERO
var twist := _waterfall_sheet_twist(rel_path)
if is_zero_approx(twist):
return Vector3.ZERO
var anchor := _waterfall_sheet_twist_anchor(rel_path)
if anchor == Vector3.ZERO:
return Vector3.ZERO
var base := Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rot)
var twisted := base * Basis(_waterfall_local_fall_axis(rel_path), twist)
return (base * anchor - twisted * anchor) * maxf(scale_value, 0.0001)
func _is_waterfall_m2_path(rel_path: String) -> bool:
var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename()
return name == "newwaterfall" or name == "elwynntallwaterfall01"
func _is_elwynn_cliffrock_m2_path(rel_path: String) -> bool:
var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename()
return name == "elwynncliffrock01" or name == "elwynncliffrock02"
func _waterfall_local_fall_axis(rel_path: String) -> Vector3:
var normalized := rel_path.replace("\\", "/").to_lower()
if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"):
return Vector3(-0.138742, 0.990329, 0.0).normalized()
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
return Vector3(-0.068962, 0.997619, 0.0).normalized()
return Vector3.UP
func _waterfall_sheet_twist(rel_path: String) -> float:
var normalized := rel_path.replace("\\", "/").to_lower()
if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"):
return PI * 0.5
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
return -PI * 0.5
return 0.0
func _waterfall_sheet_twist_anchor(rel_path: String) -> Vector3:
var normalized := rel_path.replace("\\", "/").to_lower()
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
return Vector3(-2.667799, 89.62273, 0.00129)
return Vector3.ZERO
func _cancel_wmo_build_job(tile_key: String) -> void:
if _wmo_build_jobs.has(tile_key):
_wmo_build_jobs.erase(tile_key)
_wmo_build_queue.erase(tile_key)
func _unregister_tile_wmos(state: Dictionary) -> void:
var refs: Array = state.get("wmo_refs", [])
var tile_key: String = String(state.get("key", ""))
if not tile_key.is_empty():
_cancel_wmo_build_job(tile_key)
if refs.is_empty():
state["wmo_building"] = false
return
for unique_key in refs:
if not _wmo_registry.has(unique_key):
continue
var entry: Dictionary = _wmo_registry[unique_key]
var entry_refs: Dictionary = entry["refs"]
entry_refs.erase(tile_key)
if entry_refs.is_empty():
_cancel_wmo_render_build_job(String(unique_key))
var node: Node = entry.get("node", null)
if node:
node.queue_free()
_wmo_registry.erase(unique_key)
state["wmo_refs"] = []
state["wmo_building"] = false
func _wmo_unique_key(placement: Dictionary, tile_key: String, idx: int) -> String:
var uid: int = int(placement.get("unique_id", -1))
# uniqueId is always set in vanilla/3.3.5a MODF data, but older baked caches
# predating the ADTLoader unique_id field will miss it. Fall back to a
# per-tile synthetic key — no cross-tile dedup, but also no collisions.
if uid > 0:
return "uid:%d" % uid
return "tile:%s:%d" % [tile_key, idx]
func _clear_wmo_registry() -> void:
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear()
for entry_variant in _wmo_registry.values():
if not (entry_variant is Dictionary):
continue
var entry: Dictionary = entry_variant
var node: Node = entry.get("node", null)
if node and is_instance_valid(node):
_queue_free_streamed_node(node)
_wmo_registry.clear()
if _world_wmo_root and is_instance_valid(_world_wmo_root):
_queue_free_streamed_node(_world_wmo_root)
_world_wmo_root = null
func _request_tile_m2_assets(
tile_root: Node3D,
key: String,
tile_origin: Vector3,
m2_names: PackedStringArray,
m2_placements: Array) -> Dictionary:
var result := {
"built": false,
}
if key.is_empty() or m2_names.is_empty() or m2_placements.is_empty():
return result
var existing_m2_root := tile_root.get_node_or_null("M2s")
if existing_m2_root != null:
result["built"] = not existing_m2_root.is_queued_for_deletion()
return result
if _m2_group_tasks.has(key) or _m2_build_jobs.has(key):
return result
var filtered := _reserve_tile_m2_placements(key, m2_placements)
var filtered_placements: Array = filtered.get("placements", [])
result["unique_keys"] = filtered.get("unique_keys", [])
result["skipped_unique_keys"] = filtered.get("skipped_unique_keys", [])
if filtered_placements.is_empty():
result["built"] = true
return result
var task_id: int = WorkerThreadPool.add_task(
_group_tile_m2_task.bind(key, tile_origin, m2_names, filtered_placements))
_m2_group_tasks[key] = task_id
return result
func _reserve_tile_m2_placements(tile_key: String, m2_placements: Array) -> Dictionary:
var filtered_placements: Array = []
var unique_keys: Array = []
var skipped_unique_keys: Array = []
for index in m2_placements.size():
var placement_variant = m2_placements[index]
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
var unique_key := _m2_unique_key(placement)
if unique_key.is_empty():
filtered_placements.append(placement)
continue
if _m2_unique_registry.has(unique_key):
var entry: Dictionary = _m2_unique_registry[unique_key]
var owner_tile := String(entry.get("tile", ""))
if owner_tile == tile_key and unique_keys.has(unique_key):
continue
if owner_tile != tile_key:
if not skipped_unique_keys.has(unique_key):
skipped_unique_keys.append(unique_key)
continue
_m2_unique_registry[unique_key] = {"tile": tile_key}
if not unique_keys.has(unique_key):
unique_keys.append(unique_key)
filtered_placements.append(placement)
return {
"placements": filtered_placements,
"unique_keys": unique_keys,
"skipped_unique_keys": skipped_unique_keys,
}
func _m2_unique_key(placement: Dictionary) -> String:
var uid: int = int(placement.get("unique_id", -1))
if uid <= 0:
return ""
return "uid:%d" % uid
func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = true) -> void:
var tile_key := String(state.get("key", ""))
var unique_keys: Array = state.get("m2_unique_keys", [])
if tile_key.is_empty() or unique_keys.is_empty():
state["m2_unique_keys"] = []
state["m2_skipped_unique_keys"] = []
return
var released_keys: Array = []
for unique_key_variant in unique_keys:
var unique_key := String(unique_key_variant)
if unique_key.is_empty() or not _m2_unique_registry.has(unique_key):
continue
var entry: Dictionary = _m2_unique_registry[unique_key]
if String(entry.get("tile", "")) != tile_key:
continue
_m2_unique_registry.erase(unique_key)
released_keys.append(unique_key)
state["m2_unique_keys"] = []
state["m2_skipped_unique_keys"] = []
if not notify_candidates:
return
for unique_key in released_keys:
_queue_m2_tiles_skipping_unique_key(String(unique_key), tile_key)
func _queue_m2_tiles_skipping_unique_key(unique_key: String, released_tile_key: String) -> void:
if unique_key.is_empty():
return
for tile_key_variant in _tile_states.keys():
var tile_key := String(tile_key_variant)
if tile_key == released_tile_key:
continue
var state: Dictionary = _tile_states[tile_key]
var skipped_keys: Array = state.get("m2_skipped_unique_keys", [])
if not skipped_keys.has(unique_key):
continue
if not bool(state.get("wanted", false)):
continue
if not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
continue
_cancel_m2_build_job(tile_key)
_remove_tile_m2_root(state)
state["m2_built"] = false
state["m2_skipped_unique_keys"] = []
_tile_states[tile_key] = state
_enqueue_detail_asset_sync(tile_key)
func _group_tile_m2_task(
key: String,
tile_origin: Vector3,
m2_names: PackedStringArray,
m2_placements: Array) -> void:
var groups: Dictionary = {}
for placement_variant in m2_placements:
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id < 0 or name_id >= m2_names.size():
continue
var normalized: String = str(m2_names[name_id]).replace("\\", "/")
if normalized.ends_with(".mdx") or normalized.ends_with(".mdl"):
normalized = normalized.get_basename() + ".m2"
if normalized.is_empty():
continue
var world_pos: Vector3 = placement.get("pos", Vector3.ZERO)
var pos: Vector3 = world_pos - tile_origin
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
var basis := _adt_m2_placement_basis(rot, normalized)
var offset := _adt_m2_placement_origin_offset(rot, normalized, scale_value)
var xform := Transform3D(basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos + offset)
if not groups.has(normalized):
groups[normalized] = []
(groups[normalized] as Array).append(xform)
_m2_group_result_mutex.lock()
_m2_group_result_queue.append({
"key": key,
"groups": groups,
})
_m2_group_result_mutex.unlock()
func _drain_m2_group_results() -> void:
var results: Array = []
_m2_group_result_mutex.lock()
if not _m2_group_result_queue.is_empty():
results = _m2_group_result_queue
_m2_group_result_queue = []
_m2_group_result_mutex.unlock()
for result_variant in results:
if not (result_variant is Dictionary):
continue
var result: Dictionary = result_variant
var key: String = String(result.get("key", ""))
if _m2_group_tasks.has(key):
WorkerThreadPool.wait_for_task_completion(int(_m2_group_tasks[key]))
_m2_group_tasks.erase(key)
if not _tile_states.has(key):
continue
var state: Dictionary = _tile_states[key]
if not bool(state.get("wanted", false)):
continue
if not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
continue
if bool(state.get("m2_built", false)) or _m2_build_jobs.has(key):
continue
var groups: Dictionary = result.get("groups", {})
if groups.is_empty():
state["m2_built"] = true
_tile_states[key] = state
continue
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
continue
var m2_root := Node3D.new()
m2_root.name = "M2s"
(tile_root as Node3D).add_child(m2_root)
_set_editor_owner_recursive(m2_root)
_m2_build_jobs[key] = {
"root": m2_root,
"groups": groups,
"keys": groups.keys(),
"index": 0,
"offset": 0,
"serial": 0,
}
_m2_build_queue.append(key)
func _drain_m2_animation_loads() -> void:
for normalized_rel_variant in _m2_animation_load_requests.keys():
var normalized_rel := String(normalized_rel_variant)
var pending: Dictionary = _m2_animation_load_requests[normalized_rel]
var path := String(pending.get("path", ""))
if path.is_empty():
_m2_animation_load_requests.erase(normalized_rel)
_m2_static_animation_cache[normalized_rel] = true
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
pending["status"] = status
_m2_animation_load_requests.erase(normalized_rel)
_m2_animation_finalize_queue.append(pending)
var ops_left: int = maxi(0, m2_animation_finalize_ops_per_tick)
while ops_left > 0 and not _m2_animation_finalize_queue.is_empty():
var pending: Dictionary = _m2_animation_finalize_queue.pop_front()
var normalized_rel := String(pending.get("normalized", ""))
if normalized_rel.is_empty() or _m2_animated_scene_cache.has(normalized_rel) or _m2_static_animation_cache.has(normalized_rel):
ops_left -= 1
continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
_m2_static_animation_cache[normalized_rel] = true
ops_left -= 1
continue
var path := String(pending.get("path", ""))
var resource: Resource = ResourceLoader.load_threaded_get(path)
if resource is PackedScene:
var node = (resource as PackedScene).instantiate()
if node is Node3D:
_repair_m2_animated_materials(normalized_rel, node as Node3D)
var players := _find_animation_players_recursive(node)
if not players.is_empty():
_m2_animated_scene_cache[normalized_rel] = node as Node3D
if debug_streaming:
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()])
ops_left -= 1
continue
node.free()
_m2_static_animation_cache[normalized_rel] = true
ops_left -= 1
func _drain_m2_mesh_loads() -> void:
for normalized_rel_variant in _m2_mesh_load_requests.keys():
var normalized_rel := String(normalized_rel_variant)
var pending: Dictionary = _m2_mesh_load_requests[normalized_rel]
var path: String = String(pending.get("path", ""))
if path.is_empty():
_m2_mesh_load_requests.erase(normalized_rel)
_m2_missing_cache[normalized_rel] = true
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
pending["status"] = status
_m2_mesh_load_requests.erase(normalized_rel)
_m2_mesh_finalize_queue.append(pending)
var ops_left: int = maxi(0, m2_mesh_finalize_ops_per_tick)
while ops_left > 0 and not _m2_mesh_finalize_queue.is_empty():
var pending: Dictionary = _m2_mesh_finalize_queue.pop_front()
var normalized_rel := String(pending.get("normalized", ""))
if normalized_rel.is_empty() or _m2_mesh_cache.has(normalized_rel):
ops_left -= 1
continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
_m2_missing_cache[normalized_rel] = true
ops_left -= 1
continue
var path := String(pending.get("path", ""))
var resource: Resource = ResourceLoader.load_threaded_get(path)
var mesh := _extract_first_mesh_from_m2_resource(resource)
if mesh != null:
_m2_mesh_cache[normalized_rel] = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
else:
_m2_missing_cache[normalized_rel] = true
ops_left -= 1
func _process_m2_build_jobs() -> void:
var ops_left: int = maxi(0, m2_build_groups_per_tick)
while ops_left > 0 and not _m2_build_queue.is_empty():
var key: String = String(_m2_build_queue.front())
if not _m2_build_jobs.has(key):
_m2_build_queue.pop_front()
continue
if not _tile_states.has(key):
_cancel_m2_build_job(key)
_m2_build_queue.pop_front()
continue
var state: Dictionary = _tile_states[key]
if not bool(state.get("wanted", false)) or not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
_cancel_m2_build_job(key)
_release_tile_m2_unique_keys(state)
state["m2_built"] = false
_tile_states[key] = state
_m2_build_queue.pop_front()
continue
var job: Dictionary = _m2_build_jobs[key]
var root: Node = job.get("root", null)
if root == null or not is_instance_valid(root):
_m2_build_jobs.erase(key)
_m2_build_queue.pop_front()
continue
var group_keys: Array = job.get("keys", [])
var index: int = int(job.get("index", 0))
if index >= group_keys.size():
_finish_m2_build_job(key)
_m2_build_queue.pop_front()
continue
var rel_path: String = String(group_keys[index])
var normalized_rel := _normalize_m2_rel_path(rel_path)
var groups: Dictionary = job.get("groups", {})
var transforms: Array = groups.get(rel_path, [])
var offset: int = int(job.get("offset", 0))
var animated_prototype: Node3D = null
if enable_m2_animated_instances:
animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path)
if animated_prototype == null:
animated_prototype = _get_or_load_m2_animated_prototype(rel_path)
if enable_m2_animated_instances and _m2_animation_load_requests.has(normalized_rel):
_m2_build_queue.pop_front()
_m2_build_queue.append(key)
ops_left -= 1
continue
var batch_size: int = (
maxi(1, m2_animated_instances_per_tick)
if animated_prototype != null
else maxi(1, m2_multimesh_batch_size)
)
var batch_count: int = mini(batch_size, maxi(0, transforms.size() - offset))
var serial: int = int(job.get("serial", 0))
var consumed := batch_count
if batch_count > 0:
if animated_prototype != null:
_materialize_m2_animated_batch(root as Node3D, rel_path, animated_prototype, transforms, offset, batch_count, serial)
else:
var mesh := _get_m2_mesh_or_request(rel_path)
if mesh == null:
if not _m2_missing_cache.has(normalized_rel):
_m2_build_queue.pop_front()
_m2_build_queue.append(key)
ops_left -= 1
continue
else:
_materialize_m2_group_batch(root as Node3D, rel_path, mesh, transforms, offset, batch_count, serial)
job["serial"] = serial + 1
if consumed <= 0 or offset + consumed >= transforms.size():
job["index"] = index + 1
job["offset"] = 0
else:
job["offset"] = offset + consumed
_m2_build_jobs[key] = job
ops_left -= 1
if int(job["index"]) >= group_keys.size():
_finish_m2_build_job(key)
_m2_build_queue.pop_front()
func _materialize_m2_group_batch(
m2_root: Node3D,
rel_path: String,
mesh: Mesh,
transforms: Array,
start: int,
count: int,
serial: int) -> void:
if transforms.is_empty() or count <= 0 or mesh == null:
return
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.mesh = mesh
mm.instance_count = count
for i in count:
mm.set_instance_transform(i, transforms[start + i])
var mmi := MultiMeshInstance3D.new()
mmi.name = "%s_%d" % [rel_path.get_file().get_basename(), serial]
mmi.multimesh = mm
mmi.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if m2_cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
if m2_visibility_range > 0.0:
mmi.visibility_range_end = m2_visibility_range
mmi.visibility_range_end_margin = CHUNK_SIZE
m2_root.add_child(mmi)
_set_editor_owner_recursive(mmi)
func _materialize_m2_animated_batch(
m2_root: Node3D,
rel_path: String,
prototype: Node3D,
transforms: Array,
start: int,
count: int,
serial: int) -> void:
if transforms.is_empty() or count <= 0 or prototype == null:
return
var batch_root := Node3D.new()
batch_root.name = "%s_anim_%d" % [rel_path.get_file().get_basename(), serial]
for i in count:
var instance := prototype.duplicate(Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS) as Node3D
if instance == null:
continue
instance.name = "%s_%d" % [rel_path.get_file().get_basename(), start + i]
instance.transform = transforms[start + i]
_apply_visibility_range_recursive(instance, m2_visibility_range)
_apply_shadow_cast_recursive(instance, m2_cast_shadows)
_copy_m2_native_animator_data(prototype, instance)
batch_root.add_child(instance)
_start_m2_animations(instance, rel_path, start + i)
if batch_root.get_child_count() <= 0:
batch_root.queue_free()
return
m2_root.add_child(batch_root)
_set_editor_owner_recursive(batch_root)
func _start_m2_animations(root: Node, rel_path: String, instance_index: int) -> void:
var phase := float(abs(("%s:%d" % [rel_path, instance_index]).hash()) % 1000) / 1000.0
var native_animators := _find_m2_native_animators_recursive(root)
for animator in native_animators:
if animator.has_method("prepare_runtime"):
animator.prepare_runtime()
animator.set_phase(phase)
if debug_streaming and animator.has_method("runtime_debug_state"):
var state: Dictionary = animator.runtime_debug_state()
print("M2_NATIVE_ANIMATOR path=%s instance=%d prepared=%s processing=%s mesh=%s bones=%d surfaces=%d length=%.2f" % [
_normalize_m2_rel_path(rel_path),
instance_index,
str(state.get("prepared", false)),
str(state.get("processing", false)),
str(state.get("has_mesh", false)),
int(state.get("bones", 0)),
int(state.get("surfaces", 0)),
float(state.get("length", 0.0)),
])
var players := _find_animation_players_recursive(root)
for player in players:
var animation_name := _choose_default_m2_animation(player, rel_path)
if animation_name.is_empty():
continue
for name in player.get_animation_list():
var animation := player.get_animation(name)
if animation != null:
animation.loop_mode = Animation.LOOP_LINEAR
player.play(animation_name)
var animation := player.get_animation(animation_name)
if animation != null and animation.length > 0.0:
player.seek(animation.length * phase, true)
func _copy_m2_native_animator_data(source_root: Node, target_root: Node) -> void:
var source_animators := _find_m2_native_animators_recursive(source_root)
var target_animators := _find_m2_native_animators_recursive(target_root)
for i in range(mini(source_animators.size(), target_animators.size())):
var source := source_animators[i]
var target := target_animators[i]
target.mesh_instance_path = source.mesh_instance_path
target.bones = source.bones
target.surfaces = source.surfaces
target.animation_length = source.animation_length
target.playback_speed = source.playback_speed
func _find_m2_native_animators_recursive(root: Node) -> Array[Node]:
var result: Array[Node] = []
_collect_m2_native_animators(root, result)
return result
func _collect_m2_native_animators(node: Node, result: Array[Node]) -> void:
if node.get_script() == M2_NATIVE_ANIMATOR_SCRIPT:
result.append(node)
for child in node.get_children():
_collect_m2_native_animators(child, result)
func _choose_default_m2_animation(player: AnimationPlayer, rel_path: String = "") -> String:
var lower := rel_path.to_lower()
var candidates: Array[String] = ["Stand", "Idle", "Run", "Walk"]
if lower.contains("fish"):
candidates = ["Run", "Walk", "Swim", "Stand", "Idle", "Death"]
elif lower.contains("eagle") or lower.contains("bird") or lower.contains("gull"):
candidates = ["Run", "Walk", "Swim", "Stand", "Idle"]
for candidate in candidates:
if player.has_animation(candidate):
return candidate
var names := player.get_animation_list()
for candidate in ["run", "walk", "swim", "stand", "idle", "loop"]:
for name in names:
if String(name).to_lower().contains(candidate):
return String(name)
return String(names[0]) if not names.is_empty() else ""
func _find_animation_players_recursive(root: Node) -> Array[AnimationPlayer]:
var result: Array[AnimationPlayer] = []
_collect_animation_players(root, result)
return result
func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void:
if node is AnimationPlayer:
result.append(node as AnimationPlayer)
for child in node.get_children():
_collect_animation_players(child, result)
func _finish_m2_build_job(key: String) -> void:
if not _m2_build_jobs.has(key):
return
var job: Dictionary = _m2_build_jobs[key]
var root: Node = job.get("root", null)
var built := false
if root != null and is_instance_valid(root):
built = root.get_child_count() > 0
if not built:
root.queue_free()
if _tile_states.has(key):
var state: Dictionary = _tile_states[key]
state["m2_built"] = true
_tile_states[key] = state
_m2_build_jobs.erase(key)
func _cancel_m2_build_job(key: String) -> void:
if not _m2_build_jobs.has(key):
return
var job: Dictionary = _m2_build_jobs[key]
var root: Node = job.get("root", null)
if root != null and is_instance_valid(root):
_queue_free_streamed_node(root)
_m2_build_jobs.erase(key)
func _remove_tile_m2_assets(state: Dictionary) -> void:
var key := String(state.get("key", ""))
if not key.is_empty():
_cancel_m2_build_job(key)
_release_tile_m2_unique_keys(state)
_remove_tile_m2_root(state)
func _remove_tile_m2_root(state: Dictionary) -> void:
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
return
var m2_root := tile_root.get_node_or_null("M2s")
if m2_root:
m2_root.queue_free()
func _apply_visibility_range_recursive(node: Node, range_end: float) -> void:
if range_end <= 0.0:
return
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
geometry.visibility_range_end = range_end
geometry.visibility_range_end_margin = CHUNK_SIZE
for child in node.get_children():
_apply_visibility_range_recursive(child, range_end)
func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void:
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
geometry.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
for child in node.get_children():
_apply_shadow_cast_recursive(child, cast_shadows)
func _strip_occluders_recursive(node: Node) -> void:
for child in node.get_children():
if child is OccluderInstance3D:
node.remove_child(child)
child.free()
else:
_strip_occluders_recursive(child)
func _normalize_m2_rel_path(rel_path: String) -> String:
var normalized_rel := rel_path.replace("\\", "/")
if normalized_rel.ends_with(".mdx") or normalized_rel.ends_with(".mdl"):
normalized_rel = normalized_rel.get_basename() + ".m2"
return normalized_rel
func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_mesh_cache.has(normalized_rel):
return _m2_mesh_cache[normalized_rel]
if _m2_missing_cache.has(normalized_rel):
return null
_request_m2_mesh_load(normalized_rel)
return null
func _request_m2_mesh_load(normalized_rel: String) -> void:
if normalized_rel.is_empty() or _m2_mesh_load_requests.has(normalized_rel):
return
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1":
continue
var err := ResourceLoader.load_threaded_request(
cache_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_m2_mesh_load_requests[normalized_rel] = {
"normalized": normalized_rel,
"path": cache_res_path,
}
return
break
_m2_missing_cache[normalized_rel] = true
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
if mesh == null:
return null
if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
return mesh
var data := _load_m2_raw_data_for_refresh(normalized_rel)
if data.is_empty() or not _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel, data):
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
return mesh
var rebuilt := _rebuild_m2_mesh_from_data(data)
if rebuilt != null:
return rebuilt
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
return mesh
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
if root == null:
return
for mesh_instance in _find_mesh_instances_recursive(root):
if mesh_instance == null or mesh_instance.mesh == null:
continue
var prepared := _prepare_m2_mesh_for_runtime(normalized_rel, mesh_instance.mesh)
if prepared != null:
mesh_instance.mesh = prepared
func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary:
if normalized_rel.is_empty() or not ClassDB.class_exists("M2Loader"):
return {}
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
if not FileAccess.file_exists(abs_path):
return {}
var loader = ClassDB.instantiate("M2Loader")
if loader == null:
return {}
var data: Variant = loader.call("load_m2", abs_path)
return data if data is Dictionary else {}
func _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel: String, data: Dictionary) -> bool:
if _m2_runtime_rebuild_required_cache.has(normalized_rel):
return bool(_m2_runtime_rebuild_required_cache[normalized_rel])
var needs_rebuild := _m2_raw_data_has_billboards(data) or _m2_raw_data_has_uv_rotation(data)
_m2_runtime_rebuild_required_cache[normalized_rel] = needs_rebuild
return needs_rebuild
func _m2_raw_data_has_billboards(data: Dictionary) -> bool:
for batch_variant in data.get("batches", []):
if not (batch_variant is Dictionary):
continue
var batch: Dictionary = batch_variant
if bool(batch.get("has_billboard", false)) or int(batch.get("billboard_vertex_count", 0)) > 0:
return true
return false
func _m2_raw_data_has_uv_rotation(data: Dictionary) -> bool:
var transforms: Array = data.get("texture_transforms", [])
if transforms.is_empty():
return false
var combos: PackedInt32Array = data.get("texture_transform_combos", PackedInt32Array())
if combos.is_empty():
return false
for batch_variant in data.get("batches", []):
if not (batch_variant is Dictionary):
continue
var batch: Dictionary = batch_variant
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
var base_combo := int(batch.get("texture_transform_combo_index", -1))
for stage in mini(texture_count, 4):
var combo_index := base_combo + stage
if combo_index < 0 or combo_index >= combos.size():
continue
var transform_index := int(combos[combo_index])
if transform_index < 0 or transform_index >= transforms.size():
continue
var transform: Dictionary = transforms[transform_index] if transforms[transform_index] is Dictionary else {}
var rotation: Vector4 = transform.get("rotation", Vector4(1.0, 0.0, 0.0, 1.0))
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
return true
if absf(float(transform.get("rotation_speed", 0.0))) > 0.000001:
return true
return false
func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh:
if data.is_empty():
return null
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null:
return null
var rebuilt := _find_first_mesh_recursive(prototype)
prototype.free()
return rebuilt
func _extract_first_mesh_from_m2_resource(resource: Resource) -> Mesh:
if resource is Mesh:
return resource as Mesh
if resource is PackedScene:
var node := (resource as PackedScene).instantiate()
if node == null:
return null
var mesh := _find_first_mesh_recursive(node)
node.free()
return mesh
return null
func _find_first_mesh_recursive(node: Node) -> Mesh:
if node is MeshInstance3D:
return (node as MeshInstance3D).mesh
for child in node.get_children():
var mesh := _find_first_mesh_recursive(child)
if mesh != null:
return mesh
return null
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if _m2_mesh_cache.has(normalized_rel):
return _m2_mesh_cache[normalized_rel]
var prototype: Node3D = _get_or_load_m2_prototype(rel_path)
if prototype == null:
return null
var mesh := _find_first_mesh_recursive(prototype)
if mesh != null and not normalized_rel.is_empty():
mesh = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
_m2_mesh_cache[normalized_rel] = mesh
return mesh
func _build_tile_m2_placeholders(tile_root: Node3D, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> void:
if m2_names.is_empty() or m2_placements.is_empty():
return
var tree_mesh := CylinderMesh.new()
tree_mesh.top_radius = 0.35
tree_mesh.bottom_radius = 0.45
tree_mesh.height = 6.0
tree_mesh.radial_segments = 6
var prop_mesh := BoxMesh.new()
prop_mesh.size = Vector3(1.2, 1.2, 1.2)
var tree_material := StandardMaterial3D.new()
tree_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
tree_material.albedo_color = Color(0.2, 0.75, 0.2, 1.0)
var prop_material := StandardMaterial3D.new()
prop_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
prop_material.albedo_color = Color(0.8, 0.7, 0.35, 1.0)
var tree_mm := MultiMesh.new()
tree_mm.transform_format = MultiMesh.TRANSFORM_3D
tree_mm.mesh = tree_mesh
var prop_mm := MultiMesh.new()
prop_mm.transform_format = MultiMesh.TRANSFORM_3D
prop_mm.mesh = prop_mesh
var tree_transforms: Array[Transform3D] = []
var prop_transforms: Array[Transform3D] = []
for placement_variant in m2_placements:
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id < 0 or name_id >= m2_names.size():
continue
var rel_path := str(m2_names[name_id]).to_lower()
var is_tree := rel_path.contains("tree") or rel_path.contains("bush") or rel_path.contains("shrub") or rel_path.contains("plant")
if m2_placeholder_only_trees and not is_tree:
continue
var local_pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
var xform_basis := _adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.01))
var xf := Transform3D(xform_basis, local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value))
if is_tree:
tree_transforms.append(xf)
else:
prop_transforms.append(xf)
var root := Node3D.new()
root.name = "M2Placeholders"
if not tree_transforms.is_empty():
tree_mm.instance_count = tree_transforms.size()
for i in tree_transforms.size():
tree_mm.set_instance_transform(i, tree_transforms[i])
var tree_mmi := MultiMeshInstance3D.new()
tree_mmi.name = "Trees"
tree_mmi.multimesh = tree_mm
tree_mmi.material_override = tree_material
root.add_child(tree_mmi)
_set_editor_owner_recursive(tree_mmi)
if not prop_transforms.is_empty():
prop_mm.instance_count = prop_transforms.size()
for i in prop_transforms.size():
prop_mm.set_instance_transform(i, prop_transforms[i])
var prop_mmi := MultiMeshInstance3D.new()
prop_mmi.name = "Props"
prop_mmi.multimesh = prop_mm
prop_mmi.material_override = prop_material
root.add_child(prop_mmi)
_set_editor_owner_recursive(prop_mmi)
if root.get_child_count() > 0:
tile_root.add_child(root)
_set_editor_owner_recursive(root)
func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vector3) -> Node3D:
var prototype: Node3D = _get_or_load_m2_prototype(rel_path)
if prototype == null:
return null
var node := prototype.duplicate() as Node3D
if node == null:
return null
node.name = rel_path.get_file().get_basename()
var local_pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
node.transform = Transform3D(
_adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value))
return node
func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
var prototype := _get_or_load_wmo_prototype(rel_path)
if prototype == null:
return null
var instance := prototype.duplicate()
instance.name = rel_path.get_file().get_basename()
# WMOs are parented to _terrain_root, whose position absorbs the editor
# offset — use world-space placement.pos directly (no tile_origin subtraction).
instance.position = placement.get("pos", Vector3.ZERO)
instance.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
instance.scale = Vector3.ONE * scale_value
if not enable_occlusion_culling:
var occluders := instance.get_node_or_null("Occluders")
if occluders != null:
instance.remove_child(occluders)
occluders.queue_free()
if wmo_cast_shadows:
_apply_shadow_cast_recursive(instance, true)
return instance
func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_scene_cache.has(normalized_rel):
var cached: Node3D = _m2_scene_cache[normalized_rel]
_prepare_m2_node_for_runtime(normalized_rel, cached)
return cached
if _m2_missing_cache.has(normalized_rel):
return null
# Try pre-baked cache (.tscn first, then legacy .glb)
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1":
continue
var resource: Resource = load(cache_res_path)
if resource is PackedScene:
var node = (resource as PackedScene).instantiate()
if node is Node3D:
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
_m2_scene_cache[normalized_rel] = node as Node3D
return node as Node3D
break
# Fall back to raw M2Loader
if not ClassDB.class_exists("M2Loader"):
_m2_missing_cache[normalized_rel] = true
return null
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
if not FileAccess.file_exists(abs_path):
_m2_missing_cache[normalized_rel] = true
return null
var loader = ClassDB.instantiate("M2Loader")
var data: Dictionary = loader.call("load_m2", abs_path)
if data.is_empty():
_m2_missing_cache[normalized_rel] = true
return null
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null:
_m2_missing_cache[normalized_rel] = true
return null
_m2_scene_cache[normalized_rel] = prototype
return prototype
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
return null
if _m2_animated_scene_cache.has(normalized_rel):
return _m2_animated_scene_cache[normalized_rel]
if _m2_static_animation_cache.has(normalized_rel):
return null
if not ClassDB.class_exists("M2Loader"):
_m2_static_animation_cache[normalized_rel] = true
return null
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
if not FileAccess.file_exists(abs_path):
_m2_static_animation_cache[normalized_rel] = true
return null
var loader = ClassDB.instantiate("M2Loader")
if loader == null:
_m2_static_animation_cache[normalized_rel] = true
return null
if not loader.has_method("load_m2_animated"):
_m2_static_animation_cache[normalized_rel] = true
return null
var data: Dictionary = loader.call("load_m2_animated", abs_path)
var animated_surfaces: Array = data.get("animated_surfaces", [])
if data.is_empty() or animated_surfaces.is_empty():
_m2_static_animation_cache[normalized_rel] = true
return null
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null or prototype.get_child_count() <= 0:
_m2_static_animation_cache[normalized_rel] = true
return null
_m2_animated_scene_cache[normalized_rel] = prototype
if debug_streaming:
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
normalized_rel,
animated_surfaces.size(),
(data.get("bones", []) as Array).size(),
int(data.get("animation_id", -1)),
int(data.get("animation_sequence_index", -1)),
int(data.get("animation_activity_score", 0)),
float(data.get("animation_length", 0.0)),
])
return prototype
func _is_m2_native_animation_candidate(normalized_rel: String) -> bool:
return normalized_rel.to_lower().contains("gryphonroost")
func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_animated_scene_cache.has(normalized_rel):
return _m2_animated_scene_cache[normalized_rel]
if _m2_static_animation_cache.has(normalized_rel):
return null
if _m2_animation_load_requests.has(normalized_rel):
return null
_request_m2_animation_load(normalized_rel)
return null
func _request_m2_animation_load(normalized_rel: String) -> void:
if normalized_rel.is_empty() or _m2_animation_load_requests.has(normalized_rel):
return
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
if cache_res_path.is_empty():
_m2_static_animation_cache[normalized_rel] = true
return
var err := ResourceLoader.load_threaded_request(
cache_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_m2_animation_load_requests[normalized_rel] = {
"normalized": normalized_rel,
"path": cache_res_path,
}
else:
_m2_static_animation_cache[normalized_rel] = true
func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String:
if not _is_m2_animation_allowed(normalized_rel):
return ""
if _is_m2_animation_denied(normalized_rel):
return ""
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if _glb_cache_is_safe_for_runtime_animation(cache_res_path):
return cache_res_path
return ""
func _is_m2_animation_allowed(normalized_rel: String) -> bool:
if m2_animated_allowlist_patterns.is_empty():
return false
var lower := normalized_rel.to_lower()
for pattern in m2_animated_allowlist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lower.contains(needle):
return true
return false
func _is_m2_animation_denied(normalized_rel: String) -> bool:
var lower := normalized_rel.to_lower()
for pattern in m2_animated_denylist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lower.contains(needle):
return true
return false
func _repair_m2_animated_materials(normalized_rel: String, animated_root: Node3D) -> void:
if animated_root == null:
return
var material_root := _get_or_load_m2_material_prototype(normalized_rel)
if material_root == null:
return
var source_meshes := _find_mesh_instances_recursive(material_root)
var target_meshes := _find_mesh_instances_recursive(animated_root)
if source_meshes.is_empty() or target_meshes.is_empty():
return
var fallback_material := _first_mesh_material(source_meshes)
for i in target_meshes.size():
var target := target_meshes[i]
if target == null or target.mesh == null:
continue
var source := source_meshes[mini(i, source_meshes.size() - 1)]
for surface_idx in target.mesh.get_surface_count():
var material := _mesh_instance_surface_material(source, surface_idx)
if material == null:
material = fallback_material
if material != null:
target.set_surface_override_material(surface_idx, material)
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
if normalized_rel.is_empty():
return null
if _m2_scene_cache.has(normalized_rel):
var cached: Node3D = _m2_scene_cache[normalized_rel]
_prepare_m2_node_for_runtime(normalized_rel, cached)
return cached
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn"]):
if not ResourceLoader.exists(cache_res_path):
continue
var resource: Resource = load(cache_res_path)
if resource is PackedScene:
var node = (resource as PackedScene).instantiate()
if node is Node3D:
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
_m2_scene_cache[normalized_rel] = node as Node3D
return node as Node3D
return null
func _find_mesh_instances_recursive(root: Node) -> Array[MeshInstance3D]:
var result: Array[MeshInstance3D] = []
_collect_mesh_instances(root, result)
return result
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
if node is MeshInstance3D:
result.append(node as MeshInstance3D)
for child in node.get_children():
_collect_mesh_instances(child, result)
func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material:
for mesh_instance in meshes:
if mesh_instance == null or mesh_instance.mesh == null:
continue
for surface_idx in mesh_instance.mesh.get_surface_count():
var material := _mesh_instance_surface_material(mesh_instance, surface_idx)
if material != null:
return material
return null
func _mesh_instance_surface_material(mesh_instance: MeshInstance3D, surface_idx: int) -> Material:
if mesh_instance == null or mesh_instance.mesh == null:
return null
if surface_idx < mesh_instance.get_surface_override_material_count():
var override_material := mesh_instance.get_surface_override_material(surface_idx)
if override_material != null:
return override_material
if surface_idx < mesh_instance.mesh.get_surface_count():
return mesh_instance.mesh.surface_get_material(surface_idx)
return null
func _glb_cache_is_safe_for_runtime_animation(cache_res_path: String) -> bool:
var abs_path := ProjectSettings.globalize_path(cache_res_path)
if not FileAccess.file_exists(abs_path):
return false
var file := FileAccess.open(abs_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return false
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return false
var json_len := int(file.get_32())
var chunk_type := file.get_32()
if json_len <= 0 or chunk_type != 0x4e4f534a:
return false
var json_text := file.get_buffer(json_len).get_string_from_utf8()
var parsed = JSON.parse_string(json_text)
if not (parsed is Dictionary):
return false
var gltf := parsed as Dictionary
var animations: Array = gltf.get("animations", [])
if animations.is_empty():
return false
var primitive_count := _glb_primitive_count(gltf)
if m2_animated_max_primitives > 0 and primitive_count > m2_animated_max_primitives:
return false
var schema := _glb_animation_schema(gltf)
if schema == "pivot_prefix_v1":
return true
# Older simple critter GLBs were generated before the schema marker existed.
# Accept them only after allowlist/denylist filtering and primitive-count checks.
if schema.is_empty():
return true
if debug_streaming:
print("M2_ANIM_REJECT schema=%s primitives=%d cache=%s" % [schema, primitive_count, cache_res_path])
return false
func _glb_primitive_count(gltf: Dictionary) -> int:
var count := 0
var meshes: Array = gltf.get("meshes", [])
for mesh_variant in meshes:
if not (mesh_variant is Dictionary):
continue
var primitives: Array = (mesh_variant as Dictionary).get("primitives", [])
count += primitives.size()
return count
func _glb_cache_animation_schema(cache_res_path: String) -> String:
var abs_path := ProjectSettings.globalize_path(cache_res_path)
if not FileAccess.file_exists(abs_path):
return ""
var file := FileAccess.open(abs_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return ""
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return ""
var json_len := int(file.get_32())
var chunk_type := file.get_32()
if json_len <= 0 or chunk_type != 0x4e4f534a:
return ""
var parsed = JSON.parse_string(file.get_buffer(json_len).get_string_from_utf8())
if parsed is Dictionary:
return _glb_animation_schema(parsed as Dictionary)
return ""
func _glb_animation_schema(gltf: Dictionary) -> String:
var asset: Dictionary = gltf.get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
var normalized_rel := rel_path.replace("\\", "/")
if normalized_rel.is_empty():
return null
if _wmo_prototype_cache.has(normalized_rel):
return _wmo_prototype_cache[normalized_rel]
if _wmo_missing_cache.has(normalized_rel):
return null
# Try pre-baked .tscn cache
var cache_path := wmo_cache_dir.path_join(normalized_rel.get_basename() + ".tscn")
if ResourceLoader.exists(cache_path):
var resource: Resource = load(cache_path)
if resource is PackedScene:
var node = (resource as PackedScene).instantiate()
if node is Node3D and _is_wmo_node_cache_current(node):
_wmo_prototype_cache[normalized_rel] = node as Node3D
return node as Node3D
if node is Node:
(node as Node).free()
# Fall back to live WMOLoader
if not ClassDB.class_exists("WMOLoader"):
_wmo_missing_cache[normalized_rel] = true
return null
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
if not FileAccess.file_exists(abs_path):
_wmo_missing_cache[normalized_rel] = true
return null
var loader = ClassDB.instantiate("WMOLoader")
if loader == null:
return null
var data: Dictionary = loader.call("load_wmo", abs_path)
if data.is_empty():
_wmo_missing_cache[normalized_rel] = true
return null
var prototype: Node3D = WMO_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null:
_wmo_missing_cache[normalized_rel] = true
return null
_wmo_prototype_cache[normalized_rel] = prototype
return prototype
func _get_m2_cache_resource_path(rel_path: String, ext: String = ".tscn") -> String:
var normalized := rel_path.replace("\\", "/")
var stem := normalized.get_basename()
return m2_cache_dir.path_join(stem + ext)
func _get_m2_cache_resource_paths(rel_path: String, extensions: Array[String]) -> PackedStringArray:
var normalized := rel_path.replace("\\", "/")
var lower := normalized.to_lower()
var stems := [
normalized.get_basename(),
lower.get_basename(),
normalized.get_file().get_basename(),
lower.get_file().get_basename(),
]
var result := PackedStringArray()
for ext in extensions:
for stem in stems:
if stem.is_empty():
continue
var path := m2_cache_dir.path_join(stem + ext)
if not result.has(path):
result.append(path)
return result
func _get_cached_tile_mesh(cache_key: String) -> Mesh:
if not _tile_mesh_cache.has(cache_key):
return null
_touch_tile_mesh_cache(cache_key)
return _tile_mesh_cache[cache_key]
func _store_cached_tile_mesh(cache_key: String, mesh: Mesh) -> void:
if mesh == null:
return
_tile_mesh_cache[cache_key] = mesh
_touch_tile_mesh_cache(cache_key)
_trim_tile_mesh_cache()
func _touch_tile_mesh_cache(cache_key: String) -> void:
_tile_mesh_cache_order.erase(cache_key)
_tile_mesh_cache_order.append(cache_key)
func _trim_tile_mesh_cache() -> void:
var limit := maxi(0, cached_tile_mesh_limit)
while _tile_mesh_cache_order.size() > limit:
var oldest: String = _tile_mesh_cache_order.pop_front()
_tile_mesh_cache.erase(oldest)
func _remove_tile_mesh_cache_entries(tile_key: String) -> void:
for lod in range(0, 4):
var cache_key := _tile_mesh_cache_key(tile_key, lod)
_tile_mesh_cache.erase(cache_key)
_tile_mesh_cache_order.erase(cache_key)
func _clear_tile_mesh_cache() -> void:
_tile_mesh_cache.clear()
_tile_mesh_cache_order.clear()
func _restore_quality_terrain_mesh(tile_key: String, state: Dictionary) -> Dictionary:
if not _terrain_quality_mesh_cache.has(tile_key):
return state
var cached: Dictionary = _terrain_quality_mesh_cache[tile_key]
var source := String(cached.get("source", "baked_full"))
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
_terrain_quality_mesh_cache.erase(tile_key)
_terrain_quality_mesh_cache_order.erase(tile_key)
return state
var mesh: Mesh = cached.get("mesh", null)
if mesh == null:
_terrain_quality_mesh_cache.erase(tile_key)
_terrain_quality_mesh_cache_order.erase(tile_key)
return state
state["quality_terrain_mesh"] = mesh
state["quality_terrain_source"] = source
_touch_quality_terrain_mesh_cache(tile_key)
return state
func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void:
if tile_key.is_empty() or mesh == null:
return
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
return
_terrain_quality_mesh_cache[tile_key] = {
"mesh": mesh,
"source": source,
}
_touch_quality_terrain_mesh_cache(tile_key)
_trim_quality_terrain_mesh_cache()
func _touch_quality_terrain_mesh_cache(tile_key: String) -> void:
_terrain_quality_mesh_cache_order.erase(tile_key)
_terrain_quality_mesh_cache_order.append(tile_key)
func _trim_quality_terrain_mesh_cache() -> void:
var limit := maxi(0, terrain_quality_mesh_cache_limit)
while _terrain_quality_mesh_cache_order.size() > limit:
var oldest: String = _terrain_quality_mesh_cache_order.pop_front()
_terrain_quality_mesh_cache.erase(oldest)
func _clear_quality_terrain_mesh_cache() -> void:
_terrain_quality_mesh_cache.clear()
_terrain_quality_mesh_cache_order.clear()
func _position_camera_over_world() -> void:
if _camera_initialized:
return
var camera := _get_auto_position_camera()
if camera == null:
return
var center_tx: float = (_tile_min.x + _tile_max.x) * 0.5
var center_ty: float = (_tile_min.y + _tile_max.y) * 0.5
var center := _tile_center_to_world(center_tx, center_ty)
var tile_span: int = int(max(_tile_max.x - _tile_min.x + 1, _tile_max.y - _tile_min.y + 1))
var height: float = max(2000.0, float(tile_span) * TILE_SIZE * 0.18)
camera.global_position = center + Vector3(0.0, height, height * 0.2)
camera.look_at(center, Vector3.UP)
_camera_initialized = true
## Tile (tx, ty) NW corner sits at Godot (tx*TILE_SIZE, 0, ty*TILE_SIZE).
## This matches wow_to_godot output: chunk origins ≈ (TX*TILE_SIZE, h, TY*TILE_SIZE).
func _tile_center_to_world(tile_x: float, tile_y: float) -> Vector3:
return Vector3(
(tile_x + 0.5) * TILE_SIZE,
0.0,
(tile_y + 0.5) * TILE_SIZE)
func _world_to_tile(world_pos: Vector3) -> Vector2i:
return Vector2i(
int(floor(world_pos.x / TILE_SIZE)),
int(floor(world_pos.z / TILE_SIZE)))
func _tile_key(tx: int, ty: int) -> String:
return "%d_%d" % [tx, ty]
func _set_editor_owner_recursive(node: Node) -> void:
if not Engine.is_editor_hint():
return
if not editor_persist_generated_nodes:
return
var edited_root := get_tree().edited_scene_root
if edited_root == null:
return
node.owner = edited_root
for child in node.get_children():
_set_editor_owner_recursive(child)
func _make_editor_signature() -> String:
var values := [
extracted_dir,
map_name,
quality_preset,
1 if editor_preview_enabled else 0,
editor_preview_center_x,
editor_preview_center_y,
editor_preview_tile_radius,
1 if editor_follow_view_camera else 0,
lod0_radius_chunks,
lod1_radius_chunks,
lod2_radius_chunks,
lod2_tile_radius,
warm_tile_margin,
prewarm_tile_margin,
1 if prewarm_tile_lod_instances else 0,
retain_tile_margin,
1 if terrain_full_quality_enabled else 0,
terrain_full_quality_radius_tiles,
max_concurrent_terrain_upgrade_tasks,
1 if terrain_control_splat_quality_enabled else 0,
1 if terrain_control_splat_primary else 0,
1 if terrain_control_splat_hide_fallback else 0,
terrain_control_splat_hide_fallback_radius_tiles,
terrain_control_splat_radius_tiles,
max_active_terrain_control_splat_tiles,
terrain_control_splat_cache_dir,
1 if terrain_splat_quality_enabled else 0,
terrain_splat_radius_tiles,
max_active_terrain_splat_tiles,
max_concurrent_terrain_splat_tasks,
1 if enable_m2_assets else 0,
1 if enable_m2_placeholders else 0,
m2_multimesh_batch_size,
m2_mesh_finalize_ops_per_tick,
wmo_build_instances_per_tick,
wmo_render_group_ops_per_tick,
wmo_max_runtime_scene_mb,
m2_tile_radius,
wmo_tile_radius,
1 if wmo_cast_shadows else 0,
m2_visibility_range,
wmo_visibility_range,
full_lod_prewarm_tiles,
boundary_prefetch_threshold,
1 if terrain_quality_log_enabled else 0,
terrain_quality_log_interval,
m2_cache_dir,
wmo_cache_dir,
wmo_render_cache_dir,
streaming_tile_cache_dir,
]
var parts := PackedStringArray()
for value in values:
parts.append(str(value))
return "|".join(parts)
func _release_tile_if_unused(key: String) -> void:
if not _tile_states.has(key):
return
var state: Dictionary = _tile_states[key]
if bool(state.get("retained", false)):
_tile_states[key] = state
return
if not state["chunks"].is_empty():
_tile_states[key] = state
return
if state.get("tile_lod_node", null) != null:
_tile_states[key] = state
return
if state.get("tile_lod_rid", RID()).is_valid():
_tile_states[key] = state
return
if int(state.get("tile_lod", -1)) >= 0:
_tile_states[key] = state
return
_release_tile(key)
func _tile_local_to_world_transform(state: Dictionary, local_position: Vector3) -> Transform3D:
var root: Node3D = state.get("root", null)
var world_pos := local_position
if root:
world_pos += root.global_position
return Transform3D(Basis.IDENTITY, world_pos)
func _create_render_instance(mesh: Mesh, world_transform: Transform3D) -> RID:
if mesh == null:
return RID()
var world := get_world_3d()
if world == null:
return RID()
var scenario: RID = world.scenario
if not scenario.is_valid():
return RID()
var instance_rid := RenderingServer.instance_create()
RenderingServer.instance_set_base(instance_rid, mesh.get_rid())
RenderingServer.instance_set_scenario(instance_rid, scenario)
RenderingServer.instance_set_transform(instance_rid, world_transform)
RenderingServer.instance_geometry_set_cast_shadows_setting(
instance_rid,
RenderingServer.SHADOW_CASTING_SETTING_ON if terrain_cast_shadows else RenderingServer.SHADOW_CASTING_SETTING_OFF)
return instance_rid
func _free_render_instance(instance_rid: RID) -> void:
if instance_rid.is_valid():
RenderingServer.free_rid(instance_rid)