feat(world): world-space marine snow around the viewer
Quality Gate / Quality Gate (pull_request) Failing after 3m10s
Quality Gate / Quality Gate (pull_request) Failing after 3m10s
A GPUParticles3D emitter in the dive site re-centres on the active camera every frame and restarts after a jump, so flakes keep their own slow sink and drift while the submarine moves through them. Soft additive billboard discs dimmed by the water column and faded over their life replace the sphere volume that was attached to the submarine and turned with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
48e054557c
commit
029e2b99bd
7 files changed
+127
-26
No files matched your search
@@ -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/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_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="DiveSite" parent="." instance=ExtResource("1_site")]
|
||||
@@ -28,14 +13,6 @@ unique_name_in_owner = true
|
||||
[node name="SubmarineActor" parent="." instance=ExtResource("2_submarine")]
|
||||
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")]
|
||||
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/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="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"]
|
||||
shader = ExtResource("3_water_column")
|
||||
@@ -101,5 +102,8 @@ script = ExtResource("2_terrain")
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("4_flora")
|
||||
|
||||
[node name="MarineSnow" type="GPUParticles3D" parent="."]
|
||||
script = ExtResource("5_snow")
|
||||
|
||||
[node name="Placements" type="Node3D" parent="."]
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user