diff --git a/.workbuddy-ai/memory/2026-09-21.md b/.workbuddy-ai/memory/2026-09-21.md new file mode 100644 index 0000000..28d5069 --- /dev/null +++ b/.workbuddy-ai/memory/2026-09-21.md @@ -0,0 +1,70 @@ +# 2026-09-21 + +## 程序化卡通天空(world/sky/) + +新增 `res://world/sky/cartoon_sky.gdshader` + `cartoon_sky.tres`(Sky 资源,内含调好的 ShaderMaterial), +并让 `world/main.tscn` 的 WorldEnvironment 改用它;同时把 `Sun` 的 light_color 调成暖白 +(1.0, 0.97, 0.90),项目设置开了 `rendering/anti_aliasing/quality/use_debanding`。 + +渲染顺序:渐变天空 → 地面半球 → 星空 → 太阳 → 云 → 地平线雾霭。 +全部由噪声现场生成,无贴图。参数按 Gradient / Sun / Stars / Clouds / Haze 分组暴露在 Inspector。 + +### Godot shader 踩坑(重要,下次直接避开) + +1. **`mat2(a,b,c,d)` 不合法**。Godot 着色器语言只接受列向量构造:`mat2(vec2, vec2)`。 + 同理 mat3 要用三个 vec3。报错形式是 + `Invalid arguments for the built-in function: "mat2(float,float,float,float)"`。 +2. **天空着色器的编译错误不在 `logs_read(source="editor")` 里**,除非触发一次编译。 + 可靠的取错方式:改完 → `filesystem_manage(op="scan")` → `project_run` → + `logs_read(source="editor", include_details=true)`。game 日志只给一行 + `version_get_shader: Parameter "version" is null`,没用。 +3. **`smoothstep(edge0, edge1, x)` 不要写 edge0 > edge1**,改成 `1.0 - smoothstep(...)`。 +4. 变量重名(同一函数内二次声明)会报 `Redefinition of 'x'`,直接让整个 shader 编译失败。 +5. shader 编译失败时天空渲染成**纯黑**,不是品红 —— 别以为是资源没挂上。 +6. **编辑器里的 Shader 面板会缓存旧的编译报错**。外部改完 .gdshader + scan 之后, + 面板可能仍显示上一次失败的 `mat2(float,float,float,float)` 之类信息, + 而 Debugger → Errors 已经是空的。判定方法:看 `logs_read(source="editor")` 是否为空, + 再 `project_run` 一次看天空是否正常。**不要照着这条过期报错去改代码。** + 注意:如果 Shader 标签页里留着一份旧的未保存副本,从面板里保存会把我改好的文件覆盖回去。 + +### 云的形态(花了最多时间) + +- **别用「水平云平面」投影 `uv = dir.xz / dir.y`**:低仰角处透视压缩成 10:1, + 云被拉成细条;若再对 dir.y 做硬 clamp,被钉死的距离会让噪声只随方位角变化, + 直接拉出**竖条拖影**。 +- **也别用方位角等距投影**(`uv = normalize(dir.xz) * acos(dir.y)`):天顶是极点, + 云会全部朝圆心收拢,抬头就看得见。 +- **正解是球极投影 `uv = dir.xz / (1.0 + dir.y)`**:保角 → 形状不拉伸; + 天顶只是圆盘圆心而非收缩极点;方位角天然无缝;角尺寸从地平线到天顶只放大 2 倍, + 接近真实透视。这个投影配 2D Perlin 出图最好。 +- **噪声用 Perlin 梯度噪声,不要 value noise**:value noise 在格点处梯度为零, + 阈值化后会切出一块块多边形的棱角("晶体"感)。 +- **三维 Perlin 在这台机器上不行**:sin 系哈希(`fract(sin(dot(p,k))*43758)`)在大坐标 + (fbm 第 3~4 个倍频时 dot 可达 1e4)下掉精度,高频倍频退化成纯噪声 → 云变成细丝。 + Hoskins 的 fract 系哈希精度安全,但出图仍偏块状。最终放弃 3D,回到 2D Perlin。 +- **阈值要压在 fbm 分布均值之上**。阈值贴着均值时等值线本身就很纠结, + 云会变成一张"蛛网";抬到 +0.5σ 以上才是干净的团块。 + +### 当前调好的参数 + +coverage 0.33 / scale 5.0 / edge 0.16 / warp 0.30 / horizon_fade 0.18 / speed 0.01, +云影色 (0.45,0.62,0.88),边缘辉光 (1,0.93,0.76)。 + +## Sun 阴影(当天稍后) + +`/Main/Sun` 开了 `shadow_enabled = true`;同时把 `directional_shadow_max_distance` +从默认 100 收到 40(场景只有 40×40,收窄后阴影贴图分辨率集中到实际范围,边缘干净很多), +并开 `directional_shadow_blend_splits` 消除 split 之间的接缝。 + +### 又一个 Godot MCP 坑 + +`node_set_property` 写 **bool** 属性会失败:`true` 和 `1` 都被序列化成 String, +报 `WRONG_TYPE: Cannot write String to a bool property`。 +**bool 属性只能直接改 .tscn,再 `scene_open(force_reload=true)`。** +(Color / Vector3 用 dict 传是正常的,`light_color` 就是这么改的。) + +## 待办 / 可选后续 + +- 地面还是灰白色 (0.55,0.58,0.62),被天空的蓝色环境光染成了淡紫。 + 换成草绿之类的固有色会协调很多 —— 这是目前画面里最突兀的一处。 +- `star_amount` 默认 0,太阳落到地平线以下会自动淡入星空,还没实测过夜景。 diff --git a/player/player.tscn b/player/player.tscn index 40adcfe..1b0e0dc 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -31,7 +31,7 @@ spring_length = 4.0 [node name="Camera" type="Camera3D" parent="CameraRig/SpringArm" unique_id=52294557] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 4) -fov = 70.0 +fov = 82.2 [node name="Body" type="Node3D" parent="." unique_id=394283421] diff --git a/world/main.tscn b/world/main.tscn index 06d1fe9..45e99dd 100644 --- a/world/main.tscn +++ b/world/main.tscn @@ -1,6 +1,7 @@ [gd_scene format=3 uid="uid://bp8l1uwgshc86"] [ext_resource type="PackedScene" uid="uid://bekuuw153mv03" path="res://player/player.tscn" id="1_gjxf3"] +[ext_resource type="Sky" path="res://world/sky/cartoon_sky.tres" id="2_cartoon"] [sub_resource type="BoxShape3D" id="BoxShape3D_hyim0"] size = Vector3(40, 0.5, 40) @@ -14,19 +15,11 @@ subdivide_depth = 8 albedo_color = Color(0.5529412, 0.5803922, 0.61960787, 1) roughness = 0.9 -[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_cmo1i"] -sky_top_color = Color(0.38, 0.45, 0.55, 1) -sky_horizon_color = Color(0.65, 0.67, 0.7, 1) -ground_bottom_color = Color(0.2, 0.17, 0.13, 1) -ground_horizon_color = Color(0.65, 0.67, 0.7, 1) - -[sub_resource type="Sky" id="Sky_i1eo7"] -sky_material = SubResource("ProceduralSkyMaterial_cmo1i") - [sub_resource type="Environment" id="Environment_gu74f"] background_mode = 2 -sky = SubResource("Sky_i1eo7") +sky = ExtResource("2_cartoon") ambient_light_source = 3 +reflected_light_source = 2 [node name="Main" type="Node3D" unique_id=58155864] @@ -42,6 +35,10 @@ surface_material_override/0 = SubResource("StandardMaterial3D_spvjk") [node name="Sun" type="DirectionalLight3D" parent="." unique_id=910400852] transform = Transform3D(0.8660248, 0.3830344, -0.32138094, 0, 0.6427605, 0.76606715, 0.5000011, -0.66343313, 0.5566465, 0, 0, 0) +light_color = Color(1, 0.97, 0.9, 1) +shadow_enabled = true +directional_shadow_blend_splits = true +directional_shadow_max_distance = 40.0 [node name="Player" parent="." unique_id=869687860 instance=ExtResource("1_gjxf3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0) diff --git a/world/sky/cartoon_sky.gdshader b/world/sky/cartoon_sky.gdshader new file mode 100644 index 0000000..01db25d --- /dev/null +++ b/world/sky/cartoon_sky.gdshader @@ -0,0 +1,249 @@ +shader_type sky; + +/* =========================================================================== + Cartoon Sky · 纯程序化,零贴图 + --------------------------------------------------------------------------- + 渲染顺序: 渐变天空 → 地面半球 → 星星 → 太阳 → 云层 → 地平线雾霭 + + 所有参数都暴露在 Inspector 里(按分组排列),可以实时调、不用重新编译。 + 云与星星全部由 hash / value-noise / fbm 现场生成。 + =========================================================================== */ + +// ----------------------------------------------------------- 渐变天空 ----- +group_uniforms Gradient; +uniform vec3 zenith_color : source_color = vec3(0.118, 0.357, 0.851); // 天顶 +uniform vec3 mid_color : source_color = vec3(0.353, 0.651, 0.941); // 中段 +uniform vec3 horizon_color : source_color = vec3(0.851, 0.933, 1.000); // 地平线 +uniform vec3 ground_color : source_color = vec3(0.290, 0.329, 0.384); // 地面半球 +uniform float gradient_bias : hint_range(0.10, 6.00) = 0.85; // 渐变挤压 +uniform float band_count : hint_range(0.00, 20.00) = 0.00; // 0=平滑,>0=色阶数 +uniform float band_sharpness : hint_range(0.00, 1.00) = 0.90; // 色阶硬度 + +// ---------------------------------------------------------------- 太阳 ----- +group_uniforms Sun; +uniform float follow_scene_light : hint_range(0.0, 1.0) = 1.0; // 跟随场景平行光 +uniform vec3 sun_direction = vec3(0.35, 0.55, 0.35); // 不跟随时手动指定 +uniform vec3 sun_core_color : source_color = vec3(1.000, 0.976, 0.878); +uniform vec3 sun_halo_color : source_color = vec3(1.000, 0.827, 0.478); +uniform float sun_disk_size : hint_range(0.0, 0.40) = 0.035; // 弧度 +uniform float sun_disk_softness : hint_range(0.0, 1.0) = 0.20; +uniform float sun_halo_size : hint_range(0.0, 2.00) = 0.55; +uniform float sun_halo_strength : hint_range(0.0, 4.00) = 0.65; +uniform float sun_bloom_strength : hint_range(0.0, 4.00) = 0.35; +uniform float sun_intensity : hint_range(0.0, 8.00) = 1.70; + +// ---------------------------------------------------------------- 星空 ----- +group_uniforms Stars; +uniform float star_amount : hint_range(0.0, 1.0) = 0.0; // 太阳落山后自动淡入 +uniform float star_density : hint_range(10.0, 160.0) = 60.0; +uniform float star_size : hint_range(0.005, 0.30) = 0.050; +uniform float star_twinkle : hint_range(0.0, 1.0) = 0.65; +uniform vec3 star_color : source_color = vec3(0.85, 0.90, 1.00); + +// ---------------------------------------------------------------- 云层 ----- +group_uniforms Clouds; +uniform vec4 cloud_color : source_color = vec4(1.000, 1.000, 1.000, 1.0); // 受光面 +uniform vec4 cloud_shadow_color : source_color = vec4(0.624, 0.761, 0.910, 1.0); // 背光面 +uniform vec4 cloud_rim_color : source_color = vec4(1.000, 0.914, 0.722, 1.0); // 边缘辉光 +uniform float cloud_coverage : hint_range(0.0, 1.0) = 0.46; // 云量 +uniform float cloud_scale : hint_range(0.50, 12.0) = 4.0; // 云团大小(越大云越小越多) +uniform float cloud_horizon_fade : hint_range(0.0, 0.60) = 0.18; // 地平线淡出高度(sin 仰角) +uniform float cloud_edge : hint_range(0.005, 0.60) = 0.10; // 轮廓软硬 +uniform float cloud_warp : hint_range(0.0, 3.0) = 0.55; // 揉皱程度(蓬松感) +uniform float cloud_speed : hint_range(0.0, 1.0) = 0.010; // 飘动速度 +uniform vec2 cloud_direction = vec2(1.0, 0.25); // 风向 +uniform float cloud_shading : hint_range(0.0, 1.0) = 0.55; // 明暗对比 +uniform float cloud_rim : hint_range(0.0, 2.0) = 0.40; // 描边辉光强度 +uniform float cloud_opacity : hint_range(0.0, 1.0) = 1.0; + +// ------------------------------------------------------------ 地平线雾霭 --- +group_uniforms Haze; +uniform vec3 haze_color : source_color = vec3(0.851, 0.929, 1.000); +uniform float haze_strength : hint_range(0.0, 1.0) = 0.55; +uniform float haze_height : hint_range(0.0, 0.60) = 0.14; + +// =========================================================================== +// 噪声工具 +// =========================================================================== + +float hash21(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +// 伪随机梯度方向(每个分量 -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)。相比 value noise,它在格点处不会出现「梯度为零」的平台, +// 等值线是有机的团块 —— 阈值化之后不会切出一块块多边形的棱角。 +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); +} + +// 5 个倍频的分形噪声:每个倍频先旋转再平移,避免出现轴向条纹 +float fbm(vec2 p) { + float sum = 0.0; + float amp = 0.5; + float norm = 0.0; + mat2 rot = mat2(vec2(0.80, 0.60), vec2(-0.60, 0.80)); + for (int i = 0; i < 5; i++) { + sum += amp * gradient_noise(p); + norm += amp; + p = rot * p * 2.03 + 11.31; + amp *= 0.5; + } + return sum / norm; +} + +// 卡通色阶:把 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; +} + +// 云密度场:先做一次廉价的 domain warp 把噪声揉皱,云朵才会蓬松。 +// 返回未归一化的原始 fbm —— 调用方需要它来算方向性明暗。 +float cloud_field(vec2 p, float warp_amt) { + if (warp_amt > 0.001) { + vec2 w = vec2(gradient_noise(p * 0.5 + vec2(13.7, 5.1)), + gradient_noise(p * 0.5 + vec2(2.3, 21.9))); + p += w * warp_amt; + } + return fbm(p); +} + +// =========================================================================== +// 主函数 +// =========================================================================== + +void sky() { + vec3 dir = normalize(EYEDIR); + + // ---------------------------------------------------------- 渐变天空 -- + float up = clamp(dir.y, 0.0, 1.0); + float t = pow(up, 1.0 / max(gradient_bias, 0.05)); + t = posterize(t, band_count, band_sharpness); + + vec3 col = mix(horizon_color, mid_color, smoothstep(0.02, 0.30, t)); + col = mix(col, zenith_color, smoothstep(0.30, 0.80, t)); + + // 地平线以下:压向地面色 + float below = 1.0 - smoothstep(-0.05, 0.005, dir.y); + col = mix(col, ground_color, below * 0.9); + + // --------------------------------------------------------------- 太阳 -- + vec3 sun_dir = normalize(sun_direction); + if (LIGHT0_ENABLED && follow_scene_light > 0.5) { + sun_dir = normalize(LIGHT0_DIRECTION); + } + float sun_dot = clamp(dot(dir, sun_dir), -1.0, 1.0); + // 小角度近似,比 acos 数值稳定得多 + float ang = sqrt(max(2.0 * (1.0 - sun_dot), 0.0)); + + // 太阳低于地平线时自动入夜 + float night = 1.0 - smoothstep(-0.10, 0.08, sun_dir.y); + + // --------------------------------------------------------------- 星空 -- + if (star_amount > 0.001 && night > 0.001 && dir.y > -0.02) { + vec2 suv = vec2(atan(dir.x, dir.z), dir.y); + vec2 gv = suv * star_density; + vec2 id = floor(gv); + vec2 f = fract(gv) - 0.5; + float rnd = hash21(id); + // 稀疏化:只有一小部分格子长星星 + float keep = step(0.93, rnd); + vec2 offset = (vec2(hash21(id + 3.7), hash21(id + 9.1)) - 0.5) * 0.7; + float d = length(f - offset); + float twinkle = mix(1.0, 0.30 + 0.70 * sin(TIME * (1.5 + rnd * 4.0) + rnd * 40.0), star_twinkle); + float star = (1.0 - smoothstep(0.0, max(star_size, 0.0001), d)) * keep * twinkle; + col += star_color * star * star_amount * night * smoothstep(-0.02, 0.10, dir.y); + } + + // --------------------------------------------------------------- 太阳 -- + // 硬边圆盘 + float disk_in = max(sun_disk_size * (1.0 - sun_disk_softness * 0.85), 0.0001); + float disk = 1.0 - smoothstep(disk_in, max(sun_disk_size, disk_in + 0.0001), ang); + // 光晕 + float halo = pow(1.0 - smoothstep(sun_disk_size, sun_disk_size + max(sun_halo_size, 0.0001), ang), 2.0); + // 大范围泛光 + float bloom = pow(max(sun_dot, 0.0), 12.0); + + vec3 sun_col = sun_core_color * (disk * sun_intensity) + + sun_halo_color * (halo * sun_halo_strength * sun_intensity) + + sun_halo_color * (bloom * sun_bloom_strength); + col += sun_col * (1.0 - below); + + // --------------------------------------------------------------- 云层 -- + // 球极投影:把上半球保角地映到一张圆盘上(天顶落在圆心,地平线落在半径 1)。 + // 保角 = 云朵形状不被拉伸;天顶只是圆盘的圆心,不会像方位角等距投影那样 + // 把云朝一个点收拢。云的角尺寸从地平线到天顶平滑放大 2 倍,接近真实透视。 + vec2 uv = (dir.xz / (1.0 + dir.y)) * cloud_scale; + + vec2 wind = cloud_direction; + if (length(wind) < 0.0001) { + wind = vec2(1.0, 0.0); + } + uv += normalize(wind) * (TIME * cloud_speed * 3.0); + + // 形状采样 + 朝太阳方位偏移一次的采样 → 得到方向性明暗 + vec2 sun_az = sun_dir.xz; + if (length(sun_az) < 0.0001) { + sun_az = vec2(1.0, 0.0); + } + sun_az = normalize(sun_az); + vec2 light_off = sun_az * (cloud_shading * 0.8); + + float raw = cloud_field(uv, cloud_warp); + float raw_lit = cloud_field(uv + light_off, cloud_warp); + + // 原始 fbm 落在 -0.4..0.4 附近,拉开对比后映射到 0..1 再交给阈值 + float dens = clamp(raw * 1.3 + 0.5, 0.0, 1.0); + + // 云量控制阈值:coverage 越大,阈值越低,云越多 + float thresh = mix(0.80, 0.10, cloud_coverage); + float mask = smoothstep(thresh, thresh + cloud_edge, dens); + + // 受光判定:朝太阳方向采样到的密度更低 → 当前点处在迎光侧。 + // 再按厚度压暗云芯,最后压成 3 段,得到赛璐璐风格的两面明暗。 + float lit = clamp((raw - raw_lit) * cloud_shading * 20.0 + 0.5, 0.0, 1.0); + float thickness = smoothstep(thresh, thresh + 0.5, dens); + lit = clamp(lit - thickness * 0.32, 0.0, 1.0); + lit = posterize(lit, 3.0, 0.9); + + vec3 cloud_col = mix(cloud_shadow_color.rgb, cloud_color.rgb, lit); + + // 边缘辉光:靠近轮廓、且朝太阳那一侧 + float interior = smoothstep(thresh, thresh + cloud_edge * 2.0, dens); + float rim = mask * (1.0 - interior) * smoothstep(0.45, 0.95, lit) * cloud_rim; + cloud_col += cloud_rim_color.rgb * rim; + + // 越贴近地平线越淡 + float cloud_a = mask * cloud_opacity * smoothstep(0.0, max(cloud_horizon_fade, 0.001), dir.y); + col = mix(col, cloud_col, cloud_a); + + // ----------------------------------------------------------- 地平线雾 -- + float haze = pow(1.0 - smoothstep(0.0, max(haze_height, 0.001), abs(dir.y)), 2.0); + col = mix(col, haze_color, haze * haze_strength * (1.0 - cloud_a * 0.7)); + + COLOR = col; +} diff --git a/world/sky/cartoon_sky.gdshader.uid b/world/sky/cartoon_sky.gdshader.uid new file mode 100644 index 0000000..bea8327 --- /dev/null +++ b/world/sky/cartoon_sky.gdshader.uid @@ -0,0 +1 @@ +uid://de7313djio1xm diff --git a/world/sky/cartoon_sky.tres b/world/sky/cartoon_sky.tres new file mode 100644 index 0000000..9b7f708 --- /dev/null +++ b/world/sky/cartoon_sky.tres @@ -0,0 +1,9 @@ +[gd_resource type="Sky" format=3 uid="uid://bi632suyifms5"] + +[ext_resource type="Shader" uid="uid://de7313djio1xm" path="res://world/sky/cartoon_sky.gdshader" id="1_cartoon"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_cartoon"] +shader = ExtResource("1_cartoon") + +[resource] +sky_material = SubResource("ShaderMaterial_cartoon")