海洋
This commit is contained in:
@@ -141,3 +141,94 @@ PlayerAnimator 的成员,player.gd 里已无这两个属性(测试里有一
|
||||
4. **中文字符串用 `\uXXXX` 转义极易打错字**(本轮把「抽搐」打成「抽搝」、「照抄」打成
|
||||
「照拄」,都要事后 grep 才发现)。直接传字面中文更稳。
|
||||
|
||||
|
||||
## 海洋效果场景(孤岛环海 · 纯视觉)
|
||||
|
||||
新增 `world/ocean/`:
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `water.gdshader` | 卡通海面:底色 / 菲涅尔 / 波光 / 泡沫 + 地平线雾霭 |
|
||||
| `island.gdshader` | 卡通小岛:径向剖面 + 噪声海岸线,按高度与坡度上色 |
|
||||
| `water_far_material.tres` | 远平面材质(同一个 shader,`swell_amount = 0`) |
|
||||
| `ocean_demo.gd` | 驱动脚本:推 world_time / 太阳方向 / 相机环绕 / 近平面跟随 |
|
||||
| `ocean_demo.tscn` | 效果场景,**F6 直接跑**(会把 Net 的联机菜单整层藏掉) |
|
||||
|
||||
## 关键设计(都是被逼出来的)
|
||||
|
||||
- **两层水面:近处细平面(520×520 / 208 细分 / 跟随相机)+ 远处大平面(6000×6000 / 12 细分 / 纯平)。**
|
||||
均匀网格做不了「近处要密」和「覆盖到地平线」这两件事。而天空在地平线以下是
|
||||
一片深色半球,**有限大的水面与真地平线之间必然露出深色带**(带宽 ≈ atan(相机高 / 平面半宽))。
|
||||
两层结构把带宽压到 0.25° 以内。近平面网格对齐到顶点间距(2.5m),否则会抖。
|
||||
- **波形用世界坐标锚定**(vertex 里先 `MODEL_MATRIX * VERTEX` 取世界 xz 再算高度)。
|
||||
这样近平面跟着相机平移时,海浪不会「跟着滑」。
|
||||
- **位移只用 3 组低频正弦(幅度 0.38m),细纹 + 噪点只动法线、不给位移。**
|
||||
所以顶点密度只影响大浪的圆滑度,细节全在法线里 —— 这是让粗网格也能看的关键。
|
||||
- `world_time` 由脚本推进,不用内置 `TIME`:联机两端启动时刻不同,同一时刻的浪不是同一片。
|
||||
脚本里用「绝对时钟差」而不是逐帧累加 delta(帧率不同会越走越偏)。
|
||||
- 岸线泡沫靠 `DEPTH_TEXTURE` 拿真实水深(水是半透明的,走透明队列、在不透明物之后绘制)。
|
||||
|
||||
## 踩的四个坑
|
||||
|
||||
1. **`TAU` / `PI` / `E` 是着色器语言的内置常量。** 自己 `const float TAU` 直接编译失败。
|
||||
更坑的是:**编译失败的外观是「没着色器的一团白」**,看起来像参数没调好,会往错的方向查。
|
||||
2. **只有正弦、没有噪声的法线 → 太阳波光变成一张规则网格。**
|
||||
高光落在「法线刚好平分视线与阳光」的位置,周期性法线场的高光自然是周期性的。
|
||||
3. **打散它的噪声必须用梯度噪声,不能用值噪声。** 值噪声当「高度」用看不出来,
|
||||
但拿来求导当法线就是一块块方格。(卡通天空挑 Perlin 也是这个理由,直接沿用。)
|
||||
4. **片元阶段的 `VERTEX` 是视图空间,不是世界空间。** 小岛 shader 一开始写
|
||||
`(MODEL_MATRIX * vec4(VERTEX, 1.0)).y` 当海拔用,算出来其实是相机相对高度 ——
|
||||
整座岛被染成一大块沙滩,而且「看上去好像也对」。改成 varying 从顶点传海拔。
|
||||
|
||||
另外:两组正弦方向**接近平行**时会相消,把整层退化成 1D 驻波(表现为规则条纹)。
|
||||
方向要显式排开、夹角 90° 以上,别拿「看起来不一样」的坐标去拼。
|
||||
|
||||
## 调参结论(实测)
|
||||
|
||||
- 浪尖泡沫的分母必须是**三组振幅之和**;拿 `wave_height` 当分母永远溢出到 1,泡沫铺满海面。
|
||||
- 岸线泡沫 `shore_amount` 别超过 ~0.7,否则是一圈白色描边(像贴纸不像浪)。
|
||||
- `band_count` = 3 是「有色块感」与「不变斑马线」的平衡点。
|
||||
- 细纹和噪点必须**按距离淡出**(`ripple_fade_distance`),不然远处满屏闪烁。
|
||||
|
||||
## MCP 相关
|
||||
|
||||
- 编辑器在 play 模式下**拒绝一切写操作**(`EDITOR_NOT_READY`)→ 改文件前先
|
||||
`project_manage(op="stop")`。
|
||||
- `.gdshader` 只能用 `filesystem_manage(op="write_text")` 写,**没有 patch 能力**,
|
||||
每次改都要重写整个文件。
|
||||
- `node_set_property` **设不了 `shader_parameter/*`**(报 `PROPERTY_NOT_ON_CLASS`,
|
||||
这些是虚拟属性)。要么改 shader 里的默认值,要么写一个 `.tres` 引用它。
|
||||
- 但 `node_set_property` **支持内嵌资源 + 嵌套资源路径**:
|
||||
`{"__class__": "Environment", "background_mode": 2, "sky": "res://..."}` 是可行的。
|
||||
- 最强的验证手段是 `editor_screenshot(source="game")` —— 把运行中的画面截回来直接看,
|
||||
比自己推断「应该没问题」可靠得多。三个真 bug 全是靠它发现的。
|
||||
|
||||
## 既有问题(未动)
|
||||
|
||||
`res://addons/ocean3d_lite/` 是坏的:`ocean_surface.gd` / `floating_body.gd` 都报
|
||||
`Identifier "Ocean" not declared`。看起来是以前试过的第三方海洋插件,与本次无关。
|
||||
|
||||
## 海洋 · 补充(回答「海是不是静态的」时顺手修的)
|
||||
|
||||
**海是动的**,只是编辑器预览里曾经完全冻结:
|
||||
|
||||
- 实测运行时 `world_time` 1.5 秒走 1.502(`time_scale = 1.0`),波形确实在推进。
|
||||
- 但 `_ready()` / `_process()` 在编辑器里不跑 → 没人推 `world_time` → **未运行时海是死帧**。
|
||||
这就是「看着像静态」的原因。
|
||||
- 修法:`world_time` 默认值改成 `-1`,加 `wave_time()`:`world_time >= 0 ? world_time : TIME`。
|
||||
编辑器用 TIME(自动动),运行时脚本一接管就换成同步时钟。
|
||||
**比加 `@tool` 安全** —— `@tool` 会让 `_process` 在编辑器里跑,脚本一写相机 transform
|
||||
就不断标脏场景、还会挪动编辑器里的摆位。
|
||||
- 同理把静态相机取景(`position` + `rotation = (-0.235, 0.96, 0)`)烘进场景,
|
||||
否则编辑器预览里相机朝向是 identity,看着是一片空海。
|
||||
|
||||
**新发现的 Godot 行为:`ShaderMaterial.get_shader_parameter()` 对没显式改过的参数返回 `null`。**
|
||||
Godot 不会把 shader 的 uniform 默认值灌进材质实例 —— 所以别用它读默认值。
|
||||
反过来也证明「只改 shader 默认值就能生效」。
|
||||
|
||||
**编辑器视口会持续重绘**(连拍两张 `editor_screenshot(source="cinematic")`,波形和云都位移了),
|
||||
所以能用截两张对比的办法验证「编辑器里动没动」。
|
||||
|
||||
海水运动速度(按默认值算):大浪相位速度 ≈ 6.7 m/s(波长 42 m),
|
||||
细纹漂移 ≈ 0.5 m/s,噪点漂移 ≈ 0.49 m/s。大浪看得清,细碎波光是缓慢流动。
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
shader_type spatial;
|
||||
|
||||
/* ===========================================================================
|
||||
卡通小岛 · 纯程序化,零贴图
|
||||
---------------------------------------------------------------------------
|
||||
一块平面按「径向剖面 + 噪声」抬起来,再按高度与坡度上色:
|
||||
水下/湿沙 → 沙滩 → 草地 → 岩石
|
||||
|
||||
三个细节决定了它像不像岛:
|
||||
· 海岸线加噪声扰动,否则是一个完美的圆,假得刺眼
|
||||
· 岩石不只看高度,也看坡度 —— 陡的地方自然露岩,比只按海拔分层真实得多
|
||||
· profile_power 越小越像「山」,越大越像「沙洲」。1.3 左右是个锥形岛;
|
||||
调到 2 以上就变成一块平坦的沙滩,远处看着像浮在水面的饼
|
||||
|
||||
外面的裙边主动扎到水下(dip_depth),免得平面边缘悬空露馅。
|
||||
法线用有限差分重算,因为几何被位移过了,原网格法线已经不对。
|
||||
|
||||
坑(实测):**片元阶段的 VERTEX 是视图空间,不是世界空间。**
|
||||
一开始写成 (MODEL_MATRIX * vec4(VERTEX, 1.0)).y 当海拔用,算出来其实是
|
||||
相机相对高度 —— 结果是整座岛被染成一大块沙滩,而且看上去「好像也对」。
|
||||
海拔要嘛用 varying 从顶点传下来(现在这样),
|
||||
要嘛用 (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).y,别拿 MODEL_MATRIX 去乘。
|
||||
|
||||
前提:节点摆在水面高度上,不旋转、不缩放。
|
||||
=========================================================================== */
|
||||
|
||||
render_mode cull_disabled, diffuse_toon, specular_disabled;
|
||||
|
||||
// ------------------------------------------------------------- 形状 -----
|
||||
group_uniforms Shape;
|
||||
uniform float island_radius : hint_range(2.0, 60.0) = 15.0; // 米:陆地半径
|
||||
uniform float island_height : hint_range(0.5, 30.0) = 9.0; // 米:最高点高出水面多少
|
||||
// 越小越像山、越大越像沙洲。调到 2 以上就成了一块浮在水上的饼
|
||||
uniform float profile_power : hint_range(0.5, 5.0) = 1.30;
|
||||
uniform float dip_depth : hint_range(0.0, 30.0) = 12.0; // 米:裙边扎到水下多深
|
||||
|
||||
// --------------------------------------------------------- 海岸与地表 -----
|
||||
group_uniforms Terrain;
|
||||
uniform float coast_scale : hint_range(0.01, 0.50) = 0.055; // 海岸线噪声密度
|
||||
uniform float coast_warp : hint_range(0.0, 0.60) = 0.22; // 海岸线扭曲幅度
|
||||
uniform float detail_scale : hint_range(0.02, 2.0) = 0.22; // 地表起伏密度
|
||||
uniform float detail_strength : hint_range(0.0, 8.0) = 2.20; // 地表起伏幅度(米)
|
||||
|
||||
// ------------------------------------------------------------- 上色 -----
|
||||
group_uniforms Paint;
|
||||
uniform vec3 sand_color : source_color = vec3(0.930, 0.870, 0.680);
|
||||
uniform vec3 grass_color : source_color = vec3(0.440, 0.720, 0.360);
|
||||
uniform vec3 rock_color : source_color = vec3(0.550, 0.560, 0.600);
|
||||
uniform vec3 wet_color : source_color = vec3(0.620, 0.600, 0.440); // 水下 / 水线附近的湿沙
|
||||
uniform float wet_line : hint_range(-2.0, 2.0) = 0.15; // 米:水线(y=0 就是水面)
|
||||
uniform float sand_line : hint_range(0.0, 8.0) = 0.70; // 米:沙地到此结束
|
||||
uniform float grass_line : hint_range(0.0, 20.0) = 3.40; // 米:草地到此结束,以上是岩
|
||||
uniform float rock_slope : hint_range(0.0, 4.0) = 0.70; // 坡度大于此值直接露岩
|
||||
uniform float band_soft : hint_range(0.02, 1.0) = 0.18; // 色阶过渡宽度(越小越硬)
|
||||
|
||||
// ===========================================================================
|
||||
// 噪声
|
||||
// ===========================================================================
|
||||
|
||||
float hash21(vec2 p) {
|
||||
p = fract(p * vec2(123.34, 456.21));
|
||||
p += dot(p, p + 45.32);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
float vnoise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
float a = hash21(i);
|
||||
float b = hash21(i + vec2(1.0, 0.0));
|
||||
float c = hash21(i + vec2(0.0, 1.0));
|
||||
float d = hash21(i + vec2(1.0, 1.0));
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
// 两层就够。这里值噪声可以用 —— 它只塑造「高度」,不拿它求导当法线
|
||||
//(法线是另外用有限差分在「位移后」重算的,见 vertex)。水面着色器里不能这么偷懒。
|
||||
float fbm2(vec2 p) {
|
||||
return vnoise(p) * 0.65 + vnoise(p * 2.07 + 13.1) * 0.35;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 高度场
|
||||
// ===========================================================================
|
||||
|
||||
// 返回高于水面的米数(水面 = 0)
|
||||
float island_at(vec2 p) {
|
||||
float r = length(p) / max(island_radius, 0.001);
|
||||
|
||||
// 海岸线:按噪声推拉半径,否则是一个完美的圆
|
||||
r += (fbm2(p * coast_scale) - 0.5) * coast_warp;
|
||||
|
||||
// 径向剖面:山顶有平台、山脚收得快
|
||||
float prof = 1.0 - smoothstep(0.0, 1.0, clamp(r, 0.0, 1.0));
|
||||
prof = pow(prof, max(profile_power, 0.001));
|
||||
float h = prof * island_height;
|
||||
|
||||
// 地表起伏:只作用在陆地上,海岸线附近淡出,否则海岸会碎成齿状
|
||||
h += (fbm2(p * detail_scale) - 0.5) * detail_strength * smoothstep(1.0, 0.35, r);
|
||||
|
||||
// 裙边:岛外主动扎到水下,免得平面边缘悬空
|
||||
h -= dip_depth * smoothstep(0.85, 1.30, r);
|
||||
return h;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
// 海拔从顶点传下来。**不要在片元里拿 MODEL_MATRIX 乘 VERTEX**(见文件头)
|
||||
varying float v_height;
|
||||
|
||||
void vertex() {
|
||||
vec2 p = VERTEX.xz;
|
||||
float h = island_at(p);
|
||||
VERTEX.y += h;
|
||||
v_height = h;
|
||||
|
||||
// 几何被位移过了,原网格法线不再正确 —— 用有限差分重算。
|
||||
// 四个额外采样换一个到处都对(包括噪声起伏)的法线,很划算。
|
||||
const float e = 0.35;
|
||||
float hx = island_at(p + vec2(e, 0.0)) - island_at(p - vec2(e, 0.0));
|
||||
float hz = island_at(p + vec2(0.0, e)) - island_at(p - vec2(0.0, e));
|
||||
NORMAL = normalize(vec3(-hx / (2.0 * e), 1.0, -hz / (2.0 * e)));
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float h = v_height;
|
||||
|
||||
// 陡峭度:1 - n.y。与坐标系无关,不用管 NORMAL 在哪个空间
|
||||
float steep = 1.0 - clamp(NORMAL.y, 0.0, 1.0);
|
||||
|
||||
vec3 c = sand_color;
|
||||
c = mix(c, grass_color, smoothstep(sand_line - band_soft, sand_line + band_soft, h));
|
||||
c = mix(c, rock_color, max(
|
||||
smoothstep(grass_line - band_soft, grass_line + band_soft, h),
|
||||
smoothstep(rock_slope, rock_slope * 1.6, steep)
|
||||
));
|
||||
// 水下的部分压成湿沙色
|
||||
c = mix(c, wet_color, smoothstep(wet_line + band_soft, wet_line - 0.30, h));
|
||||
|
||||
ALBEDO = c;
|
||||
ROUGHNESS = 1.0;
|
||||
METALLIC = 0.0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1l20nbw5a5cv
|
||||
@@ -0,0 +1,142 @@
|
||||
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,取反就是指向太阳
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://17aed15wn1yu
|
||||
@@ -0,0 +1,124 @@
|
||||
[gd_scene format=3 uid="uid://cmljgtibtt52v"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://17aed15wn1yu" path="res://world/ocean/ocean_demo.gd" id="1_yme6p"]
|
||||
[ext_resource type="Sky" uid="uid://bi632suyifms5" path="res://world/sky/cartoon_sky.tres" id="2_furff"]
|
||||
[ext_resource type="Shader" uid="uid://cqlv11372k13t" path="res://world/ocean/water.gdshader" id="3_t7i7y"]
|
||||
[ext_resource type="Shader" uid="uid://d1l20nbw5a5cv" path="res://world/ocean/island.gdshader" id="4_ihafm"]
|
||||
[ext_resource type="Material" path="res://world/ocean/water_far_material.tres" id="5_furff"]
|
||||
|
||||
[sub_resource type="Environment" id="Environment_nmrd5"]
|
||||
background_mode = 2
|
||||
sky = ExtResource("2_furff")
|
||||
ambient_light_source = 3
|
||||
reflected_light_source = 2
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_47m0w"]
|
||||
render_priority = 0
|
||||
shader = ExtResource("3_t7i7y")
|
||||
shader_parameter/world_time = 0.0
|
||||
shader_parameter/wind_direction = Vector2(1, 0.25)
|
||||
shader_parameter/swell_amount = 1.0
|
||||
shader_parameter/wave_height = 0.38
|
||||
shader_parameter/wave_length = 42.0
|
||||
shader_parameter/wave_speed = 1.0
|
||||
shader_parameter/choppiness = 0.25
|
||||
shader_parameter/ripple_scale = 1.4
|
||||
shader_parameter/ripple_strength = 0.16
|
||||
shader_parameter/ripple_speed = 0.7
|
||||
shader_parameter/ripple_fade_distance = 110.0
|
||||
shader_parameter/grain_scale = 0.45
|
||||
shader_parameter/grain_strength = 0.32
|
||||
shader_parameter/grain_speed = 0.22
|
||||
shader_parameter/shallow_color = Color(0.24, 0.8, 0.76, 1)
|
||||
shader_parameter/deep_color = Color(0.03, 0.2, 0.45, 1)
|
||||
shader_parameter/depth_fade = 7.0
|
||||
shader_parameter/opacity_shallow = 0.62
|
||||
shader_parameter/opacity_deep = 0.97
|
||||
shader_parameter/sky_color = Color(0.38, 0.66, 0.98, 1)
|
||||
shader_parameter/horizon_color = Color(0.78, 0.88, 1, 1)
|
||||
shader_parameter/fresnel_power = 3.2
|
||||
shader_parameter/fresnel_amount = 0.82
|
||||
shader_parameter/band_count = 3.0
|
||||
shader_parameter/band_sharpness = 0.7
|
||||
shader_parameter/sun_direction = Vector3(0.35, 0.55, 0.35)
|
||||
shader_parameter/sun_color = Color(1, 0.96, 0.85, 1)
|
||||
shader_parameter/glitter_power = 320.0
|
||||
shader_parameter/glitter_strength = 0.85
|
||||
shader_parameter/glitter_bands = 3.0
|
||||
shader_parameter/foam_color = Color(0.96, 0.99, 1, 1)
|
||||
shader_parameter/shore_depth = 1.05
|
||||
shader_parameter/shore_amount = 0.6
|
||||
shader_parameter/crest_amount = 0.18
|
||||
shader_parameter/crest_start = 0.92
|
||||
shader_parameter/crest_fade_distance = 200.0
|
||||
shader_parameter/foam_bands = 3.0
|
||||
shader_parameter/haze_color = Color(0.8, 0.89, 1, 1)
|
||||
shader_parameter/haze_strength = 0.95
|
||||
shader_parameter/haze_power = 5.0
|
||||
shader_parameter/haze_distance = 90.0
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_t7i7y"]
|
||||
size = Vector2(520, 520)
|
||||
subdivide_width = 208
|
||||
subdivide_depth = 208
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_w7758"]
|
||||
render_priority = 0
|
||||
shader = ExtResource("4_ihafm")
|
||||
shader_parameter/island_radius = 16.0
|
||||
shader_parameter/island_height = 5.5
|
||||
shader_parameter/profile_power = 1.7
|
||||
shader_parameter/dip_depth = 9.0
|
||||
shader_parameter/coast_scale = 0.055
|
||||
shader_parameter/coast_warp = 0.22
|
||||
shader_parameter/detail_scale = 0.22
|
||||
shader_parameter/detail_strength = 1.6
|
||||
shader_parameter/sand_color = Color(0.93, 0.87, 0.68, 1)
|
||||
shader_parameter/grass_color = Color(0.44, 0.72, 0.36, 1)
|
||||
shader_parameter/rock_color = Color(0.55, 0.56, 0.6, 1)
|
||||
shader_parameter/wet_color = Color(0.62, 0.6, 0.44, 1)
|
||||
shader_parameter/wet_line = 0.15
|
||||
shader_parameter/sand_line = 0.9
|
||||
shader_parameter/grass_line = 2.6
|
||||
shader_parameter/rock_slope = 0.85
|
||||
shader_parameter/band_soft = 0.18
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_marqi"]
|
||||
size = Vector2(72, 72)
|
||||
subdivide_width = 144
|
||||
subdivide_depth = 144
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_ihafm"]
|
||||
size = Vector2(6000, 6000)
|
||||
subdivide_width = 12
|
||||
subdivide_depth = 12
|
||||
|
||||
[node name="OceanDemo" type="Node3D" unique_id=941353547]
|
||||
script = ExtResource("1_yme6p")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=776578237]
|
||||
environment = SubResource("Environment_nmrd5")
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=828158479]
|
||||
transform = Transform3D(0.8253356, -0.36375266, 0.4318624, 0, 0.7648422, 0.64421767, -0.5646425, -0.5316958, 0.6312515, 0, 0, 0)
|
||||
light_color = Color(1, 0.972549, 0.9019608, 1)
|
||||
light_energy = 1.1
|
||||
shadow_enabled = true
|
||||
directional_shadow_max_distance = 90.0
|
||||
|
||||
[node name="Water" type="MeshInstance3D" parent="." unique_id=92383993]
|
||||
material_override = SubResource("ShaderMaterial_47m0w")
|
||||
mesh = SubResource("PlaneMesh_t7i7y")
|
||||
|
||||
[node name="Island" type="MeshInstance3D" parent="." unique_id=1330851337]
|
||||
material_override = SubResource("ShaderMaterial_w7758")
|
||||
mesh = SubResource("PlaneMesh_marqi")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=2033278712]
|
||||
transform = Transform3D(0.57352, -0.19042432, 0.79675174, 0, 0.9726074, 0.23245396, -0.8191916, -0.133317, 0.55780977, 37.7, 13, 26.4)
|
||||
fov = 60.0
|
||||
|
||||
[node name="WaterFar" type="MeshInstance3D" parent="." unique_id=1656990011]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.8, 0)
|
||||
material_override = ExtResource("5_furff")
|
||||
mesh = SubResource("PlaneMesh_ihafm")
|
||||
@@ -0,0 +1,364 @@
|
||||
shader_type spatial;
|
||||
|
||||
/* ===========================================================================
|
||||
卡通海面 · 纯程序化,零贴图
|
||||
---------------------------------------------------------------------------
|
||||
渲染顺序: 水体底色 → 菲涅尔天空反射 → 太阳波光 → 泡沫 → 地平线雾霭
|
||||
|
||||
四层各管一件事,缺一层就露馅:
|
||||
1 底色 水越深越暗越蓝(靠 DEPTH_TEXTURE 拿到真实水深)
|
||||
2 菲涅尔 掠射角反射天空色、垂直看透出底色 —— 水面的「亮边」全靠它,最容易漏
|
||||
3 波光 太阳的镜面反射,压成硬边色块
|
||||
4 泡沫 岸线(水浅)+ 浪尖(波峰)两处,硬边
|
||||
|
||||
波形分工(这是本着色器最重要的一个决定):
|
||||
· 位移只用 3 组低频正弦 —— 决定轮廓。幅度刻意压得很小,
|
||||
大浪交给「明暗」而不是「几何」,否则顶点一密就是碾衣板
|
||||
· 细纹 + 噪点只扰动法线、不给位移 —— 决定波光,且按距离淡出
|
||||
|
||||
**波形时间来自哪里(world_time 与 TIME 的关系):**
|
||||
world_time >= 0 用传入值。运行时由 ocean_demo.gd 每帧推进(联机要同步的时钟)
|
||||
world_time < 0 用引擎内置 TIME。**编辑器预览就是这种情况** ——
|
||||
脚本在编辑器里不跑,所以没人推 world_time;
|
||||
没这个兼底的话,编辑器里看到的海是**完全冻结**的,
|
||||
只有按 F6 跑起来才动。
|
||||
两者默认值就是 -1,所以场景一打开(未运行)海就在动,不用做任何配置。
|
||||
|
||||
**五个坑,都是实测踩出来的:**
|
||||
① 纯正弦场只要两个方向接近平行,叠加后就会退化成一道规则条纹。
|
||||
方向必须显式排开(夹角 90° 以上),别拿「看起来不一样」的坐标去拼。
|
||||
② 只有正弦、没有噪声的法线,会让**太阳波光变成一张规则网格** ——
|
||||
高光落在「法线刚好平分视线与阳光」的位置,周期性法线场的高光自然周期性。
|
||||
③ 打散它的噪声**必须是梯度噪声,不能用值噪声**。值噪声的方形格子在
|
||||
当『高度』用的时候看不出来,但拿来求导当法线,就是一块块方格。
|
||||
④ TAU / PI / E 是着色器语言的内置常量,自己再 const 一遍会直接编译失败。
|
||||
而**编译失败的外观是「没着色器的一团白」**,很容易当成「参数没调好」去瞎调。
|
||||
⑤ 片元阶段的 VERTEX 是**视图空间**,不是世界空间(小岛着色器在那里把
|
||||
MODEL_MATRIX 乘上去当海拔用,结果整座岛被染成一大块沙滩)。
|
||||
|
||||
两处刻意与 cartoon_sky.gdshader 对齐:
|
||||
· 反射色 / 雾霭色直接取那条天空的色板,海天交界才能连成一条线
|
||||
· 时间不用内置 TIME(运行时)—— 联机两端启动时刻差几秒,看到的就不是同一片海
|
||||
|
||||
关于「近处细平面 + 远处大平面」两层水面:
|
||||
均匀网格做不了「近处要密、远处要远」这两件事。天空在地平线以下
|
||||
是一片深色半球,有限大的水面边缘与真地平线之间会露出那一条深色带
|
||||
(相机高 h、平面半宽 d 时,带宽 ≈ atan(h/d))。所以:
|
||||
· 近平面小而密(2.5 m/格),跟随相机走 —— 波形是世界坐标锚定的,不会滑
|
||||
· 远平面大而疏(约 500 m/格),把边缘推到 0.25° 以内,带宽只剩两三个像素
|
||||
两者用同一个着色器,靠 swell_amount 区分(远平面 = 0,纯平)。
|
||||
|
||||
前提:本节点不能旋转(平移、缩放都可以)。波形与法线都在模型空间算。
|
||||
调好的美术取值直接写在下面的 uniform 默认值里 —— 即使 .tres 里的
|
||||
shader_parameter 被清掉,画面也不会跑偏。
|
||||
=========================================================================== */
|
||||
|
||||
render_mode blend_mix, cull_disabled, unshaded, fog_disabled;
|
||||
|
||||
// --------------------------------------------------------------- 时间 -----
|
||||
group_uniforms Time;
|
||||
// -1 = 没人接管,用引擎 TIME(编辑器预览);>= 0 = 用传入值(运行时的同步时钟)
|
||||
uniform float world_time : hint_range(-1.0, 3600.0) = -1.0;
|
||||
// 风向。大浪与细纹都沿着它及其旋转方向铺开
|
||||
uniform vec2 wind_direction = vec2(1.0, 0.25);
|
||||
|
||||
// ------------------------------------------------------- 大浪(真实位移) ---
|
||||
group_uniforms Swell;
|
||||
// 1 = 正常水面;0 = 纯平(远平面用,不参与几何)
|
||||
uniform float swell_amount : hint_range(0.0, 1.0) = 1.0;
|
||||
uniform float wave_height : hint_range(0.0, 3.0) = 0.38; // 米:最低频那组的振幅
|
||||
uniform float wave_length : hint_range(2.0, 120.0) = 42.0; // 米:最低频那组的波长
|
||||
uniform float wave_speed : hint_range(0.0, 4.0) = 1.00; // 传播速度倍率
|
||||
uniform float choppiness : hint_range(0.0, 1.0) = 0.25; // 波峰尖锐度
|
||||
|
||||
// ------------------------------------------------- 细纹(只动法线,不动位移)---
|
||||
group_uniforms Ripple;
|
||||
uniform float ripple_scale : hint_range(0.05, 6.0) = 1.40; // 1/米:细纹密度
|
||||
uniform float ripple_strength : hint_range(0.0, 1.5) = 0.16; // 坡度大小(不是高度)
|
||||
uniform float ripple_speed : hint_range(0.0, 4.0) = 0.70;
|
||||
// 米:细纹与噪点在这么远以后淡出。**不做这个远处会满屏闪烁** ——
|
||||
// 空间频率一旦落到像素频率以下就是噪点,掠射角还会把它放大
|
||||
uniform float ripple_fade_distance : hint_range(10.0, 600.0) = 110.0;
|
||||
|
||||
// --------------------------------- 噪点(打散正弦场的规则性,见文件头坑②③)---
|
||||
group_uniforms Grain;
|
||||
uniform float grain_scale : hint_range(0.02, 3.0) = 0.45; // 1/米:噪点密度
|
||||
uniform float grain_strength : hint_range(0.0, 2.0) = 0.32; // 坡度大小
|
||||
uniform float grain_speed : hint_range(0.0, 4.0) = 0.22;
|
||||
|
||||
// ----------------------------------------------------------------- 水体 -----
|
||||
group_uniforms Water;
|
||||
uniform vec3 shallow_color : source_color = vec3(0.240, 0.800, 0.760); // 浅滩
|
||||
uniform vec3 deep_color : source_color = vec3(0.030, 0.200, 0.450); // 深海
|
||||
uniform float depth_fade : hint_range(0.5, 40.0) = 7.0; // 米:多深算「全深色」
|
||||
uniform float opacity_shallow : hint_range(0.0, 1.0) = 0.62;
|
||||
uniform float opacity_deep : hint_range(0.0, 1.0) = 0.97;
|
||||
|
||||
// ------------------------------------------------------------- 天空反射 -----
|
||||
group_uniforms Reflection;
|
||||
uniform vec3 sky_color : source_color = vec3(0.380, 0.660, 0.980); // 抬头看到的天顶
|
||||
uniform vec3 horizon_color : source_color = vec3(0.780, 0.880, 1.000); // 掠射看到的地平线
|
||||
uniform float fresnel_power : hint_range(0.5, 12.0) = 3.2;
|
||||
uniform float fresnel_amount : hint_range(0.0, 1.0) = 0.82;
|
||||
// 0=完全平滑。段数越多越「卡通」,但也越容易把法线场切成条纹 ——
|
||||
// 3 是实测下来既有色块感、又不会变成斑马线的值
|
||||
uniform float band_count : hint_range(0.0, 12.0) = 3.0;
|
||||
uniform float band_sharpness : hint_range(0.0, 1.0) = 0.70;
|
||||
|
||||
// --------------------------------------------------------------- 太阳 -----
|
||||
group_uniforms Sun;
|
||||
// 方向由脚本从 Sun 节点推过来,保证与场景光照一致;未接管时用默认值
|
||||
uniform vec3 sun_direction = vec3(0.35, 0.55, 0.35);
|
||||
uniform vec3 sun_color : source_color = vec3(1.000, 0.960, 0.850);
|
||||
uniform float glitter_power : hint_range(10.0, 800.0) = 320.0; // 越大越碎
|
||||
uniform float glitter_strength : hint_range(0.0, 4.0) = 0.85;
|
||||
uniform float glitter_bands : hint_range(0.0, 8.0) = 3.0; // 波光分段
|
||||
|
||||
// --------------------------------------------------------------- 泡沫 -----
|
||||
group_uniforms Foam;
|
||||
uniform vec3 foam_color : source_color = vec3(0.960, 0.990, 1.000);
|
||||
uniform float shore_depth : hint_range(0.0, 8.0) = 1.05; // 米:多浅开始出岸线泡沫
|
||||
// 岸线泡沫很容易做过头 —— 调太满就是一圈白色描边,像贴纸不像浪。0.6 左右刚好
|
||||
uniform float shore_amount : hint_range(0.0, 2.0) = 0.60;
|
||||
uniform float crest_amount : hint_range(0.0, 2.0) = 0.18; // 浪尖泡沫
|
||||
uniform float crest_start : hint_range(0.0, 1.5) = 0.92; // 浪尖阈值(越高越稀)
|
||||
// 米:浪尖泡沫的可见距离。浪花是近景细节,让它铺到地平线只会糊成一片白网
|
||||
uniform float crest_fade_distance : hint_range(20.0, 800.0) = 200.0;
|
||||
uniform float foam_bands : hint_range(1.0, 5.0) = 3.0; // 硬边分段
|
||||
|
||||
// --------------------------------------------------------- 地平线雾霭 -----
|
||||
group_uniforms Haze;
|
||||
// 与 cartoon_sky.gdshader 的 Haze 组保持一致,海面才能在远处融进天空
|
||||
uniform vec3 haze_color : source_color = vec3(0.800, 0.890, 1.000);
|
||||
uniform float haze_strength : hint_range(0.0, 1.0) = 0.95;
|
||||
uniform float haze_power : hint_range(1.0, 20.0) = 5.0; // 越大,雾霭只集中在最贴地平线的一条
|
||||
uniform float haze_distance : hint_range(5.0, 400.0) = 90.0;
|
||||
|
||||
// ------------------------------------------- 场景深度(量「水有多深」)---
|
||||
// 在 Forward+ 下可用
|
||||
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
|
||||
|
||||
// ===========================================================================
|
||||
// 工具
|
||||
// ===========================================================================
|
||||
|
||||
// 波形时间:有人接管就用传入值,没人接管就用引擎 TIME。
|
||||
// 这是为了让场景「一打开就在动」—— 否则编辑器里看到的是冻结的海。
|
||||
float wave_time() {
|
||||
return world_time >= 0.0 ? world_time : TIME;
|
||||
}
|
||||
|
||||
// 二维旋转。写成显式形式而不是 mat2,避免行列顺序搞混
|
||||
vec2 rot2(vec2 v, float a) {
|
||||
float s = sin(a);
|
||||
float c = cos(a);
|
||||
return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
|
||||
}
|
||||
|
||||
// 卡通色阶:把 0..1 压成 n 段硬边色块(n<0.5 时原样返回 = 平滑)
|
||||
float posterize(float t, float n, float sharp) {
|
||||
if (n < 0.5) {
|
||||
return t;
|
||||
}
|
||||
float s = t * n;
|
||||
float base = floor(s);
|
||||
float fr = s - base;
|
||||
float e = mix(0.5, 0.015, clamp(sharp, 0.0, 1.0));
|
||||
return (base + smoothstep(0.5 - e, 0.5 + e, fr)) / n;
|
||||
}
|
||||
|
||||
// 伪随机梯度方向(每个分量 -1..1)。用 fract 混合而不是 sin,大坐标下也不掉精度。
|
||||
vec2 hash22(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973));
|
||||
p3 += dot(p3, vec3(p3.y, p3.z, p3.x) + 33.33);
|
||||
return fract(vec2((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y)) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
// 梯度噪声(Perlin)。比值噪声多几次乘法,但这个场合不能省:
|
||||
// 值噪声的格点是「梯度为零」的平台,求导当法线时就是一块块方格。
|
||||
float gradient_noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
// 五次插值:二阶导连续
|
||||
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||||
float a = dot(hash22(i), f);
|
||||
float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
|
||||
float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
|
||||
float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
// 两层。第二层旋转约 37° 并换频率 —— 两层同向的话网格会重新变得可辨。
|
||||
// 再多一层在掠射角下只会变成噪点,没有收益。
|
||||
float grain(vec2 p) {
|
||||
return gradient_noise(p) * 0.65 + gradient_noise(rot2(p, 0.65) * 2.07 + 17.3) * 0.35;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 高度场 —— 顶点位移和法线用的是同一个函数
|
||||
// ===========================================================================
|
||||
|
||||
// 三组正弦的振幅和。浪尖泡沫要拿它当分母,
|
||||
// 拿 wave_height 当分母会永远溢出到 1(泡沫铺满整个海面)
|
||||
float swell_amplitude_sum() {
|
||||
return wave_height * 1.79; // 1 + 0.52 + 0.27
|
||||
}
|
||||
|
||||
// 返回 (高度, dh/dx, dh/dz)
|
||||
//
|
||||
// 三组正弦的频率比取 1 : 1.73 : 2.61(≈ √3、√7),互不成整数倍,
|
||||
// 叠加后不会每过一段就重复出同样的花样。方向两两相差至少 60°。
|
||||
vec3 swell_field(vec2 p, float t) {
|
||||
vec2 d0 = normalize(wind_direction); // 随风向
|
||||
vec2 d1 = rot2(d0, 2.30); // +131.8°
|
||||
vec2 d2 = rot2(d0, -1.15); // -65.9°
|
||||
|
||||
float k = TAU / max(wave_length, 0.001);
|
||||
float k0 = k;
|
||||
float k1 = k * 1.73;
|
||||
float k2 = k * 2.61;
|
||||
|
||||
float a0 = wave_height;
|
||||
float a1 = wave_height * 0.52;
|
||||
float a2 = wave_height * 0.27;
|
||||
|
||||
float sp = t * wave_speed;
|
||||
float p0 = dot(d0, p) * k0 + sp;
|
||||
float p1 = dot(d1, p) * k1 + sp * 1.35;
|
||||
float p2 = dot(d2, p) * k2 + sp * 1.80;
|
||||
|
||||
float h = a0 * sin(p0) + a1 * sin(p1) + a2 * sin(p2);
|
||||
vec2 g = a0 * k0 * cos(p0) * d0
|
||||
+ a1 * k1 * cos(p1) * d1
|
||||
+ a2 * k2 * cos(p2) * d2;
|
||||
return vec3(h, g.x, g.y);
|
||||
}
|
||||
|
||||
// 细纹的坡度(只有坡度,没有高度)。直接返回「坡多陡」而不是「水多高」。
|
||||
//
|
||||
// 方向是**写死的固定角度**,只让第一组跟着风向走。
|
||||
// 教训:曾经写成 normalize(vec2(1.0, wind_direction.y)) 与 normalize(vec2(-2.0, 9.0)),
|
||||
// 而 wind_direction 是 (1, 0.25) —— normalize(0.25, -1) 与 normalize(-2, 9) 恰好反向平行,
|
||||
// 两组余弦相消,整层退化成一道 1D 驻波。三组夹角依次约 96° / 90°,不会再退化。
|
||||
vec2 ripple_grad(vec2 p, float t) {
|
||||
vec2 d0 = normalize(vec2(wind_direction.y, -wind_direction.x)); // 与风向垂直
|
||||
vec2 d1 = normalize(vec2(7.0, 3.0)); // 23.2°
|
||||
vec2 d2 = normalize(vec2(-3.0, 7.0)); // 113.2°
|
||||
float k = ripple_scale;
|
||||
float sp = t * ripple_speed;
|
||||
float p0 = dot(d0, p) * k + sp;
|
||||
float p1 = dot(d1, p) * k * 1.73 + sp * 1.6;
|
||||
float p2 = dot(d2, p) * k * 2.41 + sp * 0.9;
|
||||
return ripple_strength * (cos(p0) * d0 + 0.7 * cos(p1) * d1 + 0.5 * cos(p2) * d2);
|
||||
}
|
||||
|
||||
// 噪点的坡度:有限差分。
|
||||
// **它就是「波光不规则」的全部来源**(见文件头坑②)—— 只有正弦的话高光会连成网格。
|
||||
// 噪点本来就没有解析导数,而三次采样只贵一点点。
|
||||
vec2 grain_grad(vec2 p, float t) {
|
||||
float s = grain_scale;
|
||||
vec2 q = p * s + vec2(t * grain_speed, -t * grain_speed * 0.7);
|
||||
float e = 0.30;
|
||||
float g0 = grain(q);
|
||||
float gx = grain(q + vec2(e, 0.0));
|
||||
float gz = grain(q + vec2(0.0, e));
|
||||
return grain_strength * vec2(gx - g0, gz - g0) / e;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 顶点
|
||||
// ===========================================================================
|
||||
|
||||
varying vec3 v_world_pos;
|
||||
|
||||
void vertex() {
|
||||
// 波形用**世界坐标**锚定,不是模型坐标 —— 否则近平面跟着相机走的时候,
|
||||
// 海浪会跟着一起滑,看上去像船在推着水跑
|
||||
vec3 world = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
vec3 w = swell_field(world.xz, wave_time());
|
||||
|
||||
// 向波峰挤压:把波谷压平、波峰收尖,剪影才像「浪」而不是「正弦」
|
||||
float h = w.x * (1.0 + choppiness * 0.35 * sign(w.x));
|
||||
VERTEX.y += h * swell_amount;
|
||||
|
||||
NORMAL = normalize(vec3(-w.y * swell_amount, 1.0, -w.z * swell_amount));
|
||||
|
||||
v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 片元
|
||||
// ===========================================================================
|
||||
|
||||
void fragment() {
|
||||
vec2 p = v_world_pos.xz;
|
||||
float t = wave_time();
|
||||
|
||||
vec3 to_cam = CAMERA_POSITION_WORLD - v_world_pos;
|
||||
float view_dist = length(to_cam);
|
||||
vec3 view_dir = to_cam / max(view_dist, 0.001);
|
||||
vec3 sun_dir = normalize(sun_direction);
|
||||
|
||||
// ------------------------------------------------------------ 法线 ----
|
||||
vec3 swell = swell_field(p, t);
|
||||
// 细纹与噪点按距离淡出
|
||||
float ripple_fade = exp(-view_dist / max(ripple_fade_distance, 0.001));
|
||||
vec2 grad = swell.yz * swell_amount
|
||||
+ (ripple_grad(p, t) + grain_grad(p, t)) * ripple_fade;
|
||||
vec3 n = normalize(vec3(-grad.x, 1.0, -grad.y));
|
||||
|
||||
// 反射方向:既决定反射到天空的哪一段,也决定地平线雾霭的强弱
|
||||
vec3 refl = reflect(-view_dir, n);
|
||||
|
||||
// ------------------------------------------------ 场景深度 → 水深 ----
|
||||
// 水面是半透明的(下面会写 ALPHA),所以走透明队列、在不透明物之后绘制,
|
||||
// 此刻深度图里已经有岛和地形。没有这一步就没有浅滩渐变,也没有岸线泡沫。
|
||||
float raw_depth = texture(depth_texture, SCREEN_UV).r;
|
||||
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, raw_depth);
|
||||
vec4 scene_view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
|
||||
scene_view.xyz /= scene_view.w;
|
||||
float scene_dist = -scene_view.z;
|
||||
float water_dist = -VERTEX.z; // 片元里的 VERTEX 是视图空间,-z 就是沿视线的距离
|
||||
float depth = max(scene_dist - water_dist, 0.0);
|
||||
|
||||
// ------------------------------------------------------------ 1 底色 ---
|
||||
float d = clamp(depth / max(depth_fade, 0.001), 0.0, 1.0);
|
||||
vec3 body = mix(shallow_color, deep_color, d);
|
||||
|
||||
// -------------------------------------------------------- 2 菲涅尔 ---
|
||||
float fres = pow(1.0 - clamp(dot(n, view_dir), 0.0, 1.0), fresnel_power);
|
||||
fres = clamp(fres * fresnel_amount, 0.0, 1.0);
|
||||
// 压成硬边色阶 —— 水体到天空反射之间是几段色块,不是一条渐变
|
||||
fres = posterize(fres, band_count, band_sharpness);
|
||||
|
||||
vec3 sky = mix(horizon_color, sky_color, clamp(refl.y * 1.6, 0.0, 1.0));
|
||||
vec3 col = mix(body, sky, fres);
|
||||
|
||||
// ---------------------------------------------------------- 3 波光 ---
|
||||
vec3 half_v = normalize(view_dir + sun_dir);
|
||||
float glitter = pow(max(dot(n, half_v), 0.0), glitter_power) * glitter_strength;
|
||||
// 再压一次色阶:把连续的高光切成几块碎点,才是卡通水面的「闪光」
|
||||
glitter = posterize(clamp(glitter, 0.0, 1.0), glitter_bands, 0.9);
|
||||
col += sun_color * glitter;
|
||||
|
||||
// ---------------------------------------------------------- 4 泡沫 ---
|
||||
// 岸线:水浅的地方
|
||||
float shore = 1.0 - smoothstep(0.0, max(shore_depth, 0.001), depth);
|
||||
// 浪尖:拿振幅之和当分母(见 swell_amplitude_sum 的注释),并按距离淡出
|
||||
float crest = smoothstep(crest_start, 1.0, swell.x / max(swell_amplitude_sum(), 0.001));
|
||||
crest *= exp(-view_dist / max(crest_fade_distance, 0.001));
|
||||
float foam = clamp(shore * shore_amount + crest * crest_amount * swell_amount, 0.0, 1.0);
|
||||
foam = posterize(foam, foam_bands, 0.95);
|
||||
col = mix(col, foam_color, foam);
|
||||
|
||||
// ------------------------------------------------------ 地平线雾霭 ---
|
||||
// 视线越平,反射到的天空越靠近地平线 —— 而那里本就是雾霭色。
|
||||
// 于是海天交界不用画线就自然连上了(与 cartoon_sky 的 Haze 组同色)
|
||||
float far = 1.0 - exp(-view_dist / max(haze_distance, 0.001));
|
||||
float glare = pow(1.0 - clamp(refl.y, 0.0, 1.0), haze_power);
|
||||
col = mix(col, haze_color, clamp(glare * haze_strength * (0.25 + 0.75 * far), 0.0, 1.0));
|
||||
|
||||
ALBEDO = col;
|
||||
// 泡沫处不透明,否则浪花会透出底下的水,看着像一层贴纸
|
||||
ALPHA = max(mix(opacity_shallow, opacity_deep, d), foam);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqlv11372k13t
|
||||
@@ -0,0 +1,10 @@
|
||||
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://world/ocean/water.gdshader" id="1_water"]
|
||||
|
||||
[resource]
|
||||
shader = ExtResource("1_water")
|
||||
shader_parameter/swell_amount = 0.0
|
||||
shader_parameter/crest_amount = 0.0
|
||||
shader_parameter/opacity_shallow = 1.0
|
||||
shader_parameter/opacity_deep = 1.0
|
||||
Reference in New Issue
Block a user