Files
Ransom-s-wake/src/poc_b.gd
T

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)