feat(tools): polyline Canyon and Escarpment primitives with outline noise

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Fable 5.1 committed 2026-10-06 21:37:17 +02:00
1 parent 520a62863b
commit aae9bcd301
2 files changed
+156 -1

No files matched your search

+124
View File
@@ -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.
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:
@@ -36,6 +38,52 @@ class Primitive extends RefCounted:
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)
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:
var depth_m: float
@@ -119,6 +167,82 @@ class Groove extends Primitive:
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:
var point: Vector2
var normal: Vector2