138 lines
4.7 KiB
GDScript
138 lines
4.7 KiB
GDScript
@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))
|