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,
|
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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
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 = (
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||