Terrain des sites de plongée : champ de hauteur authoré, Banc du Vigilant, formes variées et rendu sous-marin #4
No files matched your search
@@ -0,0 +1,28 @@
|
|||||||
|
[gd_resource type="Resource" script_class="DiveSiteDefinition" format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://game/domain/sites/site_placement.gd" id="1_j2ii4"]
|
||||||
|
[ext_resource type="Script" path="res://game/domain/sites/site_route.gd" id="2_tj3be"]
|
||||||
|
[ext_resource type="Script" path="res://game/domain/sites/dive_site_definition.gd" id="3_usfet"]
|
||||||
|
[ext_resource type="Resource" path="res://content/sites/vehicle_test_terrain.res" id="4_awe7g"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_5juth"]
|
||||||
|
script = ExtResource("1_j2ii4")
|
||||||
|
placement_id = &"calibration_target"
|
||||||
|
kind = 1
|
||||||
|
position_m = Vector3(0, -60.44367, -58)
|
||||||
|
footprint_m = Vector3(8, 5, 7)
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_tboiu"]
|
||||||
|
script = ExtResource("1_j2ii4")
|
||||||
|
placement_id = &"navigation_beacon"
|
||||||
|
kind = 5
|
||||||
|
position_m = Vector3(0, -15, -22)
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("3_usfet")
|
||||||
|
site_id = &"vehicle_test"
|
||||||
|
display_name = "Site de calibration du véhicule"
|
||||||
|
terrain = ExtResource("4_awe7g")
|
||||||
|
playable_bounds_m = Rect2(-90, -176, 180, 204)
|
||||||
|
spawn_position_m = Vector3(0, -12, 12)
|
||||||
|
placements = Array[ExtResource("1_j2ii4")]([SubResource("Resource_5juth"), SubResource("Resource_tboiu")])
|
||||||
Binary file not shown.
@@ -0,0 +1,113 @@
|
|||||||
|
class_name DiveSite
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||||
|
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
||||||
|
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
||||||
|
const BathymetricTerrainActor = preload(
|
||||||
|
"res://game/world/sites/bathymetric_terrain_actor.gd"
|
||||||
|
)
|
||||||
|
|
||||||
|
const METAL_REFLECTIVITY := 1.0
|
||||||
|
const BEACON_COLOR := Color(0.2, 0.78, 0.9)
|
||||||
|
|
||||||
|
@onready var _terrain: BathymetricTerrainActor = %Terrain
|
||||||
|
@onready var _placements_root: Node3D = %Placements
|
||||||
|
|
||||||
|
var _definition: DiveSiteDefinition
|
||||||
|
var _placeholder_count := 0
|
||||||
|
|
||||||
|
|
||||||
|
func configure(definition: DiveSiteDefinition) -> String:
|
||||||
|
if definition == null:
|
||||||
|
return "dive site definition is missing"
|
||||||
|
var error := definition.validation_error()
|
||||||
|
if not error.is_empty():
|
||||||
|
return error
|
||||||
|
var terrain_error := _terrain.configure(definition.height_field())
|
||||||
|
if not terrain_error.is_empty():
|
||||||
|
return terrain_error
|
||||||
|
_definition = definition
|
||||||
|
for child in _placements_root.get_children():
|
||||||
|
_placements_root.remove_child(child)
|
||||||
|
child.free()
|
||||||
|
_placeholder_count = 0
|
||||||
|
for placement: SitePlacement in definition.placements:
|
||||||
|
_place(placement)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func definition() -> DiveSiteDefinition:
|
||||||
|
return _definition
|
||||||
|
|
||||||
|
|
||||||
|
func terrain() -> BathymetricTerrainActor:
|
||||||
|
return _terrain
|
||||||
|
|
||||||
|
|
||||||
|
func spawn_transform() -> Transform3D:
|
||||||
|
return _definition.spawn_transform() if _definition != null else Transform3D.IDENTITY
|
||||||
|
|
||||||
|
|
||||||
|
func placeholder_count() -> int:
|
||||||
|
return _placeholder_count
|
||||||
|
|
||||||
|
|
||||||
|
func _place(placement: SitePlacement) -> void:
|
||||||
|
if placement.is_sonar_metal():
|
||||||
|
_placements_root.add_child(_metal_placeholder(placement))
|
||||||
|
_placeholder_count += 1
|
||||||
|
elif placement.kind == SitePlacement.Kind.VESSEL_ANCHOR:
|
||||||
|
_placements_root.add_child(_beacon(placement))
|
||||||
|
_placeholder_count += 1
|
||||||
|
|
||||||
|
|
||||||
|
func _metal_placeholder(placement: SitePlacement) -> StaticBody3D:
|
||||||
|
var body := StaticBody3D.new()
|
||||||
|
body.name = String(placement.placement_id)
|
||||||
|
body.position = placement.position_m
|
||||||
|
body.rotation.y = placement.yaw_radians
|
||||||
|
body.set_meta("sonar_material", SonarMaterial.Value.METAL)
|
||||||
|
body.set_meta("sonar_reflectivity", METAL_REFLECTIVITY)
|
||||||
|
var mesh := BoxMesh.new()
|
||||||
|
mesh.size = placement.footprint_m
|
||||||
|
var mesh_instance := MeshInstance3D.new()
|
||||||
|
mesh_instance.name = "Mesh"
|
||||||
|
mesh_instance.mesh = mesh
|
||||||
|
var material := StandardMaterial3D.new()
|
||||||
|
material.albedo_color = Color(0.24, 0.18, 0.08)
|
||||||
|
material.metallic = 0.88
|
||||||
|
material.roughness = 0.27
|
||||||
|
mesh_instance.material_override = material
|
||||||
|
body.add_child(mesh_instance)
|
||||||
|
var shape := BoxShape3D.new()
|
||||||
|
shape.size = placement.footprint_m
|
||||||
|
var collision := CollisionShape3D.new()
|
||||||
|
collision.name = "Collision"
|
||||||
|
collision.shape = shape
|
||||||
|
body.add_child(collision)
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
func _beacon(placement: SitePlacement) -> MeshInstance3D:
|
||||||
|
var mesh := SphereMesh.new()
|
||||||
|
mesh.radius = 0.55
|
||||||
|
mesh.height = 1.1
|
||||||
|
var beacon := MeshInstance3D.new()
|
||||||
|
beacon.name = String(placement.placement_id)
|
||||||
|
beacon.mesh = mesh
|
||||||
|
beacon.position = placement.position_m
|
||||||
|
var material := StandardMaterial3D.new()
|
||||||
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
material.albedo_color = BEACON_COLOR
|
||||||
|
material.emission_enabled = true
|
||||||
|
material.emission = BEACON_COLOR
|
||||||
|
material.emission_energy_multiplier = 5.0
|
||||||
|
beacon.material_override = material
|
||||||
|
var light := OmniLight3D.new()
|
||||||
|
light.name = "BeaconLight"
|
||||||
|
light.omni_range = 12.0
|
||||||
|
light.light_color = BEACON_COLOR
|
||||||
|
light.light_energy = 4.0
|
||||||
|
beacon.add_child(light)
|
||||||
|
return beacon
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://game/world/sites/dive_site.gd" id="1_site"]
|
||||||
|
[ext_resource type="Script" path="res://game/world/sites/bathymetric_terrain_actor.gd" id="2_terrain"]
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="EnvironmentUnderwater"]
|
||||||
|
background_mode = 1
|
||||||
|
background_color = Color(0.005, 0.045, 0.065, 1)
|
||||||
|
ambient_light_source = 3
|
||||||
|
ambient_light_color = Color(0.12, 0.32, 0.37, 1)
|
||||||
|
ambient_light_energy = 0.42
|
||||||
|
glow_enabled = true
|
||||||
|
glow_levels/1 = 1.0
|
||||||
|
glow_levels/2 = 1.0
|
||||||
|
glow_levels/3 = 0.6
|
||||||
|
glow_levels/4 = 0.2
|
||||||
|
glow_intensity = 1.0
|
||||||
|
glow_blend_mode = 0
|
||||||
|
glow_hdr_threshold = 1.0
|
||||||
|
fog_enabled = true
|
||||||
|
fog_light_color = Color(0.07, 0.25, 0.3, 1)
|
||||||
|
fog_light_energy = 0.48
|
||||||
|
fog_density = 0.014
|
||||||
|
fog_height = -4.0
|
||||||
|
fog_height_density = 0.025
|
||||||
|
volumetric_fog_enabled = true
|
||||||
|
volumetric_fog_density = 0.025
|
||||||
|
volumetric_fog_length = 72.0
|
||||||
|
|
||||||
|
[node name="DiveSite" type="Node3D"]
|
||||||
|
script = ExtResource("1_site")
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
|
environment = SubResource("EnvironmentUnderwater")
|
||||||
|
|
||||||
|
[node name="AmbientDirection" type="DirectionalLight3D" parent="."]
|
||||||
|
rotation_degrees = Vector3(-52, -24, 0)
|
||||||
|
light_color = Color(0.39, 0.65, 0.7, 1)
|
||||||
|
light_energy = 0.38
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="Terrain" type="StaticBody3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("2_terrain")
|
||||||
|
|
||||||
|
[node name="Placements" type="Node3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
@@ -68,6 +68,12 @@ func capture(request: SonarCaptureRequest) -> SonarCaptureResult:
|
|||||||
var material := SonarMaterial.Value.ROCK
|
var material := SonarMaterial.Value.ROCK
|
||||||
var reflectivity := 0.72
|
var reflectivity := 0.72
|
||||||
if collider != null:
|
if collider != null:
|
||||||
|
if collider.has_method("sonar_material_at"):
|
||||||
|
material = int(collider.call("sonar_material_at", world_position))
|
||||||
|
reflectivity = float(
|
||||||
|
collider.call("sonar_reflectivity_at", world_position)
|
||||||
|
)
|
||||||
|
else:
|
||||||
material = int(collider.get_meta("sonar_material", material))
|
material = int(collider.get_meta("sonar_material", material))
|
||||||
reflectivity = float(
|
reflectivity = float(
|
||||||
collider.get_meta("sonar_reflectivity", reflectivity)
|
collider.get_meta("sonar_reflectivity", reflectivity)
|
||||||
|
|||||||
@@ -6,8 +6,16 @@ const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
|||||||
const BathymetricTerrainActor = preload(
|
const BathymetricTerrainActor = preload(
|
||||||
"res://game/world/sites/bathymetric_terrain_actor.gd"
|
"res://game/world/sites/bathymetric_terrain_actor.gd"
|
||||||
)
|
)
|
||||||
|
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
||||||
|
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
||||||
|
const DiveSite = preload("res://game/world/sites/dive_site.gd")
|
||||||
|
const SonarCaptureRequest = preload("res://game/application/sonar/sonar_capture_request.gd")
|
||||||
|
const ReferenceBathymetryAcquisition = preload(
|
||||||
|
"res://game/world/sonar/reference_bathymetry_acquisition.gd"
|
||||||
|
)
|
||||||
|
const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn")
|
||||||
|
|
||||||
const TEST_COUNT := 12
|
const TEST_COUNT := 23
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -18,6 +26,7 @@ func _init() -> void:
|
|||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
await _test_actor_matches_field()
|
await _test_actor_matches_field()
|
||||||
|
await _test_dive_site_scene()
|
||||||
|
|
||||||
var exit_code := 0
|
var exit_code := 0
|
||||||
if _failures.is_empty():
|
if _failures.is_empty():
|
||||||
@@ -104,3 +113,39 @@ func _collision_height(space: PhysicsDirectSpaceState3D, x: float, z: float) ->
|
|||||||
func _expect(condition: bool, label: String) -> void:
|
func _expect(condition: bool, label: String) -> void:
|
||||||
if not condition:
|
if not condition:
|
||||||
_failures.append(label)
|
_failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_dive_site_scene() -> void:
|
||||||
|
var definition := load("res://content/sites/vehicle_test.tres") as DiveSiteDefinition
|
||||||
|
_expect(definition != null, "calibration site definition loads")
|
||||||
|
if definition == null:
|
||||||
|
return
|
||||||
|
_expect(definition.validation_error().is_empty(), "calibration site definition is valid")
|
||||||
|
var started_usec := Time.get_ticks_usec()
|
||||||
|
var site := DIVE_SITE_SCENE.instantiate() as DiveSite
|
||||||
|
root.add_child(site)
|
||||||
|
_expect(site.configure(definition).is_empty(), "dive site scene accepts the definition")
|
||||||
|
var configure_ms := float(Time.get_ticks_usec() - started_usec) / 1000.0
|
||||||
|
_expect(configure_ms < 3000.0, "site configures under the 3 s load budget (%.0f ms)" % configure_ms)
|
||||||
|
await physics_frame
|
||||||
|
await physics_frame
|
||||||
|
_expect(site.terrain().tile_count() > 0, "site builds its terrain tiles")
|
||||||
|
_expect(site.placeholder_count() == 2, "calibration target and beacon are placed")
|
||||||
|
var target := site.get_node_or_null("%Placements/calibration_target") as StaticBody3D
|
||||||
|
_expect(target != null and int(target.get_meta("sonar_material")) == SonarMaterial.Value.METAL, "wreck placeholder is a metal sonar body")
|
||||||
|
_expect(site.spawn_transform().origin.is_equal_approx(Vector3(0.0, -12.0, 12.0)), "spawn comes from the definition")
|
||||||
|
|
||||||
|
var sensor := Transform3D(Basis(Vector3.UP, 0.0), Vector3(0.0, -12.0, 10.0))
|
||||||
|
sensor.basis = Basis(sensor.basis.x, sensor.basis.y, -sensor.basis.z)
|
||||||
|
var acquisition := ReferenceBathymetryAcquisition.new(site.get_world_3d())
|
||||||
|
var request := SonarCaptureRequest.new(
|
||||||
|
&"scan", &"session", sensor, 5.0, 95.0, deg_to_rad(76.0), deg_to_rad(64.0), 49, 31
|
||||||
|
)
|
||||||
|
var result = acquisition.capture(request)
|
||||||
|
_expect(result.accepted, "reference acquisition scans the baked terrain")
|
||||||
|
var materials: Dictionary = {}
|
||||||
|
for sample in result.samples:
|
||||||
|
materials[sample.material] = true
|
||||||
|
_expect(materials.has(SonarMaterial.Value.SAND) and materials.has(SonarMaterial.Value.ROCK), "acquisition reports per-cell sand and rock")
|
||||||
|
_expect(materials.has(SonarMaterial.Value.METAL), "acquisition still reports the metal placeholder")
|
||||||
|
site.queue_free()
|
||||||
@@ -31,17 +31,32 @@ func _init() -> void:
|
|||||||
DirAccess.make_dir_recursive_absolute(OUTPUT_DIRECTORY)
|
DirAccess.make_dir_recursive_absolute(OUTPUT_DIRECTORY)
|
||||||
DirAccess.make_dir_recursive_absolute(PREVIEW_DIRECTORY)
|
DirAccess.make_dir_recursive_absolute(PREVIEW_DIRECTORY)
|
||||||
var output_path := "%s/%s_terrain.res" % [OUTPUT_DIRECTORY, name]
|
var output_path := "%s/%s_terrain.res" % [OUTPUT_DIRECTORY, name]
|
||||||
var save_error := ResourceSaver.save(data, output_path)
|
var save_error := ResourceSaver.save(data, output_path, ResourceSaver.FLAG_CHANGE_PATH)
|
||||||
if save_error != OK:
|
if save_error != OK:
|
||||||
push_error("cannot save %s (%d)" % [output_path, save_error])
|
push_error("cannot save %s (%d)" % [output_path, save_error])
|
||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
|
# ResourceSaver restores the previous path after saving, so the data must
|
||||||
|
# take over the baked path explicitly for the definition to reference it.
|
||||||
|
data.take_over_path(output_path)
|
||||||
var preview_path := "%s/%s_terrain_preview.png" % [PREVIEW_DIRECTORY, name]
|
var preview_path := "%s/%s_terrain_preview.png" % [PREVIEW_DIRECTORY, name]
|
||||||
var preview_error := SiteTerrainBaker.write_preview_png(data, preview_path)
|
var preview_error := SiteTerrainBaker.write_preview_png(data, preview_path)
|
||||||
if preview_error != OK:
|
if preview_error != OK:
|
||||||
push_error("cannot write %s (%d)" % [preview_path, preview_error])
|
push_error("cannot write %s (%d)" % [preview_path, preview_error])
|
||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
|
var definition = recipe_script.definition(data)
|
||||||
|
var definition_error: String = definition.validation_error()
|
||||||
|
if not definition_error.is_empty():
|
||||||
|
push_error("recipe %s definition is invalid: %s" % [name, definition_error])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var definition_path := "%s/%s.tres" % [OUTPUT_DIRECTORY, name]
|
||||||
|
var definition_save_error := ResourceSaver.save(definition, definition_path)
|
||||||
|
if definition_save_error != OK:
|
||||||
|
push_error("cannot save %s (%d)" % [definition_path, definition_save_error])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
var rock_cells := 0
|
var rock_cells := 0
|
||||||
for material: int in data.materials:
|
for material: int in data.materials:
|
||||||
if material == SiteTerrainBaker.SonarMaterial.Value.ROCK:
|
if material == SiteTerrainBaker.SonarMaterial.Value.ROCK:
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ class_name VehicleTestTerrainRecipe
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
||||||
|
const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
|
||||||
|
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
|
||||||
|
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
||||||
|
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
||||||
|
|
||||||
|
|
||||||
static func recipe() -> SiteTerrainBaker.Recipe:
|
static func recipe() -> SiteTerrainBaker.Recipe:
|
||||||
@@ -23,3 +27,25 @@ static func recipe() -> SiteTerrainBaker.Recipe:
|
|||||||
SiteTerrainBaker.Ripples.new(Vector2(0.0, 1.0), TAU / 0.048, 0.9, Vector2.ZERO, 0.0),
|
SiteTerrainBaker.Ripples.new(Vector2(0.0, 1.0), TAU / 0.048, 0.9, Vector2.ZERO, 0.0),
|
||||||
]
|
]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition:
|
||||||
|
var field := TerrainHeightField.from_data(terrain)
|
||||||
|
var result := DiveSiteDefinition.new()
|
||||||
|
result.site_id = &"vehicle_test"
|
||||||
|
result.display_name = "Site de calibration du véhicule"
|
||||||
|
result.terrain = terrain
|
||||||
|
result.playable_bounds_m = Rect2(-90.0, -176.0, 180.0, 204.0)
|
||||||
|
result.spawn_position_m = Vector3(0.0, -12.0, 12.0)
|
||||||
|
result.spawn_yaw_radians = 0.0
|
||||||
|
var target := SitePlacement.new()
|
||||||
|
target.placement_id = &"calibration_target"
|
||||||
|
target.kind = SitePlacement.Kind.WRECK_DECOY
|
||||||
|
target.position_m = Vector3(0.0, field.elevation_at(0.0, -58.0) + 2.5, -58.0)
|
||||||
|
target.footprint_m = Vector3(8.0, 5.0, 7.0)
|
||||||
|
var anchor := SitePlacement.new()
|
||||||
|
anchor.placement_id = &"navigation_beacon"
|
||||||
|
anchor.kind = SitePlacement.Kind.VESSEL_ANCHOR
|
||||||
|
anchor.position_m = Vector3(0.0, -15.0, -22.0)
|
||||||
|
result.placements = [target, anchor]
|
||||||
|
return result
|
||||||
Reference in new issue
Block a user