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

Open
olix21 wants to merge 47 commits from olix21/worktree-terrain-site-zone1 AGit into main
pull from: olix21/worktree-terrain-site-zone1
7 changed files with 127 additions and 26 deletions
Showing only changes of commit 029e2b99bd - Show all commits

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 406 KiB

@@ -270,6 +270,12 @@ de contacts existant les sépare du fond.
semés sur la roche douce (< 30°) de 12 à 40 m en patch reefs par bruit, une 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` `MultiMesh` par tuile et par variante, shader opaque `seabed_coral.gdshader`
(couleur de sommet × teinte d'instance, marbrure, même colonne d'eau). (couleur de sommet × teinte d'instance, marbrure, même colonne d'eau).
- Particules en suspension (même jour) : `MarineSnow`, émetteur
`GPUParticles3D` en espace monde recentré sur la caméra active, boîte de
44 × 28 × 44 m, 1 800 flocons de 3 à 9 cm qui coulent à 3,5 cm/s avec
turbulence, disques doux additifs en billboard, atténués par la colonne
d'eau et fondus en début et fin de vie. Remplace le volume de sphères
attaché au sous-marin (qui tournait avec lui).
- 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
@@ -1,25 +1,10 @@
[gd_scene load_steps=7 format=3] [gd_scene load_steps=5 format=3]
[ext_resource type="PackedScene" path="res://game/world/sites/dive_site.tscn" id="1_site"] [ext_resource type="PackedScene" path="res://game/world/sites/dive_site.tscn" id="1_site"]
[ext_resource type="PackedScene" path="res://game/world/submarine/submarine_actor.tscn" id="2_submarine"] [ext_resource type="PackedScene" path="res://game/world/submarine/submarine_actor.tscn" id="2_submarine"]
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_camera_rig.tscn" id="3_camera"] [ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_camera_rig.tscn" id="3_camera"]
[ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_interior.tscn" id="4_cockpit"] [ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_interior.tscn" id="4_cockpit"]
[sub_resource type="ParticleProcessMaterial" id="SuspendedMotion"]
emission_shape = 3
emission_box_extents = Vector3(45, 28, 70)
direction = Vector3(0.1, 0.04, 0)
spread = 180.0
initial_velocity_min = 0.02
initial_velocity_max = 0.09
gravity = Vector3(0, 0, 0)
scale_min = 0.025
scale_max = 0.08
[sub_resource type="SphereMesh" id="SuspendedParticleMesh"]
radius = 0.035
height = 0.07
[node name="DiveWorld" type="Node3D"] [node name="DiveWorld" type="Node3D"]
[node name="DiveSite" parent="." instance=ExtResource("1_site")] [node name="DiveSite" parent="." instance=ExtResource("1_site")]
@@ -28,14 +13,6 @@ unique_name_in_owner = true
[node name="SubmarineActor" parent="." instance=ExtResource("2_submarine")] [node name="SubmarineActor" parent="." instance=ExtResource("2_submarine")]
unique_name_in_owner = true unique_name_in_owner = true
[node name="SuspendedParticles" type="GPUParticles3D" parent="SubmarineActor"]
amount = 360
lifetime = 14.0
preprocess = 14.0
visibility_aabb = AABB(-50, -35, -78, 100, 70, 156)
process_material = SubResource("SuspendedMotion")
draw_pass_1 = SubResource("SuspendedParticleMesh")
[node name="CockpitInterior" parent="SubmarineActor" instance=ExtResource("4_cockpit")] [node name="CockpitInterior" parent="SubmarineActor" instance=ExtResource("4_cockpit")]
unique_name_in_owner = true unique_name_in_owner = true
+5 -1
View File
@@ -1,9 +1,10 @@
[gd_scene load_steps=10 format=3] [gd_scene load_steps=11 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"] [ext_resource type="Script" path="res://game/world/sites/seabed_flora_actor.gd" id="4_flora"]
[ext_resource type="Script" path="res://game/world/sites/marine_snow.gd" id="5_snow"]
[sub_resource type="ShaderMaterial" id="WaterColumnMaterial"] [sub_resource type="ShaderMaterial" id="WaterColumnMaterial"]
shader = ExtResource("3_water_column") shader = ExtResource("3_water_column")
@@ -101,5 +102,8 @@ script = ExtResource("2_terrain")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("4_flora") script = ExtResource("4_flora")
[node name="MarineSnow" type="GPUParticles3D" parent="."]
script = ExtResource("5_snow")
[node name="Placements" type="Node3D" parent="."] [node name="Placements" type="Node3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
+76
View File
@@ -0,0 +1,76 @@
class_name MarineSnow
extends GPUParticles3D
## Suspended particles (marine snow, sediment) around the viewer. The emitter is a
## world-space box re-centred on the active camera every frame, so flakes keep their
## own slow drift while the submarine moves through them instead of travelling with
## it. Sinks a few centimetres per second with a little turbulence, as detritus does.
const BOX_EXTENTS_M := Vector3(16.0, 10.0, 16.0)
const PARTICLE_COUNT := 2400
const LIFETIME_S := 16.0
const SINK_SPEED_M_S := 0.035
const DRIFT_SPEED_M_S := 0.06
const FLAKE_SIZE_M := Vector2(0.05, 0.16)
const SNOW_SHADER = preload("res://game/world/sites/marine_snow.gdshader")
func _ready() -> void:
local_coords = false
amount = PARTICLE_COUNT
lifetime = LIFETIME_S
preprocess = LIFETIME_S
fixed_fps = 30
visibility_aabb = AABB(-BOX_EXTENTS_M * 1.5, BOX_EXTENTS_M * 3.0)
process_material = _process_material()
var quad := QuadMesh.new()
quad.size = Vector2.ONE
var material := ShaderMaterial.new()
material.shader = SNOW_SHADER
quad.material = material
draw_pass_1 = quad
emitting = true
## A jump larger than the box (spawn, teleport, first camera) would leave the viewer in
## empty water for a whole lifetime: the emitter restarts and preprocesses in place.
func _process(_delta: float) -> void:
var camera := get_viewport().get_camera_3d() if is_inside_tree() else null
if camera == null:
return
var jumped := global_position.distance_to(camera.global_position) > BOX_EXTENTS_M.y
global_position = camera.global_position
if jumped:
restart()
func _process_material() -> ParticleProcessMaterial:
var material := ParticleProcessMaterial.new()
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
material.emission_box_extents = BOX_EXTENTS_M
material.direction = Vector3(1.0, 0.0, 0.3)
material.spread = 180.0
material.initial_velocity_min = DRIFT_SPEED_M_S * 0.3
material.initial_velocity_max = DRIFT_SPEED_M_S
material.gravity = Vector3(0.0, -SINK_SPEED_M_S, 0.0)
material.turbulence_enabled = true
material.turbulence_noise_strength = 0.4
material.turbulence_noise_scale = 6.0
material.turbulence_noise_speed_random = 0.2
material.turbulence_influence_min = 0.02
material.turbulence_influence_max = 0.06
material.scale_min = FLAKE_SIZE_M.x
material.scale_max = FLAKE_SIZE_M.y
# Fade in and out through alpha so flakes never pop at the box edge or at respawn.
var alpha := Gradient.new()
alpha.set_offset(0, 0.0)
alpha.set_color(0, Color(1.0, 1.0, 1.0, 0.0))
alpha.add_point(0.15, Color(1.0, 1.0, 1.0, 1.0))
alpha.add_point(0.85, Color(1.0, 1.0, 1.0, 1.0))
alpha.set_offset(alpha.get_point_count() - 1, 1.0)
alpha.set_color(alpha.get_point_count() - 1, Color(1.0, 1.0, 1.0, 0.0))
var ramp := GradientTexture1D.new()
ramp.gradient = alpha
material.color_ramp = ramp
return material
+36
View File
@@ -0,0 +1,36 @@
shader_type spatial;
render_mode blend_add, depth_draw_never, cull_disabled, unshaded;
// Marine snow: camera-facing soft discs drawn by GPUParticles3D. Each particle is a
// unit quad; the flake is a radial falloff in UV. Brightness follows the daylight of
// the water column (so flakes dim with depth) and fades in and out over the life of
// the particle through COLOR.a set by the process material.
#include "res://game/world/sites/water_light.gdshaderinc"
uniform vec3 flake_color : source_color = vec3(0.78, 0.86, 0.9);
uniform float flake_strength = 1.4;
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.08;
varying float brightness;
void vertex() {
// Billboard: keep the particle's world position, face the camera.
vec3 world_origin = (MODEL_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
float size = length(MODEL_MATRIX[0].xyz);
mat3 view_basis = mat3(INV_VIEW_MATRIX);
vec3 world = world_origin + (view_basis[0] * VERTEX.x + view_basis[1] * VERTEX.y) * size;
VERTEX = (VIEW_MATRIX * vec4(world, 1.0)).xyz;
NORMAL = (VIEW_MATRIX * vec4(view_basis[2], 0.0)).xyz;
vec3 transmittance = water_transmittance(-world_origin.y, water_absorption_per_m, reference_depth_m);
brightness = water_daylight(transmittance, minimum_daylight) * COLOR.a;
}
void fragment() {
float radial = length(UV - vec2(0.5)) * 2.0;
float disc = 1.0 - smoothstep(0.35, 1.0, radial);
ALBEDO = flake_color * disc * brightness * flake_strength;
ALPHA = disc;
}
@@ -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 := 51 const TEST_COUNT := 52
var _failures: Array[String] = [] var _failures: Array[String] = []
@@ -210,6 +210,8 @@ func _test_dive_site_scene() -> void:
var first_pass := flora.sample_transforms(SeabedFloraActor.Kind.SEAGRASS) var first_pass := flora.sample_transforms(SeabedFloraActor.Kind.SEAGRASS)
flora.configure(field_for_flora, hash(String(definition.site_id))) 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") _expect(first_pass == flora.sample_transforms(SeabedFloraActor.Kind.SEAGRASS), "flora placement is deterministic for a seed")
var snow := site.get_node_or_null("MarineSnow") as GPUParticles3D
_expect(snow != null and snow.emitting and snow.amount > 0 and not snow.local_coords, "site carries world-space marine snow around the viewer")
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