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
@@ -8,7 +8,7 @@ const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
|
|||||||
|
|
||||||
const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res"
|
const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res"
|
||||||
|
|
||||||
const TEST_COUNT := 16
|
const TEST_COUNT := 26
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ func _init() -> void:
|
|||||||
_test_materials_and_determinism()
|
_test_materials_and_determinism()
|
||||||
_test_modifiers_and_primitive_materials()
|
_test_modifiers_and_primitive_materials()
|
||||||
_test_zone1_artefact_is_stable()
|
_test_zone1_artefact_is_stable()
|
||||||
|
_test_canyon()
|
||||||
|
_test_escarpment()
|
||||||
|
|
||||||
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)
|
||||||
@@ -113,6 +115,35 @@ func _test_modifiers_and_primitive_materials() -> void:
|
|||||||
_expect(flat_data.materials[flat_data.index_of(0, 0)] == SonarMaterial.Value.SAND, "primitive material is local")
|
_expect(flat_data.materials[flat_data.index_of(0, 0)] == SonarMaterial.Value.SAND, "primitive material is local")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_canyon() -> void:
|
||||||
|
var canyon := SiteTerrainBaker.Canyon.new(
|
||||||
|
PackedVector2Array([Vector2(-20.0, 0.0), Vector2(0.0, 0.0), Vector2(0.0, 20.0)]),
|
||||||
|
4.0, 10.0, 1.0, 0, 0.0
|
||||||
|
)
|
||||||
|
_expect(is_equal_approx(canyon.elevation_delta(-10.0, 0.0), -10.0), "canyon floor sits at depth on the first leg")
|
||||||
|
_expect(is_equal_approx(canyon.elevation_delta(1.5, 10.0), -10.0), "canyon floor is flat across its width on the second leg")
|
||||||
|
_expect(is_equal_approx(canyon.elevation_delta(-10.0, 6.0), 0.0), "canyon stops past half width plus softness")
|
||||||
|
_expect(is_equal_approx(canyon.elevation_delta(15.0, -15.0), 0.0), "canyon is zero far from the path")
|
||||||
|
_expect(is_equal_approx(canyon.elevation_delta(0.0, 20.0), 0.0), "canyon closes at its last vertex")
|
||||||
|
_expect(canyon.elevation_delta(-10.0, 4.0) < -4.0 and canyon.elevation_delta(-10.0, 4.0) > -6.0, "canyon wall is half way down at half width")
|
||||||
|
var rough := SiteTerrainBaker.Canyon.new(
|
||||||
|
PackedVector2Array([Vector2(-200.0, 0.0), Vector2(200.0, 0.0)]), 8.0, 10.0, 2.0, 7, 3.0
|
||||||
|
)
|
||||||
|
var wall_values: Dictionary = {}
|
||||||
|
for x in range(-100, 101, 10):
|
||||||
|
wall_values[snappedf(rough.elevation_delta(float(x), 8.0), 0.01)] = true
|
||||||
|
_expect(wall_values.size() > 3, "outline noise makes the canyon wall irregular along the path")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_escarpment() -> void:
|
||||||
|
var cliff := SiteTerrainBaker.Escarpment.new(
|
||||||
|
PackedVector2Array([Vector2(-50.0, 0.0), Vector2(50.0, 0.0)]), 10.0, 2.0, 0.5, 0, 0.0
|
||||||
|
)
|
||||||
|
_expect(is_equal_approx(cliff.elevation_delta(0.0, -10.0), 0.0), "escarpment leaves the left (north) side alone")
|
||||||
|
_expect(is_equal_approx(cliff.elevation_delta(0.0, 2.0), -10.0), "escarpment drops by drop_m on the right (south) side")
|
||||||
|
_expect(is_equal_approx(cliff.elevation_delta(0.0, 12.0), -15.0), "escarpment keeps falling at slope_beyond")
|
||||||
|
|
||||||
|
|
||||||
func _test_zone1_artefact_is_stable() -> void:
|
func _test_zone1_artefact_is_stable() -> void:
|
||||||
var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData
|
var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData
|
||||||
_expect(versioned != null, "versioned zone 1 terrain loads")
|
_expect(versioned != null, "versioned zone 1 terrain loads")
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field
|
|||||||
|
|
||||||
## A primitive returning this from material_at leaves the slope rule in charge.
|
## A primitive returning this from material_at leaves the slope rule in charge.
|
||||||
const NO_MATERIAL := -1
|
const NO_MATERIAL := -1
|
||||||
|
## Outline noise wavelength of about 20 m: visible irregularity, no cell-scale jitter.
|
||||||
|
const EDGE_NOISE_FREQUENCY := 0.05
|
||||||
|
|
||||||
|
|
||||||
class Primitive extends RefCounted:
|
class Primitive extends RefCounted:
|
||||||
@@ -36,6 +38,52 @@ class Primitive extends RefCounted:
|
|||||||
var along := clampf((point - start).dot(segment) / length, 0.0, length)
|
var along := clampf((point - start).dot(segment) / length, 0.0, length)
|
||||||
return smoothstep(0.0, taper_m, along) * smoothstep(0.0, taper_m, length - along)
|
return smoothstep(0.0, taper_m, along) * smoothstep(0.0, taper_m, length - along)
|
||||||
|
|
||||||
|
static func path_length(path: PackedVector2Array) -> float:
|
||||||
|
var total := 0.0
|
||||||
|
for index in range(1, path.size()):
|
||||||
|
total += path[index - 1].distance_to(path[index])
|
||||||
|
return total
|
||||||
|
|
||||||
|
## Nearest point of a polyline: x = distance to the path, y = arc length of the nearest
|
||||||
|
## point from the first vertex, z = +1 when the point lies to the right of the travel
|
||||||
|
## direction (south of an eastbound path, with +X east and +Z south), -1 to the left.
|
||||||
|
static func path_sample(point: Vector2, path: PackedVector2Array) -> Vector3:
|
||||||
|
if path.size() == 1:
|
||||||
|
return Vector3(point.distance_to(path[0]), 0.0, 1.0)
|
||||||
|
var best := Vector3(INF, 0.0, 1.0)
|
||||||
|
var arc_start := 0.0
|
||||||
|
for index in range(1, path.size()):
|
||||||
|
var start := path[index - 1]
|
||||||
|
var end := path[index]
|
||||||
|
var segment := end - start
|
||||||
|
var length := segment.length()
|
||||||
|
var t := 0.0
|
||||||
|
if length > 0.0:
|
||||||
|
t = clampf((point - start).dot(segment) / (length * length), 0.0, 1.0)
|
||||||
|
var distance := point.distance_to(start + segment * t)
|
||||||
|
if distance < best.x:
|
||||||
|
var cross := segment.x * (point.y - start.y) - segment.y * (point.x - start.x)
|
||||||
|
best = Vector3(distance, arc_start + t * length, 1.0 if cross >= 0.0 else -1.0)
|
||||||
|
arc_start += length
|
||||||
|
return best
|
||||||
|
|
||||||
|
## Deterministic low-frequency noise that roughens an outline; null when amplitude is 0.
|
||||||
|
static func edge_noise(seed: int, amplitude_m: float) -> FastNoiseLite:
|
||||||
|
if amplitude_m <= 0.0:
|
||||||
|
return null
|
||||||
|
var noise := FastNoiseLite.new()
|
||||||
|
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
noise.seed = seed
|
||||||
|
noise.frequency = EDGE_NOISE_FREQUENCY
|
||||||
|
noise.fractal_type = FastNoiseLite.FRACTAL_FBM
|
||||||
|
noise.fractal_octaves = 3
|
||||||
|
return noise
|
||||||
|
|
||||||
|
static func edge_offset(noise: FastNoiseLite, amplitude_m: float, x_m: float, z_m: float) -> float:
|
||||||
|
if noise == null:
|
||||||
|
return 0.0
|
||||||
|
return noise.get_noise_2d(x_m, z_m) * amplitude_m
|
||||||
|
|
||||||
|
|
||||||
class SlopedPlane extends Primitive:
|
class SlopedPlane extends Primitive:
|
||||||
var depth_m: float
|
var depth_m: float
|
||||||
@@ -119,6 +167,82 @@ class Groove extends Primitive:
|
|||||||
return -depth_m * inside * end_taper(point, start, end, half_width_m)
|
return -depth_m * inside * end_taper(point, start, end, half_width_m)
|
||||||
|
|
||||||
|
|
||||||
|
## Flat-floored trench along a polyline. The floor sits depth_m below the surroundings
|
||||||
|
## wherever the (noise-perturbed) distance to the path is under half_width_m minus
|
||||||
|
## wall_softness_m; the walls climb over 2 * wall_softness_m; the trench closes over one
|
||||||
|
## half-width at each end.
|
||||||
|
class Canyon extends Primitive:
|
||||||
|
var path: PackedVector2Array
|
||||||
|
var half_width_m: float
|
||||||
|
var depth_m: float
|
||||||
|
var wall_softness_m: float
|
||||||
|
var edge_amplitude_m: float
|
||||||
|
var _noise: FastNoiseLite
|
||||||
|
var _length_m: float
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
p_path: PackedVector2Array,
|
||||||
|
p_half_width_m: float,
|
||||||
|
p_depth_m: float,
|
||||||
|
p_wall_softness_m: float,
|
||||||
|
p_edge_seed: int,
|
||||||
|
p_edge_amplitude_m: float
|
||||||
|
) -> void:
|
||||||
|
path = p_path
|
||||||
|
half_width_m = p_half_width_m
|
||||||
|
depth_m = p_depth_m
|
||||||
|
wall_softness_m = p_wall_softness_m
|
||||||
|
edge_amplitude_m = p_edge_amplitude_m
|
||||||
|
_noise = edge_noise(p_edge_seed, p_edge_amplitude_m)
|
||||||
|
_length_m = path_length(p_path)
|
||||||
|
|
||||||
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
||||||
|
var sample := path_sample(Vector2(x_m, z_m), path)
|
||||||
|
var distance := sample.x + edge_offset(_noise, edge_amplitude_m, x_m, z_m)
|
||||||
|
var inside := 1.0 - smoothstep(
|
||||||
|
half_width_m - wall_softness_m,
|
||||||
|
half_width_m + wall_softness_m,
|
||||||
|
distance
|
||||||
|
)
|
||||||
|
var taper := smoothstep(0.0, half_width_m, sample.y) * smoothstep(0.0, half_width_m, _length_m - sample.y)
|
||||||
|
return -depth_m * inside * taper
|
||||||
|
|
||||||
|
|
||||||
|
## Step along a polyline: the terrain drops by drop_m on the right of the travel direction
|
||||||
|
## (south of an eastbound path), over 2 * softness_m, then keeps falling at slope_beyond.
|
||||||
|
## The crest line is roughened by outline noise. Extend the path past the field edges:
|
||||||
|
## beyond its ends the contour wraps around the end vertex.
|
||||||
|
class Escarpment extends Primitive:
|
||||||
|
var path: PackedVector2Array
|
||||||
|
var drop_m: float
|
||||||
|
var softness_m: float
|
||||||
|
var slope_beyond: float
|
||||||
|
var edge_amplitude_m: float
|
||||||
|
var _noise: FastNoiseLite
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
p_path: PackedVector2Array,
|
||||||
|
p_drop_m: float,
|
||||||
|
p_softness_m: float,
|
||||||
|
p_slope_beyond: float,
|
||||||
|
p_edge_seed: int,
|
||||||
|
p_edge_amplitude_m: float
|
||||||
|
) -> void:
|
||||||
|
path = p_path
|
||||||
|
drop_m = p_drop_m
|
||||||
|
softness_m = p_softness_m
|
||||||
|
slope_beyond = p_slope_beyond
|
||||||
|
edge_amplitude_m = p_edge_amplitude_m
|
||||||
|
_noise = edge_noise(p_edge_seed, p_edge_amplitude_m)
|
||||||
|
|
||||||
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
||||||
|
var sample := path_sample(Vector2(x_m, z_m), path)
|
||||||
|
var signed_distance := sample.x * sample.z + edge_offset(_noise, edge_amplitude_m, x_m, z_m)
|
||||||
|
var step := drop_m * smoothstep(-softness_m, softness_m, signed_distance)
|
||||||
|
var slope := maxf(signed_distance - softness_m, 0.0) * slope_beyond
|
||||||
|
return -(step + slope)
|
||||||
|
|
||||||
|
|
||||||
class Step extends Primitive:
|
class Step extends Primitive:
|
||||||
var point: Vector2
|
var point: Vector2
|
||||||
var normal: Vector2
|
var normal: Vector2
|
||||||
|
|||||||
Reference in new issue
Block a user