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)