399 lines
12 KiB
GDScript
399 lines
12 KiB
GDScript
extends Node3D
|
|
|
|
const SonarHudScript = preload("res://src/sonar_hud.gd")
|
|
const SonarModelScript = preload("res://src/sonar_model.gd")
|
|
const SonarSystemScript = preload("res://src/sonar_system.gd")
|
|
const START_POSITION := Vector3(0.0, -25.0, 22.0)
|
|
const METAL_CONTACT_POSITION := Vector3(0.0, -34.0, -45.0)
|
|
|
|
var _submarine: SubmarineController
|
|
var _sonar: Node
|
|
var _hud: CanvasLayer
|
|
var _ping_audio: AudioStreamPlayer
|
|
var _ping_audio_time_remaining := 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
_ensure_input_actions()
|
|
_build_environment()
|
|
_build_calibration_site()
|
|
|
|
_hud = SonarHudScript.new()
|
|
_hud.name = "SonarHud"
|
|
_hud.expanded_changed.connect(_on_sonar_expanded_changed)
|
|
add_child(_hud)
|
|
|
|
_submarine = SubmarineController.new()
|
|
_submarine.name = "Submarine"
|
|
_submarine.position = START_POSITION
|
|
_submarine.mission_origin = START_POSITION
|
|
_submarine.telemetry_updated.connect(_hud.update_telemetry)
|
|
add_child(_submarine)
|
|
|
|
_sonar = SonarSystemScript.new()
|
|
_sonar.name = "SonarSystem"
|
|
_sonar.ping_started.connect(_on_ping_started)
|
|
_sonar.ping_progress.connect(_on_ping_progress)
|
|
_sonar.ping_completed.connect(_on_ping_completed)
|
|
add_child(_sonar)
|
|
_sonar.setup(_submarine, self)
|
|
|
|
_ping_audio = AudioStreamPlayer.new()
|
|
_ping_audio.name = "SonarAudio"
|
|
add_child(_ping_audio)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_hud.update_sonar_pose(_submarine.global_transform)
|
|
if _ping_audio_time_remaining > 0.0:
|
|
_ping_audio_time_remaining = maxf(0.0, _ping_audio_time_remaining - delta)
|
|
if _ping_audio_time_remaining <= 0.0:
|
|
_ping_audio.stop()
|
|
_ping_audio.stream = null
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("sonar_ping"):
|
|
_sonar.request_ping()
|
|
elif event.is_action_pressed("toggle_sonar_map"):
|
|
_hud.toggle_expanded()
|
|
get_viewport().set_input_as_handled()
|
|
elif event.is_action_pressed("toggle_mouse_capture") and _hud.is_expanded():
|
|
_hud.set_expanded(false)
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func _on_ping_started() -> void:
|
|
_hud.show_ping_started(_sonar.max_range)
|
|
_play_sonar_ping()
|
|
|
|
|
|
func _on_ping_progress(
|
|
current_cells: Array[Dictionary],
|
|
previous_cells: Array[Dictionary],
|
|
progress: float
|
|
) -> void:
|
|
_hud.show_ping_progress(
|
|
current_cells,
|
|
previous_cells,
|
|
_sonar.max_range,
|
|
progress
|
|
)
|
|
|
|
|
|
func _on_ping_completed(
|
|
current_cells: Array[Dictionary],
|
|
previous_cells: Array[Dictionary]
|
|
) -> void:
|
|
_hud.show_ping_results(current_cells, previous_cells, _sonar.max_range)
|
|
|
|
|
|
func _on_sonar_expanded_changed(is_expanded: bool) -> void:
|
|
Input.mouse_mode = (
|
|
Input.MOUSE_MODE_VISIBLE
|
|
if is_expanded
|
|
else Input.MOUSE_MODE_CAPTURED
|
|
)
|
|
|
|
|
|
func _build_environment() -> void:
|
|
var world_environment := WorldEnvironment.new()
|
|
var environment := Environment.new()
|
|
environment.background_mode = Environment.BG_COLOR
|
|
environment.background_color = Color("020d13")
|
|
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
environment.ambient_light_color = Color("2b6870")
|
|
environment.ambient_light_energy = 0.46
|
|
environment.fog_enabled = true
|
|
environment.fog_light_color = Color("19434b")
|
|
environment.fog_light_energy = 0.46
|
|
environment.fog_density = 0.010
|
|
world_environment.environment = environment
|
|
add_child(world_environment)
|
|
|
|
var light := DirectionalLight3D.new()
|
|
light.rotation_degrees = Vector3(-48.0, -22.0, 0.0)
|
|
light.light_color = Color("86c8cf")
|
|
light.light_energy = 0.34
|
|
add_child(light)
|
|
_add_water_particles()
|
|
|
|
|
|
func _build_calibration_site() -> void:
|
|
_add_bathymetric_terrain()
|
|
_add_metal_contact()
|
|
|
|
|
|
func _add_bathymetric_terrain() -> void:
|
|
var terrain := StaticBody3D.new()
|
|
terrain.name = "ContinuousCalibrationSeafloor"
|
|
terrain.set_meta("sonar_material", SonarModelScript.MaterialCategory.ROCK)
|
|
terrain.set_meta("sonar_reflectivity", 0.72)
|
|
add_child(terrain)
|
|
|
|
var mesh := _build_bathymetric_terrain_mesh()
|
|
var mesh_instance := MeshInstance3D.new()
|
|
mesh_instance.mesh = mesh
|
|
var material := StandardMaterial3D.new()
|
|
material.albedo_color = Color("315f5c")
|
|
material.roughness = 0.96
|
|
mesh_instance.material_override = material
|
|
terrain.add_child(mesh_instance)
|
|
|
|
var collision := CollisionShape3D.new()
|
|
var shape := ConcavePolygonShape3D.new()
|
|
shape.set_faces(mesh.get_faces())
|
|
shape.backface_collision = true
|
|
collision.shape = shape
|
|
terrain.add_child(collision)
|
|
|
|
|
|
func _build_bathymetric_terrain_mesh() -> ArrayMesh:
|
|
var column_count := 61
|
|
var row_count := 69
|
|
var terrain_width := 150.0
|
|
var terrain_length := 170.0
|
|
var terrain_center_z := -28.0
|
|
var vertices: Array[Vector3] = []
|
|
var normals: Array[Vector3] = []
|
|
var indices: Array[int] = []
|
|
for row in row_count:
|
|
var z := terrain_center_z + lerpf(
|
|
terrain_length * 0.5,
|
|
-terrain_length * 0.5,
|
|
float(row) / float(row_count - 1)
|
|
)
|
|
for column in column_count:
|
|
var x := lerpf(
|
|
-terrain_width * 0.5,
|
|
terrain_width * 0.5,
|
|
float(column) / float(column_count - 1)
|
|
)
|
|
vertices.append(Vector3(x, _terrain_height_at(x, z), z))
|
|
normals.append(Vector3.ZERO)
|
|
|
|
for row in row_count - 1:
|
|
for column in column_count - 1:
|
|
var near_left := row * column_count + column
|
|
var near_right := near_left + 1
|
|
var far_left := (row + 1) * column_count + column
|
|
var far_right := far_left + 1
|
|
_append_terrain_triangle(
|
|
near_left,
|
|
near_right,
|
|
far_left,
|
|
vertices,
|
|
normals,
|
|
indices
|
|
)
|
|
_append_terrain_triangle(
|
|
near_right,
|
|
far_right,
|
|
far_left,
|
|
vertices,
|
|
normals,
|
|
indices
|
|
)
|
|
|
|
for normal_index in normals.size():
|
|
normals[normal_index] = normals[normal_index].normalized()
|
|
var arrays: Array = []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array(vertices)
|
|
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array(normals)
|
|
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array(indices)
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
return mesh
|
|
|
|
|
|
func _append_terrain_triangle(
|
|
first: int,
|
|
second: int,
|
|
third: int,
|
|
vertices: Array[Vector3],
|
|
normals: Array[Vector3],
|
|
indices: Array[int]
|
|
) -> void:
|
|
var normal := (vertices[second] - vertices[first]).cross(
|
|
vertices[third] - vertices[first]
|
|
).normalized()
|
|
if normal.y < 0.0:
|
|
var swapped := second
|
|
second = third
|
|
third = swapped
|
|
normal = -normal
|
|
indices.append_array([first, second, third])
|
|
normals[first] += normal
|
|
normals[second] += normal
|
|
normals[third] += normal
|
|
|
|
|
|
func _terrain_height_at(x: float, z: float) -> float:
|
|
var base := -39.4 + (z + 12.0) * 0.018
|
|
base += sin(x * 0.11) * 0.28 + cos(z * 0.085) * 0.24
|
|
var height := base
|
|
height += _gaussian_height(x, z, -12.0, -8.0, 9.2, 6.0, 8.0)
|
|
height += _gaussian_height(x, z, 14.0, -15.0, 8.2, 6.5, 8.5)
|
|
height += _gaussian_height(x, z, -10.0, -42.0, 16.0, 7.5, 14.0)
|
|
height += _gaussian_height(x, z, 10.0, -42.0, 16.0, 7.5, 14.0)
|
|
height += _gaussian_height(x, z, 0.0, -53.0, 12.0, 15.0, 7.5)
|
|
# This narrow negative ridge cuts a readable entrance into the mound. It is
|
|
# intentionally a bathymetric approximation rather than an impossible
|
|
# overhang in a height field.
|
|
height -= _gaussian_height(x, z, 0.0, -41.0, 19.0, 4.2, 16.0)
|
|
return maxf(height, base - 1.0)
|
|
|
|
|
|
func _gaussian_height(
|
|
x: float,
|
|
z: float,
|
|
center_x: float,
|
|
center_z: float,
|
|
amplitude: float,
|
|
width_x: float,
|
|
width_z: float
|
|
) -> float:
|
|
var normalized_x := (x - center_x) / maxf(width_x, 0.001)
|
|
var normalized_z := (z - center_z) / maxf(width_z, 0.001)
|
|
return amplitude * exp(-0.5 * (normalized_x * normalized_x + normalized_z * normalized_z))
|
|
|
|
|
|
func _add_rock_box(
|
|
node_name: String,
|
|
box_position: Vector3,
|
|
box_size: Vector3,
|
|
box_rotation_degrees: Vector3 = Vector3.ZERO
|
|
) -> StaticBody3D:
|
|
var body := StaticBody3D.new()
|
|
body.name = node_name
|
|
body.position = box_position
|
|
body.rotation_degrees = box_rotation_degrees
|
|
body.set_meta("sonar_material", SonarModelScript.MaterialCategory.ROCK)
|
|
body.set_meta("sonar_reflectivity", 0.68)
|
|
add_child(body)
|
|
|
|
var mesh_instance := MeshInstance3D.new()
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = box_size
|
|
mesh_instance.mesh = mesh
|
|
var material := StandardMaterial3D.new()
|
|
material.albedo_color = Color("315b59")
|
|
material.roughness = 0.92
|
|
mesh_instance.material_override = material
|
|
body.add_child(mesh_instance)
|
|
|
|
var collision := CollisionShape3D.new()
|
|
var shape := BoxShape3D.new()
|
|
shape.size = box_size
|
|
collision.shape = shape
|
|
body.add_child(collision)
|
|
return body
|
|
|
|
|
|
func _add_metal_contact() -> void:
|
|
var contact := StaticBody3D.new()
|
|
contact.name = "MetalCalibrationObject"
|
|
contact.position = Vector3(
|
|
METAL_CONTACT_POSITION.x,
|
|
_terrain_height_at(METAL_CONTACT_POSITION.x, METAL_CONTACT_POSITION.z) + 2.5,
|
|
METAL_CONTACT_POSITION.z
|
|
)
|
|
contact.rotation_degrees.y = 16.0
|
|
contact.set_meta("sonar_material", SonarModelScript.MaterialCategory.METAL)
|
|
contact.set_meta("sonar_reflectivity", 1.0)
|
|
add_child(contact)
|
|
|
|
var mesh_instance := MeshInstance3D.new()
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = Vector3(8.0, 5.0, 7.0)
|
|
mesh_instance.mesh = mesh
|
|
var material := StandardMaterial3D.new()
|
|
material.albedo_color = Color("855a35")
|
|
material.metallic = 0.88
|
|
material.roughness = 0.30
|
|
mesh_instance.material_override = material
|
|
contact.add_child(mesh_instance)
|
|
|
|
var collision := CollisionShape3D.new()
|
|
var shape := BoxShape3D.new()
|
|
shape.size = Vector3(8.0, 5.0, 7.0)
|
|
collision.shape = shape
|
|
contact.add_child(collision)
|
|
|
|
|
|
func _add_water_particles() -> void:
|
|
var particles := GPUParticles3D.new()
|
|
particles.name = "CalibrationWaterParticles"
|
|
particles.position = Vector3(0.0, -28.0, -15.0)
|
|
particles.amount = 280
|
|
particles.lifetime = 13.0
|
|
particles.preprocess = 13.0
|
|
particles.visibility_aabb = AABB(
|
|
Vector3(-55.0, -24.0, -65.0),
|
|
Vector3(110.0, 48.0, 130.0)
|
|
)
|
|
var motion := ParticleProcessMaterial.new()
|
|
motion.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
|
|
motion.emission_box_extents = Vector3(50.0, 20.0, 58.0)
|
|
motion.direction = Vector3(0.2, 0.08, 0.05)
|
|
motion.spread = 45.0
|
|
motion.initial_velocity_min = 0.02
|
|
motion.initial_velocity_max = 0.09
|
|
motion.gravity = Vector3.ZERO
|
|
motion.scale_min = 0.035
|
|
motion.scale_max = 0.10
|
|
particles.process_material = motion
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2(0.07, 0.07)
|
|
var material := StandardMaterial3D.new()
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
|
material.albedo_color = Color(0.58, 0.86, 0.90, 0.35)
|
|
quad.material = material
|
|
particles.draw_pass_1 = quad
|
|
add_child(particles)
|
|
|
|
|
|
func _play_sonar_ping() -> void:
|
|
var generator := AudioStreamGenerator.new()
|
|
generator.mix_rate = 22050.0
|
|
generator.buffer_length = 0.5
|
|
_ping_audio.stream = generator
|
|
_ping_audio.play()
|
|
var playback := _ping_audio.get_stream_playback() as AudioStreamGeneratorPlayback
|
|
if playback == null:
|
|
return
|
|
var duration := 0.42
|
|
_ping_audio_time_remaining = duration + 0.08
|
|
var frame_count := int(generator.mix_rate * duration)
|
|
for frame_index in frame_count:
|
|
var time := float(frame_index) / generator.mix_rate
|
|
var frequency := lerpf(1180.0, 640.0, time / duration)
|
|
var envelope := exp(-time * 7.5) * minf(time * 80.0, 1.0)
|
|
var sample := sin(TAU * frequency * time) * envelope * 0.22
|
|
playback.push_frame(Vector2(sample, sample))
|
|
|
|
|
|
func _ensure_input_actions() -> void:
|
|
_register_key_action("move_forward", KEY_W)
|
|
_register_key_action("move_backward", KEY_S)
|
|
_register_key_action("move_left", KEY_A)
|
|
_register_key_action("move_right", KEY_D)
|
|
_register_key_action("ascend", KEY_SPACE)
|
|
_register_key_action("descend", KEY_CTRL)
|
|
_register_key_action("boost", KEY_SHIFT)
|
|
_register_key_action("abandon_vehicle", KEY_X)
|
|
_register_key_action("toggle_mouse_capture", KEY_ESCAPE)
|
|
_register_key_action("sonar_ping", KEY_F)
|
|
_register_key_action("toggle_sonar_map", KEY_TAB)
|
|
|
|
|
|
func _register_key_action(action_name: StringName, physical_keycode: Key) -> void:
|
|
if not InputMap.has_action(action_name):
|
|
InputMap.add_action(action_name)
|
|
if InputMap.action_get_events(action_name).is_empty():
|
|
var key_event := InputEventKey.new()
|
|
key_event.physical_keycode = physical_keycode
|
|
InputMap.action_add_event(action_name, key_event)
|