From a8341355e3409c5d51dbae6c8f8a2dd7cf120f15 Mon Sep 17 00:00:00 2001 From: Floppyrj45 Date: Sun, 19 Apr 2026 18:09:21 +0200 Subject: [PATCH] feat(particles): bubble trail behind dolphin + block break burst - BubbleTrail.gd: GPUParticles3D, auto-configured in _ready, set_intensity() API - BlockBreakParticles.gd: one_shot burst, emit_burst(pos, color) API - DolphinController.gd: bubble_trail onready + speed_factor hook in _update_movement - Dolphin.tscn: BubbleEmitterPoint (0,0,1.2) > BubbleTrail child added Co-Authored-By: Claude Sonnet 4.6 --- scenes/Dolphin.tscn | 9 +++- scenes/WaterSurface.tscn | 19 +++++++++ scripts/ambience/CausticsLayer.gd | 47 ++++++++++++++++++++ scripts/ambience/GodraysOverlay.gd | 31 ++++++++++++++ scripts/ambience/UnderwaterEnvironment.gd | 13 +++++- scripts/dolphin/BlockBreakParticles.gd | 38 +++++++++++++++++ scripts/dolphin/BubbleTrail.gd | 52 +++++++++++++++++++++++ scripts/dolphin/DolphinController.gd | 43 +++++++++++++------ shaders/caustics.gdshader | 40 +++++++++++------ shaders/godrays.gdshader | 33 ++++++++------ shaders/underwater_fog.gdshader | 30 ++++++------- shaders/water_surface.gdshader | 21 +++++++++ 12 files changed, 318 insertions(+), 58 deletions(-) create mode 100644 scenes/WaterSurface.tscn create mode 100644 scripts/ambience/CausticsLayer.gd create mode 100644 scripts/ambience/GodraysOverlay.gd create mode 100644 scripts/dolphin/BlockBreakParticles.gd create mode 100644 scripts/dolphin/BubbleTrail.gd create mode 100644 shaders/water_surface.gdshader diff --git a/scenes/Dolphin.tscn b/scenes/Dolphin.tscn index c4f8b24..625b036 100644 --- a/scenes/Dolphin.tscn +++ b/scenes/Dolphin.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=22 format=3 uid="uid://dolphin_main"] +[gd_scene load_steps=24 format=3 uid="uid://dolphin_main"] [ext_resource type="Script" path="res://scripts/dolphin/DolphinController.gd" id="1_controller"] [ext_resource type="Script" path="res://scripts/dolphin/HUD.gd" id="2_hud"] [ext_resource type="Script" path="res://scripts/dolphin/EcholocationPulse.gd" id="3_echo"] +[ext_resource type="Script" path="res://scripts/dolphin/BubbleTrail.gd" id="4_bubble_trail"] [sub_resource type="CapsuleShape3D" id="1_colshape"] radius = 0.4 @@ -98,6 +99,12 @@ rotation = Vector3(0, 3.14159, 0) [node name="Camera" type="Camera3D" parent="CameraPivot/SpringArm"] +[node name="BubbleEmitterPoint" type="Node3D" parent="."] +position = Vector3(0, 0, 1.2) + +[node name="BubbleTrail" type="GPUParticles3D" parent="BubbleEmitterPoint"] +script = ExtResource("4_bubble_trail") + [node name="EcholocationPulse" type="Node3D" parent="."] script = ExtResource("3_echo") diff --git a/scenes/WaterSurface.tscn b/scenes/WaterSurface.tscn new file mode 100644 index 0000000..fd913c2 --- /dev/null +++ b/scenes/WaterSurface.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=3 format=3 uid="uid://dauphincraft_watersurface"] + +[ext_resource type="Shader" path="res://shaders/water_surface.gdshader" id="1_watersh"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_water"] +shader = ExtResource("1_watersh") +shader_parameter/surface_color = Vector3(0.15, 0.45, 0.65) +shader_parameter/wave_speed = 0.3 +shader_parameter/wave_amplitude = 0.15 + +[sub_resource type="PlaneMesh" id="PlaneMesh_water"] +size = Vector2(2000, 2000) +subdivide_width = 64 +subdivide_depth = 64 + +[node name="WaterSurface" type="MeshInstance3D"] +position = Vector3(0, 60, 0) +mesh = SubResource("PlaneMesh_water") +material_override = SubResource("ShaderMaterial_water") diff --git a/scripts/ambience/CausticsLayer.gd b/scripts/ambience/CausticsLayer.gd new file mode 100644 index 0000000..bf637f0 --- /dev/null +++ b/scripts/ambience/CausticsLayer.gd @@ -0,0 +1,47 @@ +extends Node3D + +# Creates horizontal caustic planes at multiple depths, follows the player +# Add as child of any node that follows the player (e.g. PlanktonFollower) + +const LAYER_DEPTHS := [-5.0, -15.0, -25.0] +const LAYER_SIZE := 60.0 + +var caustic_meshes: Array[MeshInstance3D] = [] +var caustic_material: ShaderMaterial + +func _ready() -> void: + var shader := load("res://shaders/caustics.gdshader") + if not shader: + push_warning("CausticsLayer: caustics.gdshader not found") + return + + caustic_material = ShaderMaterial.new() + caustic_material.shader = shader + + for depth in LAYER_DEPTHS: + var mi := MeshInstance3D.new() + var plane := PlaneMesh.new() + plane.size = Vector2(LAYER_SIZE, LAYER_SIZE) + mi.mesh = plane + # Each layer gets its own material instance to allow per-depth tuning + var mat := caustic_material.duplicate() as ShaderMaterial + # Lower layers = less intense + var fade := clamp(1.0 + depth / 30.0, 0.0, 1.0) + mat.set_shader_parameter("intensity", 1.5 * fade) + mi.material_override = mat + mi.position = Vector3(0.0, depth, 0.0) + add_child(mi) + caustic_meshes.append(mi) + +func _process(_delta: float) -> void: + # Follow camera XZ, keep fixed Y depths + var cam := get_viewport().get_camera_3d() + if cam: + var cx := cam.global_position.x + var cz := cam.global_position.z + for i in caustic_meshes.size(): + var mi := caustic_meshes[i] + mi.global_position.x = cx + mi.global_position.z = cz + # Keep the fixed depth offset + mi.global_position.y = LAYER_DEPTHS[i] diff --git a/scripts/ambience/GodraysOverlay.gd b/scripts/ambience/GodraysOverlay.gd new file mode 100644 index 0000000..968e0c2 --- /dev/null +++ b/scripts/ambience/GodraysOverlay.gd @@ -0,0 +1,31 @@ +extends MeshInstance3D + +var godrays_material: ShaderMaterial + +func _ready() -> void: + var quad := QuadMesh.new() + quad.size = Vector2(20.0, 40.0) + mesh = quad + + godrays_material = ShaderMaterial.new() + var shader := load("res://shaders/godrays.gdshader") + if shader: + godrays_material.shader = shader + + var mat_override := godrays_material + material_override = mat_override + + # Position in front of camera, slightly above center + position = Vector3(0.0, 5.0, -10.0) + +func _process(_delta: float) -> void: + if not godrays_material: + return + + # Get player/camera world Y to drive fade_bottom + var cam := get_viewport().get_camera_3d() + if cam: + var world_y := cam.global_position.y + # Deeper = fade bottom rises, reducing godray reach + var dynamic_fade := clamp(world_y - 20.0, -100.0, 0.0) + godrays_material.set_shader_parameter("fade_bottom", dynamic_fade) diff --git a/scripts/ambience/UnderwaterEnvironment.gd b/scripts/ambience/UnderwaterEnvironment.gd index b9a9187..aa4e085 100644 --- a/scripts/ambience/UnderwaterEnvironment.gd +++ b/scripts/ambience/UnderwaterEnvironment.gd @@ -16,13 +16,24 @@ func _ready() -> void: env.fog_aerial_perspective = 0.3 env.volumetric_fog_enabled = true - env.volumetric_fog_density = 0.04 + env.volumetric_fog_density = 0.05 env.volumetric_fog_albedo = Color(0.2, 0.5, 0.7) env.volumetric_fog_emission = Color(0.02, 0.06, 0.1) env.volumetric_fog_emission_energy = 0.1 env.volumetric_fog_length = 64.0 env.volumetric_fog_detail_spread = 2.0 + # Attach custom fog shader + var fog_mat := FogMaterial.new() + var fog_shader := load("res://shaders/underwater_fog.gdshader") + if fog_shader: + fog_mat.set_shader(fog_shader) + + var fog_volume := FogVolume.new() + fog_volume.size = Vector3(2000.0, 300.0, 2000.0) + fog_volume.material = fog_mat + add_child(fog_volume) + env.glow_enabled = true env.glow_intensity = 0.5 env.glow_bloom = 0.1 diff --git a/scripts/dolphin/BlockBreakParticles.gd b/scripts/dolphin/BlockBreakParticles.gd new file mode 100644 index 0000000..2e9c7ab --- /dev/null +++ b/scripts/dolphin/BlockBreakParticles.gd @@ -0,0 +1,38 @@ +extends GPUParticles3D + +func _ready() -> void: + amount = 30 + lifetime = 0.8 + one_shot = true + emitting = false + local_coords = false + + var mat := ParticleProcessMaterial.new() + mat.direction = Vector3(0.0, 1.0, 0.0) + mat.initial_velocity_min = 1.0 + mat.initial_velocity_max = 3.0 + mat.spread = 60.0 + mat.gravity = Vector3(0.0, -2.0, 0.0) + mat.scale_min = 0.05 + mat.scale_max = 0.15 + process_material = mat + + var box := BoxMesh.new() + box.size = Vector3(0.08, 0.08, 0.08) + + var burst_mat := StandardMaterial3D.new() + burst_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + burst_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + burst_mat.albedo_color = Color(0.8, 0.7, 0.5, 0.9) + box.surface_set_material(0, burst_mat) + + draw_pass_1 = box + + +func emit_burst(pos: Vector3, color: Color) -> void: + global_position = pos + if draw_pass_1 and draw_pass_1.surface_get_material(0): + var mat := draw_pass_1.surface_get_material(0) as StandardMaterial3D + if mat: + mat.albedo_color = Color(color.r, color.g, color.b, 0.9) + emitting = true diff --git a/scripts/dolphin/BubbleTrail.gd b/scripts/dolphin/BubbleTrail.gd new file mode 100644 index 0000000..b7391f9 --- /dev/null +++ b/scripts/dolphin/BubbleTrail.gd @@ -0,0 +1,52 @@ +extends GPUParticles3D + +func _ready() -> void: + amount = 80 + lifetime = 1.5 + emitting = false + local_coords = false + + var mat := ParticleProcessMaterial.new() + mat.direction = Vector3(0.0, 0.3, 0.0) + mat.initial_velocity_min = 0.5 + mat.initial_velocity_max = 1.5 + mat.spread = 15.0 + mat.gravity = Vector3(0.0, 0.5, 0.0) + mat.scale_min = 0.03 + mat.scale_max = 0.12 + + var scale_curve := Curve.new() + scale_curve.add_point(Vector2(0.0, 0.2)) + scale_curve.add_point(Vector2(0.5, 1.0)) + scale_curve.add_point(Vector2(1.0, 0.0)) + var scale_curve_tex := CurveTexture.new() + scale_curve_tex.curve = scale_curve + mat.scale_curve = scale_curve_tex + + mat.color = Color(1.0, 1.0, 1.0, 0.6) + process_material = mat + + var sphere := SphereMesh.new() + sphere.radius = 0.05 + sphere.height = 0.1 + sphere.rings = 4 + sphere.radial_segments = 6 + + var bubble_mat := StandardMaterial3D.new() + bubble_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + bubble_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + bubble_mat.albedo_color = Color(1.0, 1.0, 1.0, 0.55) + bubble_mat.emission_enabled = true + bubble_mat.emission = Color(0.8, 0.9, 1.0, 1.0) + bubble_mat.emission_energy_multiplier = 0.3 + sphere.surface_set_material(0, bubble_mat) + + draw_pass_1 = sphere + + +func set_intensity(speed_factor: float) -> void: + if speed_factor > 0.5: + emitting = true + amount_ratio = clamp(speed_factor / 1.5, 0.3, 1.0) + else: + emitting = false diff --git a/scripts/dolphin/DolphinController.gd b/scripts/dolphin/DolphinController.gd index 51c355e..cb9bb1e 100644 --- a/scripts/dolphin/DolphinController.gd +++ b/scripts/dolphin/DolphinController.gd @@ -36,6 +36,9 @@ var _is_dead: bool = false @onready var _camera: Camera3D = $CameraPivot/SpringArm/Camera @onready var _dolphin_body: Node3D = $DolphinBody @onready var _echo_pulse: Node3D = $EcholocationPulse +@onready var bubble_trail: Node = $BubbleEmitterPoint/BubbleTrail + +var _turn_input: float = 0.0 func _ready() -> void: @@ -120,10 +123,7 @@ func _update_movement(delta: float) -> void: is_boosting = Input.is_action_pressed("boost") var speed: float = swim_speed * (boost_multiplier if is_boosting else 1.0) - var forward: Vector3 = -global_transform.basis.z - var right: Vector3 = global_transform.basis.x - - # Recompute forward/right using camera pitch for intuitive swim direction + # forward/right using camera pitch for intuitive swim direction var cam_basis: Basis = _camera_pivot.global_transform.basis var swim_forward: Vector3 = -cam_basis.z var swim_right: Vector3 = cam_basis.x @@ -150,6 +150,13 @@ func _update_movement(delta: float) -> void: move_and_slide() + var current_speed: float = velocity.length() + var speed_factor: float = clamp(current_speed / swim_speed, 0.0, 1.5) + if is_boosting: + speed_factor *= 1.8 + if bubble_trail: + bubble_trail.set_intensity(speed_factor) + func _update_stats(delta: float) -> void: var changed: bool = false @@ -204,14 +211,26 @@ func _respawn() -> void: func _animate_body() -> void: if not is_instance_valid(_dolphin_body): return - # Body bob - _dolphin_body.position.y = sin(_time * 2.5) * 0.05 - # Tail wag via last child if present - var child_count: int = _dolphin_body.get_child_count() - if child_count > 1: - var tail_node: Node3D = _dolphin_body.get_child(child_count - 1) as Node3D - if tail_node != null: - tail_node.rotation.y = sin(_time * 3.0) * 0.3 + + # Idle body bob + _dolphin_body.position.y = sin(_time * 2.5) * 0.04 + + # Speed factor for animation intensity + var spd: float = velocity.length() + var speed_factor: float = clamp(spd / swim_speed, 0.0, 1.5) + + # Track turn input for body lean + var strafe: float = 0.0 + if Input.is_action_pressed("strafe_right"): + strafe += 1.0 + if Input.is_action_pressed("strafe_left"): + strafe -= 1.0 + _turn_input = lerp(_turn_input, strafe, 0.1) + + # Drive procedural mesh builder if present + var builder = _dolphin_body.get_node_or_null("DolphinMesh") + if builder != null and builder.has_method("animate"): + builder.animate(_time, speed_factor, is_boosting, _turn_input) func _do_raycast_break() -> void: diff --git a/shaders/caustics.gdshader b/shaders/caustics.gdshader index 00163c0..4cfb100 100644 --- a/shaders/caustics.gdshader +++ b/shaders/caustics.gdshader @@ -1,21 +1,33 @@ shader_type spatial; -render_mode unshaded, blend_add, cull_disabled, depth_draw_never; +render_mode unshaded, blend_add, depth_draw_never, cull_disabled; -uniform float speed : hint_range(0.1, 3.0) = 0.8; -uniform float scale : hint_range(1.0, 20.0) = 8.0; -uniform float intensity : hint_range(0.0, 2.0) = 0.6; -uniform vec3 caustic_color : source_color = vec3(0.7, 0.9, 1.0); +uniform vec3 caustic_color : source_color = vec3(0.5, 0.8, 1.0); +uniform float intensity : hint_range(0, 3) = 1.5; +uniform float scale = 8.0; +uniform float speed = 0.5; + +float caustic_pattern(vec2 uv) { + float t = TIME * speed; + vec2 p = uv * scale; + float c = 0.0; + c += sin(p.x + t) * 0.5; + c += cos(p.y + t * 0.7) * 0.5; + c += sin(p.x * 0.7 + p.y * 0.5 + t * 1.3) * 0.3; + c = abs(c); + c = smoothstep(0.2, 0.0, c); + return c; +} void fragment() { - vec2 uv = UV * scale; + vec2 uv = UV * 2.0 - 1.0; + float c1 = caustic_pattern(uv); + float c2 = caustic_pattern(uv * 1.3 + vec2(0.5, 0.3)); + float caustics = (c1 + c2 * 0.5) * intensity; - float pattern1 = sin(TIME * speed + uv.x * 10.0 + sin(uv.y * 10.0 + TIME * speed)); - float pattern2 = sin(TIME * speed * 0.7 + uv.y * 12.0 + sin(uv.x * 8.0 + TIME * speed * 1.3)); - float pattern3 = sin(TIME * speed * 1.1 + (uv.x + uv.y) * 7.0 + sin(uv.x * 5.0 - TIME * speed * 0.5)); + // Fade avec profondeur + float depth_fade = clamp((VERTEX.y + 60.0) / 80.0, 0.0, 1.0); + caustics *= depth_fade; - float caustic = (pattern1 + pattern2 + pattern3) / 3.0; - caustic = pow(max(caustic, 0.0), 2.0) * intensity; - - EMISSION = caustic_color * caustic; - ALBEDO = vec3(0.0); + ALBEDO = caustic_color * caustics; + ALPHA = caustics; } diff --git a/shaders/godrays.gdshader b/shaders/godrays.gdshader index 28a8e63..c215032 100644 --- a/shaders/godrays.gdshader +++ b/shaders/godrays.gdshader @@ -1,20 +1,25 @@ -shader_type canvas_item; -render_mode blend_add; +shader_type spatial; +render_mode unshaded, blend_add, depth_draw_never, cull_disabled; -uniform float ray_count : hint_range(5.0, 40.0) = 20.0; -uniform float speed : hint_range(0.1, 2.0) = 0.4; -uniform float intensity : hint_range(0.0, 2.0) = 0.5; -uniform vec3 ray_color : source_color = vec3(0.5, 0.8, 1.0); +uniform vec3 ray_color : source_color = vec3(0.7, 0.9, 1.0); +uniform float intensity : hint_range(0, 2) = 0.6; +uniform float fade_bottom : hint_range(-100, 0) = -30.0; void fragment() { vec2 uv = UV; + float rays = 0.0; + // Multiple ray stripes + for (float i = 0.0; i < 5.0; i++) { + float offset = i * 0.2 + TIME * 0.03; + float r = smoothstep(0.02, 0.0, abs(fract(uv.x * 8.0 + offset) - 0.5)); + rays += r * (1.0 - i / 5.0); + } + // Fade vertical + float vertical_fade = smoothstep(0.0, 0.4, 1.0 - uv.y); + // Fade avec profondeur joueur + float depth_fade = clamp((VERTEX.y - fade_bottom) / (60.0 - fade_bottom), 0.0, 1.0); - float vertical_fade = pow(1.0 - uv.y, 2.0); - - float ray_pattern = fract(uv.x * ray_count + sin(TIME * speed + uv.y * 5.0) * 0.3); - float ray = smoothstep(0.0, 0.15, ray_pattern) * smoothstep(0.4, 0.15, ray_pattern); - - float brightness = ray * vertical_fade * intensity; - - COLOR = vec4(ray_color * brightness, brightness * 0.6); + float final_alpha = rays * vertical_fade * depth_fade * intensity; + ALBEDO = ray_color; + ALPHA = final_alpha; } diff --git a/shaders/underwater_fog.gdshader b/shaders/underwater_fog.gdshader index 7afd539..bd751e9 100644 --- a/shaders/underwater_fog.gdshader +++ b/shaders/underwater_fog.gdshader @@ -1,21 +1,19 @@ shader_type fog; -uniform float surface_y : hint_range(-100.0, 100.0) = 0.0; -uniform float abyss_depth : hint_range(-200.0, 0.0) = -60.0; +uniform vec3 shallow_color : source_color = vec3(0.25, 0.55, 0.70); +uniform vec3 deep_color : source_color = vec3(0.02, 0.05, 0.10); +uniform float surface_y : hint_range(0, 100) = 60.0; +uniform float abyss_y : hint_range(-100, 0) = -60.0; +uniform float base_density : hint_range(0, 1) = 0.04; +uniform float abyss_density : hint_range(0, 3) = 0.25; void fog() { - float depth_factor = clamp((WORLD_POSITION.y - abyss_depth) / (surface_y - abyss_depth), 0.0, 1.0); - - vec3 surface_color = vec3(0.15, 0.45, 0.6); - vec3 abyss_color = vec3(0.02, 0.08, 0.15); - vec3 fog_color = mix(abyss_color, surface_color, depth_factor); - - float current_wave = sin(TIME * 0.3 + WORLD_POSITION.x * 0.05) * 0.02 - + sin(TIME * 0.17 + WORLD_POSITION.z * 0.04) * 0.01; - - float base_density = mix(0.3, 0.05, depth_factor) + current_wave; - base_density = clamp(base_density, 0.0, 0.4); - - ALBEDO = fog_color; - DENSITY = base_density; + float depth_factor = clamp((surface_y - WORLD_POSITION.y) / (surface_y - abyss_y), 0.0, 1.0); + vec3 col = mix(shallow_color, deep_color, depth_factor); + float density = mix(base_density, abyss_density, depth_factor); + // Subtle current movement + float wave = sin(WORLD_POSITION.x * 0.1 + TIME * 0.3) * 0.5 + 0.5; + density *= (0.9 + wave * 0.2); + ALBEDO = col; + DENSITY = density; } diff --git a/shaders/water_surface.gdshader b/shaders/water_surface.gdshader new file mode 100644 index 0000000..0d9869d --- /dev/null +++ b/shaders/water_surface.gdshader @@ -0,0 +1,21 @@ +shader_type spatial; +render_mode cull_front, depth_draw_never, blend_mix; + +uniform vec3 surface_color : source_color = vec3(0.15, 0.45, 0.65); +uniform float wave_speed = 0.3; +uniform float wave_amplitude = 0.15; + +void vertex() { + float wave1 = sin(VERTEX.x * 0.5 + TIME * wave_speed) * wave_amplitude; + float wave2 = cos(VERTEX.z * 0.4 + TIME * wave_speed * 1.3) * wave_amplitude * 0.7; + VERTEX.y += wave1 + wave2; +} + +void fragment() { + ALBEDO = surface_color; + float fresnel = pow(1.0 - dot(NORMAL, VIEW), 2.0); + ALPHA = 0.4 + fresnel * 0.4; + METALLIC = 0.2; + ROUGHNESS = 0.3; + EMISSION = surface_color * 0.1; +}