Add production bathymetric sonar slice
This commit is contained in:
1 parent
a18e82eb44
commit
7613daee84
28 files changed
+1825
-87
No files matched your search
@@ -0,0 +1,39 @@
|
||||
class_name SonarContact
|
||||
extends RefCounted
|
||||
|
||||
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||
|
||||
var contact_id: StringName
|
||||
var material: SonarMaterial.Value
|
||||
var center_m: Vector3
|
||||
var bounds: AABB
|
||||
var maximum_intensity: float
|
||||
var sample_count: int
|
||||
|
||||
|
||||
func _init(
|
||||
p_contact_id: StringName,
|
||||
p_material: SonarMaterial.Value,
|
||||
p_center_m: Vector3,
|
||||
p_bounds: AABB,
|
||||
p_maximum_intensity: float,
|
||||
p_sample_count: int
|
||||
) -> void:
|
||||
contact_id = p_contact_id
|
||||
material = p_material
|
||||
center_m = p_center_m
|
||||
bounds = p_bounds
|
||||
maximum_intensity = p_maximum_intensity
|
||||
sample_count = p_sample_count
|
||||
|
||||
|
||||
func validation_error() -> String:
|
||||
if contact_id.is_empty():
|
||||
return "sonar contact id is empty"
|
||||
if material == SonarMaterial.Value.UNKNOWN or not SonarMaterial.is_valid(material):
|
||||
return "sonar contact material is invalid"
|
||||
if sample_count <= 0:
|
||||
return "sonar contact sample count must be positive"
|
||||
if not is_finite(maximum_intensity) or maximum_intensity < 0.0 or maximum_intensity > 1.0:
|
||||
return "sonar contact intensity must be between 0 and 1"
|
||||
return ""
|
||||
@@ -0,0 +1,12 @@
|
||||
class_name SonarMaterial
|
||||
extends RefCounted
|
||||
|
||||
enum Value {
|
||||
UNKNOWN,
|
||||
ROCK,
|
||||
METAL,
|
||||
}
|
||||
|
||||
|
||||
static func is_valid(value: int) -> bool:
|
||||
return value >= Value.UNKNOWN and value <= Value.METAL
|
||||
@@ -0,0 +1,53 @@
|
||||
class_name SonarSample
|
||||
extends RefCounted
|
||||
|
||||
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||
|
||||
var position_m: Vector3
|
||||
var normal: Vector3
|
||||
var intensity: float
|
||||
var elevation_m: float
|
||||
var material: SonarMaterial.Value
|
||||
var observed: bool
|
||||
|
||||
|
||||
func _init(
|
||||
p_position_m: Vector3 = Vector3.ZERO,
|
||||
p_normal: Vector3 = Vector3.UP,
|
||||
p_intensity: float = 0.0,
|
||||
p_elevation_m: float = 0.0,
|
||||
p_material: SonarMaterial.Value = SonarMaterial.Value.UNKNOWN,
|
||||
p_observed: bool = false
|
||||
) -> void:
|
||||
position_m = p_position_m
|
||||
normal = p_normal
|
||||
intensity = p_intensity
|
||||
elevation_m = p_elevation_m
|
||||
material = p_material
|
||||
observed = p_observed
|
||||
|
||||
|
||||
static func unknown() -> SonarSample:
|
||||
return new()
|
||||
|
||||
|
||||
func validation_error() -> String:
|
||||
if not observed:
|
||||
if material != SonarMaterial.Value.UNKNOWN:
|
||||
return "unobserved sonar sample has a material"
|
||||
return ""
|
||||
if not _vector_is_finite(position_m):
|
||||
return "sonar sample position is not finite"
|
||||
if not _vector_is_finite(normal) or normal.length_squared() < 0.25:
|
||||
return "sonar sample normal is invalid"
|
||||
if not is_finite(intensity) or intensity < 0.0 or intensity > 1.0:
|
||||
return "sonar sample intensity must be between 0 and 1"
|
||||
if not is_finite(elevation_m):
|
||||
return "sonar sample elevation is not finite"
|
||||
if material == SonarMaterial.Value.UNKNOWN or not SonarMaterial.is_valid(material):
|
||||
return "observed sonar sample material is invalid"
|
||||
return ""
|
||||
|
||||
|
||||
func _vector_is_finite(value: Vector3) -> bool:
|
||||
return is_finite(value.x) and is_finite(value.y) and is_finite(value.z)
|
||||
@@ -0,0 +1,115 @@
|
||||
class_name SonarSampleBuffer
|
||||
extends RefCounted
|
||||
|
||||
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||
const SonarSample = preload("res://game/domain/sonar/sonar_sample.gd")
|
||||
|
||||
var columns: int
|
||||
var rows: int
|
||||
|
||||
var _positions := PackedVector3Array()
|
||||
var _normals := PackedVector3Array()
|
||||
var _intensities := PackedFloat32Array()
|
||||
var _elevations_m := PackedFloat32Array()
|
||||
var _materials := PackedInt32Array()
|
||||
var _observed := PackedByteArray()
|
||||
var _sealed := false
|
||||
|
||||
|
||||
func _init(p_columns: int, p_rows: int) -> void:
|
||||
columns = p_columns
|
||||
rows = p_rows
|
||||
var sample_count := maxi(columns, 0) * maxi(rows, 0)
|
||||
_positions.resize(sample_count)
|
||||
_normals.resize(sample_count)
|
||||
_intensities.resize(sample_count)
|
||||
_elevations_m.resize(sample_count)
|
||||
_materials.resize(sample_count)
|
||||
_observed.resize(sample_count)
|
||||
for index in sample_count:
|
||||
_normals[index] = Vector3.UP
|
||||
_materials[index] = SonarMaterial.Value.UNKNOWN
|
||||
|
||||
|
||||
func set_sample(row: int, column: int, sample: SonarSample) -> String:
|
||||
if _sealed:
|
||||
return "sonar sample buffer is sealed"
|
||||
var index := _index_of(row, column)
|
||||
if index < 0:
|
||||
return "sonar sample coordinates are outside the buffer"
|
||||
if sample == null:
|
||||
return "sonar sample is missing"
|
||||
var error := sample.validation_error()
|
||||
if not error.is_empty():
|
||||
return error
|
||||
if not sample.observed:
|
||||
return "cannot write an unobserved sonar sample"
|
||||
if _observed[index] == 1:
|
||||
return "sonar sample coordinates are duplicated"
|
||||
_positions[index] = sample.position_m
|
||||
_normals[index] = sample.normal.normalized()
|
||||
_intensities[index] = sample.intensity
|
||||
_elevations_m[index] = sample.elevation_m
|
||||
_materials[index] = sample.material
|
||||
_observed[index] = 1
|
||||
return ""
|
||||
|
||||
|
||||
func seal() -> String:
|
||||
if columns < 2 or rows < 2:
|
||||
return "sonar sample buffer dimensions must be at least 2 by 2"
|
||||
_sealed = true
|
||||
return ""
|
||||
|
||||
|
||||
func is_sealed() -> bool:
|
||||
return _sealed
|
||||
|
||||
|
||||
func sample_count() -> int:
|
||||
return columns * rows
|
||||
|
||||
|
||||
func observed_count() -> int:
|
||||
var count := 0
|
||||
for value: int in _observed:
|
||||
count += value
|
||||
return count
|
||||
|
||||
|
||||
func sample_at(row: int, column: int) -> SonarSample:
|
||||
var index := _index_of(row, column)
|
||||
if index < 0 or _observed[index] == 0:
|
||||
return SonarSample.unknown()
|
||||
return SonarSample.new(
|
||||
_positions[index],
|
||||
_normals[index],
|
||||
_intensities[index],
|
||||
_elevations_m[index],
|
||||
_materials[index],
|
||||
true
|
||||
)
|
||||
|
||||
|
||||
func validation_error() -> String:
|
||||
if columns < 2 or rows < 2:
|
||||
return "sonar sample buffer dimensions must be at least 2 by 2"
|
||||
if not _sealed:
|
||||
return "sonar sample buffer must be sealed before publication"
|
||||
var expected_size := columns * rows
|
||||
if (
|
||||
_positions.size() != expected_size
|
||||
or _normals.size() != expected_size
|
||||
or _intensities.size() != expected_size
|
||||
or _elevations_m.size() != expected_size
|
||||
or _materials.size() != expected_size
|
||||
or _observed.size() != expected_size
|
||||
):
|
||||
return "sonar sample buffer arrays are not aligned"
|
||||
return ""
|
||||
|
||||
|
||||
func _index_of(row: int, column: int) -> int:
|
||||
if row < 0 or row >= rows or column < 0 or column >= columns:
|
||||
return -1
|
||||
return row * columns + column
|
||||
@@ -0,0 +1,119 @@
|
||||
class_name SonarScan
|
||||
extends RefCounted
|
||||
|
||||
const SonarScanState = preload("res://game/domain/sonar/sonar_scan_state.gd")
|
||||
const SonarSampleBuffer = preload("res://game/domain/sonar/sonar_sample_buffer.gd")
|
||||
const SonarContact = preload("res://game/domain/sonar/sonar_contact.gd")
|
||||
|
||||
var scan_id: StringName
|
||||
var session_id: StringName
|
||||
var sensor_transform: Transform3D
|
||||
var minimum_range_m: float
|
||||
var maximum_range_m: float
|
||||
var horizontal_opening_radians: float
|
||||
var vertical_opening_radians: float
|
||||
var reveal_duration_seconds: float
|
||||
var recovery_duration_seconds: float
|
||||
var samples: SonarSampleBuffer
|
||||
var contacts: Array[SonarContact]
|
||||
var minimum_elevation_m: float
|
||||
var maximum_elevation_m: float
|
||||
var state: SonarScanState.Value
|
||||
var reveal_progress: float
|
||||
|
||||
|
||||
func _init(
|
||||
p_scan_id: StringName,
|
||||
p_session_id: StringName,
|
||||
p_sensor_transform: Transform3D,
|
||||
p_minimum_range_m: float,
|
||||
p_maximum_range_m: float,
|
||||
p_horizontal_opening_radians: float,
|
||||
p_vertical_opening_radians: float,
|
||||
p_reveal_duration_seconds: float,
|
||||
p_recovery_duration_seconds: float,
|
||||
p_samples: SonarSampleBuffer,
|
||||
p_contacts: Array[SonarContact],
|
||||
p_minimum_elevation_m: float,
|
||||
p_maximum_elevation_m: float,
|
||||
p_state: SonarScanState.Value,
|
||||
p_reveal_progress: float
|
||||
) -> void:
|
||||
scan_id = p_scan_id
|
||||
session_id = p_session_id
|
||||
sensor_transform = p_sensor_transform
|
||||
minimum_range_m = p_minimum_range_m
|
||||
maximum_range_m = p_maximum_range_m
|
||||
horizontal_opening_radians = p_horizontal_opening_radians
|
||||
vertical_opening_radians = p_vertical_opening_radians
|
||||
reveal_duration_seconds = p_reveal_duration_seconds
|
||||
recovery_duration_seconds = p_recovery_duration_seconds
|
||||
samples = p_samples
|
||||
contacts = p_contacts.duplicate()
|
||||
minimum_elevation_m = p_minimum_elevation_m
|
||||
maximum_elevation_m = p_maximum_elevation_m
|
||||
state = p_state
|
||||
reveal_progress = p_reveal_progress
|
||||
|
||||
|
||||
func with_phase(next_state: SonarScanState.Value, next_progress: float) -> SonarScan:
|
||||
return new(
|
||||
scan_id,
|
||||
session_id,
|
||||
sensor_transform,
|
||||
minimum_range_m,
|
||||
maximum_range_m,
|
||||
horizontal_opening_radians,
|
||||
vertical_opening_radians,
|
||||
reveal_duration_seconds,
|
||||
recovery_duration_seconds,
|
||||
samples,
|
||||
contacts,
|
||||
minimum_elevation_m,
|
||||
maximum_elevation_m,
|
||||
next_state,
|
||||
next_progress
|
||||
)
|
||||
|
||||
|
||||
func validation_error() -> String:
|
||||
if scan_id.is_empty() or session_id.is_empty():
|
||||
return "sonar scan identifiers must not be empty"
|
||||
if not is_finite(minimum_range_m) or minimum_range_m < 0.0:
|
||||
return "sonar minimum range is invalid"
|
||||
if not is_finite(maximum_range_m) or maximum_range_m <= minimum_range_m:
|
||||
return "sonar maximum range must exceed minimum range"
|
||||
if (
|
||||
not is_finite(horizontal_opening_radians)
|
||||
or horizontal_opening_radians <= 0.0
|
||||
or horizontal_opening_radians >= PI
|
||||
):
|
||||
return "sonar horizontal opening is invalid"
|
||||
if (
|
||||
not is_finite(vertical_opening_radians)
|
||||
or vertical_opening_radians <= 0.0
|
||||
or vertical_opening_radians >= PI
|
||||
):
|
||||
return "sonar vertical opening is invalid"
|
||||
if reveal_duration_seconds <= 0.0 or recovery_duration_seconds < 0.0:
|
||||
return "sonar phase durations are invalid"
|
||||
if samples == null:
|
||||
return "sonar sample buffer is missing"
|
||||
var buffer_error := samples.validation_error()
|
||||
if not buffer_error.is_empty():
|
||||
return buffer_error
|
||||
for contact: SonarContact in contacts:
|
||||
if contact == null:
|
||||
return "sonar contact is missing"
|
||||
var contact_error := contact.validation_error()
|
||||
if not contact_error.is_empty():
|
||||
return contact_error
|
||||
if not is_finite(minimum_elevation_m) or not is_finite(maximum_elevation_m):
|
||||
return "sonar elevation scale is not finite"
|
||||
if maximum_elevation_m <= minimum_elevation_m:
|
||||
return "sonar elevation scale is empty"
|
||||
if not SonarScanState.is_valid(state):
|
||||
return "sonar scan state is invalid"
|
||||
if not is_finite(reveal_progress) or reveal_progress < 0.0 or reveal_progress > 1.0:
|
||||
return "sonar reveal progress must be between 0 and 1"
|
||||
return ""
|
||||
@@ -0,0 +1,13 @@
|
||||
class_name SonarScanState
|
||||
extends RefCounted
|
||||
|
||||
enum Value {
|
||||
READY,
|
||||
CAPTURE,
|
||||
REVEALING,
|
||||
RECOVERY,
|
||||
}
|
||||
|
||||
|
||||
static func is_valid(value: int) -> bool:
|
||||
return value >= Value.READY and value <= Value.RECOVERY
|
||||
Reference in new issue
Block a user