431 lines
13 KiB
GDScript
431 lines
13 KiB
GDScript
class_name SonarReliefRenderer
|
|
extends SubViewport
|
|
|
|
const SonarModelScript = preload("res://src/sonar_model.gd")
|
|
|
|
@export var terrain_skirt_depth := 4.5
|
|
|
|
var _camera: Camera3D
|
|
var _current_mesh: MeshInstance3D
|
|
var _previous_mesh: MeshInstance3D
|
|
var _current_anomaly: MeshInstance3D
|
|
var _previous_anomaly: MeshInstance3D
|
|
var _submarine_marker: Node3D
|
|
var _current_cells: Array[Dictionary] = []
|
|
var _previous_cells: Array[Dictionary] = []
|
|
var _last_built_cell_count := -1
|
|
|
|
|
|
func _ready() -> void:
|
|
size = Vector2i(960, 540)
|
|
disable_3d = false
|
|
render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
world_3d = World3D.new()
|
|
_build_world()
|
|
|
|
|
|
func start_scan() -> void:
|
|
_previous_cells = _current_cells.duplicate(true)
|
|
_current_cells.clear()
|
|
_last_built_cell_count = -1
|
|
_rebuild_terrain(_previous_mesh, _previous_cells)
|
|
_rebuild_terrain(_current_mesh, _current_cells)
|
|
_update_anomaly(_previous_anomaly, _previous_cells)
|
|
_update_anomaly(_current_anomaly, _current_cells)
|
|
_previous_mesh.transparency = 0.48 if not _previous_cells.is_empty() else 1.0
|
|
_previous_anomaly.transparency = _previous_mesh.transparency
|
|
|
|
|
|
func update_scan(
|
|
current_cells: Array[Dictionary],
|
|
previous_cells: Array[Dictionary],
|
|
progress: float
|
|
) -> void:
|
|
_current_cells = current_cells.duplicate(true)
|
|
_previous_cells = previous_cells.duplicate(true)
|
|
if _current_cells.size() != _last_built_cell_count:
|
|
_last_built_cell_count = _current_cells.size()
|
|
_rebuild_terrain(_current_mesh, _current_cells)
|
|
_update_anomaly(_current_anomaly, _current_cells)
|
|
if _previous_mesh.mesh == null and not _previous_cells.is_empty():
|
|
_rebuild_terrain(_previous_mesh, _previous_cells)
|
|
_update_anomaly(_previous_anomaly, _previous_cells)
|
|
var previous_alpha := lerpf(0.48, 1.0, clampf(progress, 0.0, 1.0))
|
|
_previous_mesh.transparency = previous_alpha
|
|
_previous_anomaly.transparency = previous_alpha
|
|
|
|
|
|
func finish_scan(current_cells: Array[Dictionary]) -> void:
|
|
_current_cells = current_cells.duplicate(true)
|
|
_last_built_cell_count = _current_cells.size()
|
|
_rebuild_terrain(_current_mesh, _current_cells)
|
|
_update_anomaly(_current_anomaly, _current_cells)
|
|
_previous_cells.clear()
|
|
_previous_mesh.mesh = null
|
|
_previous_mesh.transparency = 1.0
|
|
_previous_anomaly.visible = false
|
|
_previous_anomaly.transparency = 1.0
|
|
|
|
|
|
func update_submarine_pose(submarine_transform: Transform3D) -> void:
|
|
if _camera == null:
|
|
return
|
|
var forward := -submarine_transform.basis.z
|
|
forward.y = 0.0
|
|
forward = forward.normalized()
|
|
var camera_position := submarine_transform.origin - forward * 30.0 + Vector3.UP * 65.0
|
|
var focus := submarine_transform.origin + forward * 43.0 + Vector3.DOWN * 8.0
|
|
_camera.global_position = camera_position
|
|
_camera.look_at(focus, Vector3.UP)
|
|
_submarine_marker.global_position = submarine_transform.origin
|
|
_submarine_marker.rotation = Vector3(0.0, submarine_transform.basis.get_euler().y, 0.0)
|
|
|
|
|
|
func current_face_count() -> int:
|
|
if _current_mesh.mesh == null:
|
|
return 0
|
|
return int(_current_mesh.mesh.get_faces().size() / 3)
|
|
|
|
|
|
func _build_world() -> void:
|
|
var environment_node := WorldEnvironment.new()
|
|
var environment := Environment.new()
|
|
environment.background_mode = Environment.BG_COLOR
|
|
environment.background_color = Color("020407")
|
|
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
environment.ambient_light_color = Color("8fc9d1")
|
|
environment.ambient_light_energy = 0.42
|
|
environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
|
environment_node.environment = environment
|
|
add_child(environment_node)
|
|
|
|
_previous_mesh = _make_terrain_instance("PreviousBathymetry")
|
|
_current_mesh = _make_terrain_instance("CurrentBathymetry")
|
|
_previous_anomaly = _make_anomaly_instance("PreviousAnomaly")
|
|
_current_anomaly = _make_anomaly_instance("CurrentAnomaly")
|
|
|
|
var key_light := DirectionalLight3D.new()
|
|
key_light.name = "ReliefKeyLight"
|
|
key_light.rotation_degrees = Vector3(-58.0, -34.0, 0.0)
|
|
key_light.light_color = Color("d8fbff")
|
|
key_light.light_energy = 1.25
|
|
key_light.shadow_enabled = true
|
|
add_child(key_light)
|
|
|
|
var fill_light := DirectionalLight3D.new()
|
|
fill_light.name = "ReliefFillLight"
|
|
fill_light.rotation_degrees = Vector3(-35.0, 145.0, 0.0)
|
|
fill_light.light_color = Color("3d86ad")
|
|
fill_light.light_energy = 0.38
|
|
add_child(fill_light)
|
|
|
|
_camera = Camera3D.new()
|
|
_camera.name = "BathymetryCamera"
|
|
_camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
|
_camera.size = 116.0
|
|
_camera.far = 240.0
|
|
_camera.current = true
|
|
add_child(_camera)
|
|
|
|
_build_submarine_marker()
|
|
|
|
|
|
func _make_terrain_instance(node_name: String) -> MeshInstance3D:
|
|
var instance := MeshInstance3D.new()
|
|
instance.name = node_name
|
|
var material := StandardMaterial3D.new()
|
|
material.vertex_color_use_as_albedo = true
|
|
material.roughness = 0.84
|
|
material.metallic = 0.0
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
# The scan may be inspected while the submarine pitches or passes above it.
|
|
# Rendering both sides keeps the reconstructed sheet readable from every
|
|
# permitted sonar-camera angle.
|
|
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
instance.material_override = material
|
|
add_child(instance)
|
|
return instance
|
|
|
|
|
|
func _make_anomaly_instance(node_name: String) -> MeshInstance3D:
|
|
var instance := MeshInstance3D.new()
|
|
instance.name = node_name
|
|
var crate := BoxMesh.new()
|
|
crate.size = Vector3(8.0, 4.5, 8.0)
|
|
instance.mesh = crate
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.albedo_color = Color("ff9d1f")
|
|
material.emission_enabled = true
|
|
material.emission = Color("ff6800")
|
|
material.emission_energy_multiplier = 4.0
|
|
material.roughness = 0.32
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
instance.material_override = material
|
|
instance.visible = false
|
|
add_child(instance)
|
|
return instance
|
|
|
|
|
|
func _build_submarine_marker() -> void:
|
|
_submarine_marker = Node3D.new()
|
|
_submarine_marker.name = "SubmarineReference"
|
|
add_child(_submarine_marker)
|
|
|
|
var body := MeshInstance3D.new()
|
|
var body_mesh := CapsuleMesh.new()
|
|
body_mesh.radius = 1.15
|
|
body_mesh.height = 5.8
|
|
body_mesh.radial_segments = 16
|
|
body_mesh.rings = 6
|
|
body.mesh = body_mesh
|
|
body.rotation_degrees.x = 90.0
|
|
var body_material := StandardMaterial3D.new()
|
|
body_material.albedo_color = Color("eaffff")
|
|
body_material.emission_enabled = true
|
|
body_material.emission = Color("a7f8ff")
|
|
body_material.emission_energy_multiplier = 1.45
|
|
body.material_override = body_material
|
|
_submarine_marker.add_child(body)
|
|
|
|
var nose := MeshInstance3D.new()
|
|
var nose_mesh := CylinderMesh.new()
|
|
nose_mesh.top_radius = 0.0
|
|
nose_mesh.bottom_radius = 0.72
|
|
nose_mesh.height = 1.6
|
|
nose_mesh.radial_segments = 16
|
|
nose.mesh = nose_mesh
|
|
nose.position.z = -3.5
|
|
nose.rotation_degrees.x = -90.0
|
|
nose.material_override = body_material
|
|
_submarine_marker.add_child(nose)
|
|
|
|
|
|
func _rebuild_terrain(instance: MeshInstance3D, cells: Array[Dictionary]) -> void:
|
|
var samples: Dictionary = {}
|
|
for cell: Dictionary in cells:
|
|
if String(cell.get("sample_type", "bathymetry")) != "bathymetry":
|
|
continue
|
|
samples[Vector2i(int(cell["column"]), int(cell["row"]))] = cell
|
|
if samples.size() < 4:
|
|
instance.mesh = null
|
|
return
|
|
|
|
var keys: Array[Vector2i] = []
|
|
for key: Vector2i in samples:
|
|
keys.append(key)
|
|
keys.sort_custom(func(a: Vector2i, b: Vector2i) -> bool:
|
|
return a.y < b.y or (a.y == b.y and a.x < b.x)
|
|
)
|
|
|
|
var vertices: Array[Vector3] = []
|
|
var colors: Array[Color] = []
|
|
var normals: Array[Vector3] = []
|
|
var indices: Array[int] = []
|
|
var vertex_for_key: Dictionary = {}
|
|
for key: Vector2i in keys:
|
|
var cell: Dictionary = samples[key]
|
|
vertex_for_key[key] = vertices.size()
|
|
vertices.append(cell["position"])
|
|
colors.append(_bathymetry_color(cell))
|
|
normals.append(Vector3.ZERO)
|
|
|
|
for key: Vector2i in keys:
|
|
var right_key := key + Vector2i.RIGHT
|
|
var far_key := key + Vector2i.DOWN
|
|
var far_right_key := key + Vector2i(1, 1)
|
|
if not (
|
|
vertex_for_key.has(right_key)
|
|
and vertex_for_key.has(far_key)
|
|
and vertex_for_key.has(far_right_key)
|
|
):
|
|
continue
|
|
_append_top_triangle(
|
|
int(vertex_for_key[key]),
|
|
int(vertex_for_key[right_key]),
|
|
int(vertex_for_key[far_key]),
|
|
vertices,
|
|
normals,
|
|
indices
|
|
)
|
|
_append_top_triangle(
|
|
int(vertex_for_key[right_key]),
|
|
int(vertex_for_key[far_right_key]),
|
|
int(vertex_for_key[far_key]),
|
|
vertices,
|
|
normals,
|
|
indices
|
|
)
|
|
|
|
if indices.is_empty():
|
|
instance.mesh = null
|
|
return
|
|
for normal_index in normals.size():
|
|
normals[normal_index] = normals[normal_index].normalized()
|
|
|
|
_append_terrain_skirt(samples, vertices, normals, colors, indices)
|
|
var arrays: Array = []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array(vertices)
|
|
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array(normals)
|
|
arrays[Mesh.ARRAY_COLOR] = PackedColorArray(colors)
|
|
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array(indices)
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
instance.mesh = mesh
|
|
|
|
|
|
func _append_top_triangle(
|
|
first: int,
|
|
second: int,
|
|
third: int,
|
|
vertices: Array[Vector3],
|
|
normals: Array[Vector3],
|
|
indices: Array[int]
|
|
) -> void:
|
|
var face_normal := (vertices[second] - vertices[first]).cross(
|
|
vertices[third] - vertices[first]
|
|
).normalized()
|
|
if face_normal.y < 0.0:
|
|
var swapped := second
|
|
second = third
|
|
third = swapped
|
|
face_normal = -face_normal
|
|
indices.append_array([first, second, third])
|
|
normals[first] += face_normal
|
|
normals[second] += face_normal
|
|
normals[third] += face_normal
|
|
|
|
|
|
func _append_terrain_skirt(
|
|
samples: Dictionary,
|
|
vertices: Array[Vector3],
|
|
normals: Array[Vector3],
|
|
colors: Array[Color],
|
|
indices: Array[int]
|
|
) -> void:
|
|
var row_bounds := _sample_bounds(samples)
|
|
if row_bounds.is_empty():
|
|
return
|
|
var minimum_row: int = row_bounds["min_row"]
|
|
var maximum_row: int = row_bounds["max_row"]
|
|
var minimum_column: int = row_bounds["min_column"]
|
|
var maximum_column: int = row_bounds["max_column"]
|
|
for column in range(minimum_column, maximum_column):
|
|
_append_skirt_between(
|
|
samples,
|
|
Vector2i(column, minimum_row),
|
|
Vector2i(column + 1, minimum_row),
|
|
vertices,
|
|
normals,
|
|
colors,
|
|
indices
|
|
)
|
|
_append_skirt_between(
|
|
samples,
|
|
Vector2i(column + 1, maximum_row),
|
|
Vector2i(column, maximum_row),
|
|
vertices,
|
|
normals,
|
|
colors,
|
|
indices
|
|
)
|
|
for row in range(minimum_row, maximum_row):
|
|
_append_skirt_between(
|
|
samples,
|
|
Vector2i(minimum_column, row + 1),
|
|
Vector2i(minimum_column, row),
|
|
vertices,
|
|
normals,
|
|
colors,
|
|
indices
|
|
)
|
|
_append_skirt_between(
|
|
samples,
|
|
Vector2i(maximum_column, row),
|
|
Vector2i(maximum_column, row + 1),
|
|
vertices,
|
|
normals,
|
|
colors,
|
|
indices
|
|
)
|
|
|
|
|
|
func _append_skirt_between(
|
|
samples: Dictionary,
|
|
first_key: Vector2i,
|
|
second_key: Vector2i,
|
|
vertices: Array[Vector3],
|
|
normals: Array[Vector3],
|
|
colors: Array[Color],
|
|
indices: Array[int]
|
|
) -> void:
|
|
if not samples.has(first_key) or not samples.has(second_key):
|
|
return
|
|
var first: Vector3 = samples[first_key]["position"]
|
|
var second: Vector3 = samples[second_key]["position"]
|
|
var first_bottom := first + Vector3.DOWN * terrain_skirt_depth
|
|
var second_bottom := second + Vector3.DOWN * terrain_skirt_depth
|
|
var side_normal := (second - first).cross(first_bottom - first).normalized()
|
|
var base_index := vertices.size()
|
|
vertices.append_array([first, second, first_bottom, second_bottom])
|
|
normals.append_array([side_normal, side_normal, side_normal, side_normal])
|
|
var side_color := Color("152a32")
|
|
colors.append_array([side_color, side_color, side_color, side_color])
|
|
indices.append_array([
|
|
base_index,
|
|
base_index + 1,
|
|
base_index + 2,
|
|
base_index + 1,
|
|
base_index + 3,
|
|
base_index + 2,
|
|
])
|
|
|
|
|
|
func _sample_bounds(samples: Dictionary) -> Dictionary:
|
|
var minimum_row := 1 << 30
|
|
var maximum_row := -(1 << 30)
|
|
var minimum_column := 1 << 30
|
|
var maximum_column := -(1 << 30)
|
|
for key: Vector2i in samples:
|
|
minimum_row = mini(minimum_row, key.y)
|
|
maximum_row = maxi(maximum_row, key.y)
|
|
minimum_column = mini(minimum_column, key.x)
|
|
maximum_column = maxi(maximum_column, key.x)
|
|
return {
|
|
"min_row": minimum_row,
|
|
"max_row": maximum_row,
|
|
"min_column": minimum_column,
|
|
"max_column": maximum_column,
|
|
}
|
|
|
|
|
|
func _bathymetry_color(cell: Dictionary) -> Color:
|
|
if int(cell["material"]) == SonarModelScript.MaterialCategory.METAL:
|
|
return Color("ff6b22")
|
|
var elevation := clampf(float(cell.get("elevation", 0.0)), 0.0, 1.0)
|
|
var color: Color
|
|
if elevation < 0.28:
|
|
color = Color("173f91").lerp(Color("168fc1"), elevation / 0.28)
|
|
elif elevation < 0.52:
|
|
color = Color("168fc1").lerp(Color("36b979"), (elevation - 0.28) / 0.24)
|
|
elif elevation < 0.76:
|
|
color = Color("36b979").lerp(Color("d7d93d"), (elevation - 0.52) / 0.24)
|
|
else:
|
|
color = Color("d7d93d").lerp(Color("ee5734"), (elevation - 0.76) / 0.24)
|
|
var return_strength := clampf(float(cell.get("intensity", 0.5)), 0.0, 1.0)
|
|
return color.lerp(Color.WHITE, return_strength * 0.08)
|
|
|
|
|
|
func _update_anomaly(instance: MeshInstance3D, cells: Array[Dictionary]) -> void:
|
|
var position_sum := Vector3.ZERO
|
|
var anomaly_count := 0
|
|
for cell: Dictionary in cells:
|
|
if int(cell.get("material", SonarModelScript.MaterialCategory.ROCK)) != SonarModelScript.MaterialCategory.METAL:
|
|
continue
|
|
position_sum += cell["position"]
|
|
anomaly_count += 1
|
|
instance.visible = anomaly_count > 0
|
|
if anomaly_count > 0:
|
|
instance.global_position = position_sum / float(anomaly_count) + Vector3.UP * 1.4
|