Files
2026-09-23 17:37:07 +08:00

121 lines
5.3 KiB
GDScript

extends Node3D
## 可复用的海面驱动 · 只管「让海动起来」,不含任何玩法交互
##
## 三件事:
## 1. 推进 world_time —— 波形相位统一由这里给,不用着色器的内置 TIME
## 2. 把场景里的平行光方向推给水面着色器,让波光与实际光照同向
## 3. 近处细网格跟着当前相机平移(远处那层大平面不动)
##
## 用法:把 sea.tscn 实例化到任意场景即可,不需要接线。
## 相机是运行时从 viewport 取的(玩家的第三人称相机、编辑器视口相机都算),
## 所以本文件不依赖任何具体玩家或相机节点。
##
## 关于 world_time(这是本文件最重要的一个决定):
## 着色器里的 TIME 是「各进程自己的启动计时」。联机时两端启动时刻差几秒,
## 同一时刻看到的浪就不是同一片 —— 两个玩家站在一片海上,浪却不一样,一眼就看出来。
## 所以波形相位必须来自一个「大家都同意的时钟」:
## · 现在:Time.get_ticks_msec() 减去本节点启动时刻
## · 要联机同步时:把 _start_msec 换成主机下发的时间基准(一个 float,RPC 一次即可)
## 注意用「绝对时钟差」而不是逐帧累加 delta —— 两端帧率不同,累加会越走越偏。
##
## 水面材质是**自动收集**的:场景里任何挂了带 world_time 的 ShaderMaterial 的
## MeshInstance3D 都会被接管。之后再加几层水(远平面、沼泽、水坑)不用改代码。
@export_group("水面")
## 跟随相机的那层近水面(细网格)。留空则取名为 Water 的子节点
@export var near_water: MeshInstance3D
## 网格对齐步长(米)。应当 ≈ 顶点间距:波形是世界坐标锚定的,
## 不对齐的话顶点会在波形上「滑动」,看上去像水面在抖
@export var follow_snap: float = 2.5
## 波形推进倍率。1.0 = 着色器里的默认速度;0 = 冻结
@export var time_scale: float = 1.0
@export_group("光照")
## 提供太阳方向的光源。留空则自动找:父节点下的 Sun → 全场景第一个平行光
@export var sun: DirectionalLight3D
var _start_msec: int = 0
var _water_materials: Array[ShaderMaterial] = []
func _ready() -> void:
_start_msec = Time.get_ticks_msec()
if near_water == null:
near_water = get_node_or_null("Water") as MeshInstance3D
if sun == null:
sun = _find_sun()
_collect_water_materials()
if _water_materials.is_empty():
push_warning("%s 没找到任何水面材质,海不会动" % get_path())
_follow_camera()
func _process(_delta: float) -> void:
# 绝对时钟差,不是逐帧累加(原因见文件头)
var t := float(Time.get_ticks_msec() - _start_msec) / 1000.0 * time_scale
# 「指向太阳」= 平行光节点的 +Z。
# 平行光沿自己的 -Z 照射(与相机朝向同一套约定),所以 +Z 才是朝天;
# 天空着色器里那个 LIGHT0_DIRECTION 也是这个方向 —— 它的 y 必须为正,否则天空会进夜色。
# 取反是最容易犯的错:符号错了不会报错,只是波光整片消失(half-vector 朝下,
# dot(N, H) 恒为负),看上去像「波光强度调太小了」。
var sun_dir := Vector3.UP if sun == null else sun.global_transform.basis.z
for mat in _water_materials:
mat.set_shader_parameter("world_time", t)
mat.set_shader_parameter("sun_direction", sun_dir)
_follow_camera()
## 把本子树里每个「声明了 world_time 的水面材质」都收进来。
## 用 uniform 名字认水面,比按文件名或目录判断稳:改文件名、复制一份出来调参都还能认出。
func _collect_water_materials() -> void:
_water_materials.clear()
var seen := {}
for node in find_children("*", "MeshInstance3D", true, false):
var mi := node as MeshInstance3D
if mi == null:
continue
var mat := mi.material_override as ShaderMaterial
if mat == null or mat.shader == null:
continue
var key := mat.get_instance_id()
if seen.has(key):
continue
if not _declares_world_time(mat):
continue
seen[key] = true
_water_materials.append(mat)
func _declares_world_time(mat: ShaderMaterial) -> bool:
for uniform in mat.shader.get_shader_uniform_list():
if str(uniform.get("name")) == "world_time":
return true
return false
func _find_sun() -> DirectionalLight3D:
var parent := get_parent()
if parent != null:
var named := parent.find_child("Sun", true, false) as DirectionalLight3D
if named != null:
return named
var root := get_tree().current_scene if get_tree().current_scene != null else get_tree().root
for node in root.find_children("*", "DirectionalLight3D", true, false):
return node as DirectionalLight3D
return null
## 近平面跟着相机平移。均匀网格没法同时做到「近处够密」和「覆盖到地平线」,
## 所以近处用跟着相机走的小密网,远处另有一层大平面(它的 swell_amount = 0,纯平)。
## 波形在世界空间锚定,所以平面平移不会让浪跟着滑。
func _follow_camera() -> void:
if near_water == null:
return
var cam := get_viewport().get_camera_3d() if get_viewport() != null else null
if cam == null:
return
var step := maxf(follow_snap, 0.01)
var c := cam.global_position
near_water.global_position.x = roundf(c.x / step) * step
near_water.global_position.z = roundf(c.z / step) * step