diff --git a/.workbuddy/memory/2026-09-23.md b/.workbuddy/memory/2026-09-23.md index b92e89a..287de8d 100644 --- a/.workbuddy/memory/2026-09-23.md +++ b/.workbuddy/memory/2026-09-23.md @@ -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。大浪看得清,细碎波光是缓慢流动。 + diff --git a/world/ocean/island.gdshader b/world/ocean/island.gdshader new file mode 100644 index 0000000..6061ade --- /dev/null +++ b/world/ocean/island.gdshader @@ -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; +} diff --git a/world/ocean/island.gdshader.uid b/world/ocean/island.gdshader.uid new file mode 100644 index 0000000..20358b0 --- /dev/null +++ b/world/ocean/island.gdshader.uid @@ -0,0 +1 @@ +uid://d1l20nbw5a5cv diff --git a/world/ocean/ocean_demo.gd b/world/ocean/ocean_demo.gd new file mode 100644 index 0000000..d1d3bc0 --- /dev/null +++ b/world/ocean/ocean_demo.gd @@ -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 diff --git a/world/ocean/ocean_demo.gd.uid b/world/ocean/ocean_demo.gd.uid new file mode 100644 index 0000000..b69b183 --- /dev/null +++ b/world/ocean/ocean_demo.gd.uid @@ -0,0 +1 @@ +uid://17aed15wn1yu diff --git a/world/ocean/ocean_demo.tscn b/world/ocean/ocean_demo.tscn new file mode 100644 index 0000000..ba05756 --- /dev/null +++ b/world/ocean/ocean_demo.tscn @@ -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") diff --git a/world/ocean/water.gdshader b/world/ocean/water.gdshader new file mode 100644 index 0000000..ac6eba9 --- /dev/null +++ b/world/ocean/water.gdshader @@ -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); +} diff --git a/world/ocean/water.gdshader.uid b/world/ocean/water.gdshader.uid new file mode 100644 index 0000000..89366d2 --- /dev/null +++ b/world/ocean/water.gdshader.uid @@ -0,0 +1 @@ +uid://cqlv11372k13t diff --git a/world/ocean/water_far_material.tres b/world/ocean/water_far_material.tres new file mode 100644 index 0000000..9c5cc87 --- /dev/null +++ b/world/ocean/water_far_material.tres @@ -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