netfishing/world/generation/terrain_chunk_analyzer.gd

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