Review findings: the mesa forced rock onto the canyon bed, two runners declared the wrong TEST_COUNT, and the escarpment check could pass on the slope alone. Canyon gains floor_material, the showcase is re-baked with sand beds, the contract measures the step across the crest band, counts distinct ledges, and probes the crevasse against the open plateau. Degenerate recipe parameters now fail an assert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
601 lines
21 KiB
GDScript
601 lines
21 KiB
GDScript
extends RefCounted
|
|
|
|
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
|
const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.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
|
|
## Outline noise wavelength of about 20 m: visible irregularity, no cell-scale jitter.
|
|
const EDGE_NOISE_FREQUENCY := 0.05
|
|
|
|
|
|
class Primitive extends RefCounted:
|
|
func elevation_delta(_x_m: float, _z_m: float) -> float:
|
|
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:
|
|
var segment := end - start
|
|
var length_squared := segment.length_squared()
|
|
if length_squared == 0.0:
|
|
return point.distance_to(start)
|
|
var t := clampf((point - start).dot(segment) / length_squared, 0.0, 1.0)
|
|
return point.distance_to(start + segment * t)
|
|
|
|
static func end_taper(point: Vector2, start: Vector2, end: Vector2, taper_m: float) -> float:
|
|
if taper_m <= 0.0:
|
|
return 1.0
|
|
var segment := end - start
|
|
var length := segment.length()
|
|
if length == 0.0:
|
|
return 1.0
|
|
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
|
|
var slope_x: float
|
|
var slope_z: float
|
|
var reference_x_m: float
|
|
var reference_z_m: float
|
|
|
|
func _init(p_depth_m: float, p_slope_x: float, p_slope_z: float, p_reference_x_m: float, p_reference_z_m: float) -> void:
|
|
depth_m = p_depth_m
|
|
slope_x = p_slope_x
|
|
slope_z = p_slope_z
|
|
reference_x_m = p_reference_x_m
|
|
reference_z_m = p_reference_z_m
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
return -depth_m + slope_x * (x_m - reference_x_m) + slope_z * (z_m - reference_z_m)
|
|
|
|
|
|
class Blob extends Primitive:
|
|
var centre: Vector2
|
|
var sigma_x_m: float
|
|
var sigma_z_m: float
|
|
var amplitude_m: float
|
|
|
|
func _init(p_centre: Vector2, p_sigma_x_m: float, p_sigma_z_m: float, p_amplitude_m: float) -> void:
|
|
centre = p_centre
|
|
sigma_x_m = p_sigma_x_m
|
|
sigma_z_m = p_sigma_z_m
|
|
amplitude_m = p_amplitude_m
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
var dx := (x_m - centre.x) / sigma_x_m
|
|
var dz := (z_m - centre.y) / sigma_z_m
|
|
return amplitude_m * exp(-(dx * dx + dz * dz))
|
|
|
|
|
|
class Ridge extends Primitive:
|
|
var start: Vector2
|
|
var end: Vector2
|
|
var sigma_m: float
|
|
var amplitude_m: float
|
|
var taper_m: float
|
|
|
|
func _init(p_start: Vector2, p_end: Vector2, p_sigma_m: float, p_amplitude_m: float, p_taper_m: float) -> void:
|
|
start = p_start
|
|
end = p_end
|
|
sigma_m = p_sigma_m
|
|
amplitude_m = p_amplitude_m
|
|
taper_m = p_taper_m
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
var point := Vector2(x_m, z_m)
|
|
var distance := distance_to_segment(point, start, end)
|
|
var profile := exp(-pow(distance / sigma_m, 2.0))
|
|
return amplitude_m * profile * end_taper(point, start, end, taper_m)
|
|
|
|
|
|
class Groove extends Primitive:
|
|
var start: Vector2
|
|
var end: Vector2
|
|
var half_width_m: float
|
|
var depth_m: float
|
|
var wall_softness_m: float
|
|
|
|
func _init(p_start: Vector2, p_end: Vector2, p_half_width_m: float, p_depth_m: float, p_wall_softness_m: float) -> void:
|
|
start = p_start
|
|
end = p_end
|
|
half_width_m = p_half_width_m
|
|
depth_m = p_depth_m
|
|
wall_softness_m = p_wall_softness_m
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
var point := Vector2(x_m, z_m)
|
|
var distance := distance_to_segment(point, start, end)
|
|
var inside := 1.0 - smoothstep(
|
|
half_width_m - wall_softness_m,
|
|
half_width_m + wall_softness_m,
|
|
distance
|
|
)
|
|
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. floor_material (a SonarMaterial value, or NO_MATERIAL) is
|
|
## imposed on the flat floor, so a canyon cut through a rock outcrop keeps a sand bed.
|
|
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 floor_material: int
|
|
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,
|
|
p_floor_material: int
|
|
) -> void:
|
|
assert(p_path.size() >= 2, "a canyon path needs at least two vertices")
|
|
assert(p_half_width_m > 0.0, "a canyon needs a positive half width")
|
|
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
|
|
floor_material = p_floor_material
|
|
_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:
|
|
return -depth_m * _presence(x_m, z_m)
|
|
|
|
func material_at(x_m: float, z_m: float) -> int:
|
|
# Full presence means the flat floor: inside the walls and clear of the end tapers.
|
|
if floor_material != NO_MATERIAL and _presence(x_m, z_m) >= 0.999:
|
|
return floor_material
|
|
return NO_MATERIAL
|
|
|
|
## 1 on the flat floor, 0 outside the trench, in between on the walls and end tapers.
|
|
func _presence(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 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. The path must extend past the field
|
|
## edges: beyond an end vertex the drop is decided by the half-plane of the last segment,
|
|
## which leaves a seam along the path's extension.
|
|
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:
|
|
assert(p_path.size() >= 2, "an escarpment path needs at least two vertices")
|
|
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)
|
|
|
|
|
|
## Flat-topped elliptical mound (or, with a negative height, a flat-bottomed pit) whose
|
|
## flank spans wall_width_m inside a noise-roughened outline. With rock_top the whole
|
|
## mound above 30 % of its height is rock, sommet plat compris.
|
|
class Outcrop extends Primitive:
|
|
var centre: Vector2
|
|
var radius_x_m: float
|
|
var radius_z_m: float
|
|
var height_m: float
|
|
var wall_width_m: float
|
|
var edge_amplitude_m: float
|
|
var rock_top: bool
|
|
var _noise: FastNoiseLite
|
|
var _mean_radius_m: float
|
|
|
|
func _init(
|
|
p_centre: Vector2,
|
|
p_radius_x_m: float,
|
|
p_radius_z_m: float,
|
|
p_height_m: float,
|
|
p_wall_width_m: float,
|
|
p_edge_seed: int,
|
|
p_edge_amplitude_m: float,
|
|
p_rock_top: bool
|
|
) -> void:
|
|
assert(p_radius_x_m > 0.0 and p_radius_z_m > 0.0, "an outcrop needs positive radii")
|
|
centre = p_centre
|
|
radius_x_m = p_radius_x_m
|
|
radius_z_m = p_radius_z_m
|
|
height_m = p_height_m
|
|
wall_width_m = p_wall_width_m
|
|
edge_amplitude_m = p_edge_amplitude_m
|
|
rock_top = p_rock_top
|
|
_noise = edge_noise(p_edge_seed, p_edge_amplitude_m)
|
|
_mean_radius_m = (p_radius_x_m + p_radius_z_m) * 0.5
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
var dx := (x_m - centre.x) / radius_x_m
|
|
var dz := (z_m - centre.y) / radius_z_m
|
|
var edge_distance := (sqrt(dx * dx + dz * dz) - 1.0) * _mean_radius_m
|
|
edge_distance += edge_offset(_noise, edge_amplitude_m, x_m, z_m)
|
|
return height_m * (1.0 - smoothstep(-wall_width_m, 0.0, edge_distance))
|
|
|
|
func material_at(x_m: float, z_m: float) -> int:
|
|
if rock_top and absf(elevation_delta(x_m, z_m)) > 0.3 * absf(height_m):
|
|
return SonarMaterial.Value.ROCK
|
|
return NO_MATERIAL
|
|
|
|
|
|
## count half-ellipsoid boulders scattered uniformly in a disc by a seeded generator.
|
|
## Boulders combine by maximum, so overlapping blocks never pile into one mound, and every
|
|
## cell they cover is rock.
|
|
class BoulderField extends Primitive:
|
|
var centre: Vector2
|
|
var radius_m: float
|
|
var height_ratio: float
|
|
var _boulders := PackedVector3Array() # x, z, radius
|
|
var _reach_m: float
|
|
|
|
func _init(
|
|
p_centre: Vector2,
|
|
p_radius_m: float,
|
|
p_seed: int,
|
|
p_count: int,
|
|
p_boulder_radius_min_m: float,
|
|
p_boulder_radius_max_m: float,
|
|
p_height_ratio: float
|
|
) -> void:
|
|
centre = p_centre
|
|
radius_m = p_radius_m
|
|
height_ratio = p_height_ratio
|
|
_reach_m = p_radius_m + p_boulder_radius_max_m
|
|
var generator := RandomNumberGenerator.new()
|
|
generator.seed = p_seed
|
|
for _index in p_count:
|
|
var distance := p_radius_m * sqrt(generator.randf())
|
|
var angle := TAU * generator.randf()
|
|
var boulder_radius := generator.randf_range(p_boulder_radius_min_m, p_boulder_radius_max_m)
|
|
_boulders.append(Vector3(
|
|
p_centre.x + distance * cos(angle),
|
|
p_centre.y + distance * sin(angle),
|
|
boulder_radius
|
|
))
|
|
|
|
func boulders() -> PackedVector3Array:
|
|
return _boulders
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
if Vector2(x_m, z_m).distance_to(centre) > _reach_m:
|
|
return 0.0
|
|
var highest := 0.0
|
|
for boulder in _boulders:
|
|
var dx := x_m - boulder.x
|
|
var dz := z_m - boulder.y
|
|
var squared := dx * dx + dz * dz
|
|
var radius_squared := boulder.z * boulder.z
|
|
if squared < radius_squared:
|
|
highest = maxf(highest, boulder.z * height_ratio * sqrt(1.0 - squared / radius_squared))
|
|
return highest
|
|
|
|
func material_at(x_m: float, z_m: float) -> int:
|
|
if elevation_delta(x_m, z_m) > 0.0:
|
|
return SonarMaterial.Value.ROCK
|
|
return NO_MATERIAL
|
|
|
|
|
|
class Step extends Primitive:
|
|
var point: Vector2
|
|
var normal: Vector2
|
|
var drop_m: float
|
|
var softness_m: float
|
|
var slope_beyond: float
|
|
|
|
func _init(p_point: Vector2, p_normal: Vector2, p_drop_m: float, p_softness_m: float, p_slope_beyond: float) -> void:
|
|
point = p_point
|
|
normal = p_normal.normalized()
|
|
drop_m = p_drop_m
|
|
softness_m = p_softness_m
|
|
slope_beyond = p_slope_beyond
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
var signed_distance := (Vector2(x_m, z_m) - point).dot(normal)
|
|
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 Ripples extends Primitive:
|
|
var direction: Vector2
|
|
var wavelength_m: float
|
|
var amplitude_m: float
|
|
var centre: Vector2
|
|
var radius_m: float
|
|
|
|
func _init(p_direction: Vector2, p_wavelength_m: float, p_amplitude_m: float, p_centre: Vector2, p_radius_m: float) -> void:
|
|
direction = p_direction.normalized()
|
|
wavelength_m = p_wavelength_m
|
|
amplitude_m = p_amplitude_m
|
|
centre = p_centre
|
|
radius_m = p_radius_m
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
var point := Vector2(x_m, z_m)
|
|
var phase := point.dot(direction) * TAU / wavelength_m
|
|
var falloff := 1.0
|
|
if radius_m > 0.0:
|
|
falloff = 1.0 - smoothstep(radius_m * 0.6, radius_m, point.distance_to(centre))
|
|
return amplitude_m * sin(phase) * falloff
|
|
|
|
|
|
class Roughness extends Primitive:
|
|
var amplitude_m: float
|
|
var _noise: FastNoiseLite
|
|
|
|
func _init(p_seed: int, p_frequency: float, p_amplitude_m: float) -> void:
|
|
amplitude_m = p_amplitude_m
|
|
_noise = FastNoiseLite.new()
|
|
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
_noise.seed = p_seed
|
|
_noise.frequency = p_frequency
|
|
_noise.fractal_type = FastNoiseLite.FRACTAL_FBM
|
|
_noise.fractal_octaves = 3
|
|
|
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
|
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
|
|
|
|
|
|
## Quantises steep ground into horizontal ledges layer_height_m apart: the stratified
|
|
## walls of a canyon or cliff. Flat floors and summits are below minimum_slope_degrees and
|
|
## stay untouched; the effect is full from full_slope_degrees. sharpness 0 leaves the
|
|
## field unchanged, 1 cuts hard steps. radius_m 0 applies everywhere.
|
|
class Strata extends Modifier:
|
|
var layer_height_m: float
|
|
var sharpness: float
|
|
var centre: Vector2
|
|
var radius_m: float
|
|
var _minimum_tangent: float
|
|
var _full_tangent: float
|
|
|
|
func _init(
|
|
p_layer_height_m: float,
|
|
p_sharpness: float,
|
|
p_minimum_slope_degrees: float,
|
|
p_full_slope_degrees: float,
|
|
p_centre: Vector2,
|
|
p_radius_m: float
|
|
) -> void:
|
|
assert(p_layer_height_m > 0.0, "strata need a positive layer height")
|
|
layer_height_m = p_layer_height_m
|
|
sharpness = clampf(p_sharpness, 0.0, 0.999)
|
|
centre = p_centre
|
|
radius_m = p_radius_m
|
|
_minimum_tangent = tan(deg_to_rad(p_minimum_slope_degrees))
|
|
_full_tangent = tan(deg_to_rad(p_full_slope_degrees))
|
|
|
|
func modify(x_m: float, z_m: float, elevation_m: float, gradient: Vector2) -> float:
|
|
var weight := smoothstep(_minimum_tangent, _full_tangent, gradient.length())
|
|
if radius_m > 0.0:
|
|
weight *= 1.0 - smoothstep(radius_m * 0.8, radius_m, Vector2(x_m, z_m).distance_to(centre))
|
|
if weight <= 0.0:
|
|
return elevation_m
|
|
var layer := floorf(elevation_m / layer_height_m)
|
|
var fraction := elevation_m / layer_height_m - layer
|
|
var sharpened := clampf((fraction - 0.5) / (1.0 - sharpness) + 0.5, 0.0, 1.0)
|
|
return lerpf(elevation_m, (layer + sharpened) * layer_height_m, weight)
|
|
|
|
|
|
class MaterialPatch extends RefCounted:
|
|
var centre: Vector2
|
|
var radius_m: float
|
|
var material: int
|
|
|
|
func _init(p_centre: Vector2, p_radius_m: float, p_material: int) -> void:
|
|
centre = p_centre
|
|
radius_m = p_radius_m
|
|
material = p_material
|
|
|
|
|
|
class Recipe extends RefCounted:
|
|
var columns: int = 2
|
|
var rows: int = 2
|
|
var cell_size_m: float = 1.0
|
|
var origin_x_m: float = 0.0
|
|
var origin_z_m: float = 0.0
|
|
var floor_elevation_m: float = -1000.0
|
|
var rock_slope_degrees: float = 20.0
|
|
var rock_reflectivity: float = 0.72
|
|
var sand_reflectivity: float = 0.45
|
|
var primitives: Array[Primitive] = []
|
|
var modifiers: Array[Modifier] = []
|
|
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:
|
|
var data := SiteTerrainData.new()
|
|
data.columns = recipe.columns
|
|
data.rows = recipe.rows
|
|
data.cell_size_m = recipe.cell_size_m
|
|
data.origin_x_m = recipe.origin_x_m
|
|
data.origin_z_m = recipe.origin_z_m
|
|
data.rock_reflectivity = recipe.rock_reflectivity
|
|
data.sand_reflectivity = recipe.sand_reflectivity
|
|
var cell_count := recipe.columns * recipe.rows
|
|
# Packed arrays are passed by value in GDScript: fill locals, assign once.
|
|
var elevations := PackedFloat32Array()
|
|
var materials := PackedByteArray()
|
|
elevations.resize(cell_count)
|
|
materials.resize(cell_count)
|
|
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 elevation := 0.0
|
|
for primitive: Primitive in recipe.primitives:
|
|
elevation += primitive.elevation_delta(x_m, z_m)
|
|
elevations[data.index_of(column, row)] = elevation
|
|
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.materials = materials
|
|
var field := TerrainHeightField.from_data(data)
|
|
if field == null:
|
|
return null
|
|
var rock_cosine := cos(deg_to_rad(recipe.rock_slope_degrees))
|
|
for row in recipe.rows:
|
|
for column in recipe.columns:
|
|
if field.normal_at_cell(column, row).y < rock_cosine:
|
|
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 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
|
|
if Vector2(x_m, z_m).distance_to(patch.centre) <= patch.radius_m:
|
|
materials[data.index_of(column, row)] = patch.material
|
|
data.materials = materials
|
|
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:
|
|
var field := TerrainHeightField.from_data(data)
|
|
if field == null:
|
|
return ERR_INVALID_DATA
|
|
var image := Image.create(data.columns, data.rows, false, Image.FORMAT_RGB8)
|
|
var span := maxf(field.maximum_elevation_m() - field.minimum_elevation_m(), 0.001)
|
|
for row in data.rows:
|
|
for column in data.columns:
|
|
var ratio := (field.elevation_at_cell(column, row) - field.minimum_elevation_m()) / span
|
|
var color := Color(ratio, ratio, ratio)
|
|
if field.material_at_cell(column, row) == SonarMaterial.Value.ROCK:
|
|
color.r = minf(color.r + 0.16, 1.0)
|
|
image.set_pixel(column, row, color)
|
|
return image.save_png(path)
|