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
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://crrgsnvuf3ttv"
|
||||||
|
path="res://.godot/imported/vehicle_test_terrain_preview.png-4e68e296dd4625902c8d2c03244ab133.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/briefs/captures/vehicle_test_terrain_preview.png"
|
||||||
|
dest_files=["res://.godot/imported/vehicle_test_terrain_preview.png-4e68e296dd4625902c8d2c03244ab133.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -123,6 +123,14 @@ check_godot_log \
|
|||||||
"production site application unit tests" \
|
"production site application unit tests" \
|
||||||
"$QUALITY_TEMP_DIRECTORY/site-application-unit-console.log"
|
"$QUALITY_TEMP_DIRECTORY/site-application-unit-console.log"
|
||||||
|
|
||||||
|
printf 'Quality gate: site baker unit tests\n'
|
||||||
|
"$GODOT_EXECUTABLE" --headless \
|
||||||
|
--log-file "$QUALITY_TEMP_DIRECTORY/site-baker-unit.log" \
|
||||||
|
--path . \
|
||||||
|
--script tests/unit/run_site_baker_tests.gd \
|
||||||
|
2>&1 | tee "$QUALITY_TEMP_DIRECTORY/site-baker-unit-console.log"
|
||||||
|
check_godot_log "site baker unit tests" "$QUALITY_TEMP_DIRECTORY/site-baker-unit-console.log"
|
||||||
|
|
||||||
printf 'Quality gate: production foundation integration\n'
|
printf 'Quality gate: production foundation integration\n'
|
||||||
"$GODOT_EXECUTABLE" --headless \
|
"$GODOT_EXECUTABLE" --headless \
|
||||||
--log-file "$QUALITY_TEMP_DIRECTORY/foundation-integration.log" \
|
--log-file "$QUALITY_TEMP_DIRECTORY/foundation-integration.log" \
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const SEARCH_ROOTS := [
|
|||||||
"res://src",
|
"res://src",
|
||||||
"res://game",
|
"res://game",
|
||||||
"res://content",
|
"res://content",
|
||||||
|
"res://tools",
|
||||||
"res://scenes",
|
"res://scenes",
|
||||||
"res://tests",
|
"res://tests",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
||||||
|
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
||||||
|
const VehicleTestRecipe = preload("res://tools/sites/vehicle_test_terrain_recipe.gd")
|
||||||
|
|
||||||
|
const TEST_COUNT := 10
|
||||||
|
|
||||||
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_test_plane_and_blob()
|
||||||
|
_test_materials_and_determinism()
|
||||||
|
|
||||||
|
if _failures.is_empty():
|
||||||
|
print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
for failure: String in _failures:
|
||||||
|
push_error(failure)
|
||||||
|
print("Site baker unit tests: %d failed." % _failures.size())
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _small_recipe() -> SiteTerrainBaker.Recipe:
|
||||||
|
var recipe := SiteTerrainBaker.Recipe.new()
|
||||||
|
recipe.columns = 21
|
||||||
|
recipe.rows = 21
|
||||||
|
recipe.cell_size_m = 2.0
|
||||||
|
recipe.origin_x_m = -20.0
|
||||||
|
recipe.origin_z_m = -20.0
|
||||||
|
recipe.floor_elevation_m = -80.0
|
||||||
|
recipe.rock_slope_degrees = 20.0
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
|
func _test_plane_and_blob() -> void:
|
||||||
|
var recipe := _small_recipe()
|
||||||
|
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.1, 0.0, 0.0))
|
||||||
|
var data := SiteTerrainBaker.bake(recipe)
|
||||||
|
_expect(data != null and data.validation_error().is_empty(), "plane recipe bakes valid data")
|
||||||
|
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -30.0), "plane depth at the reference point")
|
||||||
|
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 20)], -28.0), "plane slopes along +Z")
|
||||||
|
_expect(data.materials[data.index_of(10, 10)] == SonarMaterial.Value.SAND, "gentle plane is sand")
|
||||||
|
|
||||||
|
recipe.primitives.append(SiteTerrainBaker.Blob.new(Vector2(0.0, 0.0), 4.0, 4.0, 10.0))
|
||||||
|
data = SiteTerrainBaker.bake(recipe)
|
||||||
|
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -20.0), "blob adds its amplitude at the centre")
|
||||||
|
_expect(data.materials[data.index_of(12, 10)] == SonarMaterial.Value.ROCK, "steep blob flank is rock")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_materials_and_determinism() -> void:
|
||||||
|
var recipe := _small_recipe()
|
||||||
|
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0))
|
||||||
|
recipe.primitives.append(SiteTerrainBaker.Roughness.new(1933, 0.05, 0.5))
|
||||||
|
recipe.material_patches.append(
|
||||||
|
SiteTerrainBaker.MaterialPatch.new(Vector2(10.0, 10.0), 3.0, SonarMaterial.Value.ROCK)
|
||||||
|
)
|
||||||
|
var first := SiteTerrainBaker.bake(recipe)
|
||||||
|
var second := SiteTerrainBaker.bake(recipe)
|
||||||
|
_expect(first.elevations_m == second.elevations_m, "same recipe bakes identical elevations")
|
||||||
|
_expect(first.materials == second.materials, "same recipe bakes identical materials")
|
||||||
|
_expect(first.materials[first.index_of(15, 15)] == SonarMaterial.Value.ROCK, "material patch overrides the slope rule")
|
||||||
|
var calibration := SiteTerrainBaker.bake(VehicleTestRecipe.recipe())
|
||||||
|
_expect(calibration != null and calibration.validation_error().is_empty(), "calibration recipe bakes valid data")
|
||||||
|
|
||||||
|
|
||||||
|
func _expect(condition: bool, label: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
_failures.append(label)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
||||||
|
|
||||||
|
const RECIPES := {
|
||||||
|
"vehicle_test": "res://tools/sites/vehicle_test_terrain_recipe.gd",
|
||||||
|
"zone1_vigilant": "res://tools/sites/zone1_vigilant_recipe.gd",
|
||||||
|
}
|
||||||
|
const OUTPUT_DIRECTORY := "res://content/sites"
|
||||||
|
const PREVIEW_DIRECTORY := "res://docs/briefs/captures"
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
var arguments := OS.get_cmdline_user_args()
|
||||||
|
if arguments.is_empty() or not RECIPES.has(arguments[0]):
|
||||||
|
push_error("usage: -- <%s>" % " | ".join(RECIPES.keys()))
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var name: String = arguments[0]
|
||||||
|
var recipe_script := load(RECIPES[name]) as GDScript
|
||||||
|
if recipe_script == null:
|
||||||
|
push_error("recipe script %s is missing" % RECIPES[name])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var recipe: SiteTerrainBaker.Recipe = recipe_script.recipe()
|
||||||
|
var data := SiteTerrainBaker.bake(recipe)
|
||||||
|
if data == null:
|
||||||
|
push_error("recipe %s did not bake valid terrain" % name)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
DirAccess.make_dir_recursive_absolute(OUTPUT_DIRECTORY)
|
||||||
|
DirAccess.make_dir_recursive_absolute(PREVIEW_DIRECTORY)
|
||||||
|
var output_path := "%s/%s_terrain.res" % [OUTPUT_DIRECTORY, name]
|
||||||
|
var save_error := ResourceSaver.save(data, output_path)
|
||||||
|
if save_error != OK:
|
||||||
|
push_error("cannot save %s (%d)" % [output_path, save_error])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var preview_path := "%s/%s_terrain_preview.png" % [PREVIEW_DIRECTORY, name]
|
||||||
|
var preview_error := SiteTerrainBaker.write_preview_png(data, preview_path)
|
||||||
|
if preview_error != OK:
|
||||||
|
push_error("cannot write %s (%d)" % [preview_path, preview_error])
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var rock_cells := 0
|
||||||
|
for material: int in data.materials:
|
||||||
|
if material == SiteTerrainBaker.SonarMaterial.Value.ROCK:
|
||||||
|
rock_cells += 1
|
||||||
|
var minimum := data.elevations_m[0]
|
||||||
|
var maximum := data.elevations_m[0]
|
||||||
|
for elevation: float in data.elevations_m:
|
||||||
|
minimum = minf(minimum, elevation)
|
||||||
|
maximum = maxf(maximum, elevation)
|
||||||
|
print("Baked %s: %d x %d cells, elevation %.1f .. %.1f m, rock %.0f %%" % [
|
||||||
|
name, data.columns, data.rows, minimum, maximum,
|
||||||
|
100.0 * float(rock_cells) / float(data.materials.size())
|
||||||
|
])
|
||||||
|
quit(0)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class_name VehicleTestTerrainRecipe
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
|
||||||
|
|
||||||
|
|
||||||
|
static func recipe() -> SiteTerrainBaker.Recipe:
|
||||||
|
var result := SiteTerrainBaker.Recipe.new()
|
||||||
|
result.columns = 61
|
||||||
|
result.rows = 69
|
||||||
|
result.cell_size_m = 3.0
|
||||||
|
result.origin_x_m = -90.0
|
||||||
|
result.origin_z_m = -176.0
|
||||||
|
result.floor_elevation_m = -95.0
|
||||||
|
result.rock_slope_degrees = 20.0
|
||||||
|
result.primitives = [
|
||||||
|
SiteTerrainBaker.SlopedPlane.new(40.0, 0.0, 0.19, 0.0, 28.0),
|
||||||
|
SiteTerrainBaker.Blob.new(Vector2(-24.0, -48.0), sqrt(210.0), sqrt(980.0), 13.0),
|
||||||
|
SiteTerrainBaker.Blob.new(Vector2(25.0, -58.0), sqrt(250.0), sqrt(1150.0), 15.0),
|
||||||
|
SiteTerrainBaker.Blob.new(Vector2(0.0, -66.0), sqrt(120.0), sqrt(2500.0), -8.5),
|
||||||
|
SiteTerrainBaker.Blob.new(Vector2(-5.0, -125.0), sqrt(900.0), sqrt(620.0), 8.0),
|
||||||
|
SiteTerrainBaker.Ripples.new(Vector2(1.0, 0.0), TAU / 0.075, 0.9, Vector2.ZERO, 0.0),
|
||||||
|
SiteTerrainBaker.Ripples.new(Vector2(0.0, 1.0), TAU / 0.048, 0.9, Vector2.ZERO, 0.0),
|
||||||
|
]
|
||||||
|
return result
|
||||||
Reference in new issue
Block a user