оптимизация рендера
This commit is contained in:
@@ -2,7 +2,7 @@ extends SceneTree
|
||||
|
||||
## Usage:
|
||||
## godot --headless --path <project> --script res://src/tools/bake_adt_terrain_cache.gd -- \
|
||||
## --map Azeroth --jobs 4 --force
|
||||
## --map Azeroth --jobs auto --force
|
||||
##
|
||||
## `--jobs N` starts N separate Godot worker processes. Process isolation keeps
|
||||
## ResourceSaver/Image/loader state independent while using multiple CPU cores.
|
||||
@@ -37,7 +37,7 @@ func _initialize() -> void:
|
||||
_get_arg_value(
|
||||
args,
|
||||
"--full-texture-size",
|
||||
str(legacy_texture_size if legacy_texture_size != null else 4196)
|
||||
str(legacy_texture_size if legacy_texture_size != null else 2048)
|
||||
).to_int()
|
||||
)
|
||||
var coarse_texture_size := maxi(
|
||||
@@ -48,11 +48,12 @@ func _initialize() -> void:
|
||||
str(legacy_texture_size if legacy_texture_size != null else 512)
|
||||
).to_int()
|
||||
)
|
||||
var tile_x: Variant = _get_optional_int_arg(args, "--tile-x")
|
||||
var tile_y: Variant = _get_optional_int_arg(args, "--tile-y")
|
||||
var jobs := maxi(1, _get_arg_value(args, "--jobs", "1").to_int())
|
||||
var worker_index: Variant = _get_optional_int_arg(args, "--worker-index")
|
||||
var worker_count := maxi(1, _get_arg_value(args, "--worker-count", "1").to_int())
|
||||
var tile_x: Variant = _get_optional_int_arg(args, "--tile-x")
|
||||
var tile_y: Variant = _get_optional_int_arg(args, "--tile-y")
|
||||
var default_jobs := "1" if tile_x != null or tile_y != null else "auto"
|
||||
var jobs := _parse_jobs_arg(_get_arg_value(args, "--jobs", default_jobs))
|
||||
var force := args.has("--force")
|
||||
|
||||
if jobs > 1 and worker_index == null:
|
||||
@@ -127,18 +128,16 @@ func _initialize() -> void:
|
||||
_progress_update(tx, ty, baked, skipped, failed)
|
||||
continue
|
||||
|
||||
var full_payload: Dictionary = _builder.build_baked_tile_render_payload(
|
||||
var payloads: Dictionary = _builder.build_baked_tile_render_payload_pair(
|
||||
data,
|
||||
_image_cache,
|
||||
ProjectSettings.globalize_path(extracted_dir),
|
||||
0,
|
||||
full_texture_size)
|
||||
var coarse_payload: Dictionary = _builder.build_baked_tile_render_payload(
|
||||
data,
|
||||
_image_cache,
|
||||
ProjectSettings.globalize_path(extracted_dir),
|
||||
full_texture_size,
|
||||
3,
|
||||
coarse_texture_size)
|
||||
var full_payload: Dictionary = payloads.get("full", {})
|
||||
var coarse_payload: Dictionary = payloads.get("coarse", {})
|
||||
|
||||
if full_payload.is_empty():
|
||||
push_warning("Bake produced empty full mesh: %s" % source_res_path)
|
||||
@@ -396,6 +395,13 @@ func _get_optional_int_arg(args: PackedStringArray, name: String):
|
||||
return null
|
||||
|
||||
|
||||
func _parse_jobs_arg(value: String) -> int:
|
||||
var normalized := value.strip_edges().to_lower()
|
||||
if normalized == "auto":
|
||||
return clampi(OS.get_processor_count() - 2, 1, 8)
|
||||
return maxi(1, normalized.to_int())
|
||||
|
||||
|
||||
func _normalize_res_path(path: String) -> String:
|
||||
if path.begins_with("res://"):
|
||||
return path
|
||||
|
||||
+10
-4
@@ -114,10 +114,16 @@ func _bake_terrain(map_name: String, map_dir: String, extracted: String,
|
||||
if data.is_empty() or not data.has("chunks"):
|
||||
failed += 1; continue
|
||||
|
||||
var full_payload := builder.build_baked_tile_render_payload(
|
||||
data, _image_cache, ProjectSettings.globalize_path(extracted), 0, full_tex)
|
||||
var coarse_payload := builder.build_baked_tile_render_payload(
|
||||
data, _image_cache, ProjectSettings.globalize_path(extracted), 3, coarse_tex)
|
||||
var payloads: Dictionary = builder.build_baked_tile_render_payload_pair(
|
||||
data,
|
||||
_image_cache,
|
||||
ProjectSettings.globalize_path(extracted),
|
||||
0,
|
||||
full_tex,
|
||||
3,
|
||||
coarse_tex)
|
||||
var full_payload: Dictionary = payloads.get("full", {})
|
||||
var coarse_payload: Dictionary = payloads.get("coarse", {})
|
||||
if full_payload.is_empty():
|
||||
failed += 1; continue
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
extends SceneTree
|
||||
|
||||
## Converts the heavy baked ADT cache into a lightweight runtime streaming cache.
|
||||
##
|
||||
## Usage:
|
||||
## godot --headless --path <project> --script res://src/tools/build_adt_streaming_cache.gd -- \
|
||||
## --map Azeroth --input res://data/cache/baked_terrain_v2 --output res://data/cache/baked_terrain_stream_v1 --force
|
||||
|
||||
const BAKED_TILE_SCRIPT := preload("res://src/resources/baked_adt_tile.gd")
|
||||
const STREAMING_TILE_SCRIPT := preload("res://src/resources/streaming_adt_tile.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var map_name := _arg(args, "--map", "Azeroth")
|
||||
var input_dir := _res(_arg(args, "--input", "res://data/cache/baked_terrain_v2"))
|
||||
var output_dir := _res(_arg(args, "--output", "res://data/cache/baked_terrain_stream_v1"))
|
||||
var force := args.has("--force")
|
||||
|
||||
var in_map_dir := input_dir.path_join(map_name)
|
||||
var out_map_dir := output_dir.path_join(map_name)
|
||||
var in_map_abs := ProjectSettings.globalize_path(in_map_dir)
|
||||
var out_map_abs := ProjectSettings.globalize_path(out_map_dir)
|
||||
if not DirAccess.dir_exists_absolute(in_map_abs):
|
||||
push_error("Input baked terrain cache not found: %s" % in_map_dir)
|
||||
quit(1)
|
||||
return
|
||||
DirAccess.make_dir_recursive_absolute(out_map_abs)
|
||||
|
||||
var dir := DirAccess.open(in_map_dir)
|
||||
if dir == null:
|
||||
push_error("Cannot open input cache dir: %s" % in_map_dir)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var files := dir.get_files()
|
||||
files.sort()
|
||||
var converted := 0
|
||||
var skipped := 0
|
||||
var failed := 0
|
||||
var started_ms := Time.get_ticks_msec()
|
||||
|
||||
for file_name in files:
|
||||
if not file_name.ends_with(".res"):
|
||||
continue
|
||||
var in_path := in_map_dir.path_join(file_name)
|
||||
var out_path := out_map_dir.path_join(file_name)
|
||||
if not force and ResourceLoader.exists(out_path):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
var baked: Resource = load(in_path)
|
||||
if baked == null or baked.get_script() != BAKED_TILE_SCRIPT:
|
||||
push_warning("Invalid baked tile: %s" % in_path)
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
var stream_tile: Resource = STREAMING_TILE_SCRIPT.new()
|
||||
stream_tile.set("format_version", STREAMING_TILE_SCRIPT.FORMAT_VERSION)
|
||||
stream_tile.set("map_name", str(baked.get("map_name")))
|
||||
stream_tile.set("tile_x", int(baked.get("tile_x")))
|
||||
stream_tile.set("tile_y", int(baked.get("tile_y")))
|
||||
stream_tile.set("tile_origin", baked.get("tile_origin"))
|
||||
stream_tile.set("texture_size", int(baked.get("coarse_texture_size")))
|
||||
stream_tile.set("wmo_names", baked.get("wmo_names"))
|
||||
stream_tile.set("wmo_placements", baked.get("wmo_placements"))
|
||||
stream_tile.set("m2_names", baked.get("m2_names"))
|
||||
stream_tile.set("m2_placements", baked.get("m2_placements"))
|
||||
stream_tile.set("terrain_mesh", baked.get("coarse_mesh"))
|
||||
|
||||
var err := ResourceSaver.save(stream_tile, out_path)
|
||||
if err != OK:
|
||||
push_warning("Failed to save streaming tile %s (err=%d)" % [out_path, err])
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
converted += 1
|
||||
if converted % 25 == 0:
|
||||
print("Converted %d tiles..." % converted)
|
||||
|
||||
var elapsed_ms := Time.get_ticks_msec() - started_ms
|
||||
print("Streaming cache finished. converted=%d skipped=%d failed=%d elapsed=%.2fs" % [
|
||||
converted, skipped, failed, float(elapsed_ms) / 1000.0
|
||||
])
|
||||
quit(0 if failed == 0 else 2)
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
return args[idx + 1]
|
||||
return default
|
||||
|
||||
|
||||
func _res(path: String) -> String:
|
||||
if path.begins_with("res://") or path.begins_with("user://"):
|
||||
return path
|
||||
return "res://" + path.trim_prefix("./").trim_prefix("/")
|
||||
@@ -0,0 +1 @@
|
||||
uid://ctokf2tfwftl7
|
||||
@@ -0,0 +1,182 @@
|
||||
extends SceneTree
|
||||
|
||||
## Builds a lightweight WMO render cache for runtime streaming.
|
||||
##
|
||||
## Usage:
|
||||
## godot --headless --path <project> --script res://src/tools/build_wmo_streaming_cache.gd -- \
|
||||
## --map Azeroth --extracted res://data/extracted --output res://data/cache/wmo_render_v1 --force
|
||||
|
||||
const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd")
|
||||
const WMO_STREAMING_SCRIPT := preload("res://src/resources/wmo_streaming_resource.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var map_name := _arg(args, "--map", "Azeroth")
|
||||
var extracted := _res(_arg(args, "--extracted", "res://data/extracted"))
|
||||
var output_dir := _res(_arg(args, "--output", "res://data/cache/wmo_render_v1"))
|
||||
var force := args.has("--force")
|
||||
|
||||
if not ClassDB.class_exists("ADTLoader") or not ClassDB.class_exists("WMOLoader"):
|
||||
push_error("GDExtension not loaded. Rebuild first.")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir))
|
||||
|
||||
var unique := _collect_unique_wmos(map_name, extracted)
|
||||
if unique.is_empty():
|
||||
push_error("No WMO references found for map: %s" % map_name)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var wmo_loader = ClassDB.instantiate("WMOLoader")
|
||||
var baked := 0
|
||||
var skipped := 0
|
||||
var failed := 0
|
||||
var total := unique.size()
|
||||
var index := 0
|
||||
var started_ms := Time.get_ticks_msec()
|
||||
|
||||
for rel_path_variant in unique.keys():
|
||||
index += 1
|
||||
var rel_path := String(rel_path_variant)
|
||||
var out_path := _get_wmo_render_path(output_dir, rel_path)
|
||||
if not force and ResourceLoader.exists(out_path):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
var abs_wmo := ProjectSettings.globalize_path(extracted.path_join(rel_path))
|
||||
if not FileAccess.file_exists(abs_wmo):
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
var data: Dictionary = wmo_loader.call("load_wmo", abs_wmo)
|
||||
if data.is_empty():
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
var node: Node3D = WMO_BUILDER_SCRIPT.build(data, extracted)
|
||||
if node == null:
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
var render_resource: Resource = WMO_STREAMING_SCRIPT.new()
|
||||
render_resource.set("format_version", WMO_STREAMING_SCRIPT.FORMAT_VERSION)
|
||||
render_resource.set("source_path", rel_path)
|
||||
_collect_render_payload(node, render_resource)
|
||||
node.free()
|
||||
|
||||
if (render_resource.get("meshes") as Array).is_empty() and (render_resource.get("multimeshes") as Array).is_empty():
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out_path.get_base_dir()))
|
||||
var err := ResourceSaver.save(render_resource, out_path)
|
||||
if err != OK:
|
||||
push_warning("save failed for %s: %d" % [out_path, err])
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
baked += 1
|
||||
if index % 10 == 0 or index == total:
|
||||
print("[%d/%d] baked=%d skipped=%d failed=%d" % [index, total, baked, skipped, failed])
|
||||
|
||||
var elapsed_ms := Time.get_ticks_msec() - started_ms
|
||||
print("WMO render cache finished. baked=%d skipped=%d failed=%d elapsed=%.2fs" % [
|
||||
baked,
|
||||
skipped,
|
||||
failed,
|
||||
float(elapsed_ms) / 1000.0,
|
||||
])
|
||||
WMO_BUILDER_SCRIPT.clear_caches()
|
||||
quit(0 if failed == 0 else 2)
|
||||
|
||||
|
||||
func _collect_unique_wmos(map_name: String, extracted: String) -> Dictionary:
|
||||
var unique: Dictionary = {}
|
||||
var adt_loader = ClassDB.instantiate("ADTLoader")
|
||||
var map_dir := extracted.path_join("World/Maps/%s" % map_name)
|
||||
var dir := DirAccess.open(map_dir)
|
||||
if dir == null:
|
||||
push_error("Cannot open map dir: %s" % map_dir)
|
||||
return unique
|
||||
|
||||
print("Scanning ADT tiles...")
|
||||
var scanned := 0
|
||||
for file_name in dir.get_files():
|
||||
if not file_name.ends_with(".adt"):
|
||||
continue
|
||||
var parts := file_name.trim_suffix(".adt").split("_")
|
||||
if parts.size() != 3 or parts[0] != map_name:
|
||||
continue
|
||||
var data: Dictionary = adt_loader.call("load_adt",
|
||||
ProjectSettings.globalize_path(map_dir.path_join(file_name)))
|
||||
if data.is_empty():
|
||||
continue
|
||||
for rel in data.get("wmo_names", PackedStringArray()):
|
||||
var normalized := str(rel).replace("\\", "/").to_lower()
|
||||
if not normalized.is_empty():
|
||||
unique[normalized] = true
|
||||
scanned += 1
|
||||
|
||||
print("Found %d unique WMO models from %d tiles." % [unique.size(), scanned])
|
||||
return unique
|
||||
|
||||
|
||||
func _collect_render_payload(
|
||||
node: Node,
|
||||
render_resource: Resource,
|
||||
parent_transform: Transform3D = Transform3D.IDENTITY) -> void:
|
||||
if node.name == "Occluders":
|
||||
return
|
||||
|
||||
var node_transform := parent_transform
|
||||
if node is Node3D:
|
||||
node_transform = parent_transform * (node as Node3D).transform
|
||||
|
||||
if node is MeshInstance3D:
|
||||
var mesh_instance := node as MeshInstance3D
|
||||
if mesh_instance.mesh != null:
|
||||
var names: PackedStringArray = render_resource.get("mesh_names")
|
||||
var meshes: Array = render_resource.get("meshes")
|
||||
var transforms: Array = render_resource.get("mesh_transforms")
|
||||
names.append(mesh_instance.name)
|
||||
meshes.append(mesh_instance.mesh)
|
||||
transforms.append(node_transform)
|
||||
render_resource.set("mesh_names", names)
|
||||
render_resource.set("meshes", meshes)
|
||||
render_resource.set("mesh_transforms", transforms)
|
||||
elif node is MultiMeshInstance3D:
|
||||
var multimesh_instance := node as MultiMeshInstance3D
|
||||
if multimesh_instance.multimesh != null:
|
||||
var names: PackedStringArray = render_resource.get("multimesh_names")
|
||||
var multimeshes: Array = render_resource.get("multimeshes")
|
||||
var transforms: Array = render_resource.get("multimesh_transforms")
|
||||
names.append(multimesh_instance.name)
|
||||
multimeshes.append(multimesh_instance.multimesh)
|
||||
transforms.append(node_transform)
|
||||
render_resource.set("multimesh_names", names)
|
||||
render_resource.set("multimeshes", multimeshes)
|
||||
render_resource.set("multimesh_transforms", transforms)
|
||||
|
||||
for child in node.get_children():
|
||||
_collect_render_payload(child, render_resource, node_transform)
|
||||
|
||||
|
||||
func _get_wmo_render_path(output_dir: String, rel_path: String) -> String:
|
||||
var normalized := rel_path.replace("\\", "/")
|
||||
return output_dir.path_join(normalized.get_basename() + ".res")
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
return args[idx + 1]
|
||||
return default
|
||||
|
||||
|
||||
func _res(path: String) -> String:
|
||||
if path.begins_with("res://") or path.begins_with("user://"):
|
||||
return path
|
||||
return "res://" + path.trim_prefix("./").trim_prefix("/")
|
||||
@@ -0,0 +1 @@
|
||||
uid://d16p5f4t34jpy
|
||||
Reference in New Issue
Block a user