@tool ## Streams Azeroth terrain around the active camera 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://resources/baked_adt_tile.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 REQUIRED_BAKED_TILE_FORMAT_VERSION := 4 const TILE_SIZE := 533.33333 const CHUNK_SIZE := TILE_SIZE / 16.0 @export var extracted_dir: String = "res://extracted" @export var map_name: String = "Azeroth" @export var camera_path: NodePath ## 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 ## 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 @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 = 8 @export var warm_tile_margin: int = 1 @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 use_rendering_server_for_terrain: bool = true @export var use_baked_tile_cache: bool = true @export var baked_tile_cache_dir: String = "res://cache/baked_terrain_v2" @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 @export var m2_cache_dir: String = "res://cache/m2_glb" @export var wmo_cache_dir: String = "res://cache/wmo_tscn" @export var terrain_cast_shadows: bool = false @export var m2_cast_shadows: bool = false @export var editor_reload_now: bool = false @export var debug_streaming: bool = false var _builder var _map_dir := "" var _abs_map_dir := "" 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 _tile_loading_tasks: Dictionary = {} 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 _last_focus_pos := Vector3.ZERO var _dbg_chunks_created := 0 var _dbg_chunks_null := 0 var _dbg_last_report := 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 _wmo_prototype_cache: Dictionary = {} var _wmo_missing_cache: Dictionary = {} var _m2_scene_cache: Dictionary = {} var _m2_missing_cache: Dictionary = {} func _ready() -> void: if not ClassDB.class_exists("ADTLoader"): push_error("ADTLoader not found. Rebuild GDExtension first.") return _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() _set_editor_owner_recursive(_terrain_root) _scan_available_tiles() if _available_tiles.is_empty(): 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 Engine.is_editor_hint(): _tick_editor_streaming() elif auto_position_camera: _position_camera_over_world() set_process(true) func _process(delta: float) -> void: if _available_tiles.is_empty(): return # Queues are always drained — works in both editor and game. _drain_tile_load_results() _process_queues() _update_accum += delta if _update_accum < update_interval: return _update_accum = 0.0 if Engine.is_editor_hint(): _tick_editor_streaming() else: _refresh_streaming_targets(false) func _exit_tree() -> void: _shutting_down = true _wait_for_tile_tasks() _clear_streamed_world() 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_stream_camera() if editor_camera: focus_world += editor_camera.global_position _refresh_editor_streaming_targets_at(focus_world, false) func _scan_available_tiles() -> 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) print("StreamingWorld: found %d ADT tiles for %s" % [_available_tiles.size(), map_name]) func _refresh_streaming_targets(force: bool) -> void: var camera := _get_stream_camera() if camera == null: return _terrain_root.position = Vector3.ZERO _refresh_streaming_targets_at(camera.global_position, force) ## Core streaming update — works for both game (camera pos) and editor (preview center pos). 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, 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 chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin # Coarse tile radius covers everything beyond lod2 at the cheapest LOD. var total_tile_radius: int = maxi(chunk_tile_radius, lod2_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, 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, 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) state["wanted"] = wanted 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) else: state["desired_lods"] = {} state["desired_tile_lod"] = -1 _tile_states[key] = state _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)) 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: _tile_lod_create_queue.append({ "tile": key, "lod": desired_tile_lod, "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 # 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 ops_left > 0 and not _tile_lod_remove_queue.is_empty(): _remove_tile_lod(_tile_lod_remove_queue.pop_back()) ops_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 _start_tile_load_async(request: Dictionary) -> void: var key: String = request["key"] if _tile_loading_tasks.has(key): 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 = [] _tile_result_mutex.lock() if not _tile_result_queue.is_empty(): results = _tile_result_queue _tile_result_queue = [] _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 = [] for key in _tile_loading_tasks.keys(): var pending: Dictionary = _tile_loading_tasks[key] if pending.get("mode", "raw") != "baked": 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 baked_tile: Resource = ResourceLoader.load_threaded_get(pending["path"]) if not _should_accept_loaded_tile(request): continue if baked_tile == null or baked_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, baked_tile) 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"])) _tile_loading_tasks.clear() _tile_result_mutex.lock() _tile_result_queue.clear() _tile_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: var chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin radius = maxi(chunk_tile_radius, lod2_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_assets: _build_tile_m2_assets( tile_root, tile_origin, data.get("m2_names", PackedStringArray()), data.get("m2_placements", [])) if enable_m2_placeholders: _build_tile_m2_placeholders( tile_root, tile_origin, data.get("m2_names", PackedStringArray()), data.get("m2_placements", [])) if enable_wmo: _build_tile_wmos( tile_root, tile_origin, data.get("wmo_names", PackedStringArray()), data.get("wmo_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, "desired_tile_lod": -1, "wanted": true, "root": tile_root, } # 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) _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 if enable_m2_assets: 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 [] _build_tile_m2_assets( tile_root, tile_origin, baked_m2_names, baked_m2_placements) if enable_wmo: 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 [] _build_tile_wmos( tile_root, tile_origin, baked_wmo_names, baked_wmo_placements) 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, "desired_tile_lod": -1, "wanted": true, "root": tile_root, } state["desired_tile_lod"] = _compute_desired_tile_lod(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 _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"]) if int(state.get("tile_lod", -1)) == lod: return var old_node: Node = state.get("tile_lod_node", null) var old_rid: RID = state.get("tile_lod_rid", RID()) state["tile_lod"] = lod state["tile_lod_node"] = null state["tile_lod_rid"] = RID() state["tile_lod_mesh"] = null var cache_key := _tile_mesh_cache_key(key, lod) var mesh: Mesh = _get_cached_tile_mesh(cache_key) if mesh == null: 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) 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(): 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.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() _tile_states[key] = state 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 state["wanted"] 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"] = -1 if not state["wanted"] and state["chunks"].is_empty(): _release_tile_if_unused(key) else: _tile_states[key] = state func _release_tile(key: String) -> void: if not _tile_states.has(key): return 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) 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() _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() _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) 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() _tile_states.clear() _clear_tile_mesh_cache() 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 _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 if not use_chunk_terrain_lods: if tile_dist_sq <= chunk_radius_sq: return 0 if tile_dist_sq <= tile_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 _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 _get_stream_camera() -> Camera3D: if Engine.is_editor_hint(): # The 3D editor viewport camera lives in EditorInterface, not get_viewport(). # get_viewport().get_camera_3d() from a @tool script returns whichever Camera3D # belongs to the edited scene (often null), so editor-view movement is invisible # unless we reach into EditorInterface. var vp := EditorInterface.get_editor_viewport_3d(0) if vp != null: var cam := vp.get_camera_3d() if cam: return cam if camera_path != NodePath(): var from_path := get_node_or_null(camera_path) if from_path is Camera3D: return from_path var viewport_camera := get_viewport().get_camera_3d() if viewport_camera: return viewport_camera for child in get_children(): if child is Camera3D and child.current: return child return null 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_baked_tile_mesh(state: Dictionary, lod: int) -> Mesh: var baked_tile: Resource = state.get("baked_tile", null) 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 _build_tile_wmos(tile_root: Node3D, tile_origin: Vector3, wmo_names: PackedStringArray, wmo_placements: Array) -> void: if wmo_names.is_empty() or wmo_placements.is_empty(): return var wmo_root := Node3D.new() wmo_root.name = "WMOs" tile_root.add_child(wmo_root) _set_editor_owner_recursive(wmo_root) for placement_variant in wmo_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 >= wmo_names.size(): continue var rel_path: String = str(wmo_names[name_id]).replace("\\", "/") var instance := _instantiate_wmo(rel_path, placement, tile_origin) if instance == null: continue wmo_root.add_child(instance) _set_editor_owner_recursive(instance) func _build_tile_m2_assets(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 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 rel_path: String = str(m2_names[name_id]).replace("\\", "/") var normalized := rel_path if normalized.ends_with(".mdx") or normalized.ends_with(".mdl"): normalized = normalized.get_basename() + ".m2" if normalized.is_empty(): continue var 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 := Transform3D(Basis.from_euler(rot).scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos) if not groups.has(normalized): groups[normalized] = [] (groups[normalized] as Array).append(xform) if groups.is_empty(): return var m2_root := Node3D.new() m2_root.name = "M2s" tile_root.add_child(m2_root) _set_editor_owner_recursive(m2_root) for rel_path in groups.keys(): var transforms: Array = groups[rel_path] var mesh: Mesh = _get_or_load_m2_mesh(rel_path) if mesh == null: continue var mm := MultiMesh.new() mm.transform_format = MultiMesh.TRANSFORM_3D mm.mesh = mesh mm.instance_count = transforms.size() for i in transforms.size(): mm.set_instance_transform(i, transforms[i]) var mmi := MultiMeshInstance3D.new() mmi.name = rel_path.get_file().get_basename() mmi.multimesh = mm mmi.cast_shadow = ( GeometryInstance3D.SHADOW_CASTING_SETTING_ON if m2_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF ) m2_root.add_child(mmi) _set_editor_owner_recursive(mmi) if m2_root.get_child_count() == 0: m2_root.queue_free() func _get_or_load_m2_mesh(rel_path: String) -> Mesh: var prototype: Node3D = _get_or_load_m2_prototype(rel_path) if prototype == null: return null for child in prototype.get_children(): if child is MeshInstance3D: return (child as MeshInstance3D).mesh return null 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 := Basis.from_euler(rot).scaled(Vector3.ONE * maxf(scale_value, 0.01)) var xf := Transform3D(xform_basis, local_pos) 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() node.position = placement.get("pos", Vector3.ZERO) - tile_origin node.rotation = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) node.scale = Vector3.ONE * scale_value return node func _instantiate_wmo(rel_path: String, placement: Dictionary, tile_origin: Vector3) -> 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() instance.position = placement.get("pos", Vector3.ZERO) - tile_origin var rot: Vector3 = placement.get("rot", Vector3.ZERO) rot.y += PI instance.rotation = rot var scale_value: float = float(placement.get("scale", 1.0)) instance.scale = Vector3.ONE * scale_value return instance func _get_or_load_m2_prototype(rel_path: String) -> Node3D: 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" if normalized_rel.is_empty(): return null if _m2_scene_cache.has(normalized_rel): return _m2_scene_cache[normalized_rel] 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_path(normalized_rel, ".tscn"), _get_m2_cache_resource_path(normalized_rel, ".glb")]: 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: _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_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: _wmo_prototype_cache[normalized_rel] = node as Node3D return node as Node3D # 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_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 _clear_tile_mesh_cache() -> void: _tile_mesh_cache.clear() _tile_mesh_cache_order.clear() func _position_camera_over_world() -> void: if _camera_initialized: return var camera := _get_stream_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: return "%s|%s|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%s" % [ extracted_dir, map_name, 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, 1 if enable_m2_assets else 0, 1 if enable_m2_placeholders else 0, m2_cache_dir, ] func _release_tile_if_unused(key: String) -> void: if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] if state["wanted"]: _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)