Add freshwater habitats and saltwater tide ribbons
This commit is contained in:
parent
53fdc9ef21
commit
69b7969e25
40 changed files with 1323 additions and 148 deletions
138
world/water/shoreline_ribbon_baker.gd
Normal file
138
world/water/shoreline_ribbon_baker.gd
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
@tool
|
||||
class_name ShorelineRibbonBaker
|
||||
extends Node
|
||||
|
||||
@export var water_bodies: Array[ShorelineRibbonConfig] = []
|
||||
@export_enum("Off", "Raw", "Simplified", "Smoothed") var debug_path_stage := 0
|
||||
@export_tool_button("Rebuild Shoreline Ribbons")
|
||||
var rebuild_shorelines: Callable = rebuild_all
|
||||
|
||||
|
||||
func rebuild_all() -> Array[Dictionary]:
|
||||
var results: Array[Dictionary] = []
|
||||
for configuration: ShorelineRibbonConfig in water_bodies:
|
||||
var result := _rebuild(configuration)
|
||||
if result.is_empty():
|
||||
return []
|
||||
results.append(result)
|
||||
_update_debug_display(results)
|
||||
return results
|
||||
|
||||
|
||||
func _rebuild(configuration: ShorelineRibbonConfig) -> Dictionary:
|
||||
if configuration == null:
|
||||
push_error("Shoreline ribbon configuration is missing.")
|
||||
return {}
|
||||
if configuration.water_type != WaterType.Type.SALT_WATER:
|
||||
return {
|
||||
"skipped": true,
|
||||
"water_type": configuration.water_type,
|
||||
"output_path": configuration.output_resource_path,
|
||||
}
|
||||
var source := get_node_or_null(configuration.terrain_source) as Node3D
|
||||
if source == null:
|
||||
push_error(
|
||||
"Shoreline terrain source was not found: %s"
|
||||
% configuration.terrain_source
|
||||
)
|
||||
return {}
|
||||
if configuration.output_resource_path.is_empty():
|
||||
push_error("Shoreline output resource path is empty.")
|
||||
return {}
|
||||
var faces := _terrain_faces(source)
|
||||
if faces.is_empty():
|
||||
push_error("Configured shoreline terrain source exposes no triangles.")
|
||||
return {}
|
||||
var result := ShorelineRibbonGenerator.generate(
|
||||
faces,
|
||||
configuration.water_height,
|
||||
configuration.generation_bounds,
|
||||
configuration.water_reference,
|
||||
configuration.water_is_inside,
|
||||
configuration.simplification_tolerance,
|
||||
configuration.smoothing_iterations,
|
||||
configuration.resample_spacing
|
||||
)
|
||||
var output_directory := configuration.output_resource_path.get_base_dir()
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_directory))
|
||||
var save_error := ResourceSaver.save(
|
||||
result["mesh"], configuration.output_resource_path
|
||||
)
|
||||
if save_error != OK:
|
||||
push_error(
|
||||
"Failed to save %s: %s"
|
||||
% [configuration.output_resource_path, error_string(save_error)]
|
||||
)
|
||||
return {}
|
||||
result["output_path"] = configuration.output_resource_path
|
||||
result["water_height"] = configuration.water_height
|
||||
result["water_type"] = configuration.water_type
|
||||
result["skipped"] = false
|
||||
return result
|
||||
|
||||
|
||||
func _update_debug_display(results: Array[Dictionary]) -> void:
|
||||
var previous := get_node_or_null("_ShorelinePathDebug")
|
||||
if previous != null:
|
||||
previous.queue_free()
|
||||
if not Engine.is_editor_hint() or debug_path_stage == 0:
|
||||
return
|
||||
var debug_mesh := ImmediateMesh.new()
|
||||
var debug_material := StandardMaterial3D.new()
|
||||
debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
debug_material.albedo_color = Color(1.0, 0.35, 0.65, 1.0)
|
||||
var stage_key: String = [
|
||||
"", "debug_raw_paths", "debug_simplified_paths", "debug_smoothed_paths"
|
||||
][debug_path_stage]
|
||||
for result: Dictionary in results:
|
||||
if result.get("skipped", false):
|
||||
continue
|
||||
var height := float(result["water_height"]) + 0.06
|
||||
for path_data: Dictionary in result[stage_key]:
|
||||
var points: PackedVector2Array = path_data["points"]
|
||||
if points.size() < 2:
|
||||
continue
|
||||
debug_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP, debug_material)
|
||||
for point: Vector2 in points:
|
||||
debug_mesh.surface_add_vertex(Vector3(point.x, height, point.y))
|
||||
if path_data["closed"]:
|
||||
debug_mesh.surface_add_vertex(Vector3(points[0].x, height, points[0].y))
|
||||
debug_mesh.surface_end()
|
||||
var debug_instance := MeshInstance3D.new()
|
||||
debug_instance.name = "_ShorelinePathDebug"
|
||||
debug_instance.mesh = debug_mesh
|
||||
add_child(debug_instance)
|
||||
|
||||
|
||||
func _terrain_faces(source: Node3D) -> PackedVector3Array:
|
||||
var result := PackedVector3Array()
|
||||
if source is CollisionShape3D:
|
||||
var collision_shape := source as CollisionShape3D
|
||||
var concave_shape := collision_shape.shape as ConcavePolygonShape3D
|
||||
if concave_shape == null:
|
||||
return result
|
||||
for vertex: Vector3 in concave_shape.get_faces():
|
||||
result.append(_to_map_space(source, vertex))
|
||||
return result
|
||||
if source is MeshInstance3D:
|
||||
var mesh_instance := source as MeshInstance3D
|
||||
if mesh_instance.mesh == null:
|
||||
return result
|
||||
for surface: int in mesh_instance.mesh.get_surface_count():
|
||||
var arrays := mesh_instance.mesh.surface_get_arrays(surface)
|
||||
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
if indices.is_empty():
|
||||
for vertex: Vector3 in vertices:
|
||||
result.append(_to_map_space(source, vertex))
|
||||
else:
|
||||
for index: int in indices:
|
||||
result.append(_to_map_space(source, vertices[index]))
|
||||
return result
|
||||
|
||||
|
||||
func _to_map_space(source: Node3D, vertex: Vector3) -> Vector3:
|
||||
var map_root := get_parent() as Node3D
|
||||
if map_root == null:
|
||||
return source.to_global(vertex)
|
||||
return map_root.to_local(source.to_global(vertex))
|
||||
1
world/water/shoreline_ribbon_baker.gd.uid
Normal file
1
world/water/shoreline_ribbon_baker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bj704rvwwh77b
|
||||
14
world/water/shoreline_ribbon_config.gd
Normal file
14
world/water/shoreline_ribbon_config.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class_name ShorelineRibbonConfig
|
||||
extends Resource
|
||||
|
||||
@export_node_path("CollisionShape3D", "MeshInstance3D") var terrain_source: NodePath
|
||||
@export var water_type: WaterType.Type = WaterType.Type.FRESH_WATER
|
||||
@export var water_height := 0.0
|
||||
@export var generation_bounds := Rect2()
|
||||
@export var water_reference := Vector2.ZERO
|
||||
@export var water_is_inside := true
|
||||
@export_file("*.tres") var output_resource_path := ""
|
||||
@export_group("Optional Smoothing Overrides")
|
||||
@export var simplification_tolerance := -1.0
|
||||
@export_range(-1, 4, 1) var smoothing_iterations := -1
|
||||
@export var resample_spacing := -1.0
|
||||
1
world/water/shoreline_ribbon_config.gd.uid
Normal file
1
world/water/shoreline_ribbon_config.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c8hk3eojujyax
|
||||
443
world/water/shoreline_ribbon_generator.gd
Normal file
443
world/water/shoreline_ribbon_generator.gd
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
class_name ShorelineRibbonGenerator
|
||||
extends RefCounted
|
||||
|
||||
const WATER_PLANE_EPSILON := 0.001
|
||||
const ENDPOINT_MERGE_TOLERANCE := 0.03
|
||||
const LOOP_CLOSURE_TOLERANCE := 0.06
|
||||
const MINIMUM_FRAGMENT_LENGTH := 2.0
|
||||
const SIMPLIFICATION_TOLERANCE := 0.20
|
||||
const SMOOTHING_ITERATIONS := 2
|
||||
const RESAMPLE_SPACING := 0.22
|
||||
const RIBBON_WIDTH := 0.85
|
||||
const LAND_INSET := 0.10
|
||||
const SURFACE_OFFSET := 0.018
|
||||
|
||||
|
||||
static func generate(
|
||||
faces: PackedVector3Array,
|
||||
water_height: float,
|
||||
bounds: Rect2,
|
||||
water_reference: Vector2,
|
||||
water_is_inside: bool,
|
||||
simplification_override := -1.0,
|
||||
smoothing_iterations_override := -1,
|
||||
resample_spacing_override := -1.0
|
||||
) -> Dictionary:
|
||||
var simplification := (
|
||||
SIMPLIFICATION_TOLERANCE
|
||||
if simplification_override < 0.0
|
||||
else simplification_override
|
||||
)
|
||||
var smoothing_iterations := (
|
||||
SMOOTHING_ITERATIONS
|
||||
if smoothing_iterations_override < 0
|
||||
else smoothing_iterations_override
|
||||
)
|
||||
var resample_spacing := (
|
||||
RESAMPLE_SPACING
|
||||
if resample_spacing_override < 0.0
|
||||
else resample_spacing_override
|
||||
)
|
||||
var segments := _extract_segments(faces, water_height, bounds)
|
||||
var raw_paths := _stitch_segments(segments)
|
||||
var processed_paths: Array[Dictionary] = []
|
||||
var simplified_paths: Array[Dictionary] = []
|
||||
var raw_point_count := 0
|
||||
var simplified_point_count := 0
|
||||
var smoothed_point_count := 0
|
||||
for path_data: Dictionary in raw_paths:
|
||||
var raw_points: PackedVector2Array = path_data["points"]
|
||||
var closed: bool = path_data["closed"]
|
||||
if _path_length(raw_points, closed) < MINIMUM_FRAGMENT_LENGTH:
|
||||
continue
|
||||
var simplified := _simplify(raw_points, closed, simplification)
|
||||
simplified_paths.append({"points": simplified, "closed": closed})
|
||||
var smoothed := _chaikin(simplified, closed, smoothing_iterations)
|
||||
var resampled := _resample(smoothed, closed, resample_spacing)
|
||||
if resampled.size() < (3 if closed else 2):
|
||||
continue
|
||||
raw_point_count += raw_points.size()
|
||||
simplified_point_count += simplified.size()
|
||||
smoothed_point_count += resampled.size()
|
||||
processed_paths.append({"points": resampled, "closed": closed})
|
||||
var mesh := _build_mesh(
|
||||
processed_paths,
|
||||
water_height,
|
||||
water_reference,
|
||||
water_is_inside
|
||||
)
|
||||
return {
|
||||
"mesh": mesh,
|
||||
"segment_count": segments.size(),
|
||||
"loop_count": processed_paths.size(),
|
||||
"raw_point_count": raw_point_count,
|
||||
"simplified_point_count": simplified_point_count,
|
||||
"smoothed_point_count": smoothed_point_count,
|
||||
"triangle_count": _mesh_triangle_count(mesh),
|
||||
"debug_raw_paths": raw_paths,
|
||||
"debug_simplified_paths": simplified_paths,
|
||||
"debug_smoothed_paths": processed_paths,
|
||||
}
|
||||
|
||||
|
||||
static func _extract_segments(
|
||||
faces: PackedVector3Array,
|
||||
water_height: float,
|
||||
bounds: Rect2
|
||||
) -> Array[PackedVector2Array]:
|
||||
var segments: Array[PackedVector2Array] = []
|
||||
for index: int in range(0, faces.size(), 3):
|
||||
var triangle: Array[Vector3] = [
|
||||
faces[index], faces[index + 1], faces[index + 2]
|
||||
]
|
||||
var hits := PackedVector2Array()
|
||||
for edge: int in 3:
|
||||
var a := triangle[edge]
|
||||
var b := triangle[(edge + 1) % 3]
|
||||
var distance_a := a.y - water_height
|
||||
var distance_b := b.y - water_height
|
||||
if (
|
||||
absf(distance_a) <= WATER_PLANE_EPSILON
|
||||
and absf(distance_b) <= WATER_PLANE_EPSILON
|
||||
):
|
||||
continue
|
||||
if not (
|
||||
distance_a * distance_b < 0.0
|
||||
or absf(distance_a) <= WATER_PLANE_EPSILON
|
||||
or absf(distance_b) <= WATER_PLANE_EPSILON
|
||||
):
|
||||
continue
|
||||
var denominator := distance_a - distance_b
|
||||
var amount := (
|
||||
0.0
|
||||
if absf(denominator) <= WATER_PLANE_EPSILON
|
||||
else distance_a / denominator
|
||||
)
|
||||
var hit3 := a.lerp(b, clampf(amount, 0.0, 1.0))
|
||||
var hit := Vector2(hit3.x, hit3.z)
|
||||
if bounds.has_point(hit) and not _contains_near(hits, hit):
|
||||
hits.append(hit)
|
||||
if (
|
||||
hits.size() == 2
|
||||
and hits[0].distance_to(hits[1]) > WATER_PLANE_EPSILON
|
||||
):
|
||||
segments.append(hits)
|
||||
return segments
|
||||
|
||||
|
||||
static func _stitch_segments(
|
||||
segments: Array[PackedVector2Array]
|
||||
) -> Array[Dictionary]:
|
||||
var point_by_key: Dictionary = {}
|
||||
var adjacency: Dictionary = {}
|
||||
var unused_edges: Dictionary = {}
|
||||
for segment: PackedVector2Array in segments:
|
||||
var a_key := _point_key(segment[0])
|
||||
var b_key := _point_key(segment[1])
|
||||
if a_key == b_key:
|
||||
continue
|
||||
point_by_key[a_key] = segment[0]
|
||||
point_by_key[b_key] = segment[1]
|
||||
if not adjacency.has(a_key):
|
||||
adjacency[a_key] = []
|
||||
if not adjacency.has(b_key):
|
||||
adjacency[b_key] = []
|
||||
var edge_key := _edge_key(a_key, b_key)
|
||||
if unused_edges.has(edge_key):
|
||||
continue
|
||||
(adjacency[a_key] as Array).append(b_key)
|
||||
(adjacency[b_key] as Array).append(a_key)
|
||||
unused_edges[edge_key] = true
|
||||
|
||||
var starts: Array = []
|
||||
for key: Vector2i in adjacency:
|
||||
if (adjacency[key] as Array).size() != 2:
|
||||
starts.append(key)
|
||||
for key: Vector2i in adjacency:
|
||||
if not starts.has(key):
|
||||
starts.append(key)
|
||||
|
||||
var paths: Array[Dictionary] = []
|
||||
for start: Vector2i in starts:
|
||||
while _has_unused_neighbor(start, adjacency, unused_edges):
|
||||
var walked := _walk_path(start, point_by_key, adjacency, unused_edges)
|
||||
if (walked["points"] as PackedVector2Array).size() >= 2:
|
||||
paths.append(walked)
|
||||
return paths
|
||||
|
||||
|
||||
static func _walk_path(
|
||||
start: Vector2i,
|
||||
point_by_key: Dictionary,
|
||||
adjacency: Dictionary,
|
||||
unused_edges: Dictionary
|
||||
) -> Dictionary:
|
||||
var points := PackedVector2Array()
|
||||
var previous := Vector2i(2147483647, 2147483647)
|
||||
var current := start
|
||||
var closed := false
|
||||
var guard := unused_edges.size() + 2
|
||||
while guard > 0:
|
||||
guard -= 1
|
||||
points.append(point_by_key[current])
|
||||
var next_key := Vector2i(2147483647, 2147483647)
|
||||
for candidate: Vector2i in adjacency[current]:
|
||||
if candidate == previous and (adjacency[current] as Array).size() > 1:
|
||||
continue
|
||||
if unused_edges.get(_edge_key(current, candidate), false):
|
||||
next_key = candidate
|
||||
break
|
||||
if next_key.x == 2147483647:
|
||||
for candidate: Vector2i in adjacency[current]:
|
||||
if unused_edges.get(_edge_key(current, candidate), false):
|
||||
next_key = candidate
|
||||
break
|
||||
if next_key.x == 2147483647:
|
||||
break
|
||||
unused_edges[_edge_key(current, next_key)] = false
|
||||
previous = current
|
||||
current = next_key
|
||||
if current == start:
|
||||
closed = true
|
||||
break
|
||||
return {"points": points, "closed": closed}
|
||||
|
||||
|
||||
static func _simplify(
|
||||
points: PackedVector2Array,
|
||||
closed: bool,
|
||||
tolerance: float
|
||||
) -> PackedVector2Array:
|
||||
if points.size() <= (4 if closed else 2):
|
||||
return points
|
||||
if not closed:
|
||||
return _rdp_open(points, tolerance)
|
||||
var split_a := 0
|
||||
var split_b := 1
|
||||
var greatest_distance := 0.0
|
||||
for a: int in points.size():
|
||||
for b: int in range(a + 1, points.size()):
|
||||
var distance := points[a].distance_squared_to(points[b])
|
||||
if distance > greatest_distance:
|
||||
greatest_distance = distance
|
||||
split_a = a
|
||||
split_b = b
|
||||
var first_arc := _closed_arc(points, split_a, split_b)
|
||||
var second_arc := _closed_arc(points, split_b, split_a)
|
||||
var first_result := _rdp_open(first_arc, tolerance)
|
||||
var second_result := _rdp_open(second_arc, tolerance)
|
||||
var result := PackedVector2Array()
|
||||
for index: int in first_result.size() - 1:
|
||||
result.append(first_result[index])
|
||||
for index: int in second_result.size() - 1:
|
||||
result.append(second_result[index])
|
||||
return result if result.size() >= 4 else points
|
||||
|
||||
|
||||
static func _rdp_open(
|
||||
points: PackedVector2Array,
|
||||
tolerance: float
|
||||
) -> PackedVector2Array:
|
||||
if points.size() <= 2:
|
||||
return points
|
||||
var greatest_distance := 0.0
|
||||
var split_index := 0
|
||||
for index: int in range(1, points.size() - 1):
|
||||
var distance := _point_segment_distance(
|
||||
points[index], points[0], points[points.size() - 1]
|
||||
)
|
||||
if distance > greatest_distance:
|
||||
greatest_distance = distance
|
||||
split_index = index
|
||||
if greatest_distance <= tolerance:
|
||||
return PackedVector2Array([points[0], points[points.size() - 1]])
|
||||
var left := _rdp_open(points.slice(0, split_index + 1), tolerance)
|
||||
var right := _rdp_open(points.slice(split_index), tolerance)
|
||||
var result := PackedVector2Array()
|
||||
for index: int in left.size() - 1:
|
||||
result.append(left[index])
|
||||
result.append_array(right)
|
||||
return result
|
||||
|
||||
|
||||
static func _closed_arc(
|
||||
points: PackedVector2Array,
|
||||
start: int,
|
||||
finish: int
|
||||
) -> PackedVector2Array:
|
||||
var result := PackedVector2Array()
|
||||
var index := start
|
||||
result.append(points[index])
|
||||
while index != finish:
|
||||
index = (index + 1) % points.size()
|
||||
result.append(points[index])
|
||||
return result
|
||||
|
||||
|
||||
static func _chaikin(
|
||||
points: PackedVector2Array,
|
||||
closed: bool,
|
||||
iterations: int
|
||||
) -> PackedVector2Array:
|
||||
var result := points
|
||||
for _iteration: int in iterations:
|
||||
var next := PackedVector2Array()
|
||||
if not closed:
|
||||
next.append(result[0])
|
||||
var edge_count := result.size() if closed else result.size() - 1
|
||||
for index: int in edge_count:
|
||||
var a := result[index]
|
||||
var b := result[(index + 1) % result.size()]
|
||||
next.append(a.lerp(b, 0.25))
|
||||
next.append(a.lerp(b, 0.75))
|
||||
if not closed:
|
||||
next.append(result[result.size() - 1])
|
||||
result = next
|
||||
return result
|
||||
|
||||
|
||||
static func _resample(
|
||||
points: PackedVector2Array,
|
||||
closed: bool,
|
||||
spacing: float
|
||||
) -> PackedVector2Array:
|
||||
var total_length := _path_length(points, closed)
|
||||
if total_length <= spacing:
|
||||
return points
|
||||
var count := maxi(roundi(total_length / spacing), 3 if closed else 2)
|
||||
var actual_spacing := total_length / float(count if closed else count - 1)
|
||||
var result := PackedVector2Array()
|
||||
var edge := 0
|
||||
var edge_start_distance := 0.0
|
||||
var edge_length := points[0].distance_to(points[1])
|
||||
for sample: int in count:
|
||||
var target := actual_spacing * sample
|
||||
while target > edge_start_distance + edge_length and edge < points.size() - 1:
|
||||
edge_start_distance += edge_length
|
||||
edge += 1
|
||||
if edge >= points.size() - 1:
|
||||
edge_length = points[edge].distance_to(points[0]) if closed else 0.0
|
||||
else:
|
||||
edge_length = points[edge].distance_to(points[edge + 1])
|
||||
var next_index := (edge + 1) % points.size()
|
||||
var amount := (
|
||||
0.0
|
||||
if edge_length <= WATER_PLANE_EPSILON
|
||||
else (target - edge_start_distance) / edge_length
|
||||
)
|
||||
result.append(points[edge].lerp(points[next_index], clampf(amount, 0.0, 1.0)))
|
||||
return result
|
||||
|
||||
|
||||
static func _build_mesh(
|
||||
paths: Array[Dictionary],
|
||||
water_height: float,
|
||||
water_reference: Vector2,
|
||||
water_is_inside: bool
|
||||
) -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var uvs := PackedVector2Array()
|
||||
var indices := PackedInt32Array()
|
||||
for path_data: Dictionary in paths:
|
||||
var points: PackedVector2Array = path_data["points"]
|
||||
var closed: bool = path_data["closed"]
|
||||
var base_index := vertices.size()
|
||||
var path_distance := 0.0
|
||||
for index: int in points.size():
|
||||
if index > 0:
|
||||
path_distance += points[index - 1].distance_to(points[index])
|
||||
var previous := points[(index - 1 + points.size()) % points.size()]
|
||||
var following := points[(index + 1) % points.size()]
|
||||
if not closed:
|
||||
previous = points[maxi(index - 1, 0)]
|
||||
following = points[mini(index + 1, points.size() - 1)]
|
||||
var tangent := previous.direction_to(following)
|
||||
var water_normal := Vector2(-tangent.y, tangent.x)
|
||||
var toward_reference := points[index].direction_to(water_reference)
|
||||
if (
|
||||
(water_is_inside and water_normal.dot(toward_reference) < 0.0)
|
||||
or (not water_is_inside and water_normal.dot(toward_reference) > 0.0)
|
||||
):
|
||||
water_normal = -water_normal
|
||||
var land_point := points[index] - water_normal * LAND_INSET
|
||||
var water_point := points[index] + water_normal * (RIBBON_WIDTH - LAND_INSET)
|
||||
vertices.append(Vector3(land_point.x, water_height + SURFACE_OFFSET, land_point.y))
|
||||
vertices.append(Vector3(water_point.x, water_height + SURFACE_OFFSET, water_point.y))
|
||||
normals.append(Vector3.UP)
|
||||
normals.append(Vector3.UP)
|
||||
uvs.append(Vector2(path_distance, 0.0))
|
||||
uvs.append(Vector2(path_distance, 1.0))
|
||||
var edge_count := points.size() if closed else points.size() - 1
|
||||
for index: int in edge_count:
|
||||
var next := (index + 1) % points.size()
|
||||
var a := base_index + index * 2
|
||||
var b := a + 1
|
||||
var c := base_index + next * 2
|
||||
var d := c + 1
|
||||
indices.append_array(PackedInt32Array([a, c, b, b, c, d]))
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
if not vertices.is_empty():
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
|
||||
static func _point_key(point: Vector2) -> Vector2i:
|
||||
return Vector2i(
|
||||
roundi(point.x / ENDPOINT_MERGE_TOLERANCE),
|
||||
roundi(point.y / ENDPOINT_MERGE_TOLERANCE)
|
||||
)
|
||||
|
||||
|
||||
static func _edge_key(a: Vector2i, b: Vector2i) -> String:
|
||||
if a.x < b.x or (a.x == b.x and a.y <= b.y):
|
||||
return "%d:%d|%d:%d" % [a.x, a.y, b.x, b.y]
|
||||
return "%d:%d|%d:%d" % [b.x, b.y, a.x, a.y]
|
||||
|
||||
|
||||
static func _has_unused_neighbor(
|
||||
key: Vector2i,
|
||||
adjacency: Dictionary,
|
||||
unused_edges: Dictionary
|
||||
) -> bool:
|
||||
for neighbor: Vector2i in adjacency[key]:
|
||||
if unused_edges.get(_edge_key(key, neighbor), false):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _contains_near(points: PackedVector2Array, point: Vector2) -> bool:
|
||||
for existing: Vector2 in points:
|
||||
if existing.distance_to(point) <= WATER_PLANE_EPSILON:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _point_segment_distance(point: Vector2, a: Vector2, b: Vector2) -> float:
|
||||
var segment := b - a
|
||||
if segment.length_squared() <= WATER_PLANE_EPSILON:
|
||||
return point.distance_to(a)
|
||||
var amount := clampf((point - a).dot(segment) / segment.length_squared(), 0.0, 1.0)
|
||||
return point.distance_to(a + segment * amount)
|
||||
|
||||
|
||||
static func _path_length(points: PackedVector2Array, closed: bool) -> float:
|
||||
var result := 0.0
|
||||
for index: int in points.size() - 1:
|
||||
result += points[index].distance_to(points[index + 1])
|
||||
if closed and points.size() > 2:
|
||||
result += points[points.size() - 1].distance_to(points[0])
|
||||
return result
|
||||
|
||||
|
||||
static func _mesh_triangle_count(mesh: ArrayMesh) -> int:
|
||||
if mesh.get_surface_count() == 0:
|
||||
return 0
|
||||
var arrays := mesh.surface_get_arrays(0)
|
||||
return (arrays[Mesh.ARRAY_INDEX] as PackedInt32Array).size() / 3
|
||||
1
world/water/shoreline_ribbon_generator.gd.uid
Normal file
1
world/water/shoreline_ribbon_generator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dhp7ctspabll
|
||||
26
world/water/water_type.gd
Normal file
26
world/water/water_type.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
class_name WaterType
|
||||
extends RefCounted
|
||||
|
||||
enum Type {
|
||||
FRESH_WATER,
|
||||
SALT_WATER,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
const FRESH_WATER_MASK := 1 << Type.FRESH_WATER
|
||||
const SALT_WATER_MASK := 1 << Type.SALT_WATER
|
||||
const ALL_FISHABLE_MASK := FRESH_WATER_MASK | SALT_WATER_MASK
|
||||
|
||||
|
||||
static func mask_for(type: Type) -> int:
|
||||
return 1 << int(type) if type != Type.OTHER else 0
|
||||
|
||||
|
||||
static func label(type: Type) -> String:
|
||||
match type:
|
||||
Type.FRESH_WATER:
|
||||
return "Fresh Water"
|
||||
Type.SALT_WATER:
|
||||
return "Salt Water"
|
||||
_:
|
||||
return "Other"
|
||||
1
world/water/water_type.gd.uid
Normal file
1
world/water/water_type.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://nao2xukkyoc8
|
||||
Loading…
Add table
Add a link
Reference in a new issue