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
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user