refactor(M03): extract M2 placement transform resolver
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
class_name M2PlacementTransformResolver
|
||||
extends RefCounted
|
||||
|
||||
## Stateless ADT M2 placement orientation and origin-correction rules.
|
||||
|
||||
const CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
|
||||
|
||||
## Resolves the unscaled Godot-space basis for one ADT M2 placement.
|
||||
func resolve_basis(rotation_radians: Vector3, relative_m2_path: String = "") -> Basis:
|
||||
if not _is_waterfall_path(relative_m2_path):
|
||||
return _resolve_regular_basis(rotation_radians, relative_m2_path)
|
||||
|
||||
var placement_basis := Basis.from_euler(rotation_radians)
|
||||
return (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
|
||||
* placement_basis
|
||||
* Basis(
|
||||
_resolve_waterfall_local_fall_axis(relative_m2_path),
|
||||
_resolve_waterfall_sheet_twist(relative_m2_path)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
## Resolves local origin compensation for a twisted waterfall sheet. Regular
|
||||
## models and waterfall models without a measured anchor return zero.
|
||||
func resolve_origin_offset(
|
||||
rotation_radians: Vector3,
|
||||
relative_m2_path: String,
|
||||
scale_value: float
|
||||
) -> Vector3:
|
||||
if not _is_waterfall_path(relative_m2_path):
|
||||
return Vector3.ZERO
|
||||
var twist_radians := _resolve_waterfall_sheet_twist(relative_m2_path)
|
||||
if is_zero_approx(twist_radians):
|
||||
return Vector3.ZERO
|
||||
var anchor_local_position := _resolve_waterfall_sheet_twist_anchor(relative_m2_path)
|
||||
if anchor_local_position == Vector3.ZERO:
|
||||
return Vector3.ZERO
|
||||
var placement_basis := (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
|
||||
* Basis.from_euler(rotation_radians)
|
||||
)
|
||||
var twisted_basis := placement_basis * Basis(
|
||||
_resolve_waterfall_local_fall_axis(relative_m2_path),
|
||||
twist_radians
|
||||
)
|
||||
return (
|
||||
placement_basis * anchor_local_position
|
||||
- twisted_basis * anchor_local_position
|
||||
) * maxf(scale_value, 0.0001)
|
||||
|
||||
|
||||
func _resolve_regular_basis(rotation_radians: Vector3, relative_m2_path: String) -> Basis:
|
||||
if not _is_elwynn_cliffrock_path(relative_m2_path):
|
||||
return Basis.from_euler(rotation_radians)
|
||||
return (
|
||||
Basis(Vector3.UP, rotation_radians.y + CLIFFROCK_WORLD_YAW_OFFSET)
|
||||
* Basis(Vector3.BACK, rotation_radians.x)
|
||||
* Basis(Vector3.RIGHT, -rotation_radians.z)
|
||||
)
|
||||
|
||||
|
||||
func _is_waterfall_path(relative_m2_path: String) -> bool:
|
||||
var model_name := _normalized_model_name(relative_m2_path)
|
||||
return model_name == "newwaterfall" or model_name == "elwynntallwaterfall01"
|
||||
|
||||
|
||||
func _is_elwynn_cliffrock_path(relative_m2_path: String) -> bool:
|
||||
var model_name := _normalized_model_name(relative_m2_path)
|
||||
return model_name == "elwynncliffrock01" or model_name == "elwynncliffrock02"
|
||||
|
||||
|
||||
func _normalized_model_name(relative_m2_path: String) -> String:
|
||||
return relative_m2_path.replace("\\", "/").to_lower().get_file().get_basename()
|
||||
|
||||
|
||||
func _resolve_waterfall_local_fall_axis(relative_m2_path: String) -> Vector3:
|
||||
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
|
||||
if normalized_path.ends_with("newwaterfall.m2") or normalized_path.ends_with("newwaterfall.mdx"):
|
||||
return Vector3(-0.138742, 0.990329, 0.0).normalized()
|
||||
if (
|
||||
normalized_path.ends_with("elwynntallwaterfall01.m2")
|
||||
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
|
||||
):
|
||||
return Vector3(-0.068962, 0.997619, 0.0).normalized()
|
||||
return Vector3.UP
|
||||
|
||||
|
||||
func _resolve_waterfall_sheet_twist(relative_m2_path: String) -> float:
|
||||
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
|
||||
if normalized_path.ends_with("newwaterfall.m2") or normalized_path.ends_with("newwaterfall.mdx"):
|
||||
return PI * 0.5
|
||||
if (
|
||||
normalized_path.ends_with("elwynntallwaterfall01.m2")
|
||||
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
|
||||
):
|
||||
return -PI * 0.5
|
||||
return 0.0
|
||||
|
||||
|
||||
func _resolve_waterfall_sheet_twist_anchor(relative_m2_path: String) -> Vector3:
|
||||
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
|
||||
if (
|
||||
normalized_path.ends_with("elwynntallwaterfall01.m2")
|
||||
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
|
||||
):
|
||||
return Vector3(-2.667799, 89.62273, 0.00129)
|
||||
return Vector3.ZERO
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcnmp7y8ph3qa
|
||||
@@ -27,6 +27,9 @@ const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_placement_transform_resolver.gd"
|
||||
)
|
||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||
@@ -39,8 +42,6 @@ const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
|
||||
|
||||
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||
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"
|
||||
@@ -213,6 +214,7 @@ var _m2_build_queue: Array = []
|
||||
var _m2_unique_placement_registry := (
|
||||
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
)
|
||||
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
|
||||
var _m2_mesh_cache: Dictionary = {}
|
||||
var _m2_mesh_load_requests: Dictionary = {}
|
||||
var _m2_mesh_finalize_queue: Array = []
|
||||
@@ -3978,86 +3980,6 @@ func _rebuild_cached_wmo_material(material: Material) -> Material:
|
||||
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)
|
||||
@@ -4216,8 +4138,12 @@ func _group_tile_m2_task(
|
||||
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 basis: Basis = _m2_placement_transform_resolver.resolve_basis(rot, normalized)
|
||||
var offset: Vector3 = _m2_placement_transform_resolver.resolve_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):
|
||||
@@ -4905,8 +4831,18 @@ func _build_tile_m2_placeholders(tile_root: Node3D, tile_origin: Vector3, m2_nam
|
||||
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))
|
||||
var xform_basis := _m2_placement_transform_resolver.resolve_basis(
|
||||
rot,
|
||||
rel_path
|
||||
).scaled(Vector3.ONE * maxf(scale_value, 0.01))
|
||||
var xf := Transform3D(
|
||||
xform_basis,
|
||||
local_pos + _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
rel_path,
|
||||
scale_value
|
||||
)
|
||||
)
|
||||
if is_tree:
|
||||
tree_transforms.append(xf)
|
||||
else:
|
||||
@@ -4956,8 +4892,16 @@ func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vecto
|
||||
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))
|
||||
_m2_placement_transform_resolver.resolve_basis(
|
||||
rot,
|
||||
rel_path
|
||||
).scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
|
||||
local_pos + _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
rel_path,
|
||||
scale_value
|
||||
)
|
||||
)
|
||||
return node
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free formula, dependency and timing regression for ADT M2 transforms.
|
||||
|
||||
const RESOLVER_SCRIPT := preload("res://src/render/m2/m2_placement_transform_resolver.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
const CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_regular_basis(failures)
|
||||
_verify_cliffrock_basis(failures)
|
||||
_verify_new_waterfall_basis(failures)
|
||||
_verify_tall_waterfall_basis_and_offset(failures)
|
||||
_verify_path_normalization_and_scale_clamp(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_PLACEMENT_TRANSFORM_RESOLVER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_PLACEMENT_TRANSFORM_RESOLVER PASS cases=9 iterations=10000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_regular_basis(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.2, -0.7, 1.1)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, "World/Generic/Tree.m2"),
|
||||
Basis.from_euler(rotation),
|
||||
"regular basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", rotation, "World/Generic/Tree.m2", 3.0),
|
||||
Vector3.ZERO,
|
||||
"regular offset",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_cliffrock_basis(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.3, 0.4, -0.2)
|
||||
var expected_basis := (
|
||||
Basis(Vector3.UP, rotation.y + CLIFFROCK_WORLD_YAW_OFFSET)
|
||||
* Basis(Vector3.BACK, rotation.x)
|
||||
* Basis(Vector3.RIGHT, -rotation.z)
|
||||
)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, "WORLD\\AZEROTH\\ElwynnCliffRock01.MDX"),
|
||||
expected_basis,
|
||||
"cliffrock basis",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_new_waterfall_basis(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(-0.1, 0.8, 0.2)
|
||||
var fall_axis := Vector3(-0.138742, 0.990329, 0.0).normalized()
|
||||
var expected_basis := (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
|
||||
* Basis.from_euler(rotation)
|
||||
* Basis(fall_axis, PI * 0.5)
|
||||
)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, "world/x/NewWaterfall.m2"),
|
||||
expected_basis,
|
||||
"new waterfall basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", rotation, "world/x/NewWaterfall.m2", 1.0),
|
||||
Vector3.ZERO,
|
||||
"new waterfall offset",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_tall_waterfall_basis_and_offset(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.05, -0.2, 0.15)
|
||||
var path := "world/x/ElwynnTallWaterfall01.m2"
|
||||
var fall_axis := Vector3(-0.068962, 0.997619, 0.0).normalized()
|
||||
var placement_basis := (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rotation)
|
||||
)
|
||||
var expected_basis := placement_basis * Basis(fall_axis, -PI * 0.5)
|
||||
var anchor := Vector3(-2.667799, 89.62273, 0.00129)
|
||||
var expected_offset := (placement_basis * anchor - expected_basis * anchor) * 1.5
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, path),
|
||||
expected_basis,
|
||||
"tall waterfall basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", rotation, path, 1.5),
|
||||
expected_offset,
|
||||
"tall waterfall offset",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_path_normalization_and_scale_clamp(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var path := "WORLD\\X\\ELWYNNTALLWATERFALL01.MDX"
|
||||
var minimum_offset: Vector3 = resolver.call("resolve_origin_offset", Vector3.ZERO, path, 0.0001)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", Vector3.ZERO, path, -9.0),
|
||||
minimum_offset,
|
||||
"negative scale clamp",
|
||||
failures
|
||||
)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", Vector3.ZERO, "ElwynnCliffRock02.m2"),
|
||||
Basis(Vector3.UP, PI),
|
||||
"second cliffrock",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(
|
||||
source.contains("M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()"),
|
||||
"loader composes resolver",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
source.count("_m2_placement_transform_resolver.resolve_basis("),
|
||||
3,
|
||||
"three basis adapters",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
source.count("_m2_placement_transform_resolver.resolve_origin_offset("),
|
||||
3,
|
||||
"three offset adapters",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not source.contains("func _adt_m2_placement_basis("),
|
||||
"loader omits basis formula",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not source.contains("func _waterfall_sheet_twist("),
|
||||
"loader omits waterfall formula",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.1, 0.2, 0.3)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(10000):
|
||||
var path := (
|
||||
"world/x/ElwynnTallWaterfall01.m2"
|
||||
if iteration % 4 == 0
|
||||
else "world/generic/Tree.m2"
|
||||
)
|
||||
resolver.call("resolve_basis", rotation, path)
|
||||
resolver.call("resolve_origin_offset", rotation, path, 1.0)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "10000 transform pairs bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_basis_near(
|
||||
actual_basis: Basis,
|
||||
expected_basis: Basis,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_basis.is_equal_approx(expected_basis):
|
||||
failures.append("%s basis mismatch" % label)
|
||||
|
||||
|
||||
func _expect_vector_near(
|
||||
actual_value: Vector3,
|
||||
expected_value: Vector3,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_value.is_equal_approx(expected_value):
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_equal(
|
||||
actual_value: int,
|
||||
expected_value: int,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4a7gtg46jrdn
|
||||
Reference in New Issue
Block a user