216 lines
5.7 KiB
GDScript
216 lines
5.7 KiB
GDScript
class_name TerrainChunkAnalyzer
|
|
extends RefCounted
|
|
|
|
const DEFAULT_EDGE_EPSILON := 0.005
|
|
const DEFAULT_PROFILE_QUANTIZATION := 0.001
|
|
|
|
|
|
static func create_variants(
|
|
definition: TerrainChunkDefinition,
|
|
chunk_size: float,
|
|
edge_epsilon := DEFAULT_EDGE_EPSILON,
|
|
profile_quantization := DEFAULT_PROFILE_QUANTIZATION,
|
|
) -> Array[TerrainChunkVariant]:
|
|
var variants: Array[TerrainChunkVariant] = []
|
|
if definition == null or definition.packed_scene == null:
|
|
return variants
|
|
var chunk_root := definition.packed_scene.instantiate()
|
|
var primary_mesh := find_primary_mesh(
|
|
chunk_root,
|
|
definition.primary_mesh_name,
|
|
)
|
|
if primary_mesh == null or primary_mesh.mesh == null:
|
|
push_error(
|
|
"Terrain chunk %s has no MeshInstance3D named %s."
|
|
% [definition.stable_id, definition.primary_mesh_name]
|
|
)
|
|
chunk_root.free()
|
|
return variants
|
|
|
|
var mesh_transform := _transform_relative_to(primary_mesh, chunk_root)
|
|
var boundary_points := _collect_boundary_points(
|
|
primary_mesh.mesh,
|
|
mesh_transform,
|
|
chunk_size,
|
|
edge_epsilon,
|
|
)
|
|
for quarter_turns: int in 4:
|
|
if not definition.allows_quarter_turn(quarter_turns):
|
|
continue
|
|
var rotated_points := _rotate_points(boundary_points, quarter_turns)
|
|
var variant := TerrainChunkVariant.new()
|
|
variant.definition = definition
|
|
variant.quarter_turns = quarter_turns
|
|
variant.edge_profiles = _build_profiles(
|
|
rotated_points,
|
|
chunk_size,
|
|
edge_epsilon,
|
|
profile_quantization,
|
|
)
|
|
variants.append(variant)
|
|
chunk_root.free()
|
|
return variants
|
|
|
|
|
|
static func find_primary_mesh(
|
|
root: Node,
|
|
primary_mesh_name: StringName,
|
|
) -> MeshInstance3D:
|
|
if root is MeshInstance3D and root.name == primary_mesh_name:
|
|
return root as MeshInstance3D
|
|
for candidate: Node in root.find_children(
|
|
"*",
|
|
"MeshInstance3D",
|
|
true,
|
|
false,
|
|
):
|
|
var mesh_instance := candidate as MeshInstance3D
|
|
if mesh_instance != null and mesh_instance.name == primary_mesh_name:
|
|
return mesh_instance
|
|
return null
|
|
|
|
|
|
static func _transform_relative_to(node: Node3D, root: Node) -> Transform3D:
|
|
var relative_transform := Transform3D.IDENTITY
|
|
var current: Node = node
|
|
while current != null and current != root:
|
|
if current is Node3D:
|
|
relative_transform = (
|
|
(current as Node3D).transform * relative_transform
|
|
)
|
|
current = current.get_parent()
|
|
return relative_transform
|
|
|
|
|
|
static func _collect_boundary_points(
|
|
mesh: Mesh,
|
|
mesh_transform: Transform3D,
|
|
chunk_size: float,
|
|
edge_epsilon: float,
|
|
) -> PackedVector3Array:
|
|
var result := PackedVector3Array()
|
|
var half_size := chunk_size * 0.5
|
|
var unique_points: Dictionary[Vector3i, bool] = {}
|
|
for surface_index: int in mesh.get_surface_count():
|
|
var arrays := mesh.surface_get_arrays(surface_index)
|
|
if arrays.is_empty():
|
|
continue
|
|
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
for source_vertex: Vector3 in vertices:
|
|
var vertex := mesh_transform * source_vertex
|
|
if not _is_boundary_point(vertex, half_size, edge_epsilon):
|
|
continue
|
|
var key := Vector3i(
|
|
roundi(vertex.x / edge_epsilon),
|
|
roundi(vertex.y / edge_epsilon),
|
|
roundi(vertex.z / edge_epsilon),
|
|
)
|
|
if unique_points.has(key):
|
|
continue
|
|
unique_points[key] = true
|
|
result.append(vertex)
|
|
return result
|
|
|
|
|
|
static func _is_boundary_point(
|
|
point: Vector3,
|
|
half_size: float,
|
|
tolerance: float,
|
|
) -> bool:
|
|
return (
|
|
absf(absf(point.x) - half_size) <= tolerance
|
|
or absf(absf(point.z) - half_size) <= tolerance
|
|
)
|
|
|
|
|
|
static func _rotate_points(
|
|
points: PackedVector3Array,
|
|
quarter_turns: int,
|
|
) -> PackedVector3Array:
|
|
var result := PackedVector3Array()
|
|
var angle := float(posmod(quarter_turns, 4)) * PI * 0.5
|
|
for point: Vector3 in points:
|
|
result.append(point.rotated(Vector3.UP, angle))
|
|
return result
|
|
|
|
|
|
static func _build_profiles(
|
|
boundary_points: PackedVector3Array,
|
|
chunk_size: float,
|
|
edge_epsilon: float,
|
|
profile_quantization: float,
|
|
) -> Array[TerrainChunkEdgeProfile]:
|
|
var profiles: Array[TerrainChunkEdgeProfile] = []
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
var edge := edge_value as TerrainChunkTopology.Edge
|
|
profiles.append(
|
|
_build_profile(
|
|
edge,
|
|
boundary_points,
|
|
chunk_size * 0.5,
|
|
edge_epsilon,
|
|
profile_quantization,
|
|
)
|
|
)
|
|
return profiles
|
|
|
|
|
|
static func _build_profile(
|
|
edge: TerrainChunkTopology.Edge,
|
|
boundary_points: PackedVector3Array,
|
|
half_size: float,
|
|
edge_epsilon: float,
|
|
profile_quantization: float,
|
|
) -> TerrainChunkEdgeProfile:
|
|
var unique_points: Dictionary[Vector2i, bool] = {}
|
|
var profile_points: Array[Vector2] = []
|
|
for point: Vector3 in boundary_points:
|
|
if not _point_is_on_edge(point, edge, half_size, edge_epsilon):
|
|
continue
|
|
var tangent := (
|
|
point.x
|
|
if edge in [
|
|
TerrainChunkTopology.Edge.NORTH,
|
|
TerrainChunkTopology.Edge.SOUTH,
|
|
]
|
|
else point.z
|
|
)
|
|
var key := Vector2i(
|
|
roundi(tangent / profile_quantization),
|
|
roundi(point.y / profile_quantization),
|
|
)
|
|
if unique_points.has(key):
|
|
continue
|
|
unique_points[key] = true
|
|
profile_points.append(
|
|
Vector2(key.x, key.y) * profile_quantization
|
|
)
|
|
profile_points.sort_custom(_profile_point_less_than)
|
|
return TerrainChunkEdgeProfile.new(
|
|
edge,
|
|
PackedVector2Array(profile_points),
|
|
)
|
|
|
|
|
|
static func _point_is_on_edge(
|
|
point: Vector3,
|
|
edge: TerrainChunkTopology.Edge,
|
|
half_size: float,
|
|
tolerance: float,
|
|
) -> bool:
|
|
match edge:
|
|
TerrainChunkTopology.Edge.NORTH:
|
|
return absf(point.z + half_size) <= tolerance
|
|
TerrainChunkTopology.Edge.EAST:
|
|
return absf(point.x - half_size) <= tolerance
|
|
TerrainChunkTopology.Edge.SOUTH:
|
|
return absf(point.z - half_size) <= tolerance
|
|
TerrainChunkTopology.Edge.WEST:
|
|
return absf(point.x + half_size) <= tolerance
|
|
return false
|
|
|
|
|
|
static func _profile_point_less_than(a: Vector2, b: Vector2) -> bool:
|
|
if is_equal_approx(a.x, b.x):
|
|
return a.y < b.y
|
|
return a.x < b.x
|