feat(tools): declarative site terrain baker and calibration recipe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
84e66d304a
commit
8ab1289d10
9 files changed
+461
No files matched your search
@@ -0,0 +1,258 @@
|
||||
class_name SiteTerrainBaker
|
||||
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")
|
||||
|
||||
|
||||
class Primitive extends RefCounted:
|
||||
func elevation_delta(_x_m: float, _z_m: float) -> float:
|
||||
return 0.0
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 material_patches: Array[MaterialPatch] = []
|
||||
|
||||
|
||||
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)] = maxf(elevation, recipe.floor_elevation_m)
|
||||
materials[data.index_of(column, row)] = 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 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
|
||||
|
||||
|
||||
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)
|
||||
Reference in new issue
Block a user