init
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
@@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
@@ -0,0 +1,117 @@
|
||||
[gd_scene format=3 uid="uid://jven27ktracb"]
|
||||
|
||||
[ext_resource type="Material" uid="uid://6p5bg1nyso6d" path="res://materials/stylized_water.tres" id="1_water_material"]
|
||||
[ext_resource type="Script" uid="uid://co5xetl8amg4j" path="res://scripts/ocean.gd" id="2_ocean_script"]
|
||||
[ext_resource type="Script" uid="uid://c0n1x050jvspr" path="res://scripts/boat.gd" id="3_1bvp3"]
|
||||
[ext_resource type="Script" uid="uid://46jiwugeccqg" path="res://scripts/camera_rig.gd" id="4_1bvp3"]
|
||||
[ext_resource type="Script" uid="uid://b500yf6hema7c" path="res://scripts/stats_label.gd" id="5_lquwl"]
|
||||
[ext_resource type="Script" uid="uid://dpohf1tg5e2gs" path="res://scripts/world_manager.gd" id="6_7mycd"]
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="SkyMaterial_1"]
|
||||
sky_horizon_color = Color(0.66, 0.67, 0.68, 1)
|
||||
ground_horizon_color = Color(0.66, 0.67, 0.68, 1)
|
||||
|
||||
[sub_resource type="Sky" id="Sky_1"]
|
||||
sky_material = SubResource("SkyMaterial_1")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_1"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_1")
|
||||
tonemap_mode = 2
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_ocean"]
|
||||
size = Vector2(200, 200)
|
||||
subdivide_width = 200
|
||||
subdivide_depth = 200
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_seabed"]
|
||||
size = Vector3(200, 1, 200)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_seabed"]
|
||||
albedo_color = Color(0.76, 0.7, 0.5, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape_seabed"]
|
||||
size = Vector3(200, 1, 200)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_crate"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_crate"]
|
||||
albedo_color = Color(0.72, 0.53, 0.32, 1)
|
||||
roughness = 0.9
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape_crate"]
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1242452333]
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=431656791]
|
||||
environment = SubResource("Environment_1")
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=144458330]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.642788, 0.766044, 0, -0.766044, 0.642788, 0, 20, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="Ocean" type="MeshInstance3D" parent="." unique_id=312504233 groups=["follow_focus", "ocean"]]
|
||||
material_override = ExtResource("1_water_material")
|
||||
mesh = SubResource("PlaneMesh_ocean")
|
||||
script = ExtResource("2_ocean_script")
|
||||
sea_height = 1.0
|
||||
sea_choppy = 8.0
|
||||
metadata/follow_step = 1.0
|
||||
|
||||
[node name="Seabed" type="StaticBody3D" parent="." unique_id=1919476025 groups=["follow_focus"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -4.5, 0)
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="Seabed" unique_id=1209469388]
|
||||
mesh = SubResource("BoxMesh_seabed")
|
||||
surface_material_override/0 = SubResource("Material_seabed")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Seabed" unique_id=111957760]
|
||||
shape = SubResource("BoxShape_seabed")
|
||||
|
||||
[node name="Crate" type="RigidBody3D" parent="." unique_id=1409256205 groups=["shift_with_origin"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, 0)
|
||||
mass = 5.0
|
||||
script = ExtResource("3_1bvp3")
|
||||
buoyancy = 1.5
|
||||
full_force_depth = 0.8
|
||||
probe_extents = Vector3(0.35, 0, 2.25)
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="Crate" unique_id=1406951740]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0)
|
||||
mesh = SubResource("BoxMesh_crate")
|
||||
surface_material_override/0 = SubResource("Material_crate")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Crate" unique_id=1190807384]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0)
|
||||
shape = SubResource("BoxShape_crate")
|
||||
|
||||
[node name="CameraRig" type="Node3D" parent="." unique_id=2139913829 node_paths=PackedStringArray("target", "cam") groups=["shift_with_origin"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9396926, 0.34202012, 0, -0.34202012, 0.9396926, 0, 0, 0)
|
||||
script = ExtResource("4_1bvp3")
|
||||
target = NodePath("../Crate")
|
||||
cam = NodePath("PlayerCamera")
|
||||
|
||||
[node name="PlayerCamera" type="Camera3D" parent="CameraRig" unique_id=553919400]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 8)
|
||||
|
||||
[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1275537335]
|
||||
|
||||
[node name="StatsLabel" type="Label" parent="CanvasLayer" unique_id=15552030 node_paths=PackedStringArray("boat", "world")]
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -76.0
|
||||
offset_top = -52.0
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 0
|
||||
script = ExtResource("5_lquwl")
|
||||
boat = NodePath("../../Crate")
|
||||
world = NodePath("../../WorldManager")
|
||||
|
||||
[node name="WorldManager" type="Node" parent="." unique_id=1879897241 node_paths=PackedStringArray("focus", "ocean")]
|
||||
script = ExtResource("6_7mycd")
|
||||
focus = NodePath("../Crate")
|
||||
ocean = NodePath("../Ocean")
|
||||
shift_threshold = 50.0
|
||||
@@ -0,0 +1,57 @@
|
||||
[gd_resource type="ShaderMaterial" format=3 uid="uid://6p5bg1nyso6d"]
|
||||
|
||||
[ext_resource type="Shader" uid="uid://cst73i45poweu" path="res://shaders/ocean.gdshader" id="1_water_shader"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_caustics"]
|
||||
colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1)
|
||||
|
||||
[sub_resource type="FastNoiseLite" id="FastNoiseLite_caustics"]
|
||||
noise_type = 2
|
||||
frequency = 0.05
|
||||
cellular_return_type = 4
|
||||
|
||||
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_caustics"]
|
||||
noise = SubResource("FastNoiseLite_caustics")
|
||||
color_ramp = SubResource("Gradient_caustics")
|
||||
seamless = true
|
||||
|
||||
[resource]
|
||||
render_priority = 0
|
||||
shader = ExtResource("1_water_shader")
|
||||
shader_parameter/max_depth = 10.0
|
||||
shader_parameter/underwater_fog_color = Color(0, 0.05, 0.1, 1)
|
||||
shader_parameter/fade_start_depth = 0.5
|
||||
shader_parameter/wave_time = 0.0
|
||||
shader_parameter/sea_height = 1.3
|
||||
shader_parameter/sea_choppy = 4.0
|
||||
shader_parameter/sea_freq = 0.08
|
||||
shader_parameter/ITER_GEOMETRY = 3
|
||||
shader_parameter/ITER_FRAGMENT = 5
|
||||
shader_parameter/refraction_strength = 1.0
|
||||
shader_parameter/refraction_distance_fade = 5.0
|
||||
shader_parameter/normal_epsilon = 0.01
|
||||
shader_parameter/normal_smoothness_dist = 50.0
|
||||
shader_parameter/base_tint_color = Color(0.439, 0.973, 1, 1)
|
||||
shader_parameter/deep_color = Color(0, 0.341, 0.29, 1)
|
||||
shader_parameter/water_absorption = Color(0.3, 0.06, 0.02, 1)
|
||||
shader_parameter/roughness = 0.125
|
||||
shader_parameter/metallic = 0.0
|
||||
shader_parameter/specular = 0.5
|
||||
shader_parameter/caustics_texture = SubResource("NoiseTexture2D_caustics")
|
||||
shader_parameter/caustics_scale = 0.2
|
||||
shader_parameter/caustics_speed = 0.1
|
||||
shader_parameter/caustics_intensity = 0.8
|
||||
shader_parameter/caustics_depth_fade = 0.7
|
||||
shader_parameter/foam_color = Color(1, 1, 1, 1)
|
||||
shader_parameter/foam_depth_start = 0.8
|
||||
shader_parameter/foam_depth_end = 0.0
|
||||
shader_parameter/foam_noise_scale = 2.0
|
||||
shader_parameter/foam_noise_speed = 1.0
|
||||
shader_parameter/foam_cutoff = 0.7
|
||||
shader_parameter/foam_crest_threshold = 0.7
|
||||
shader_parameter/foam_crest_amount = 2.0
|
||||
shader_parameter/foam_edge_color = Color(0, 0, 0.05, 1)
|
||||
shader_parameter/foam_edge_offset = Vector2(0.02, 0.02)
|
||||
shader_parameter/voronoi_scale = 9.0
|
||||
shader_parameter/voronoi_strength = 0.8
|
||||
shader_parameter/wave_offset = Vector2(0, 0)
|
||||
@@ -0,0 +1,3 @@
|
||||
[gd_scene format=3 uid="uid://buq17wtyky42j"]
|
||||
|
||||
[node name="Node" type="Node" unique_id=2000830144]
|
||||
@@ -0,0 +1,56 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="boatgame"
|
||||
run/main_scene="res://main.tscn"
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
|
||||
[display]
|
||||
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/aspect="expand"
|
||||
|
||||
[input]
|
||||
|
||||
throttle_increase={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
throttle_decrease={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
rudder_left={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
rudder_right={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
throttle_stop={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":120,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
@@ -0,0 +1,51 @@
|
||||
class_name Boat
|
||||
extends FloatingBody
|
||||
|
||||
## All force values are per-kilogram (they get multiplied by mass), so the
|
||||
## boat handles the same if you change its mass later.
|
||||
@export var engine_power := 12.0 # forward thrust, m/s² at full throttle
|
||||
@export var reverse_ratio := 0.4 # reverse is weaker, like a real prop
|
||||
@export var throttle_response := 1.5 # how fast the engine spools up/down
|
||||
@export var rudder_strength := 2.5 # turning torque at speed
|
||||
@export var keel_grip := 3.0 # resistance to sliding sideways
|
||||
|
||||
var desired_throttle := 0.0
|
||||
var throttle := 0.0
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
super(delta)
|
||||
|
||||
if Input.is_action_pressed("throttle_increase"):
|
||||
desired_throttle = move_toward(desired_throttle, 1, 0.005)
|
||||
elif Input.is_action_pressed("throttle_decrease"):
|
||||
desired_throttle = move_toward(desired_throttle, -1, 0.006)
|
||||
elif Input.is_action_pressed("throttle_stop"):
|
||||
desired_throttle = 0.0
|
||||
|
||||
throttle = move_toward(throttle, desired_throttle, throttle_response * delta)
|
||||
|
||||
# boat's forward direction, flattened onto the water plane
|
||||
var forward := -global_basis.z
|
||||
forward.y = 0.0
|
||||
forward = forward.normalized()
|
||||
|
||||
# --- engine ---
|
||||
var power := engine_power * (reverse_ratio if throttle < 0.0 else 1.0)
|
||||
apply_central_force(forward * throttle * power * mass * submerged_ratio)
|
||||
|
||||
# --- rudder: only bites when water flows past it ---
|
||||
var steer_input := Input.get_axis("rudder_right", "rudder_left")
|
||||
var forward_speed := linear_velocity.dot(forward)
|
||||
var rudder_bite := clampf(forward_speed / 3.0, -1.0, 1.0)
|
||||
apply_torque(Vector3.UP * steer_input * rudder_strength * rudder_bite * mass * submerged_ratio)
|
||||
|
||||
if submerged_ratio <= 0.0:
|
||||
return # airborne: no thrust, no rudder, no keelffffff
|
||||
|
||||
# --- keel: kill sideways sliding ---
|
||||
var right := global_basis.x
|
||||
right.y = 0.0
|
||||
right = right.normalized()
|
||||
var lateral_speed := linear_velocity.dot(right)
|
||||
apply_central_force(-right * lateral_speed * keel_grip * mass * submerged_ratio)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0n1x050jvspr
|
||||
@@ -0,0 +1,31 @@
|
||||
extends Node3D
|
||||
|
||||
@export var target: Node3D
|
||||
@export var sensitivity := 0.005
|
||||
@export var follow_speed := 5.0
|
||||
@export var cam: Camera3D
|
||||
|
||||
var _dragging := false
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
_dragging = event.pressed
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
cam.position.z = maxf(cam.position.z - 1.0, 3.0)
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
cam.position.z = minf(cam.position.z + 1.0, 30.0)
|
||||
elif event is InputEventMouseMotion and _dragging:
|
||||
rotation.y -= event.relative.x * sensitivity
|
||||
rotation.x = clampf(
|
||||
rotation.x - event.relative.y * sensitivity,
|
||||
deg_to_rad(-80.0),
|
||||
deg_to_rad(-10.0)
|
||||
)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if target:
|
||||
global_position = global_position.lerp(target.global_position, follow_speed * delta)
|
||||
@@ -0,0 +1 @@
|
||||
uid://46jiwugeccqg
|
||||
@@ -0,0 +1,68 @@
|
||||
class_name FloatingBody
|
||||
extends RigidBody3D
|
||||
|
||||
## Buoyant rigid body. Add Marker3D children as float probes (e.g. the four
|
||||
## corners of a hull); each probe pushes up in proportion to how deep it is
|
||||
## below the ocean surface, which makes the body bob and tilt with the waves.
|
||||
|
||||
## Upward acceleration as a multiple of gravity when fully submerged.
|
||||
## > 1.0 floats, < 1.0 sinks. Higher rides higher in the water.
|
||||
@export var buoyancy := 2.0
|
||||
## Probe depth (in meters) at which buoyant force reaches full strength.
|
||||
@export var full_force_depth := 0.4
|
||||
## Linear drag applied while in the water.
|
||||
@export var water_drag := 1.5
|
||||
## Rotational drag applied while in the water.
|
||||
@export var water_angular_drag := 1.0
|
||||
|
||||
## Probes generated across the hull footprint if no Marker3D children exist.
|
||||
@export var probe_extents := Vector3(0.35, 0.0, 0.9) # half-width, height, half-length
|
||||
@export var probe_grid := Vector2i(2, 4) # columns (x), rows (z)
|
||||
|
||||
@export var probe_damping := 4.0
|
||||
|
||||
var _probe_offsets: Array[Vector3] = [] # local space
|
||||
|
||||
var _ocean: Ocean
|
||||
var _probes: Array[Marker3D] = []
|
||||
var _gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||||
var submerged_ratio := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_ocean = get_tree().get_first_node_in_group("ocean") as Ocean
|
||||
# manual markers take priority if present...
|
||||
for child in get_children():
|
||||
if child is Marker3D:
|
||||
_probe_offsets.append(child.position)
|
||||
# ...otherwise generate a grid over the hull footprint
|
||||
if _probe_offsets.is_empty():
|
||||
for col in probe_grid.x:
|
||||
for row in probe_grid.y:
|
||||
_probe_offsets.append(Vector3(
|
||||
lerpf(-probe_extents.x, probe_extents.x, col / float(probe_grid.x - 1)),
|
||||
probe_extents.y,
|
||||
lerpf(-probe_extents.z, probe_extents.z, row / float(probe_grid.y - 1))))
|
||||
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if _ocean == null or _probe_offsets.is_empty():
|
||||
return
|
||||
submerged_ratio = 0.0
|
||||
for offset in _probe_offsets:
|
||||
var pos := global_transform * offset
|
||||
var depth := _ocean.get_wave_height(pos) - pos.y
|
||||
if depth > 0.0:
|
||||
var factor := clampf(depth / full_force_depth, 0.0, 1.0)
|
||||
submerged_ratio += factor / _probe_offsets.size()
|
||||
var force := Vector3.UP * _gravity * mass * buoyancy * factor / _probe_offsets.size()
|
||||
apply_force(force, pos - global_position)
|
||||
var r := pos - global_position
|
||||
# velocity of this specific point on the hull (body motion + rotation)
|
||||
var point_velocity := linear_velocity + angular_velocity.cross(r)
|
||||
var damp := Vector3.UP * -point_velocity.y * probe_damping * factor * mass / _probe_offsets.size()
|
||||
apply_force(force + damp, r)
|
||||
if submerged_ratio > 0.0:
|
||||
apply_central_force(-linear_velocity * water_drag * mass * submerged_ratio)
|
||||
apply_torque(-angular_velocity * water_angular_drag * mass * submerged_ratio)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bsvmv1v1r4h2n
|
||||
@@ -0,0 +1,125 @@
|
||||
class_name Ocean
|
||||
extends MeshInstance3D
|
||||
|
||||
## Drives the stylized water shader and mirrors its wave math on the CPU so
|
||||
## physics (buoyancy probes) can query the exact rendered surface height.
|
||||
## These exports are pushed to the shader as uniforms in _ready(), and the
|
||||
## shader's `wave_time` is driven from here every frame — the shader itself
|
||||
## never reads TIME for displacement, so the two can't drift apart.
|
||||
|
||||
## All of these are safe to change from game logic at any time (directly or
|
||||
## via a Tween); the setters forward them to the shader. Abrupt jumps in
|
||||
## height/choppy/freq visibly snap the surface, so tween those for things
|
||||
## like a storm rolling in.
|
||||
@export var sea_height := 0.1:
|
||||
set(value):
|
||||
sea_height = value
|
||||
_push("sea_height", value)
|
||||
@export var sea_choppy := 4.0:
|
||||
set(value):
|
||||
sea_choppy = value
|
||||
_push("sea_choppy", value)
|
||||
## Phase advance per second. Not a shader uniform: speed is integrated into
|
||||
## _phase on the CPU, so changing it mid-game is always smooth.
|
||||
@export var sea_speed := 1.5
|
||||
@export var sea_freq := 0.08:
|
||||
set(value):
|
||||
sea_freq = value
|
||||
_push("sea_freq", value)
|
||||
@export var iter_geometry := 3:
|
||||
set(value):
|
||||
iter_geometry = value
|
||||
_push("ITER_GEOMETRY", value)
|
||||
## Node the ocean mesh stays centered under (camera rig or boat).
|
||||
## Half-size of the rendered ocean square, in meters.
|
||||
@export var render_distance := 100.0:
|
||||
set(value):
|
||||
render_distance = value
|
||||
if is_node_ready():
|
||||
_rebuild_mesh()
|
||||
## World-space distance between mesh vertices; smaller = more wave detail.
|
||||
|
||||
@export var vertex_spacing := 1.0
|
||||
var _phase := 0.0
|
||||
var _wave_offset := Vector2.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
_push("sea_height", sea_height)
|
||||
_push("sea_choppy", sea_choppy)
|
||||
_push("sea_freq", sea_freq)
|
||||
_push("ITER_GEOMETRY", iter_geometry)
|
||||
_rebuild_mesh()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_phase += delta * sea_speed
|
||||
_push("wave_time", _phase)
|
||||
|
||||
|
||||
func _push(param: String, value: Variant) -> void:
|
||||
# Setters can fire during scene load before material_override is assigned;
|
||||
# _ready() re-pushes everything once the node is complete.
|
||||
var mat := material_override as ShaderMaterial
|
||||
if mat:
|
||||
mat.set_shader_parameter(param, value)
|
||||
|
||||
|
||||
## Water surface height (world Y) at the given world position. Mirrors the
|
||||
## shader's map() at ITER_GEOMETRY iterations; the shader outputs an absolute
|
||||
## world-space height, independent of this node's own Y.
|
||||
func get_wave_height(world_pos: Vector3) -> float:
|
||||
var uv := Vector2((world_pos.x + _wave_offset.x) * 0.75, world_pos.z + _wave_offset.y)
|
||||
var freq := sea_freq
|
||||
var amp := sea_height
|
||||
var choppy := sea_choppy
|
||||
var h := 0.0
|
||||
var ts := _phase
|
||||
for i in iter_geometry:
|
||||
var d := _sea_octave((uv + Vector2(ts, ts)) * freq, choppy)
|
||||
d += _sea_octave((uv - Vector2(ts, ts)) * freq, choppy)
|
||||
h += d * amp
|
||||
# uv *= octave_m, with octave_m = mat2(vec2(1.6, 1.2), vec2(-1.2, 1.6))
|
||||
uv = Vector2(1.6 * uv.x + 1.2 * uv.y, -1.2 * uv.x + 1.6 * uv.y)
|
||||
freq *= 1.9
|
||||
amp *= 0.22
|
||||
choppy = lerpf(choppy, 1.0, 0.2)
|
||||
return h
|
||||
|
||||
|
||||
static func _sea_octave(uv: Vector2, choppy: float) -> float:
|
||||
var n := _noise(uv)
|
||||
uv += Vector2(n, n)
|
||||
var wv := Vector2(1.0 - absf(sin(uv.x)), 1.0 - absf(sin(uv.y)))
|
||||
var swv := Vector2(absf(cos(uv.x)), absf(cos(uv.y)))
|
||||
wv = Vector2(lerpf(wv.x, swv.x, wv.x), lerpf(wv.y, swv.y, wv.y))
|
||||
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy)
|
||||
|
||||
|
||||
static func _noise(p: Vector2) -> float:
|
||||
var i := p.floor()
|
||||
var f := p - i
|
||||
var u := f * f * (Vector2(3.0, 3.0) - 2.0 * f)
|
||||
return -1.0 + 2.0 * lerpf(
|
||||
lerpf(_hash12(i), _hash12(i + Vector2(1, 0)), u.x),
|
||||
lerpf(_hash12(i + Vector2(0, 1)), _hash12(i + Vector2(1, 1)), u.x), u.y)
|
||||
|
||||
|
||||
# Mirrors the shader's hash12(); the & 0xFFFFFFFF masks emulate 32-bit
|
||||
# unsigned wraparound on GDScript's 64-bit ints.
|
||||
static func _hash12(p: Vector2) -> float:
|
||||
var qx := (int(p.x) * 1597334677) & 0xFFFFFFFF
|
||||
var qy := (int(p.y) * 3812015801) & 0xFFFFFFFF
|
||||
var n := ((qx ^ qy) * 1597334677) & 0xFFFFFFFF
|
||||
return float(n) / 4294967295.0
|
||||
|
||||
|
||||
func _rebuild_mesh() -> void:
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(render_distance, render_distance) * 2.0
|
||||
plane.subdivide_width = int(render_distance * 2.0 / vertex_spacing) - 1
|
||||
plane.subdivide_depth = plane.subdivide_width
|
||||
mesh = plane
|
||||
|
||||
func shift_origin(shift: Vector3) -> void:
|
||||
_wave_offset += Vector2(shift.x, shift.z)
|
||||
_push("wave_offset", _wave_offset)
|
||||
@@ -0,0 +1 @@
|
||||
uid://co5xetl8amg4j
|
||||
@@ -0,0 +1,27 @@
|
||||
extends Label
|
||||
|
||||
@export var boat: Boat # drag the Crate into this slot in the inspector
|
||||
@export var world: WorldManager
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if boat == null:
|
||||
text = "no boat"
|
||||
return
|
||||
|
||||
var forward := -boat.global_basis.z
|
||||
forward.y = 0.0
|
||||
var speed := boat.linear_velocity.dot(forward.normalized())
|
||||
var pos := world.true_position(boat)
|
||||
|
||||
text = "Throttle: %+.0f %%
|
||||
Speed: %.1f m/s
|
||||
Submerged: %.0f %%
|
||||
Position: (%.0f, %.0f)
|
||||
" % [
|
||||
boat.throttle * 100.0,
|
||||
speed,
|
||||
boat.submerged_ratio * 100.0,
|
||||
boat.global_position.x,
|
||||
boat.global_position.z,
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
uid://b500yf6hema7c
|
||||
@@ -0,0 +1,47 @@
|
||||
class_name WorldManager
|
||||
extends Node
|
||||
|
||||
## Owns "where is the player and what stays centered on them":
|
||||
## recenters scenery in the follow_focus group, and shifts the origin
|
||||
## (teleporting the shift_with_origin group) before float precision degrades.
|
||||
|
||||
@export var focus: Node3D # the boat
|
||||
@export var ocean: Ocean
|
||||
## Distance from origin that triggers an origin shift.
|
||||
@export var shift_threshold := 2048.0
|
||||
## Default recenter step; nodes can override with a follow_step metadata.
|
||||
## Keep it a multiple of the ocean's vertex_spacing.
|
||||
@export var follow_step := 16.0
|
||||
|
||||
## True world position of the current local origin (grows over a long voyage).
|
||||
var origin_offset := Vector3.ZERO
|
||||
|
||||
|
||||
func true_position(node: Node3D) -> Vector3:
|
||||
return node.global_position + origin_offset
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
_maybe_shift_origin()
|
||||
_recenter_followers()
|
||||
|
||||
|
||||
func _maybe_shift_origin() -> void:
|
||||
var p := focus.global_position
|
||||
if Vector2(p.x, p.z).length() < shift_threshold:
|
||||
return
|
||||
var shift := Vector3(snappedf(p.x, follow_step), 0.0, snappedf(p.z, follow_step))
|
||||
origin_offset += shift
|
||||
for node in get_tree().get_nodes_in_group("shift_with_origin"):
|
||||
node.global_position -= shift
|
||||
ocean.shift_origin(shift)
|
||||
|
||||
|
||||
func _recenter_followers() -> void:
|
||||
for node in get_tree().get_nodes_in_group("follow_focus"):
|
||||
var step: float = node.get_meta("follow_step", follow_step)
|
||||
var new_x := snappedf(focus.global_position.x, step)
|
||||
var new_z := snappedf(focus.global_position.z, step)
|
||||
if new_x != node.global_position.x or new_z != node.global_position.z:
|
||||
node.global_position.x = new_x
|
||||
node.global_position.z = new_z
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpohf1tg5e2gs
|
||||
@@ -0,0 +1,363 @@
|
||||
// Stylized 3D water shader, assembled from the gameidea.org tutorial:
|
||||
// https://gameidea.org/2026/02/01/creating-a-stylized-3d-water-shader/
|
||||
// Seascape-style FBM waves + depth fog, Beer-Lambert absorption, refraction,
|
||||
// caustics and voronoi foam.
|
||||
//
|
||||
// Adaptation for this project: wave displacement uses the `wave_time` uniform
|
||||
// (driven by ocean.gd) instead of TIME, so the CPU-side get_wave_height()
|
||||
// used for buoyancy stays in sync with the rendered surface. Purely cosmetic
|
||||
// animation (foam, caustics) still uses TIME.
|
||||
shader_type spatial;
|
||||
render_mode depth_draw_always;
|
||||
|
||||
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
|
||||
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap;
|
||||
|
||||
uniform float max_depth : hint_range(0.1, 50.0, 0.1) = 10.0;
|
||||
uniform vec3 underwater_fog_color : source_color = vec3(0.0, 0.05, 0.1);
|
||||
uniform float fade_start_depth : hint_range(0.0, 50.0, 0.1) = 0.5;
|
||||
|
||||
// wave parameters (pushed from ocean.gd -- change them there, not here,
|
||||
// or buoyancy physics will desync from the rendered surface).
|
||||
// wave_time is accumulated phase (delta * sea_speed integrated on the CPU),
|
||||
// so speed changes mid-game never make the surface jump.
|
||||
uniform float wave_time = 0.0;
|
||||
uniform float sea_height : hint_range(0.0, 5.0) = 1.3;
|
||||
uniform float sea_choppy : hint_range(0.0, 10.0) = 4.0;
|
||||
uniform float sea_freq : hint_range(0.0, 0.5) = 0.08;
|
||||
// iterations
|
||||
uniform int ITER_GEOMETRY = 3;
|
||||
uniform int ITER_FRAGMENT = 5;
|
||||
const mat2 octave_m = mat2(vec2(1.6, 1.2), vec2(-1.2, 1.6));
|
||||
|
||||
// refraction settings
|
||||
uniform float refraction_strength : hint_range(0.0, 2.0, 0.01) = 1.0;
|
||||
uniform float refraction_distance_fade : hint_range(0.0, 100.0, 1.0) = 5.0;
|
||||
// for smoothness part
|
||||
uniform float normal_epsilon : hint_range(0.001, 1.0) = 0.01;
|
||||
uniform float normal_smoothness_dist : hint_range(0.0, 1000.0) = 50.0;
|
||||
|
||||
uniform vec3 base_tint_color : source_color = vec3(0.439, 0.973, 1.0);
|
||||
uniform vec3 deep_color : source_color = vec3(0.0, 0.341, 0.29);
|
||||
// beer-lambert absorption
|
||||
uniform vec3 water_absorption : source_color = vec3(0.3, 0.06, 0.02);
|
||||
|
||||
uniform float roughness : hint_range(0.0, 1.0) = 0.125;
|
||||
uniform float metallic : hint_range(0.0, 1.0) = 0.0;
|
||||
uniform float specular : hint_range(0.0, 1.0) = 0.5;
|
||||
|
||||
// caustics uniforms
|
||||
uniform sampler2D caustics_texture : filter_linear_mipmap, repeat_enable;
|
||||
uniform float caustics_scale : hint_range(0.1, 10.0, 0.1) = 2.0;
|
||||
uniform float caustics_speed : hint_range(0.0, 1.0, 0.01) = 0.1;
|
||||
uniform float caustics_intensity : hint_range(0.0, 2.0, 0.1) = 0.8;
|
||||
uniform float caustics_depth_fade : hint_range(0.0, 1.0, 0.01) = 0.7;
|
||||
|
||||
// foam uniforms
|
||||
uniform vec3 foam_color : source_color = vec3(1.0, 1.0, 1.0);
|
||||
uniform float foam_depth_start : hint_range(0.0, 5.0, 0.01) = 0.8;
|
||||
uniform float foam_depth_end : hint_range(0.0, 5.0, 0.01) = 0.0;
|
||||
uniform float foam_noise_scale : hint_range(0.0, 10.0, 0.01) = 2.0;
|
||||
uniform float foam_noise_speed : hint_range(0.0, 2.0, 0.01) = 1.0;
|
||||
uniform float foam_cutoff : hint_range(0.0, 1.0, 0.01) = 0.7;
|
||||
uniform float foam_crest_threshold : hint_range(0.0, 1.0, 0.01) = 0.7;
|
||||
uniform float foam_crest_amount : hint_range(0.0, 10.0, 0.1) = 2.0;
|
||||
|
||||
uniform vec3 foam_edge_color : source_color = vec3(0.0, 0.0, 0.05);
|
||||
uniform vec2 foam_edge_offset = vec2(0.02, 0.02);
|
||||
uniform float voronoi_scale : hint_range(0.0, 50.0) = 9.0;
|
||||
uniform float voronoi_strength : hint_range(0.0, 8.0) = 0.8;
|
||||
|
||||
uniform vec2 wave_offset = vec2(0.0);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wave functions
|
||||
// ---------------------------------------------------------------------------
|
||||
float hash12(vec2 p) {
|
||||
uvec2 q = uvec2(ivec2(p)) * uvec2(1597334677u, 3812015801u);
|
||||
uint n = (q.x ^ q.y) * 1597334677u;
|
||||
return float(n) * (1.0 / 4294967295.0);
|
||||
}
|
||||
|
||||
float noise(in vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return -1.0 + 2.0 * mix(mix(hash12(i + vec2(0.0, 0.0)), hash12(i + vec2(1.0, 0.0)), u.x),
|
||||
mix(hash12(i + vec2(0.0, 1.0)), hash12(i + vec2(1.0, 1.0)), u.x), u.y);
|
||||
}
|
||||
|
||||
float sea_octave(vec2 uv, float choppy) {
|
||||
uv += noise(uv);
|
||||
vec2 wv = 1.0 - abs(sin(uv));
|
||||
vec2 swv = abs(cos(uv));
|
||||
wv = mix(wv, swv, wv);
|
||||
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
|
||||
}
|
||||
|
||||
float map(vec3 p, float time) {
|
||||
float freq = sea_freq;
|
||||
float amp = sea_height;
|
||||
float choppy = sea_choppy;
|
||||
vec2 uv = p.xz;
|
||||
uv.x *= 0.75;
|
||||
float h = 0.0;
|
||||
for (int i = 0; i < ITER_GEOMETRY; i++) {
|
||||
float d = sea_octave((uv + time) * freq, choppy);
|
||||
d += sea_octave((uv - time) * freq, choppy);
|
||||
h += d * amp;
|
||||
uv *= octave_m;
|
||||
freq *= 1.9;
|
||||
amp *= 0.22;
|
||||
choppy = mix(choppy, 1.0, 0.2);
|
||||
}
|
||||
return p.y - h;
|
||||
}
|
||||
|
||||
float map_detailed(vec3 p, float time) {
|
||||
float freq = sea_freq;
|
||||
float amp = sea_height;
|
||||
float choppy = sea_choppy;
|
||||
vec2 uv = p.xz;
|
||||
uv.x *= 0.75;
|
||||
float h = 0.0;
|
||||
for (int i = 0; i < ITER_FRAGMENT; i++) {
|
||||
float d = sea_octave((uv + time) * freq, choppy);
|
||||
d += sea_octave((uv - time) * freq, choppy);
|
||||
h += d * amp;
|
||||
uv *= octave_m;
|
||||
freq *= 1.9;
|
||||
amp *= 0.22;
|
||||
choppy = mix(choppy, 1.0, 0.2);
|
||||
}
|
||||
return p.y - h;
|
||||
}
|
||||
|
||||
vec3 get_normal_detailed(vec3 p, float eps, float time) {
|
||||
vec3 n;
|
||||
n.y = map_detailed(p, time);
|
||||
n.x = map_detailed(vec3(p.x + eps, p.y, p.z), time) - n.y;
|
||||
n.z = map_detailed(vec3(p.x, p.y, p.z + eps), time) - n.y;
|
||||
n.y = eps;
|
||||
return normalize(n);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// foam noise functions
|
||||
// ---------------------------------------------------------------------------
|
||||
float hash13(vec3 p) {
|
||||
uvec3 q = uvec3(ivec3(p)) * uvec3(1597334677u, 3812015801u, 2798796415u);
|
||||
uint n = (q.x ^ q.y ^ q.z) * 1597334677u;
|
||||
return float(n) * (1.0 / 4294967295.0);
|
||||
}
|
||||
|
||||
float hash3d(vec3 p) { return hash13(p); }
|
||||
|
||||
vec2 hash22(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.xx + p3.yz) * p3.zy);
|
||||
}
|
||||
|
||||
// voronoi cell noise: distance to nearest animated point per grid cell,
|
||||
// ideal for simulating foam clumps and bubbles
|
||||
float voronoi(vec2 uv) {
|
||||
vec2 n = floor(uv);
|
||||
vec2 f = fract(uv);
|
||||
float m_dist = 1.0;
|
||||
for (int j = -1; j <= 1; j++) {
|
||||
for (int i = -1; i <= 1; i++) {
|
||||
vec2 g = vec2(float(i), float(j));
|
||||
vec2 o = hash22(n + g);
|
||||
// animate the point within the cell
|
||||
o = 0.5 + 0.5 * sin(TIME * foam_noise_speed + 6.2831 * o);
|
||||
vec2 r = g - f + o;
|
||||
float d = dot(r, r);
|
||||
m_dist = min(m_dist, d);
|
||||
}
|
||||
}
|
||||
return m_dist;
|
||||
}
|
||||
|
||||
float noise3d(in vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(
|
||||
mix(
|
||||
mix(hash3d(i + vec3(0.0, 0.0, 0.0)), hash3d(i + vec3(1.0, 0.0, 0.0)), f.x),
|
||||
mix(hash3d(i + vec3(0.0, 1.0, 0.0)), hash3d(i + vec3(1.0, 1.0, 0.0)), f.x), f.y),
|
||||
mix(
|
||||
mix(hash3d(i + vec3(0.0, 0.0, 1.0)), hash3d(i + vec3(1.0, 0.0, 1.0)), f.x),
|
||||
mix(hash3d(i + vec3(0.0, 1.0, 1.0)), hash3d(i + vec3(1.0, 1.0, 1.0)), f.x), f.y), f.z);
|
||||
}
|
||||
|
||||
float fbm_voronoi(vec2 uv) {
|
||||
float v = 0.0;
|
||||
float a = 0.5;
|
||||
vec2 shift = vec2(100.0);
|
||||
mat2 rot = mat2(vec2(cos(0.5), sin(0.5)), vec2(-sin(0.5), cos(0.5)));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float val = 1.0 - voronoi(uv);
|
||||
val = pow(val, 2.0); // sharpen bubbles
|
||||
v += a * val;
|
||||
uv = rot * uv * 2.0 + shift;
|
||||
a *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// GET LINEAR DEPTH FROM DEPTH TEXTURE
|
||||
float get_linear_depth(sampler2D d_tex, vec2 uv, mat4 inv_proj) {
|
||||
float depth = texture(d_tex, uv).x;
|
||||
vec3 ndc = vec3(uv * 2.0 - 1.0, depth);
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
ndc.z = depth * 2.0 - 1.0;
|
||||
#endif
|
||||
vec4 view = inv_proj * vec4(ndc, 1.0);
|
||||
return -view.z / view.w;
|
||||
}
|
||||
|
||||
varying vec3 world_vert;
|
||||
varying float wave_height;
|
||||
varying vec3 vertex_normal_world;
|
||||
|
||||
void vertex() {
|
||||
vec3 world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
world_pos.xz += wave_offset; // ← add
|
||||
wave_height = -map(world_pos, wave_time);
|
||||
VERTEX.y = wave_height;
|
||||
world_vert = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
world_vert.xz += wave_offset; // ← add
|
||||
float vertex_eps = 0.1;
|
||||
float h_center = wave_height;
|
||||
float h_x = -map(world_pos + vec3(vertex_eps, 0.0, 0.0), wave_time);
|
||||
float h_z = -map(world_pos + vec3(0.0, 0.0, vertex_eps), wave_time);
|
||||
vec3 n_geom = normalize(vec3(h_center - h_x, vertex_eps, h_center - h_z));
|
||||
vertex_normal_world = n_geom;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
// linear depth of the water surface itself
|
||||
float water_linear_depth = -VERTEX.z;
|
||||
// linear depth of what's behind the water
|
||||
float bg_linear_depth = get_linear_depth(DEPTH_TEXTURE, SCREEN_UV, INV_PROJECTION_MATRIX);
|
||||
float thickness = max(0.0, bg_linear_depth - water_linear_depth);
|
||||
|
||||
// normals & distance smoothing
|
||||
float dist_to_cam = length(VERTEX);
|
||||
float lod_epsilon = max(normal_epsilon, dist_to_cam * 0.005);
|
||||
vec3 detailed_normal = get_normal_detailed(world_vert, lod_epsilon, wave_time);
|
||||
float smooth_factor = clamp((dist_to_cam - normal_smoothness_dist) / 200.0, 0.0, 1.0);
|
||||
vec3 final_normal = mix(detailed_normal, vertex_normal_world, smooth_factor);
|
||||
NORMAL = (VIEW_MATRIX * vec4(final_normal, 0.0)).xyz;
|
||||
|
||||
// refraction offset calculation
|
||||
vec3 view_vertex_normal = (VIEW_MATRIX * vec4(vertex_normal_world, 0.0)).xyz;
|
||||
vec2 normal_offset = NORMAL.xy - view_vertex_normal.xy;
|
||||
float ref_dist_factor = clamp(refraction_distance_fade / max(0.1, dist_to_cam), 0.0, 1.0);
|
||||
float ref_depth_mask = smoothstep(0.0, max_depth, thickness);
|
||||
vec2 refraction_offset = normal_offset * refraction_strength * ref_dist_factor * ref_depth_mask * 0.05;
|
||||
|
||||
vec2 distorted_uv = SCREEN_UV + refraction_offset;
|
||||
|
||||
// fix ghosting for pixels that are on boundary so they dont make water look weird
|
||||
float distorted_bg_depth = get_linear_depth(DEPTH_TEXTURE, distorted_uv, INV_PROJECTION_MATRIX);
|
||||
if (distorted_bg_depth < water_linear_depth - 0.001) {
|
||||
distorted_uv = SCREEN_UV;
|
||||
distorted_bg_depth = bg_linear_depth;
|
||||
}
|
||||
|
||||
vec3 screen_color = texture(SCREEN_TEXTURE, distorted_uv).rgb;
|
||||
thickness = max(0.0, distorted_bg_depth - water_linear_depth);
|
||||
|
||||
// caustics
|
||||
vec4 bg_ndc = vec4(distorted_uv * 2.0 - 1.0, texture(DEPTH_TEXTURE, distorted_uv).x, 1.0);
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
bg_ndc.z = bg_ndc.z * 2.0 - 1.0;
|
||||
#endif
|
||||
vec4 bg_world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * bg_ndc;
|
||||
vec3 bg_world_pos = bg_world.xyz / bg_world.w;
|
||||
if (thickness > 0.0) {
|
||||
vec2 caustics_uv1 = bg_world_pos.xz * caustics_scale;
|
||||
caustics_uv1.x += TIME * caustics_speed;
|
||||
vec2 caustics_uv2 = bg_world_pos.xz * caustics_scale * 0.7;
|
||||
caustics_uv2.y -= TIME * caustics_speed * 0.8;
|
||||
float caustics_sample1 = texture(caustics_texture, caustics_uv1).r;
|
||||
float caustics_sample2 = texture(caustics_texture, caustics_uv2).r;
|
||||
float caustics_value = caustics_sample1 * caustics_sample2;
|
||||
float caustics_fade = 1.0 - clamp(thickness * caustics_depth_fade, 0.0, 1.0);
|
||||
screen_color += caustics_value * caustics_intensity * caustics_fade;
|
||||
}
|
||||
|
||||
// underwater fog ratio
|
||||
float fade_range = max(0.001, max_depth - fade_start_depth);
|
||||
float depth_ratio = clamp((thickness - fade_start_depth) / fade_range, 0.0, 1.0);
|
||||
|
||||
// beer-lambert absorption
|
||||
vec3 transmittance = exp(-thickness * water_absorption);
|
||||
vec3 water_volume_color = mix(base_tint_color, deep_color, depth_ratio);
|
||||
vec3 apparent_seabed_color = screen_color * water_volume_color; // screen_color = seabed color
|
||||
ALBEDO = mix(underwater_fog_color, apparent_seabed_color, transmittance);
|
||||
|
||||
// foam calculation
|
||||
float foam_mask_primary = 0.0;
|
||||
float foam_mask_shadow = 0.0;
|
||||
|
||||
// density factor: 1.0 at shore/crests, 0.0 at deep water
|
||||
float depth_foam_factor = smoothstep(foam_depth_start, foam_depth_end, thickness);
|
||||
float wave_crest_factor = smoothstep(foam_crest_threshold, foam_crest_threshold - 0.1, final_normal.y);
|
||||
wave_crest_factor *= foam_crest_amount;
|
||||
float foam_level = clamp(depth_foam_factor + wave_crest_factor, 0.0, 1.0);
|
||||
|
||||
// domain warping for swirling, flowing foam motion
|
||||
vec2 flow_uv = world_vert.xz * 0.5 + TIME * 0.05 * foam_noise_speed;
|
||||
vec2 warp = vec2(
|
||||
noise(flow_uv),
|
||||
noise(flow_uv + vec2(5.2, 1.3))
|
||||
) * 0.5;
|
||||
|
||||
if (distorted_bg_depth > water_linear_depth) {
|
||||
vec2 foam_uv = world_vert.xz * foam_noise_scale + warp;
|
||||
|
||||
// emergence noise creates pulsing/clumping so foam doesn't appear everywhere at once
|
||||
float emergence_noise = noise3d(vec3(world_vert.xz * 0.5, TIME * 0.2 * foam_noise_speed));
|
||||
emergence_noise = smoothstep(0.0, 1.0, emergence_noise * 0.5 + 0.5);
|
||||
|
||||
// near shores and crests, emergence is forced to full strength
|
||||
float effective_emergence = mix(emergence_noise, 1.0, foam_level);
|
||||
|
||||
// main foam sample
|
||||
float foam_noise_val = fbm_voronoi(foam_uv);
|
||||
foam_noise_val *= effective_emergence;
|
||||
|
||||
// shadow sample at slight offset, faking thickness and separation from the water
|
||||
vec2 foam_uv_shadow = (world_vert.xz + foam_edge_offset) * foam_noise_scale + warp;
|
||||
float foam_noise_shadow_val = fbm_voronoi(foam_uv_shadow);
|
||||
foam_noise_shadow_val *= effective_emergence;
|
||||
|
||||
// clumping/mask generation
|
||||
float combined_noise = foam_noise_val + foam_level;
|
||||
float combined_shadow = foam_noise_shadow_val + foam_level;
|
||||
|
||||
foam_mask_primary = smoothstep(foam_cutoff + 0.5, foam_cutoff + 0.6, combined_noise);
|
||||
float shadow_shape = smoothstep(foam_cutoff + 0.5, foam_cutoff + 0.6, combined_shadow);
|
||||
foam_mask_shadow = clamp(shadow_shape - foam_mask_primary, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// voronoi bubble texture inside the foam
|
||||
float voronoi_val = 1.0 - sqrt(voronoi(world_vert.xz * voronoi_scale + warp));
|
||||
voronoi_val = smoothstep(0.2, 0.8, voronoi_val);
|
||||
|
||||
float bubble_alpha = 1.0 - clamp((1.0 - voronoi_val) * voronoi_strength, 0.0, 1.0);
|
||||
|
||||
// apply foam
|
||||
ALBEDO = mix(ALBEDO, foam_edge_color, foam_mask_shadow);
|
||||
ALBEDO = mix(ALBEDO, foam_color, foam_mask_primary * bubble_alpha);
|
||||
|
||||
// make foam rougher
|
||||
ROUGHNESS = mix(roughness, 0.8, (foam_mask_primary * bubble_alpha) + foam_mask_shadow);
|
||||
|
||||
SPECULAR = specular;
|
||||
METALLIC = metallic;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cst73i45poweu
|
||||
Reference in New Issue
Block a user