126 lines
4.2 KiB
GDScript
126 lines
4.2 KiB
GDScript
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)
|