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
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 576 KiB After Width: | Height: | Size: 577 KiB |
|
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
|
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
|
||||
@@ -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,
|
||||
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
|
||||
`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
|
||||
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
|
||||
|
||||
@@ -182,7 +182,8 @@ Plan de masse (nord en haut) :
|
||||
`terrain_showcase_canyon.png` (gradins et fond ridé), `_crevasse.png`,
|
||||
`_boulders.png`, `_spurs.png`, `_escarpment.png` (face de la falaise vue du
|
||||
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
|
||||
(brief du Banc du Vigilant, §7) ; brouillard et lumière restent ceux de la
|
||||
scène de site.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,8 +2,9 @@ class_name SeabedFloraActor
|
||||
extends Node3D
|
||||
|
||||
## 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
|
||||
## MultiMeshInstance3D per terrain tile and per kind, with a visibility range, no
|
||||
## seed: seagrass tufts on gently sloping sand in the lit band, sea fans on rock, and
|
||||
## 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
|
||||
## 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 SeabedFloraAssets = preload("res://game/world/sites/seabed_flora_assets.gd")
|
||||
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
|
||||
# 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_VISIBILITY_M := 170.0
|
||||
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] = [
|
||||
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 colors := PackedColorArray()
|
||||
var custom := PackedColorArray()
|
||||
var variants := PackedInt32Array()
|
||||
|
||||
|
||||
var _field: TerrainHeightField
|
||||
@@ -48,10 +64,12 @@ var _seed := 0
|
||||
var _assets: SeabedFloraAssets
|
||||
var _grass_material: ShaderMaterial
|
||||
var _fan_material: ShaderMaterial
|
||||
var _coral_material: ShaderMaterial
|
||||
var _reef_noise: FastNoiseLite
|
||||
var _meadow_noise: FastNoiseLite
|
||||
var _fan_noise: FastNoiseLite
|
||||
var _counts := {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0}
|
||||
var _samples := {Kind.SEAGRASS: [], Kind.SEA_FAN: []}
|
||||
var _counts := {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0, Kind.CORAL_HEAD: 0}
|
||||
var _samples := {Kind.SEAGRASS: [], Kind.SEA_FAN: [], Kind.CORAL_HEAD: []}
|
||||
|
||||
|
||||
func configure(field: TerrainHeightField, seed: int) -> String:
|
||||
@@ -64,25 +82,28 @@ func configure(field: TerrainHeightField, seed: int) -> String:
|
||||
_assets = SeabedFloraAssets.build(seed)
|
||||
_grass_material = _material(_assets.blade_texture)
|
||||
_fan_material = _material(_assets.fan_texture)
|
||||
_coral_material = ShaderMaterial.new()
|
||||
_coral_material.shader = CORAL_SHADER
|
||||
_meadow_noise = _noise(seed + 11, 0.012)
|
||||
_fan_noise = _noise(seed + 23, 0.03)
|
||||
_reef_noise = _noise(seed + 37, 0.02)
|
||||
var tiles := _tiles()
|
||||
var grass: Array[TilePlacements] = []
|
||||
var fans: Array[TilePlacements] = []
|
||||
var corals: Array[TilePlacements] = []
|
||||
for tile in tiles:
|
||||
var grass_tile := TilePlacements.new()
|
||||
grass_tile.cells = tile
|
||||
grass.append(grass_tile)
|
||||
var fan_tile := TilePlacements.new()
|
||||
fan_tile.cells = tile
|
||||
fans.append(fan_tile)
|
||||
for list: Array[TilePlacements] in [grass, fans, corals]:
|
||||
var placements := TilePlacements.new()
|
||||
placements.cells = tile
|
||||
list.append(placements)
|
||||
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)
|
||||
for index in tiles.size():
|
||||
_add_multimesh(Kind.SEAGRASS, grass[index])
|
||||
_add_multimesh(Kind.SEA_FAN, fans[index])
|
||||
_add_coral_multimeshes(corals[index])
|
||||
return ""
|
||||
|
||||
|
||||
@@ -102,8 +123,8 @@ func _clear() -> void:
|
||||
for child in get_children():
|
||||
remove_child(child)
|
||||
child.free()
|
||||
_counts = {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0}
|
||||
_samples = {Kind.SEAGRASS: [], Kind.SEA_FAN: []}
|
||||
_counts = {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0, Kind.CORAL_HEAD: 0}
|
||||
_samples = {Kind.SEAGRASS: [], Kind.SEA_FAN: [], Kind.CORAL_HEAD: []}
|
||||
|
||||
|
||||
func _tiles() -> Array[Rect2i]:
|
||||
@@ -122,7 +143,7 @@ func _tiles() -> Array[Rect2i]:
|
||||
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 cell := _field.cell_size_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
|
||||
_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_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),
|
||||
@@ -233,7 +255,72 @@ func _place_fans(out: TilePlacements, generator: RandomNumberGenerator, x_min: f
|
||||
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()
|
||||
if count == 0:
|
||||
return
|
||||
@@ -241,7 +328,13 @@ func _add_multimesh(kind: Kind, placements: TilePlacements) -> void:
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
multimesh.use_colors = 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
|
||||
for index in count:
|
||||
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])
|
||||
var instance := MultiMeshInstance3D.new()
|
||||
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.y / TILE_CELLS,
|
||||
]
|
||||
instance.multimesh = multimesh
|
||||
instance.material_override = _grass_material if kind == Kind.SEAGRASS else _fan_material
|
||||
instance.visibility_range_end = GRASS_VISIBILITY_M if kind == Kind.SEAGRASS else FAN_VISIBILITY_M
|
||||
instance.material_override = [_grass_material, _fan_material, _coral_material][kind]
|
||||
instance.visibility_range_end = [GRASS_VISIBILITY_M, FAN_VISIBILITY_M, CORAL_VISIBILITY_M][kind]
|
||||
instance.visibility_range_end_margin = 20.0
|
||||
instance.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
|
||||
instance.cast_shadow = (
|
||||
|
||||
@@ -4,17 +4,23 @@ extends RefCounted
|
||||
## 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
|
||||
## (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_HEIGHT := 256
|
||||
const BLADE_COUNT := 5
|
||||
const FAN_TEXTURE_SIZE := 256
|
||||
const CORAL_VARIANT_COUNT := 3
|
||||
const CORAL_RINGS := 14
|
||||
const CORAL_SEGMENTS := 20
|
||||
|
||||
var tuft_mesh: ArrayMesh
|
||||
var fan_mesh: ArrayMesh
|
||||
var blade_texture: ImageTexture
|
||||
var fan_texture: ImageTexture
|
||||
var coral_meshes: Array[ArrayMesh] = []
|
||||
|
||||
|
||||
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.tuft_mesh = _crossed_quads(3)
|
||||
result.fan_mesh = _crossed_quads(1)
|
||||
for variant in CORAL_VARIANT_COUNT:
|
||||
result.coral_meshes.append(_coral_head(seed + 10 + variant, variant))
|
||||
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
|
||||
## so the texture's bottom row sits on the ground.
|
||||
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 SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres"
|
||||
const GROOVE_PROBE_OFFSET_M := 25.0
|
||||
const TEST_COUNT := 49
|
||||
const TEST_COUNT := 51
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -279,6 +279,24 @@ func _test_zone1_site() -> void:
|
||||
for sample in result.samples:
|
||||
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")
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
|
||||