feat(world): seabed flora and encrusting growth on rock
Quality Gate / Quality Gate (pull_request) Failing after 2m58s
SeabedFloraActor scatters seagrass tufts on flat sand in the lit band and sea fans on rock between 15 and 60 m, deterministically from the site id, one MultiMesh per tile and kind, with procedural alpha-cut meshes and textures (SeabedFloraAssets), vertex sway and wrapped lighting, no collision and no sonar presence. Placement reads the field arrays directly and runs on the worker thread pool. The seabed shader grows turf algae on sunlit rock, coralline crusts in patches and encrusting sponges in the shade, fading out between 40 and 75 m. The water-column optics move to a shared shader include. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
No files matched your search
|
Before Width: | Height: | Size: 459 KiB After Width: | Height: | Size: 528 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 318 KiB |
@@ -255,6 +255,17 @@ de contacts existant les sépare du fond.
|
|||||||
assemblés ensuite sur le thread principal.
|
assemblés ensuite sur le thread principal.
|
||||||
- Props low-poly posés à la main, en nombre borné : blocs d'éboulis au pied
|
- Props low-poly posés à la main, en nombre borné : blocs d'éboulis au pied
|
||||||
du tombant, têtes de corail sur les patch reefs, rien d'animé.
|
du tombant, têtes de corail sur les patch reefs, rien d'animé.
|
||||||
|
- Flore et encroûtantes (2026-10-07, demande d'Olivier) : `SeabedFloraActor`
|
||||||
|
sème, depuis le champ de hauteur et une graine dérivée de l'identifiant du
|
||||||
|
site, des touffes d'herbier sur le sable plat de la bande éclairée (pleines
|
||||||
|
à 20 m, absentes à 36 m, en prairies par bruit) et des gorgones sur la roche
|
||||||
|
de 15 à 60 m, cap croisé sur la houle ; une `MultiMesh` par tuile et par
|
||||||
|
espèce, maillages et textures à découpe alpha procéduraux
|
||||||
|
(`SeabedFloraAssets`), ondulation en vertex shader, aucune collision ni
|
||||||
|
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`.
|
||||||
- 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
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ func elevations_m() -> PackedFloat32Array:
|
|||||||
return _data.elevations_m
|
return _data.elevations_m
|
||||||
|
|
||||||
|
|
||||||
|
## Row-major sonar materials, same layout as elevations_m.
|
||||||
|
func materials() -> PackedByteArray:
|
||||||
|
return _data.materials
|
||||||
|
|
||||||
|
|
||||||
func elevation_at_cell(column: int, row: int) -> float:
|
func elevation_at_cell(column: int, row: int) -> float:
|
||||||
return _data.elevations_m[_data.index_of(_clamp_column(column), _clamp_row(row))]
|
return _data.elevations_m[_data.index_of(_clamp_column(column), _clamp_row(row))]
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
|||||||
const BathymetricTerrainActor = preload(
|
const BathymetricTerrainActor = preload(
|
||||||
"res://game/world/sites/bathymetric_terrain_actor.gd"
|
"res://game/world/sites/bathymetric_terrain_actor.gd"
|
||||||
)
|
)
|
||||||
|
const SeabedFloraActor = preload("res://game/world/sites/seabed_flora_actor.gd")
|
||||||
|
|
||||||
const METAL_REFLECTIVITY := 1.0
|
const METAL_REFLECTIVITY := 1.0
|
||||||
const BEACON_COLOR := Color(0.2, 0.78, 0.9)
|
const BEACON_COLOR := Color(0.2, 0.78, 0.9)
|
||||||
|
|
||||||
@onready var _terrain: BathymetricTerrainActor = %Terrain
|
@onready var _terrain: BathymetricTerrainActor = %Terrain
|
||||||
|
@onready var _flora: SeabedFloraActor = %Flora
|
||||||
@onready var _placements_root: Node3D = %Placements
|
@onready var _placements_root: Node3D = %Placements
|
||||||
|
|
||||||
var _definition: DiveSiteDefinition
|
var _definition: DiveSiteDefinition
|
||||||
@@ -29,6 +31,11 @@ func configure(definition: DiveSiteDefinition) -> String:
|
|||||||
var terrain_error := _terrain.configure(field)
|
var terrain_error := _terrain.configure(field)
|
||||||
if not terrain_error.is_empty():
|
if not terrain_error.is_empty():
|
||||||
return terrain_error
|
return terrain_error
|
||||||
|
if _flora != null:
|
||||||
|
# The flora is seeded by the site id, so a site always grows the same meadows.
|
||||||
|
var flora_error := _flora.configure(field, hash(String(definition.site_id)))
|
||||||
|
if not flora_error.is_empty():
|
||||||
|
return flora_error
|
||||||
_definition = definition
|
_definition = definition
|
||||||
for child in _placements_root.get_children():
|
for child in _placements_root.get_children():
|
||||||
_placements_root.remove_child(child)
|
_placements_root.remove_child(child)
|
||||||
@@ -47,6 +54,10 @@ func terrain() -> BathymetricTerrainActor:
|
|||||||
return _terrain
|
return _terrain
|
||||||
|
|
||||||
|
|
||||||
|
func flora() -> SeabedFloraActor:
|
||||||
|
return _flora
|
||||||
|
|
||||||
|
|
||||||
func spawn_transform() -> Transform3D:
|
func spawn_transform() -> Transform3D:
|
||||||
return _definition.spawn_transform() if _definition != null else Transform3D.IDENTITY
|
return _definition.spawn_transform() if _definition != null else Transform3D.IDENTITY
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[gd_scene load_steps=9 format=3]
|
[gd_scene load_steps=10 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://game/world/sites/dive_site.gd" id="1_site"]
|
[ext_resource type="Script" path="res://game/world/sites/dive_site.gd" id="1_site"]
|
||||||
[ext_resource type="Script" path="res://game/world/sites/bathymetric_terrain_actor.gd" id="2_terrain"]
|
[ext_resource type="Script" path="res://game/world/sites/bathymetric_terrain_actor.gd" id="2_terrain"]
|
||||||
[ext_resource type="Shader" path="res://game/world/sites/water_column_fog.gdshader" id="3_water_column"]
|
[ext_resource type="Shader" path="res://game/world/sites/water_column_fog.gdshader" id="3_water_column"]
|
||||||
|
[ext_resource type="Script" path="res://game/world/sites/seabed_flora_actor.gd" id="4_flora"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="WaterColumnMaterial"]
|
[sub_resource type="ShaderMaterial" id="WaterColumnMaterial"]
|
||||||
shader = ExtResource("3_water_column")
|
shader = ExtResource("3_water_column")
|
||||||
@@ -96,5 +97,9 @@ material = SubResource("WaterColumnMaterial")
|
|||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("2_terrain")
|
script = ExtResource("2_terrain")
|
||||||
|
|
||||||
|
[node name="Flora" type="Node3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("4_flora")
|
||||||
|
|
||||||
[node name="Placements" type="Node3D" parent="."]
|
[node name="Placements" type="Node3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
render_mode cull_disabled, depth_draw_opaque;
|
||||||
|
|
||||||
|
// Seagrass tufts and sea fans as alpha-cut billboards instanced by MultiMesh. The mesh
|
||||||
|
// is unit sized (y from 0 to 1); the instance transform gives size and heading, the
|
||||||
|
// instance COLOR the species tint, INSTANCE_CUSTOM.x the sway phase and
|
||||||
|
// INSTANCE_CUSTOM.y the sway weight (grass bends, fans barely). Daylight follows the
|
||||||
|
// same water column as the seabed; the submarine's projectors are not attenuated.
|
||||||
|
|
||||||
|
#include "res://game/world/sites/water_light.gdshaderinc"
|
||||||
|
|
||||||
|
uniform sampler2D albedo_map : source_color, filter_linear_mipmap;
|
||||||
|
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 sway_amplitude = 0.09;
|
||||||
|
uniform float sway_speed = 0.8;
|
||||||
|
uniform float alpha_threshold = 0.5;
|
||||||
|
uniform float translucency = 0.7;
|
||||||
|
|
||||||
|
varying vec3 world_position;
|
||||||
|
varying float daylight;
|
||||||
|
varying vec3 chroma;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
float phase = INSTANCE_CUSTOM.x * TAU;
|
||||||
|
float weight = INSTANCE_CUSTOM.y * VERTEX.y * VERTEX.y;
|
||||||
|
float swing = sin(TIME * sway_speed + phase) + 0.35 * sin(TIME * sway_speed * 2.3 + phase * 1.7);
|
||||||
|
VERTEX.x += swing * sway_amplitude * weight;
|
||||||
|
VERTEX.z += cos(TIME * sway_speed * 0.7 + phase) * sway_amplitude * 0.5 * weight;
|
||||||
|
world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
vec3 transmittance = water_transmittance(-world_position.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 sample = texture(albedo_map, UV);
|
||||||
|
ALPHA = sample.a;
|
||||||
|
ALPHA_SCISSOR_THRESHOLD = alpha_threshold;
|
||||||
|
ALBEDO = sample.rgb * COLOR.rgb * chroma;
|
||||||
|
if (!FRONT_FACING) {
|
||||||
|
NORMAL = -NORMAL;
|
||||||
|
}
|
||||||
|
AO = daylight;
|
||||||
|
AO_LIGHT_AFFECT = 0.35;
|
||||||
|
ROUGHNESS = 0.85;
|
||||||
|
METALLIC = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void light() {
|
||||||
|
float sun = water_sun_factor(LIGHT_IS_DIRECTIONAL, daylight);
|
||||||
|
// Thin blades and fan nets let light through and are lit from both faces: the
|
||||||
|
// lambert term is wrapped around the blade, so a vertical blade under an
|
||||||
|
// overhead sun is not black.
|
||||||
|
float facing = dot(NORMAL, LIGHT);
|
||||||
|
float lambert = clamp(facing, 0.0, 1.0);
|
||||||
|
float wrapped = clamp(abs(facing) * 0.5 + 0.5, 0.0, 1.0) * translucency;
|
||||||
|
DIFFUSE_LIGHT += (lambert + wrapped) * ATTENUATION * LIGHT_COLOR * ALBEDO * sun / PI;
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
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
|
||||||
|
## collision and no sonar presence. Placement is pure and runs on the worker thread
|
||||||
|
## pool; the multimeshes are built afterwards on the main thread.
|
||||||
|
|
||||||
|
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
|
||||||
|
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")
|
||||||
|
|
||||||
|
enum Kind { SEAGRASS, SEA_FAN }
|
||||||
|
|
||||||
|
const TILE_CELLS := 64
|
||||||
|
# Seagrass: meadows on flat sand, dense to 20 m, gone by 36 m (Caribbean Syringodium
|
||||||
|
# and Halophila thin out with the light).
|
||||||
|
const GRASS_SPACING_M := 2.0
|
||||||
|
const GRASS_DEPTH_FULL_M := 20.0
|
||||||
|
const GRASS_DEPTH_NONE_M := 36.0
|
||||||
|
const GRASS_SLOPE_COSINE := 0.9659 # cos(15 deg)
|
||||||
|
const GRASS_MEADOW_THRESHOLD := 0.12
|
||||||
|
const GRASS_VISIBILITY_M := 110.0
|
||||||
|
# Sea fans: on rock from 15 to 60 m, heading across the swell.
|
||||||
|
const FAN_SPACING_M := 5.0
|
||||||
|
const FAN_DEPTH_MIN_M := 15.0
|
||||||
|
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)
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Placements of one tile for one kind, computed off the main thread.
|
||||||
|
class TilePlacements extends RefCounted:
|
||||||
|
var cells := Rect2i()
|
||||||
|
var transforms: Array[Transform3D] = []
|
||||||
|
var colors := PackedColorArray()
|
||||||
|
var custom := PackedColorArray()
|
||||||
|
|
||||||
|
|
||||||
|
var _field: TerrainHeightField
|
||||||
|
var _seed := 0
|
||||||
|
var _assets: SeabedFloraAssets
|
||||||
|
var _grass_material: ShaderMaterial
|
||||||
|
var _fan_material: ShaderMaterial
|
||||||
|
var _meadow_noise: FastNoiseLite
|
||||||
|
var _fan_noise: FastNoiseLite
|
||||||
|
var _counts := {Kind.SEAGRASS: 0, Kind.SEA_FAN: 0}
|
||||||
|
var _samples := {Kind.SEAGRASS: [], Kind.SEA_FAN: []}
|
||||||
|
|
||||||
|
|
||||||
|
func configure(field: TerrainHeightField, seed: int) -> String:
|
||||||
|
if field == null:
|
||||||
|
return "flora field is missing"
|
||||||
|
_clear()
|
||||||
|
_field = field
|
||||||
|
_seed = seed
|
||||||
|
if _assets == null:
|
||||||
|
_assets = SeabedFloraAssets.build(seed)
|
||||||
|
_grass_material = _material(_assets.blade_texture)
|
||||||
|
_fan_material = _material(_assets.fan_texture)
|
||||||
|
_meadow_noise = _noise(seed + 11, 0.012)
|
||||||
|
_fan_noise = _noise(seed + 23, 0.03)
|
||||||
|
var tiles := _tiles()
|
||||||
|
var grass: Array[TilePlacements] = []
|
||||||
|
var fans: 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)
|
||||||
|
var group := WorkerThreadPool.add_group_task(
|
||||||
|
_place_tile.bind(grass, fans), 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])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func instance_count(kind: Kind) -> int:
|
||||||
|
return int(_counts[kind])
|
||||||
|
|
||||||
|
|
||||||
|
## World transforms of the first few instances of a kind, for inspection and tests.
|
||||||
|
func sample_transforms(kind: Kind) -> Array[Transform3D]:
|
||||||
|
var result: Array[Transform3D] = []
|
||||||
|
for transform in _samples[kind]:
|
||||||
|
result.append(transform)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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: []}
|
||||||
|
|
||||||
|
|
||||||
|
func _tiles() -> Array[Rect2i]:
|
||||||
|
var tiles: Array[Rect2i] = []
|
||||||
|
var last_column := _field.columns() - 1
|
||||||
|
var last_row := _field.rows() - 1
|
||||||
|
var start_row := 0
|
||||||
|
while start_row < last_row:
|
||||||
|
var end_row := mini(start_row + TILE_CELLS, last_row)
|
||||||
|
var start_column := 0
|
||||||
|
while start_column < last_column:
|
||||||
|
var end_column := mini(start_column + TILE_CELLS, last_column)
|
||||||
|
tiles.append(Rect2i(start_column, start_row, end_column - start_column, end_row - start_row))
|
||||||
|
start_column = end_column
|
||||||
|
start_row = end_row
|
||||||
|
return tiles
|
||||||
|
|
||||||
|
|
||||||
|
func _place_tile(tile_index: int, grass: Array[TilePlacements], fans: Array[TilePlacements]) -> void:
|
||||||
|
var tile := grass[tile_index].cells
|
||||||
|
var cell := _field.cell_size_m()
|
||||||
|
var origin := _field.origin_m()
|
||||||
|
var x_min := origin.x + float(tile.position.x) * cell
|
||||||
|
var z_min := origin.y + float(tile.position.y) * cell
|
||||||
|
var x_max := origin.x + float(tile.end.x) * cell
|
||||||
|
var z_max := origin.y + float(tile.end.y) * cell
|
||||||
|
var generator := RandomNumberGenerator.new()
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
## Cheap rejections first, straight from the field arrays (cell reads, one noise read),
|
||||||
|
## random draws only for accepted candidates: the draw sequence stays deterministic
|
||||||
|
## because every decision is.
|
||||||
|
func _place_grass(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 sand := SonarMaterial.Value.SAND
|
||||||
|
var slope_limit := (1.0 / (GRASS_SLOPE_COSINE * GRASS_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() * GRASS_SPACING_M
|
||||||
|
var pz := z + generator.randf() * GRASS_SPACING_M
|
||||||
|
x += GRASS_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
|
||||||
|
var depth := -elevations[index]
|
||||||
|
if depth >= GRASS_DEPTH_NONE_M or materials[index] != sand:
|
||||||
|
continue
|
||||||
|
var meadow := _meadow_noise.get_noise_2d(px, pz)
|
||||||
|
if meadow < GRASS_MEADOW_THRESHOLD:
|
||||||
|
continue
|
||||||
|
# Slope by central differences: (dz_dx^2 + dz_dz^2) against tan^2 of the limit.
|
||||||
|
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 density := 1.0 - smoothstep(GRASS_DEPTH_FULL_M, GRASS_DEPTH_NONE_M, depth)
|
||||||
|
if generator.randf() > density * clampf((meadow - GRASS_MEADOW_THRESHOLD) * 4.0, 0.3, 1.0):
|
||||||
|
continue
|
||||||
|
var yaw := generator.randf() * TAU
|
||||||
|
var size := generator.randf_range(0.6, 1.1)
|
||||||
|
var phase := generator.randf()
|
||||||
|
var tint := generator.randf_range(0.85, 1.15)
|
||||||
|
var elevation := _field.elevation_at(px, pz)
|
||||||
|
var basis := Basis(Vector3.UP, yaw).scaled(Vector3(size, size * 0.9, size))
|
||||||
|
out.transforms.append(Transform3D(basis, Vector3(px, elevation - 0.03, pz)))
|
||||||
|
out.colors.append(Color(0.9 * tint, 1.0 * tint, 0.85 * tint))
|
||||||
|
out.custom.append(Color(phase, 1.0, 0.0, 0.0))
|
||||||
|
z += GRASS_SPACING_M
|
||||||
|
|
||||||
|
|
||||||
|
func _place_fans(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 heading := atan2(-FAN_HEADING.y, FAN_HEADING.x)
|
||||||
|
var z := z_min
|
||||||
|
while z < z_max:
|
||||||
|
var x := x_min
|
||||||
|
while x < x_max:
|
||||||
|
var px := x + generator.randf() * FAN_SPACING_M
|
||||||
|
var pz := z + generator.randf() * FAN_SPACING_M
|
||||||
|
x += FAN_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 < FAN_DEPTH_MIN_M or depth > FAN_DEPTH_MAX_M:
|
||||||
|
continue
|
||||||
|
if generator.randf() > FAN_KEEP_PROBABILITY or _fan_noise.get_noise_2d(px, pz) < -0.1:
|
||||||
|
continue
|
||||||
|
var yaw := heading + generator.randf_range(-0.45, 0.45)
|
||||||
|
var width := generator.randf_range(1.0, 2.4)
|
||||||
|
var height := generator.randf_range(0.9, 2.0)
|
||||||
|
var phase := generator.randf()
|
||||||
|
var color: Color = FAN_COLORS[generator.randi_range(0, FAN_COLORS.size() - 1)]
|
||||||
|
var elevation := _field.elevation_at(px, pz)
|
||||||
|
var normal := _field.normal_at_cell(column, row)
|
||||||
|
# Stand up, leaning a little with the rock face.
|
||||||
|
var up := (Vector3.UP * 0.6 + normal * 0.4).normalized()
|
||||||
|
var forward := Vector3(cos(yaw), 0.0, -sin(yaw))
|
||||||
|
var right := forward.cross(up).normalized()
|
||||||
|
forward = up.cross(right).normalized()
|
||||||
|
var basis := Basis(right * width, up * height, forward)
|
||||||
|
out.transforms.append(Transform3D(basis, Vector3(px, elevation - 0.05, pz)))
|
||||||
|
out.colors.append(color)
|
||||||
|
out.custom.append(Color(phase, 0.12, 0.0, 0.0))
|
||||||
|
z += FAN_SPACING_M
|
||||||
|
|
||||||
|
|
||||||
|
func _add_multimesh(kind: Kind, placements: TilePlacements) -> void:
|
||||||
|
var count := placements.transforms.size()
|
||||||
|
if count == 0:
|
||||||
|
return
|
||||||
|
var multimesh := MultiMesh.new()
|
||||||
|
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
|
||||||
|
multimesh.instance_count = count
|
||||||
|
for index in count:
|
||||||
|
multimesh.set_instance_transform(index, placements.transforms[index])
|
||||||
|
multimesh.set_instance_color(index, placements.colors[index])
|
||||||
|
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",
|
||||||
|
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.visibility_range_end_margin = 20.0
|
||||||
|
instance.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
|
||||||
|
instance.cast_shadow = (
|
||||||
|
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF if kind == Kind.SEAGRASS
|
||||||
|
else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||||
|
)
|
||||||
|
add_child(instance)
|
||||||
|
_counts[kind] = int(_counts[kind]) + count
|
||||||
|
var samples: Array = _samples[kind]
|
||||||
|
for index in mini(count, 4):
|
||||||
|
if samples.size() < 16:
|
||||||
|
samples.append(placements.transforms[index])
|
||||||
|
|
||||||
|
|
||||||
|
func _material(texture: ImageTexture) -> ShaderMaterial:
|
||||||
|
var material := ShaderMaterial.new()
|
||||||
|
material.shader = FLORA_SHADER
|
||||||
|
material.set_shader_parameter(&"albedo_map", texture)
|
||||||
|
return material
|
||||||
|
|
||||||
|
|
||||||
|
static func _noise(seed: int, frequency: float) -> FastNoiseLite:
|
||||||
|
var noise := FastNoiseLite.new()
|
||||||
|
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
noise.seed = seed
|
||||||
|
noise.frequency = frequency
|
||||||
|
noise.fractal_octaves = 3
|
||||||
|
return noise
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
class_name SeabedFloraAssets
|
||||||
|
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.
|
||||||
|
|
||||||
|
const BLADE_TEXTURE_WIDTH := 128
|
||||||
|
const BLADE_TEXTURE_HEIGHT := 256
|
||||||
|
const BLADE_COUNT := 5
|
||||||
|
const FAN_TEXTURE_SIZE := 256
|
||||||
|
|
||||||
|
var tuft_mesh: ArrayMesh
|
||||||
|
var fan_mesh: ArrayMesh
|
||||||
|
var blade_texture: ImageTexture
|
||||||
|
var fan_texture: ImageTexture
|
||||||
|
|
||||||
|
|
||||||
|
static func build(seed: int) -> SeabedFloraAssets:
|
||||||
|
var result := SeabedFloraAssets.new()
|
||||||
|
result.blade_texture = _texture_from(_blade_image(seed))
|
||||||
|
result.fan_texture = _texture_from(_fan_image(seed + 1))
|
||||||
|
result.tuft_mesh = _crossed_quads(3)
|
||||||
|
result.fan_mesh = _crossed_quads(1)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
var vertices := PackedVector3Array()
|
||||||
|
var normals := PackedVector3Array()
|
||||||
|
var uvs := PackedVector2Array()
|
||||||
|
var indices := PackedInt32Array()
|
||||||
|
for quad in count:
|
||||||
|
var angle := PI * float(quad) / float(count)
|
||||||
|
var right := Vector3(cos(angle), 0.0, sin(angle)) * 0.5
|
||||||
|
var normal := Vector3(-sin(angle), 0.0, cos(angle))
|
||||||
|
var base := vertices.size()
|
||||||
|
vertices.append_array([-right, right, right + Vector3.UP, -right + Vector3.UP])
|
||||||
|
for _corner in 4:
|
||||||
|
normals.append(normal)
|
||||||
|
uvs.append_array([Vector2(0.0, 1.0), Vector2(1.0, 1.0), Vector2(1.0, 0.0), Vector2(0.0, 0.0)])
|
||||||
|
indices.append_array([base, base + 2, base + 1, base, base + 3, base + 2])
|
||||||
|
var arrays: Array = []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||||
|
arrays[Mesh.ARRAY_INDEX] = indices
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
## A few strap-shaped blades rising from the bottom edge, each with its own curve,
|
||||||
|
## width and length, darker towards the edges and the base. Alpha outside the blades.
|
||||||
|
static func _blade_image(seed: int) -> Image:
|
||||||
|
var generator := RandomNumberGenerator.new()
|
||||||
|
generator.seed = seed
|
||||||
|
var image := Image.create(BLADE_TEXTURE_WIDTH, BLADE_TEXTURE_HEIGHT, false, Image.FORMAT_RGBA8)
|
||||||
|
image.fill(Color(0.0, 0.0, 0.0, 0.0))
|
||||||
|
for blade in BLADE_COUNT:
|
||||||
|
var root_x := (float(blade) + 0.5) / float(BLADE_COUNT) * float(BLADE_TEXTURE_WIDTH)
|
||||||
|
var bend := generator.randf_range(-22.0, 22.0)
|
||||||
|
var curve := generator.randf_range(0.6, 1.4)
|
||||||
|
var length := generator.randf_range(0.72, 1.0)
|
||||||
|
var half_width := generator.randf_range(3.5, 6.0)
|
||||||
|
var hue_shift := generator.randf_range(-0.05, 0.05)
|
||||||
|
for y in BLADE_TEXTURE_HEIGHT:
|
||||||
|
var t := 1.0 - float(y) / float(BLADE_TEXTURE_HEIGHT - 1) # 0 at the base, 1 at the tip
|
||||||
|
if t > length:
|
||||||
|
continue
|
||||||
|
var centre := root_x + bend * pow(t, curve)
|
||||||
|
var width := half_width * (1.0 - 0.6 * t * t)
|
||||||
|
for x in range(int(floorf(centre - width)), int(ceilf(centre + width)) + 1):
|
||||||
|
if x < 0 or x >= BLADE_TEXTURE_WIDTH:
|
||||||
|
continue
|
||||||
|
var edge := absf(float(x) - centre) / width
|
||||||
|
if edge > 1.0:
|
||||||
|
continue
|
||||||
|
var shade := (0.7 + 0.3 * (1.0 - edge * edge)) * (0.75 + 0.25 * t)
|
||||||
|
var color := Color(0.46 + hue_shift, 0.66 + hue_shift * 0.5, 0.3, 1.0) * shade
|
||||||
|
color.a = 1.0
|
||||||
|
image.set_pixel(x, y, color)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
## A sea fan: branching net inside a half-ellipse rising from a short stem. Ridged
|
||||||
|
## noise above a threshold gives the branches, denser near the stem.
|
||||||
|
static func _fan_image(seed: int) -> Image:
|
||||||
|
var noise := FastNoiseLite.new()
|
||||||
|
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
noise.seed = seed
|
||||||
|
noise.frequency = 0.045
|
||||||
|
noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED
|
||||||
|
noise.fractal_octaves = 3
|
||||||
|
var image := Image.create(FAN_TEXTURE_SIZE, FAN_TEXTURE_SIZE, false, Image.FORMAT_RGBA8)
|
||||||
|
image.fill(Color(0.0, 0.0, 0.0, 0.0))
|
||||||
|
var centre_x := float(FAN_TEXTURE_SIZE) * 0.5
|
||||||
|
for y in FAN_TEXTURE_SIZE:
|
||||||
|
var height := 1.0 - float(y) / float(FAN_TEXTURE_SIZE - 1) # 0 at the base
|
||||||
|
for x in FAN_TEXTURE_SIZE:
|
||||||
|
var dx := (float(x) - centre_x) / (float(FAN_TEXTURE_SIZE) * 0.48)
|
||||||
|
var dy := (height - 0.1) / 0.9
|
||||||
|
var inside := dx * dx + dy * dy
|
||||||
|
var stem := absf(dx) < 0.03 and height < 0.25
|
||||||
|
if inside > 1.0 and not stem:
|
||||||
|
continue
|
||||||
|
var ridge := noise.get_noise_2d(float(x), float(y)) * 0.5 + 0.5
|
||||||
|
var density_threshold := 0.5 + 0.22 * inside
|
||||||
|
if not stem and ridge < density_threshold:
|
||||||
|
continue
|
||||||
|
var shade := 0.7 + 0.3 * ridge
|
||||||
|
image.set_pixel(x, y, Color(shade, shade * 0.92, shade * 0.85, 1.0))
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
static func _texture_from(image: Image) -> ImageTexture:
|
||||||
|
image.generate_mipmaps()
|
||||||
|
return ImageTexture.create_from_image(image)
|
||||||
@@ -15,8 +15,20 @@ render_mode depth_draw_opaque;
|
|||||||
// projectors are not attenuated, so they stay useful on the drop-off. Everything is
|
// projectors are not attenuated, so they stay useful on the drop-off. Everything is
|
||||||
// normalised at reference_depth_m, where the look is the authored palette.
|
// normalised at reference_depth_m, where the look is the authored palette.
|
||||||
|
|
||||||
|
#include "res://game/world/sites/water_light.gdshaderinc"
|
||||||
|
|
||||||
uniform vec3 sand_color : source_color = vec3(0.56, 0.52, 0.42);
|
uniform vec3 sand_color : source_color = vec3(0.56, 0.52, 0.42);
|
||||||
uniform vec3 rock_color : source_color = vec3(0.22, 0.21, 0.19);
|
uniform vec3 rock_color : source_color = vec3(0.22, 0.21, 0.19);
|
||||||
|
// Encrusting growth on rock: turf algae on sunlit faces, coralline crusts in patches,
|
||||||
|
// encrusting sponges in the shade. Patches come from the grain map read at large
|
||||||
|
// scales; all of it thins out with depth as the light goes.
|
||||||
|
uniform vec3 turf_color : source_color = vec3(0.3, 0.38, 0.2);
|
||||||
|
uniform vec3 coralline_color : source_color = vec3(0.6, 0.4, 0.46);
|
||||||
|
uniform vec3 sponge_color : source_color = vec3(0.82, 0.4, 0.18);
|
||||||
|
uniform float growth_tile_m = 4.2;
|
||||||
|
uniform float growth_strength = 1.0;
|
||||||
|
uniform float growth_fade_start_m = 40.0;
|
||||||
|
uniform float growth_fade_end_m = 75.0;
|
||||||
uniform float sand_slope_cosine = 0.9397; // cos(20 deg)
|
uniform float sand_slope_cosine = 0.9397; // cos(20 deg)
|
||||||
uniform float rock_slope_cosine = 0.7660; // cos(40 deg)
|
uniform float rock_slope_cosine = 0.7660; // cos(40 deg)
|
||||||
// Per-channel absorption of clear tropical water, per metre, scaled for readability.
|
// Per-channel absorption of clear tropical water, per metre, scaled for readability.
|
||||||
@@ -123,19 +135,29 @@ void fragment() {
|
|||||||
// Albedo: mottled rock, lightly grained sand.
|
// Albedo: mottled rock, lightly grained sand.
|
||||||
vec3 rock_albedo = rock_color * (1.0 + (grain - 0.5) * 2.0 * grain_albedo_contrast);
|
vec3 rock_albedo = rock_color * (1.0 + (grain - 0.5) * 2.0 * grain_albedo_contrast);
|
||||||
vec3 sand_albedo = sand_color * (1.0 + (grain - 0.5) * 0.3);
|
vec3 sand_albedo = sand_color * (1.0 + (grain - 0.5) * 0.3);
|
||||||
|
float depth_m = -world_position.y;
|
||||||
|
|
||||||
|
// Encrusting growth on the rock.
|
||||||
|
float growth_light = (1.0 - smoothstep(growth_fade_start_m, growth_fade_end_m, depth_m)) * growth_strength;
|
||||||
|
float sunlit = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||||
|
float patch_a = triplanar_grain(rock_grain_map, world_position / growth_tile_m + vec3(0.21, 0.73, 0.37), n);
|
||||||
|
float patch_b = triplanar_grain(rock_grain_map, world_position / (growth_tile_m * 0.53) + vec3(0.61, 0.11, 0.9), n);
|
||||||
|
float turf_mask = smoothstep(0.4, 0.58, patch_a) * sunlit * growth_light;
|
||||||
|
float coralline_mask = smoothstep(0.6, 0.74, patch_b) * growth_light;
|
||||||
|
float sponge_mask = smoothstep(0.62, 0.74, patch_a * 0.5 + patch_b * 0.5) * (1.0 - 0.6 * sunlit) * growth_light;
|
||||||
|
float grain_mottle = 1.0 + (grain - 0.5) * 0.6;
|
||||||
|
rock_albedo = mix(rock_albedo, turf_color * grain_mottle, turf_mask);
|
||||||
|
rock_albedo = mix(rock_albedo, coralline_color * grain_mottle, coralline_mask);
|
||||||
|
rock_albedo = mix(rock_albedo, sponge_color * grain_mottle, sponge_mask);
|
||||||
|
float rock_roughness = 0.78 + 0.12 * grain;
|
||||||
|
rock_roughness = mix(rock_roughness, 0.6, coralline_mask);
|
||||||
|
rock_roughness = mix(rock_roughness, 0.9, sponge_mask);
|
||||||
vec3 albedo = mix(sand_albedo, rock_albedo, rockness);
|
vec3 albedo = mix(sand_albedo, rock_albedo, rockness);
|
||||||
|
|
||||||
// Water column: spectral transmittance relative to the reference depth.
|
// Water column: spectral transmittance relative to the reference depth.
|
||||||
float depth_m = -world_position.y;
|
vec3 transmittance = water_transmittance(depth_m, water_absorption_per_m, reference_depth_m);
|
||||||
vec3 transmittance = exp(-water_absorption_per_m * (depth_m - reference_depth_m));
|
daylight = water_daylight(transmittance, minimum_daylight);
|
||||||
float brightness = dot(transmittance, vec3(0.2126, 0.7152, 0.0722));
|
ALBEDO = albedo * water_chroma(transmittance, water_absorption_per_m, reference_depth_m);
|
||||||
vec3 chroma = transmittance / max(brightness, 0.001);
|
|
||||||
daylight = max(brightness, minimum_daylight);
|
|
||||||
// Normalise so the authored palette holds at the reference depth once the
|
|
||||||
// reference transmittance (red already filtered by 24 m of water) is applied.
|
|
||||||
vec3 reference_chroma = exp(-water_absorption_per_m * reference_depth_m);
|
|
||||||
reference_chroma /= dot(reference_chroma, vec3(0.2126, 0.7152, 0.0722));
|
|
||||||
ALBEDO = albedo * chroma * reference_chroma;
|
|
||||||
|
|
||||||
// Detail shading: pits of the grain map and ripple troughs read as occlusion;
|
// Detail shading: pits of the grain map and ripple troughs read as occlusion;
|
||||||
// the daylight factor dims the sky ambient with depth.
|
// the daylight factor dims the sky ambient with depth.
|
||||||
@@ -143,14 +165,14 @@ void fragment() {
|
|||||||
float sand_ao = mix(1.0, 0.8 + 0.2 * clamp(sand_tangent.z, 0.0, 1.0), grain_ao_strength * 0.6);
|
float sand_ao = mix(1.0, 0.8 + 0.2 * clamp(sand_tangent.z, 0.0, 1.0), grain_ao_strength * 0.6);
|
||||||
AO = mix(sand_ao, rock_ao, rockness) * daylight;
|
AO = mix(sand_ao, rock_ao, rockness) * daylight;
|
||||||
AO_LIGHT_AFFECT = 0.35;
|
AO_LIGHT_AFFECT = 0.35;
|
||||||
ROUGHNESS = mix(0.95, 0.78 + 0.12 * grain, rockness);
|
ROUGHNESS = mix(0.95, rock_roughness, rockness);
|
||||||
METALLIC = 0.0;
|
METALLIC = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lambert diffuse; the sun (directional) is dimmed by the water column, other lights
|
// Lambert diffuse; the sun (directional) is dimmed by the water column, other lights
|
||||||
// (submarine projectors) are not. Sand and rock are rough enough to skip specular.
|
// (submarine projectors) are not. Sand and rock are rough enough to skip specular.
|
||||||
void light() {
|
void light() {
|
||||||
float sun_factor = LIGHT_IS_DIRECTIONAL ? daylight : 1.0;
|
float sun_factor = water_sun_factor(LIGHT_IS_DIRECTIONAL, daylight);
|
||||||
float lambert = clamp(dot(NORMAL, LIGHT), 0.0, 1.0);
|
float lambert = clamp(dot(NORMAL, LIGHT), 0.0, 1.0);
|
||||||
DIFFUSE_LIGHT += lambert * ATTENUATION * LIGHT_COLOR * ALBEDO * sun_factor / PI;
|
DIFFUSE_LIGHT += lambert * ATTENUATION * LIGHT_COLOR * ALBEDO * sun_factor / PI;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Shared water-column optics for the seabed and flora shaders: per-channel
|
||||||
|
// Beer-Lambert transmittance relative to a reference depth, split into a scalar
|
||||||
|
// daylight factor (dims the sun and the sky ambient) and a chroma (tints albedo).
|
||||||
|
// Artificial lights are never attenuated: see water_sun_factor.
|
||||||
|
|
||||||
|
const vec3 WATER_LUMINANCE_WEIGHTS = vec3(0.2126, 0.7152, 0.0722);
|
||||||
|
|
||||||
|
vec3 water_transmittance(float depth_m, vec3 absorption_per_m, float reference_depth_m) {
|
||||||
|
return exp(-absorption_per_m * (depth_m - reference_depth_m));
|
||||||
|
}
|
||||||
|
|
||||||
|
float water_daylight(vec3 transmittance, float minimum_daylight) {
|
||||||
|
return max(dot(transmittance, WATER_LUMINANCE_WEIGHTS), minimum_daylight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chroma of the transmittance, normalised so the authored palette holds at the
|
||||||
|
// reference depth once the reference transmittance (red already filtered) is applied.
|
||||||
|
vec3 water_chroma(vec3 transmittance, vec3 absorption_per_m, float reference_depth_m) {
|
||||||
|
float brightness = max(dot(transmittance, WATER_LUMINANCE_WEIGHTS), 0.001);
|
||||||
|
vec3 reference = exp(-absorption_per_m * reference_depth_m);
|
||||||
|
reference /= dot(reference, WATER_LUMINANCE_WEIGHTS);
|
||||||
|
return transmittance / brightness * reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
float water_sun_factor(bool light_is_directional, float daylight) {
|
||||||
|
return light_is_directional ? daylight : 1.0;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition
|
|||||||
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
||||||
const DiveSite = preload("res://game/world/sites/dive_site.gd")
|
const DiveSite = preload("res://game/world/sites/dive_site.gd")
|
||||||
const SeabedDetailTextures = preload("res://game/world/sites/seabed_detail_textures.gd")
|
const SeabedDetailTextures = preload("res://game/world/sites/seabed_detail_textures.gd")
|
||||||
|
const SeabedFloraActor = preload("res://game/world/sites/seabed_flora_actor.gd")
|
||||||
const SonarCaptureRequest = preload("res://game/application/sonar/sonar_capture_request.gd")
|
const SonarCaptureRequest = preload("res://game/application/sonar/sonar_capture_request.gd")
|
||||||
const ReferenceBathymetryAcquisition = preload(
|
const ReferenceBathymetryAcquisition = preload(
|
||||||
"res://game/world/sonar/reference_bathymetry_acquisition.gd"
|
"res://game/world/sonar/reference_bathymetry_acquisition.gd"
|
||||||
@@ -19,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 := 44
|
const TEST_COUNT := 49
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -188,6 +189,27 @@ func _test_dive_site_scene() -> void:
|
|||||||
await physics_frame
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
_expect(site.terrain().tile_count() > 0, "site builds its terrain tiles")
|
_expect(site.terrain().tile_count() > 0, "site builds its terrain tiles")
|
||||||
|
var flora := site.flora()
|
||||||
|
var grass_count := flora.instance_count(SeabedFloraActor.Kind.SEAGRASS)
|
||||||
|
var fan_count := flora.instance_count(SeabedFloraActor.Kind.SEA_FAN)
|
||||||
|
_expect(grass_count > 0 and fan_count > 0, "calibration site grows seagrass and sea fans (%d, %d)" % [grass_count, fan_count])
|
||||||
|
var field_for_flora := definition.height_field()
|
||||||
|
var grass_on_sand := true
|
||||||
|
var grass_in_band := true
|
||||||
|
for transform in flora.sample_transforms(SeabedFloraActor.Kind.SEAGRASS):
|
||||||
|
var at := transform.origin
|
||||||
|
grass_on_sand = grass_on_sand and field_for_flora.material_at(at.x, at.z) == SonarMaterial.Value.SAND
|
||||||
|
grass_in_band = grass_in_band and -at.y < SeabedFloraActor.GRASS_DEPTH_NONE_M and absf(at.y - field_for_flora.elevation_at(at.x, at.z)) < 0.1
|
||||||
|
_expect(grass_on_sand, "seagrass stands on sand")
|
||||||
|
_expect(grass_in_band, "seagrass stays in the lit band and on the seabed")
|
||||||
|
var fans_on_rock := true
|
||||||
|
for transform in flora.sample_transforms(SeabedFloraActor.Kind.SEA_FAN):
|
||||||
|
var at := transform.origin
|
||||||
|
fans_on_rock = fans_on_rock and field_for_flora.material_at(at.x, at.z) == SonarMaterial.Value.ROCK
|
||||||
|
_expect(fans_on_rock, "sea fans stand on rock")
|
||||||
|
var first_pass := flora.sample_transforms(SeabedFloraActor.Kind.SEAGRASS)
|
||||||
|
flora.configure(field_for_flora, hash(String(definition.site_id)))
|
||||||
|
_expect(first_pass == flora.sample_transforms(SeabedFloraActor.Kind.SEAGRASS), "flora placement is deterministic for a seed")
|
||||||
var water_column := site.get_node_or_null("WaterColumn") as FogVolume
|
var water_column := site.get_node_or_null("WaterColumn") as FogVolume
|
||||||
_expect(
|
_expect(
|
||||||
water_column != null and water_column.material is ShaderMaterial
|
water_column != null and water_column.material is ShaderMaterial
|
||||||
|
|||||||