шейдер воды

This commit is contained in:
2026-06-27 18:58:18 +04:00
parent b051012920
commit 8738c2495d
2 changed files with 137 additions and 2 deletions
+136 -2
View File
@@ -57,6 +57,10 @@ const QUALITY_HIGH := "High"
@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.
@@ -170,6 +174,11 @@ 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 = {}
@@ -285,6 +294,10 @@ func _process(delta: float) -> void:
_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
@@ -871,7 +884,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
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 detail=%d m2task=%d m2mesh=%d m2build=%d wmobuild=%d wmogroups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] %s" % [
print("HITCH %.2fms refresh=%s queues[tile=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d detail=%d m2task=%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(),
@@ -879,6 +892,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
_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_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
@@ -969,7 +983,7 @@ func _tick_runtime_stats(delta: float) -> void:
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 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_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" % [
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_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,
@@ -978,6 +992,7 @@ func _tick_runtime_stats(delta: float) -> void:
_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,
@@ -1889,6 +1904,100 @@ func _drain_terrain_splat_results() -> void:
_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":
@@ -1980,6 +2089,15 @@ func _wait_for_tile_tasks() -> void:
_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"]):
@@ -2050,6 +2168,7 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
"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,
@@ -2125,6 +2244,7 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
"wanted": true,
"retained": true,
"root": tile_root,
"water_loaded": false,
"m2_names": baked_m2_names,
"m2_placements": baked_m2_placements,
"m2_built": false,
@@ -2136,6 +2256,7 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
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()
@@ -2198,6 +2319,7 @@ func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource)
"wanted": true,
"retained": true,
"root": tile_root,
"water_loaded": false,
"m2_names": stream_m2_names,
"m2_placements": stream_m2_placements,
"m2_built": false,
@@ -2210,6 +2332,7 @@ func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource)
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
@@ -2492,6 +2615,11 @@ func _release_tile(key: String) -> void:
_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)
@@ -2522,6 +2650,12 @@ func _clear_streamed_world() -> void:
_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():