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
@@ -3,8 +3,12 @@ extends SceneTree
|
|||||||
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||||
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
||||||
const VehicleTestRecipe = preload("res://tools/sites/vehicle_test_terrain_recipe.gd")
|
const VehicleTestRecipe = preload("res://tools/sites/vehicle_test_terrain_recipe.gd")
|
||||||
|
const Zone1Recipe = preload("res://tools/sites/zone1_vigilant_recipe.gd")
|
||||||
|
const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
|
||||||
|
|
||||||
const TEST_COUNT := 10
|
const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res"
|
||||||
|
|
||||||
|
const TEST_COUNT := 16
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -12,6 +16,8 @@ var _failures: Array[String] = []
|
|||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
_test_plane_and_blob()
|
_test_plane_and_blob()
|
||||||
_test_materials_and_determinism()
|
_test_materials_and_determinism()
|
||||||
|
_test_modifiers_and_primitive_materials()
|
||||||
|
_test_zone1_artefact_is_stable()
|
||||||
|
|
||||||
if _failures.is_empty():
|
if _failures.is_empty():
|
||||||
print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT)
|
print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT)
|
||||||
@@ -66,6 +72,59 @@ func _test_materials_and_determinism() -> void:
|
|||||||
_expect(calibration != null and calibration.validation_error().is_empty(), "calibration recipe bakes valid data")
|
_expect(calibration != null and calibration.validation_error().is_empty(), "calibration recipe bakes valid data")
|
||||||
|
|
||||||
|
|
||||||
|
## Test doubles for the extension points: a modifier that lifts the field by one metre
|
||||||
|
## and a primitive that forces rock on a flat plane.
|
||||||
|
class Lift extends SiteTerrainBaker.Modifier:
|
||||||
|
func modify(_x_m: float, _z_m: float, elevation_m: float, _gradient: Vector2) -> float:
|
||||||
|
return elevation_m + 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class GradientProbe extends SiteTerrainBaker.Modifier:
|
||||||
|
var gradient_at_centre := Vector2.ZERO
|
||||||
|
|
||||||
|
func modify(x_m: float, z_m: float, elevation_m: float, gradient: Vector2) -> float:
|
||||||
|
if is_zero_approx(x_m) and is_zero_approx(z_m):
|
||||||
|
gradient_at_centre = gradient
|
||||||
|
return elevation_m
|
||||||
|
|
||||||
|
|
||||||
|
class RockDisc extends SiteTerrainBaker.Primitive:
|
||||||
|
func material_at(x_m: float, z_m: float) -> int:
|
||||||
|
if Vector2(x_m, z_m).length() <= 5.0:
|
||||||
|
return SonarMaterial.Value.ROCK
|
||||||
|
return SiteTerrainBaker.NO_MATERIAL
|
||||||
|
|
||||||
|
|
||||||
|
func _test_modifiers_and_primitive_materials() -> void:
|
||||||
|
var recipe := _small_recipe()
|
||||||
|
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.2, 0.0, 0.0, 0.0))
|
||||||
|
var probe := GradientProbe.new()
|
||||||
|
recipe.modifiers.append(probe)
|
||||||
|
recipe.modifiers.append(Lift.new())
|
||||||
|
var data := SiteTerrainBaker.bake(recipe)
|
||||||
|
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -29.0), "modifier runs after the primitive sum")
|
||||||
|
_expect(probe.gradient_at_centre.is_equal_approx(Vector2(0.2, 0.0)), "modifier receives the local gradient (%s)" % probe.gradient_at_centre)
|
||||||
|
|
||||||
|
var flat := _small_recipe()
|
||||||
|
flat.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0))
|
||||||
|
flat.primitives.append(RockDisc.new())
|
||||||
|
var flat_data := SiteTerrainBaker.bake(flat)
|
||||||
|
_expect(flat_data.materials[flat_data.index_of(10, 10)] == SonarMaterial.Value.ROCK, "primitive material wins over the slope rule")
|
||||||
|
_expect(flat_data.materials[flat_data.index_of(0, 0)] == SonarMaterial.Value.SAND, "primitive material is local")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_zone1_artefact_is_stable() -> void:
|
||||||
|
var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData
|
||||||
|
_expect(versioned != null, "versioned zone 1 terrain loads")
|
||||||
|
if versioned == null:
|
||||||
|
return
|
||||||
|
var baked := SiteTerrainBaker.bake(Zone1Recipe.recipe())
|
||||||
|
_expect(
|
||||||
|
baked.elevations_m == versioned.elevations_m and baked.materials == versioned.materials,
|
||||||
|
"zone 1 recipe still bakes the versioned artefact byte for byte"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -5,10 +5,19 @@ const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
|
|||||||
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
|
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
|
||||||
|
|
||||||
|
|
||||||
|
## A primitive returning this from material_at leaves the slope rule in charge.
|
||||||
|
const NO_MATERIAL := -1
|
||||||
|
|
||||||
|
|
||||||
class Primitive extends RefCounted:
|
class Primitive extends RefCounted:
|
||||||
func elevation_delta(_x_m: float, _z_m: float) -> float:
|
func elevation_delta(_x_m: float, _z_m: float) -> float:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
## Material imposed by this primitive at (x, z), or NO_MATERIAL. Applied after the
|
||||||
|
## slope rule, in recipe order, before the material patches.
|
||||||
|
func material_at(_x_m: float, _z_m: float) -> int:
|
||||||
|
return NO_MATERIAL
|
||||||
|
|
||||||
static func distance_to_segment(point: Vector2, start: Vector2, end: Vector2) -> float:
|
static func distance_to_segment(point: Vector2, start: Vector2, end: Vector2) -> float:
|
||||||
var segment := end - start
|
var segment := end - start
|
||||||
var length_squared := segment.length_squared()
|
var length_squared := segment.length_squared()
|
||||||
@@ -171,6 +180,13 @@ class Roughness extends Primitive:
|
|||||||
return _noise.get_noise_2d(x_m, z_m) * amplitude_m
|
return _noise.get_noise_2d(x_m, z_m) * amplitude_m
|
||||||
|
|
||||||
|
|
||||||
|
## Reshapes the summed elevation once every primitive has been added. gradient is the
|
||||||
|
## local slope Vector2(dE/dx, dE/dz) of the field being modified, by central differences.
|
||||||
|
class Modifier extends RefCounted:
|
||||||
|
func modify(_x_m: float, _z_m: float, elevation_m: float, _gradient: Vector2) -> float:
|
||||||
|
return elevation_m
|
||||||
|
|
||||||
|
|
||||||
class MaterialPatch extends RefCounted:
|
class MaterialPatch extends RefCounted:
|
||||||
var centre: Vector2
|
var centre: Vector2
|
||||||
var radius_m: float
|
var radius_m: float
|
||||||
@@ -193,9 +209,12 @@ class Recipe extends RefCounted:
|
|||||||
var rock_reflectivity: float = 0.72
|
var rock_reflectivity: float = 0.72
|
||||||
var sand_reflectivity: float = 0.45
|
var sand_reflectivity: float = 0.45
|
||||||
var primitives: Array[Primitive] = []
|
var primitives: Array[Primitive] = []
|
||||||
|
var modifiers: Array[Modifier] = []
|
||||||
var material_patches: Array[MaterialPatch] = []
|
var material_patches: Array[MaterialPatch] = []
|
||||||
|
|
||||||
|
|
||||||
|
## Bake order: primitive sum, modifiers in order, floor clamp, slope material,
|
||||||
|
## primitive materials in order, material patches.
|
||||||
static func bake(recipe: Recipe) -> SiteTerrainData:
|
static func bake(recipe: Recipe) -> SiteTerrainData:
|
||||||
var data := SiteTerrainData.new()
|
var data := SiteTerrainData.new()
|
||||||
data.columns = recipe.columns
|
data.columns = recipe.columns
|
||||||
@@ -218,8 +237,12 @@ static func bake(recipe: Recipe) -> SiteTerrainData:
|
|||||||
var elevation := 0.0
|
var elevation := 0.0
|
||||||
for primitive: Primitive in recipe.primitives:
|
for primitive: Primitive in recipe.primitives:
|
||||||
elevation += primitive.elevation_delta(x_m, z_m)
|
elevation += primitive.elevation_delta(x_m, z_m)
|
||||||
elevations[data.index_of(column, row)] = maxf(elevation, recipe.floor_elevation_m)
|
elevations[data.index_of(column, row)] = elevation
|
||||||
materials[data.index_of(column, row)] = SonarMaterial.Value.SAND
|
for modifier: Modifier in recipe.modifiers:
|
||||||
|
elevations = _apply_modifier(recipe, modifier, elevations)
|
||||||
|
for index in cell_count:
|
||||||
|
elevations[index] = maxf(elevations[index], recipe.floor_elevation_m)
|
||||||
|
materials[index] = SonarMaterial.Value.SAND
|
||||||
data.elevations_m = elevations
|
data.elevations_m = elevations
|
||||||
data.materials = materials
|
data.materials = materials
|
||||||
var field := TerrainHeightField.from_data(data)
|
var field := TerrainHeightField.from_data(data)
|
||||||
@@ -230,6 +253,14 @@ static func bake(recipe: Recipe) -> SiteTerrainData:
|
|||||||
for column in recipe.columns:
|
for column in recipe.columns:
|
||||||
if field.normal_at_cell(column, row).y < rock_cosine:
|
if field.normal_at_cell(column, row).y < rock_cosine:
|
||||||
materials[data.index_of(column, row)] = SonarMaterial.Value.ROCK
|
materials[data.index_of(column, row)] = SonarMaterial.Value.ROCK
|
||||||
|
for primitive: Primitive in recipe.primitives:
|
||||||
|
for row in recipe.rows:
|
||||||
|
var z_m := recipe.origin_z_m + float(row) * recipe.cell_size_m
|
||||||
|
for column in recipe.columns:
|
||||||
|
var x_m := recipe.origin_x_m + float(column) * recipe.cell_size_m
|
||||||
|
var material := primitive.material_at(x_m, z_m)
|
||||||
|
if material != NO_MATERIAL:
|
||||||
|
materials[data.index_of(column, row)] = material
|
||||||
for patch: MaterialPatch in recipe.material_patches:
|
for patch: MaterialPatch in recipe.material_patches:
|
||||||
for row in recipe.rows:
|
for row in recipe.rows:
|
||||||
var z_m := recipe.origin_z_m + float(row) * recipe.cell_size_m
|
var z_m := recipe.origin_z_m + float(row) * recipe.cell_size_m
|
||||||
@@ -241,6 +272,31 @@ static func bake(recipe: Recipe) -> SiteTerrainData:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the modified copy of elevations; the gradient of each cell is read from the
|
||||||
|
## unmodified input so the modifier sees one consistent field.
|
||||||
|
static func _apply_modifier(recipe: Recipe, modifier: Modifier, source: PackedFloat32Array) -> PackedFloat32Array:
|
||||||
|
var columns := recipe.columns
|
||||||
|
var rows := recipe.rows
|
||||||
|
var cell_size := recipe.cell_size_m
|
||||||
|
var result := PackedFloat32Array()
|
||||||
|
result.resize(source.size())
|
||||||
|
for row in rows:
|
||||||
|
var z_m := recipe.origin_z_m + float(row) * cell_size
|
||||||
|
var north := maxi(row - 1, 0)
|
||||||
|
var south := mini(row + 1, rows - 1)
|
||||||
|
for column in columns:
|
||||||
|
var x_m := recipe.origin_x_m + float(column) * cell_size
|
||||||
|
var west := maxi(column - 1, 0)
|
||||||
|
var east := mini(column + 1, columns - 1)
|
||||||
|
var gradient := Vector2(
|
||||||
|
(source[row * columns + east] - source[row * columns + west]) / (float(east - west) * cell_size),
|
||||||
|
(source[south * columns + column] - source[north * columns + column]) / (float(south - north) * cell_size)
|
||||||
|
)
|
||||||
|
var index := row * columns + column
|
||||||
|
result[index] = modifier.modify(x_m, z_m, source[index], gradient)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
static func write_preview_png(data: SiteTerrainData, path: String) -> Error:
|
static func write_preview_png(data: SiteTerrainData, path: String) -> Error:
|
||||||
var field := TerrainHeightField.from_data(data)
|
var field := TerrainHeightField.from_data(data)
|
||||||
if field == null:
|
if field == null:
|
||||||
|
|||||||
Reference in new issue
Block a user