Terrain des sites de plongée : champ de hauteur authoré, Banc du Vigilant, formes variées et rendu sous-marin #4

Open
olix21 wants to merge 47 commits from olix21/worktree-terrain-site-zone1 AGit into main
pull from: olix21/worktree-terrain-site-zone1
12 changed files with 293 additions and 24 deletions
Showing only changes of commit 48e054557c - Show all commits

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 KiB

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bfhdetospcaup"
path="res://.godot/imported/zone1_vigilant_patch_reef.png-c13e4b56c466e2ab61173a145fd844d2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/briefs/captures/zone1_vigilant_patch_reef.png"
dest_files=["res://.godot/imported/zone1_vigilant_patch_reef.png-c13e4b56c466e2ab61173a145fd844d2.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
+5 -1
View File
@@ -265,7 +265,11 @@ de contacts existant les sépare du fond.
présence sonar. Le shader roche ajoute gazon algal sur les faces éclairées, présence sonar. Le shader roche ajoute gazon algal sur les faces éclairées,
croûtes corallines par plaques et éponges encroûtantes à l'ombre, le tout croûtes corallines par plaques et éponges encroûtantes à l'ombre, le tout
s'éteignant entre 40 et 75 m. L'optique de la colonne d'eau est partagée par s'éteignant entre 40 et 75 m. L'optique de la colonne d'eau est partagée par
`water_light.gdshaderinc`. `water_light.gdshaderinc`. Têtes de corail en volume (même jour) : trois
dômes bosselés procéduraux (massif, cerveau à crêtes, lobé) à base plate,
semés sur la roche douce (< 30°) de 12 à 40 m en patch reefs par bruit, une
`MultiMesh` par tuile et par variante, shader opaque `seabed_coral.gdshader`
(couleur de sommet × teinte d'instance, marbrure, même colonne d'eau).
- Brouillard, lumière directionnelle et particules de la scène de test - Brouillard, lumière directionnelle et particules de la scène de test
actuelle sont conservés et réglés par site. actuelle sont conservés et réglés par site.
- Passe lumière du 2026-10-06 (`dive_site.tscn`) : l'ambiance vient d'un ciel - Passe lumière du 2026-10-06 (`dive_site.tscn`) : l'ambiance vient d'un ciel
+2 -1
View File
@@ -182,7 +182,8 @@ Plan de masse (nord en haut) :
`terrain_showcase_canyon.png` (gradins et fond ridé), `_crevasse.png`, `terrain_showcase_canyon.png` (gradins et fond ridé), `_crevasse.png`,
`_boulders.png`, `_spurs.png`, `_escarpment.png` (face de la falaise vue du `_boulders.png`, `_spurs.png`, `_escarpment.png` (face de la falaise vue du
pied) ; pour le Banc du Vigilant recuit, `zone1_vigilant_tombant.png` pied) ; pour le Banc du Vigilant recuit, `zone1_vigilant_tombant.png`
(gradins du tombant) et `zone1_vigilant_plateau.png` (plateau ondulé). Les (gradins du tombant), `zone1_vigilant_plateau.png` (plateau ondulé) et
`zone1_vigilant_patch_reef.png` (têtes de corail autour du caboteur). Les
captures sont faites avec le shader à normal maps et ombrage de détail captures sont faites avec le shader à normal maps et ombrage de détail
(brief du Banc du Vigilant, §7) ; brouillard et lumière restent ceux de la (brief du Banc du Vigilant, §7) ; brouillard et lumière restent ceux de la
scène de site. scène de site.
+39
View File
@@ -0,0 +1,39 @@
shader_type spatial;
render_mode depth_draw_opaque;
// Massive coral heads: opaque, vertex-coloured (mesh shade times the instance tint),
// lit under the same water column as the seabed. INSTANCE_CUSTOM.x is a per-head
// mottling phase; the submarine's projectors are not attenuated.
#include "res://game/world/sites/water_light.gdshaderinc"
uniform vec3 water_absorption_per_m = vec3(0.11, 0.04, 0.032);
uniform float reference_depth_m = 24.0;
uniform float minimum_daylight = 0.05;
uniform float mottle_strength = 0.18;
varying float daylight;
varying vec3 chroma;
varying float mottle;
void vertex() {
vec3 world = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 transmittance = water_transmittance(-world.y, water_absorption_per_m, reference_depth_m);
daylight = water_daylight(transmittance, minimum_daylight);
chroma = water_chroma(transmittance, water_absorption_per_m, reference_depth_m);
mottle = sin(VERTEX.x * 9.0 + INSTANCE_CUSTOM.x * TAU) * sin(VERTEX.z * 7.0 + VERTEX.y * 5.0);
}
void fragment() {
ALBEDO = COLOR.rgb * (1.0 + mottle * mottle_strength) * chroma;
AO = daylight;
AO_LIGHT_AFFECT = 0.35;
ROUGHNESS = 0.8;
METALLIC = 0.0;
}
void light() {
float sun = water_sun_factor(LIGHT_IS_DIRECTIONAL, daylight);
float lambert = clamp(dot(NORMAL, LIGHT), 0.0, 1.0);
DIFFUSE_LIGHT += lambert * ATTENUATION * LIGHT_COLOR * ALBEDO * sun / PI;
}
+113 -20
View File
@@ -2,8 +2,9 @@ class_name SeabedFloraActor
extends Node3D extends Node3D
## Scatters the seabed flora of a site from its height field, deterministically from a ## Scatters the seabed flora of a site from its height field, deterministically from a
## seed: seagrass tufts on gently sloping sand in the lit band, sea fans on rock. One ## seed: seagrass tufts on gently sloping sand in the lit band, sea fans on rock, and
## MultiMeshInstance3D per terrain tile and per kind, with a visibility range, no ## massive coral heads on the gentle rock of patch reefs. One MultiMeshInstance3D per
## terrain tile and per kind (per variant for corals), with a visibility range, no
## collision and no sonar presence. Placement is pure and runs on the worker thread ## collision and no sonar presence. Placement is pure and runs on the worker thread
## pool; the multimeshes are built afterwards on the main thread. ## pool; the multimeshes are built afterwards on the main thread.
@@ -11,8 +12,9 @@ const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd") const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const SeabedFloraAssets = preload("res://game/world/sites/seabed_flora_assets.gd") const SeabedFloraAssets = preload("res://game/world/sites/seabed_flora_assets.gd")
const FLORA_SHADER = preload("res://game/world/sites/seabed_flora.gdshader") const FLORA_SHADER = preload("res://game/world/sites/seabed_flora.gdshader")
const CORAL_SHADER = preload("res://game/world/sites/seabed_coral.gdshader")
enum Kind { SEAGRASS, SEA_FAN } enum Kind { SEAGRASS, SEA_FAN, CORAL_HEAD }
const TILE_CELLS := 64 const TILE_CELLS := 64
# Seagrass: meadows on flat sand, dense to 20 m, gone by 36 m (Caribbean Syringodium # Seagrass: meadows on flat sand, dense to 20 m, gone by 36 m (Caribbean Syringodium
@@ -30,6 +32,19 @@ const FAN_DEPTH_MAX_M := 60.0
const FAN_KEEP_PROBABILITY := 0.45 const FAN_KEEP_PROBABILITY := 0.45
const FAN_VISIBILITY_M := 170.0 const FAN_VISIBILITY_M := 170.0
const FAN_HEADING := Vector2(0.8, 0.6) const FAN_HEADING := Vector2(0.8, 0.6)
# Coral heads: patch reefs are the gentle (< 30 deg) rock of the sunlit band, 12 to
# 40 m, clustered by noise; heads are 0.6 to 2.2 m across.
const CORAL_SPACING_M := 3.0
const CORAL_DEPTH_MIN_M := 12.0
const CORAL_DEPTH_FADE_START_M := 30.0
const CORAL_DEPTH_MAX_M := 40.0
const CORAL_SLOPE_COSINE := 0.866 # cos(30 deg)
const CORAL_REEF_THRESHOLD := 0.05
const CORAL_KEEP_PROBABILITY := 0.55
const CORAL_VISIBILITY_M := 150.0
const CORAL_COLORS: Array[Color] = [
Color(0.72, 0.58, 0.3), Color(0.5, 0.52, 0.28), Color(0.68, 0.48, 0.36), Color(0.62, 0.6, 0.42),
]
const FAN_COLORS: Array[Color] = [ const FAN_COLORS: Array[Color] = [
Color(0.55, 0.35, 0.62), Color(0.85, 0.42, 0.2), Color(0.8, 0.7, 0.3), Color(0.6, 0.3, 0.45), Color(0.55, 0.35, 0.62), Color(0.85, 0.42, 0.2), Color(0.8, 0.7, 0.3), Color(0.6, 0.3, 0.45),
] ]
@@ -41,6 +56,7 @@ class TilePlacements extends RefCounted:
var transforms: Array[Transform3D] = [] var transforms: Array[Transform3D] = []
var colors := PackedColorArray() var colors := PackedColorArray()
var custom := PackedColorArray() var custom := PackedColorArray()
var variants := PackedInt32Array()
var _field: TerrainHeightField var _field: TerrainHeightField
@@ -48,10 +64,12 @@ var _seed := 0
var _assets: SeabedFloraAssets var _assets: SeabedFloraAssets
var _grass_material: ShaderMaterial var _grass_material: ShaderMaterial
var _fan_material: ShaderMaterial var _fan_material: ShaderMaterial
var _coral_material: ShaderMaterial
var _reef_noise: FastNoiseLite
var _meadow_noise: FastNoiseLite var _meadow_noise: FastNoiseLite
var _fan_noise: FastNoiseLite var _fan_noise: FastNoiseLite
var _counts := {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0} var _counts := {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0, Kind.CORAL_HEAD: 0}
var _samples := {Kind.SEAGRASS: [], Kind.SEA_FAN: []} var _samples := {Kind.SEAGRASS: [], Kind.SEA_FAN: [], Kind.CORAL_HEAD: []}
func configure(field: TerrainHeightField, seed: int) -> String: func configure(field: TerrainHeightField, seed: int) -> String:
@@ -64,25 +82,28 @@ func configure(field: TerrainHeightField, seed: int) -> String:
_assets = SeabedFloraAssets.build(seed) _assets = SeabedFloraAssets.build(seed)
_grass_material = _material(_assets.blade_texture) _grass_material = _material(_assets.blade_texture)
_fan_material = _material(_assets.fan_texture) _fan_material = _material(_assets.fan_texture)
_coral_material = ShaderMaterial.new()
_coral_material.shader = CORAL_SHADER
_meadow_noise = _noise(seed + 11, 0.012) _meadow_noise = _noise(seed + 11, 0.012)
_fan_noise = _noise(seed + 23, 0.03) _fan_noise = _noise(seed + 23, 0.03)
_reef_noise = _noise(seed + 37, 0.02)
var tiles := _tiles() var tiles := _tiles()
var grass: Array[TilePlacements] = [] var grass: Array[TilePlacements] = []
var fans: Array[TilePlacements] = [] var fans: Array[TilePlacements] = []
var corals: Array[TilePlacements] = []
for tile in tiles: for tile in tiles:
var grass_tile := TilePlacements.new() for list: Array[TilePlacements] in [grass, fans, corals]:
grass_tile.cells = tile var placements := TilePlacements.new()
grass.append(grass_tile) placements.cells = tile
var fan_tile := TilePlacements.new() list.append(placements)
fan_tile.cells = tile
fans.append(fan_tile)
var group := WorkerThreadPool.add_group_task( var group := WorkerThreadPool.add_group_task(
_place_tile.bind(grass, fans), tiles.size(), -1, true, "seabed flora" _place_tile.bind(grass, fans, corals), tiles.size(), -1, true, "seabed flora"
) )
WorkerThreadPool.wait_for_group_task_completion(group) WorkerThreadPool.wait_for_group_task_completion(group)
for index in tiles.size(): for index in tiles.size():
_add_multimesh(Kind.SEAGRASS, grass[index]) _add_multimesh(Kind.SEAGRASS, grass[index])
_add_multimesh(Kind.SEA_FAN, fans[index]) _add_multimesh(Kind.SEA_FAN, fans[index])
_add_coral_multimeshes(corals[index])
return "" return ""
@@ -102,8 +123,8 @@ func _clear() -> void:
for child in get_children(): for child in get_children():
remove_child(child) remove_child(child)
child.free() child.free()
_counts = {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0} _counts = {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0, Kind.CORAL_HEAD: 0}
_samples = {Kind.SEAGRASS: [], Kind.SEA_FAN: []} _samples = {Kind.SEAGRASS: [], Kind.SEA_FAN: [], Kind.CORAL_HEAD: []}
func _tiles() -> Array[Rect2i]: func _tiles() -> Array[Rect2i]:
@@ -122,7 +143,7 @@ func _tiles() -> Array[Rect2i]:
return tiles return tiles
func _place_tile(tile_index: int, grass: Array[TilePlacements], fans: Array[TilePlacements]) -> void: func _place_tile(tile_index: int, grass: Array[TilePlacements], fans: Array[TilePlacements], corals: Array[TilePlacements]) -> void:
var tile := grass[tile_index].cells var tile := grass[tile_index].cells
var cell := _field.cell_size_m() var cell := _field.cell_size_m()
var origin := _field.origin_m() var origin := _field.origin_m()
@@ -134,6 +155,7 @@ func _place_tile(tile_index: int, grass: Array[TilePlacements], fans: Array[Tile
generator.seed = _seed * 7919 + tile_index generator.seed = _seed * 7919 + tile_index
_place_grass(grass[tile_index], generator, x_min, x_max, z_min, z_max) _place_grass(grass[tile_index], generator, x_min, x_max, z_min, z_max)
_place_fans(fans[tile_index], generator, x_min, x_max, z_min, z_max) _place_fans(fans[tile_index], generator, x_min, x_max, z_min, z_max)
_place_corals(corals[tile_index], generator, x_min, x_max, z_min, z_max)
## Cheap rejections first, straight from the field arrays (cell reads, one noise read), ## Cheap rejections first, straight from the field arrays (cell reads, one noise read),
@@ -233,7 +255,72 @@ func _place_fans(out: TilePlacements, generator: RandomNumberGenerator, x_min: f
z += FAN_SPACING_M z += FAN_SPACING_M
func _add_multimesh(kind: Kind, placements: TilePlacements) -> void: func _place_corals(out: TilePlacements, generator: RandomNumberGenerator, x_min: float, x_max: float, z_min: float, z_max: float) -> void:
var origin := _field.origin_m()
var cell := _field.cell_size_m()
var columns := _field.columns()
var rows := _field.rows()
var elevations := _field.elevations_m()
var materials := _field.materials()
var rock := SonarMaterial.Value.ROCK
var slope_limit := (1.0 / (CORAL_SLOPE_COSINE * CORAL_SLOPE_COSINE) - 1.0) * (2.0 * cell) * (2.0 * cell)
var z := z_min
while z < z_max:
var x := x_min
while x < x_max:
var px := x + generator.randf() * CORAL_SPACING_M
var pz := z + generator.randf() * CORAL_SPACING_M
x += CORAL_SPACING_M
var column := int(roundf((px - origin.x) / cell))
var row := int(roundf((pz - origin.y) / cell))
if px >= x_max or pz >= z_max or column < 1 or row < 1 or column >= columns - 1 or row >= rows - 1:
continue
var index := row * columns + column
if materials[index] != rock:
continue
var depth := -elevations[index]
if depth < CORAL_DEPTH_MIN_M or depth > CORAL_DEPTH_MAX_M:
continue
var rise_x := elevations[index + 1] - elevations[index - 1]
var rise_z := elevations[index + columns] - elevations[index - columns]
if rise_x * rise_x + rise_z * rise_z > slope_limit:
continue
var reef := _reef_noise.get_noise_2d(px, pz)
if reef < CORAL_REEF_THRESHOLD:
continue
var density := (1.0 - smoothstep(CORAL_DEPTH_FADE_START_M, CORAL_DEPTH_MAX_M, depth)) * CORAL_KEEP_PROBABILITY
if generator.randf() > density * clampf((reef - CORAL_REEF_THRESHOLD) * 5.0, 0.4, 1.0):
continue
var yaw := generator.randf() * TAU
var radius := generator.randf_range(0.35, 1.3)
var squash := generator.randf_range(0.7, 1.15)
var phase := generator.randf()
var variant := generator.randi_range(0, _assets.coral_meshes.size() - 1)
var color: Color = CORAL_COLORS[generator.randi_range(0, CORAL_COLORS.size() - 1)]
var elevation := _field.elevation_at(px, pz)
var basis := Basis(Vector3.UP, yaw).scaled(Vector3(radius, radius * squash, radius))
out.transforms.append(Transform3D(basis, Vector3(px, elevation - 0.08, pz)))
out.colors.append(color)
out.custom.append(Color(phase, 0.0, 0.0, 0.0))
out.variants.append(variant)
z += CORAL_SPACING_M
## Coral heads of one tile, one MultiMesh per mesh variant.
func _add_coral_multimeshes(placements: TilePlacements) -> void:
for variant in _assets.coral_meshes.size():
var subset := TilePlacements.new()
subset.cells = placements.cells
for index in placements.transforms.size():
if placements.variants[index] != variant:
continue
subset.transforms.append(placements.transforms[index])
subset.colors.append(placements.colors[index])
subset.custom.append(placements.custom[index])
_add_multimesh(Kind.CORAL_HEAD, subset, variant)
func _add_multimesh(kind: Kind, placements: TilePlacements, variant: int = 0) -> void:
var count := placements.transforms.size() var count := placements.transforms.size()
if count == 0: if count == 0:
return return
@@ -241,7 +328,13 @@ func _add_multimesh(kind: Kind, placements: TilePlacements) -> void:
multimesh.transform_format = MultiMesh.TRANSFORM_3D multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.use_colors = true multimesh.use_colors = true
multimesh.use_custom_data = true multimesh.use_custom_data = true
multimesh.mesh = _assets.tuft_mesh if kind == Kind.SEAGRASS else _assets.fan_mesh match kind:
Kind.SEAGRASS:
multimesh.mesh = _assets.tuft_mesh
Kind.SEA_FAN:
multimesh.mesh = _assets.fan_mesh
Kind.CORAL_HEAD:
multimesh.mesh = _assets.coral_meshes[variant]
multimesh.instance_count = count multimesh.instance_count = count
for index in count: for index in count:
multimesh.set_instance_transform(index, placements.transforms[index]) multimesh.set_instance_transform(index, placements.transforms[index])
@@ -249,13 +342,13 @@ func _add_multimesh(kind: Kind, placements: TilePlacements) -> void:
multimesh.set_instance_custom_data(index, placements.custom[index]) multimesh.set_instance_custom_data(index, placements.custom[index])
var instance := MultiMeshInstance3D.new() var instance := MultiMeshInstance3D.new()
instance.name = "%s_%d_%d" % [ instance.name = "%s_%d_%d" % [
"Seagrass" if kind == Kind.SEAGRASS else "SeaFans", ["Seagrass", "SeaFans", "Corals%d" % variant][kind],
placements.cells.position.x / TILE_CELLS, placements.cells.position.x / TILE_CELLS,
placements.cells.position.y / TILE_CELLS, placements.cells.position.y / TILE_CELLS,
] ]
instance.multimesh = multimesh instance.multimesh = multimesh
instance.material_override = _grass_material if kind == Kind.SEAGRASS else _fan_material instance.material_override = [_grass_material, _fan_material, _coral_material][kind]
instance.visibility_range_end = GRASS_VISIBILITY_M if kind == Kind.SEAGRASS else FAN_VISIBILITY_M instance.visibility_range_end = [GRASS_VISIBILITY_M, FAN_VISIBILITY_M, CORAL_VISIBILITY_M][kind]
instance.visibility_range_end_margin = 20.0 instance.visibility_range_end_margin = 20.0
instance.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF instance.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
instance.cast_shadow = ( instance.cast_shadow = (
+75 -1
View File
@@ -4,17 +4,23 @@ extends RefCounted
## Procedural meshes and alpha-cut textures for the seabed flora, built synchronously ## Procedural meshes and alpha-cut textures for the seabed flora, built synchronously
## from a seed so no asset is needed and every build is identical: a seagrass tuft ## from a seed so no asset is needed and every build is identical: a seagrass tuft
## (three crossed unit quads carrying a strap-blade texture) and a sea fan (one unit ## (three crossed unit quads carrying a strap-blade texture) and a sea fan (one unit
## quad carrying a branching net texture). Meshes are unit sized, y from 0 to 1. ## quad carrying a branching net texture), and three massive coral heads (bumpy
## domes with a flat base: boulder, brain-ridged and lobed). Meshes are unit sized,
## y from 0 to 1.
const BLADE_TEXTURE_WIDTH := 128 const BLADE_TEXTURE_WIDTH := 128
const BLADE_TEXTURE_HEIGHT := 256 const BLADE_TEXTURE_HEIGHT := 256
const BLADE_COUNT := 5 const BLADE_COUNT := 5
const FAN_TEXTURE_SIZE := 256 const FAN_TEXTURE_SIZE := 256
const CORAL_VARIANT_COUNT := 3
const CORAL_RINGS := 14
const CORAL_SEGMENTS := 20
var tuft_mesh: ArrayMesh var tuft_mesh: ArrayMesh
var fan_mesh: ArrayMesh var fan_mesh: ArrayMesh
var blade_texture: ImageTexture var blade_texture: ImageTexture
var fan_texture: ImageTexture var fan_texture: ImageTexture
var coral_meshes: Array[ArrayMesh] = []
static func build(seed: int) -> SeabedFloraAssets: static func build(seed: int) -> SeabedFloraAssets:
@@ -23,9 +29,77 @@ static func build(seed: int) -> SeabedFloraAssets:
result.fan_texture = _texture_from(_fan_image(seed + 1)) result.fan_texture = _texture_from(_fan_image(seed + 1))
result.tuft_mesh = _crossed_quads(3) result.tuft_mesh = _crossed_quads(3)
result.fan_mesh = _crossed_quads(1) result.fan_mesh = _crossed_quads(1)
for variant in CORAL_VARIANT_COUNT:
result.coral_meshes.append(_coral_head(seed + 10 + variant, variant))
return result return result
## Dome of unit radius and unit height on a flat base, displaced by seeded noise:
## variant 0 a boulder coral (broad bumps), 1 a brain coral (fine ridges), 2 a lobed
## head (deep lobes). Vertex colour carries the shade (crevices darker).
static func _coral_head(seed: int, variant: int) -> ArrayMesh:
var noise := FastNoiseLite.new()
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
noise.seed = seed
noise.fractal_octaves = 3
var frequency := [0.9, 2.6, 0.6][variant] as float
var amplitude := [0.12, 0.07, 0.3][variant] as float
noise.frequency = frequency
noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED if variant == 1 else FastNoiseLite.FRACTAL_FBM
var vertices := PackedVector3Array()
var colors := PackedColorArray()
var indices := PackedInt32Array()
# Rings from the top (ring 0, a single pole) down to the base rim; the base is a
# flat fan closed on a centre vertex.
var ring_vertex_count := CORAL_SEGMENTS
for ring in range(CORAL_RINGS + 1):
var polar := (PI * 0.5) * float(ring) / float(CORAL_RINGS) # 0 at the pole, pi/2 at the rim
for segment in CORAL_SEGMENTS:
var azimuth := TAU * float(segment) / float(CORAL_SEGMENTS)
var direction := Vector3(sin(polar) * cos(azimuth), cos(polar), sin(polar) * sin(azimuth))
var bump := noise.get_noise_3d(direction.x * 3.0, direction.y * 3.0, direction.z * 3.0)
# Keep the rim on the ground: the displacement fades towards the base.
var radius := 1.0 + bump * amplitude * (0.35 + 0.65 * cos(polar))
var position := direction * radius
position.y = maxf(position.y, 0.0)
vertices.append(position)
var shade := clampf(0.78 + bump * 0.9, 0.5, 1.1)
colors.append(Color(shade, shade, shade))
for ring in CORAL_RINGS:
for segment in CORAL_SEGMENTS:
var next_segment := (segment + 1) % CORAL_SEGMENTS
var a := ring * ring_vertex_count + segment
var b := ring * ring_vertex_count + next_segment
var c := (ring + 1) * ring_vertex_count + segment
var d := (ring + 1) * ring_vertex_count + next_segment
if ring > 0:
indices.append_array([a, b, c])
indices.append_array([b, d, c])
var centre := vertices.size()
vertices.append(Vector3.ZERO)
colors.append(Color(0.6, 0.6, 0.6))
var rim := CORAL_RINGS * ring_vertex_count
for segment in CORAL_SEGMENTS:
indices.append_array([centre, rim + segment, rim + (segment + 1) % CORAL_SEGMENTS])
var normals := PackedVector3Array()
normals.resize(vertices.size())
for index in range(0, indices.size(), 3):
var face_normal := (vertices[indices[index + 2]] - vertices[indices[index]]).cross(vertices[indices[index + 1]] - vertices[indices[index]])
for corner in 3:
normals[indices[index + corner]] += face_normal
for index in normals.size():
normals[index] = normals[index].normalized() if normals[index].length() > 0.0 else Vector3.UP
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_COLOR] = colors
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
## Vertical unit quads crossed around the y axis, each 1 wide and 1 tall, UV mapped ## Vertical unit quads crossed around the y axis, each 1 wide and 1 tall, UV mapped
## so the texture's bottom row sits on the ground. ## so the texture's bottom row sits on the ground.
static func _crossed_quads(count: int) -> ArrayMesh: static func _crossed_quads(count: int) -> ArrayMesh:
@@ -20,7 +20,7 @@ const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn")
const ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres" const ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres"
const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres" const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres"
const GROOVE_PROBE_OFFSET_M := 25.0 const GROOVE_PROBE_OFFSET_M := 25.0
const TEST_COUNT := 49 const TEST_COUNT := 51
var _failures: Array[String] = [] var _failures: Array[String] = []
@@ -279,6 +279,24 @@ func _test_zone1_site() -> void:
for sample in result.samples: for sample in result.samples:
has_sand = has_sand or sample.material == SonarMaterial.Value.SAND has_sand = has_sand or sample.material == SonarMaterial.Value.SAND
_expect(result.accepted and has_sand, "a reference scan from the zone 1 spawn reads the sand plateau") _expect(result.accepted and has_sand, "a reference scan from the zone 1 spawn reads the sand plateau")
# The calibration site lies below 40 m, so coral heads are checked on the patch reef here.
var flora := site.flora()
var coral_count := flora.instance_count(SeabedFloraActor.Kind.CORAL_HEAD)
_expect(coral_count > 0, "the Banc du Vigilant grows coral heads on its patch reef (%d)" % coral_count)
var field_for_corals := definition.height_field()
var corals_on_gentle_rock := true
for transform in flora.sample_transforms(SeabedFloraActor.Kind.CORAL_HEAD):
var at := transform.origin
var column := int(roundf((at.x - field_for_corals.origin_m().x) / field_for_corals.cell_size_m()))
var row := int(roundf((at.z - field_for_corals.origin_m().y) / field_for_corals.cell_size_m()))
corals_on_gentle_rock = (
corals_on_gentle_rock
and field_for_corals.material_at(at.x, at.z) == SonarMaterial.Value.ROCK
and field_for_corals.normal_at_cell(column, row).y >= SeabedFloraActor.CORAL_SLOPE_COSINE - 0.05
and -at.y >= SeabedFloraActor.CORAL_DEPTH_MIN_M and -at.y <= SeabedFloraActor.CORAL_DEPTH_MAX_M
)
_expect(corals_on_gentle_rock, "coral heads sit on gentle rock in the lit band")
site.queue_free() site.queue_free()