144 lines
5.7 KiB
GDScript
144 lines
5.7 KiB
GDScript
extends Node3D
|
|
## 海洋效果场景 · 只做视觉,不含任何玩法交互
|
|
##
|
|
## 四件事:
|
|
## 1. 推进 world_time —— 波形相位统一由这里给,不用着色器的内置 TIME
|
|
## 2. 把 Sun 节点方向推给水面着色器,让波光与场景光照同向
|
|
## 3. 近处细网格跟着相机平移(远处那层大平面不动)
|
|
## 4. 缓慢环绕相机,水面动起来才看得出来
|
|
##
|
|
## 直接用 F6(运行当前场景)即可,不依赖 Net / MultiplayerSpawner。
|
|
##
|
|
## 关于 world_time(这是本文件最重要的一个决定):
|
|
## 着色器里的 TIME 是「各进程自己的启动计时」。联机时两端启动时刻差几秒,
|
|
## 同一时刻看到的浪就不是同一片 —— 两个玩家站在一片海上,浪却不一样,一眼就看出来。
|
|
## 所以波形相位必须来自一个「大家都同意的时钟」:
|
|
## · 单机 / 效果场景:用 Time.get_ticks_msec() 自己减去启动时刻(本文件现在就是这样)
|
|
## · 联机:把这个基准时刻换成主机下发的同步时间戳(一个 float,RPC 一次即可)
|
|
## 注意用「绝对时钟差」而不是逐帧累加 delta —— 两端帧率不同,累加会越走越偏。
|
|
|
|
@export_group("水面")
|
|
## 跟随相机的那层近水面(细网格)。留空则取名为 Water 的子节点
|
|
@export var near_water: MeshInstance3D
|
|
## 网格对齐步长(米)。应当 ≈ 顶点间距:波形是世界坐标锚定的,
|
|
## 不对齐的话顶点会在波形上「滑动」,看上去像水面在抖
|
|
@export var follow_snap: float = 2.5
|
|
## 波形推进倍率。1.0 = 着色器里的默认速度
|
|
@export var time_scale: float = 1.0
|
|
|
|
@export_group("相机")
|
|
## 是否自动环绕(关掉就停在当前机位,方便截图)
|
|
@export var orbit_enabled: bool = true
|
|
## 环绕角速度(度/秒)
|
|
@export var orbit_speed_deg: float = 5.0
|
|
## 起始方位角(度)
|
|
@export var orbit_start_deg: float = 35.0
|
|
## 环绕半径(米)
|
|
@export var orbit_radius: float = 46.0
|
|
## 相机离水面高度(米)
|
|
@export var orbit_height: float = 13.0
|
|
## 注视点离水面高度(米)。略高于水面,构图不会被岛顶顶到
|
|
@export var look_at_height: float = 2.0
|
|
|
|
@onready var sun: DirectionalLight3D = $Sun
|
|
@onready var camera: Camera3D = $Camera3D
|
|
|
|
var _start_msec: int = 0
|
|
var _orbit_angle: float = 0.0
|
|
## 场景里所有水面材质(近平面 + 远平面)。
|
|
## 自动收集,不靠手工把每个材质拖到导出版位上
|
|
var _water_materials: Array[ShaderMaterial] = []
|
|
|
|
|
|
func _ready() -> void:
|
|
_start_msec = Time.get_ticks_msec()
|
|
_orbit_angle = deg_to_rad(orbit_start_deg)
|
|
_hide_net_ui()
|
|
if near_water == null:
|
|
near_water = get_node_or_null("Water") as MeshInstance3D
|
|
_collect_water_materials()
|
|
if _water_materials.is_empty():
|
|
push_warning("没找到任何水面材质,水面不会动")
|
|
_update_camera()
|
|
_follow_camera()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
# 绝对时钟差,不是逐帧累加(原因见文件头)
|
|
var t := float(Time.get_ticks_msec() - _start_msec) / 1000.0 * time_scale
|
|
var sun_dir := Vector3.UP
|
|
if sun != null:
|
|
# 平行光的照射方向是节点的 -Z,取反就是指向太阳
|
|
# 「指向太阳」= 平行光节点的 +Z(沿 -Z 照射)。取反会让波光整片消失,且不报错
|
|
sun_dir = 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)
|
|
|
|
if orbit_enabled:
|
|
_orbit_angle += deg_to_rad(orbit_speed_deg) * delta
|
|
_update_camera()
|
|
_follow_camera()
|
|
|
|
|
|
## 把场景里每个「声明了 world_time 的水面材质」都收进来。
|
|
## 这样再加几层水面(远平面、沼泽、水坑)都不用改代码,也不用手工接线。
|
|
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)
|
|
|
|
|
|
## 用「有没有 world_time 这个 uniform」认水面,比按文件名/目录判断稳:
|
|
## 改文件名、复制一份材质出来调参,都还能认出来
|
|
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 _update_camera() -> void:
|
|
if camera == null:
|
|
return
|
|
var center := global_position + Vector3(0.0, look_at_height, 0.0)
|
|
camera.global_position = global_position + Vector3(
|
|
cos(_orbit_angle) * orbit_radius,
|
|
orbit_height,
|
|
sin(_orbit_angle) * orbit_radius
|
|
)
|
|
camera.look_at(center, Vector3.UP)
|
|
|
|
|
|
## 近平面跟着相机平移。均匀网格没法同时做到「近处够密」和「覆盖到地平线」,
|
|
## 所以近处用跟着相机走的小密网,远处另有一层大平面(它的 swell_amount = 0,纯平)。
|
|
func _follow_camera() -> void:
|
|
if near_water == null or camera == null:
|
|
return
|
|
var step := maxf(follow_snap, 0.01)
|
|
var c := camera.global_position
|
|
near_water.global_position.x = roundf(c.x / step) * step
|
|
near_water.global_position.z = roundf(c.z / step) * step
|
|
|
|
|
|
## Net 是 autoload 的联机菜单,一 _ready 就把整个 UI 铺上来。
|
|
## 本场景只是看效果,用不到它 —— 整层藏掉。不改它的逻辑,
|
|
## 退出本场景后其它场景照常(_ready 重新跑一次)。
|
|
func _hide_net_ui() -> void:
|
|
var net := get_node_or_null("/root/Net")
|
|
if net is CanvasLayer:
|
|
(net as CanvasLayer).visible = false
|