Files
agame/.workbuddy/memory/MEMORY.md
T
2026-10-15 21:51:30 +08:00

12 KiB
Raw Blame History

agame 项目长期笔记

Godot 4.7.2(Forward+,Jolt Physics,D3D12)。编辑器位于 D:\_yvmou\Godot\Godot_v4.7.2-stable_win64.exe\Godot_v4.7.2-stable_win64.exe(另有 mono 版)。

代码约定

  • GDScript 警告被当作错误(至少 inference_on_variant)。不要写成 var x := some_variant_returning_call();要显式写类型,例如 var s: Script = node.get_script() as Script。改完脚本务必用 --check-only --script res://xxx.gd 自查。
  • 缩进用 tab,注释用中文(项目既有风格)。

无头验证的正确姿势(踩过坑)

  • --check-only --script res://x.gd:GDScript 语法校验,有效。
  • godot --headless --editor --path <proj> --quit:验证编辑器插件能否加载,有效。
  • 不要用 extends SceneTree 的 --script 脚本去测 _ready() 相关行为: 这种模式下节点不会真正进入场景树(is_inside_tree() 为 false),_ready 不执行, 会得出完全错误的结论。正确做法是造一个临时场景,用位置参数当主场景跑: godot --headless --path <proj> res://tools/_tmp_probe.tscn。
  • 即使是 --headless(dummy 渲染后端),shader 编译错误仍会被报告 (SHADER ERROR: ...),所以可以用无头运行来校验 shader 语法。

玩家 = LimboAI LimboHSM(2026-10-15 起)

  • LimboAI v1.8.1 是 GDExtension 版(addons/limboai 只有 bin/,无 plugin.cfg), 不需要启用 editor plugin;ClassDB 里没有 HSMPlayer —— LimboHSM/LimboState 本身就是 Node。
  • 玩家状态机在 character/player/hsm/:根 player_hsm.gd(Grounded⇄Airborne)+ Grounded{Idle,Walk,Sprint,Crouch,Sneak} + Airborne{Jump,Fall},全是场景节点、脚本挂状态上。
  • 分工:HSM 管「是什么状态」(写 motor.target_speed/crouching/sprinting + 调 animator.play_state),motor 只管物理(重力/加速度/跳跃缓冲 + try_jump), animator 只管「怎么播」(play_state + set_gait 刷倍率)。player.gd 管线第一位 hsm.update(delta)(MANUAL 模式,仅权威端 initialize+set_active;远端靠 Sync 复制)。
  • 走/站立转换按输入判定(非旧版的速度阈值)——顶墙时播 walk 不播 idle,是预期行为。
  • 用法要点:子机 _enter 里可 set_initial_state() 动态挑入口子状态;环境判定 (起跳/踏空/落地)放叶子状态的 _update,别放父机;落地判定要带 vy<=0 门槛。
  • 踩坑:新 class_name 后无头 --check-only 报「Could not find type」= 全局类缓存没刷新, 让编辑器 scan 一次即解;MCP node_set_property 写 enum int 失败 → 手改 tscn + 场景 force_reload。

toon 卡通渲染管线

  • shader/cel-shader-base.gdshader(USE_* 宏开关各模块)+ shader/includes/*.gdshaderinc, 上游是 godot4-cel-shader 项目。
  • shader/toon.tres 是材质模板;shader/outline.gdshader 作为 next_pass 描边。
  • shader/toon_apply.gd(@tool Node3D)给每个 MeshInstance3D 复制一份独立材质, 因为每个模型有各自的贴图,所以必须独立(不能共享一份材质)。
  • 已在项目设置里用了 global uniform:diffuse_curve、specular_smoothness、 fresnel_smoothness、outline_width、outline_color。 ⚠️ 但"全局实时生效"是错的(2026-10-15 实测纠正):编辑器只在启动时 把 [shader_globals] 注册进 RenderingServer,之后外部改 project.godot(或 MCP 的 project_manage settings_set)不会影响正在运行的编辑器视口。 手动改完全局参数必须重启编辑器(或临时用 test_run 跑一段 RenderingServer.global_shader_parameter_set() 把值推进去)才能看到效果。 这正是当初"改了 outline_width 却毫无变化"的原因。
  • outline_width 的实际像素宽度约为 width × 0.057(1364px 视口实测校准), 所以原来的 2.0 实际只有约 0.1px —— 等于没描边。要看到 2px 描边需要 width ≈ 30。
  • 贴图尺寸极不统一(289 张 PNG / 24 种尺寸,1024²、512²、4096²、32² 混杂), 因此不适合 Texture2DArray + per-instance uniform 方案(显存会爆且要额外打包流水线)。
  • 编辑器插件 addons/toon_refresh/:改 toon.tres 时自动重刷 toon_apply 材质。 只覆盖材质级参数(color/specular/fresnel/uv),管不到 global uniform。
  • ⚠️ fresnel(轮廓光)不能给大平面用:fresnel_light() 以 1 - dot(normal, view) 为激活量,水平地面/天花板稍远就是掠射角 → 直接饱和成满强度, 整片平面被 fresnel_color 点亮。"远处地面发白"就是这么来的 (原来是 (0,0,0) 贡献恒 0 所以没暴露,2026-10-15 改成 (0.4,0.55,0.8) 后显形)。 真要留轮廓光就把 a 压到 ≤0.05,或把颜色调暗到接近物色。
  • ⚠️ MCP material_manage(op="apply_to_node") 直接改磁盘 .tscn, 且重新序列化时会删除失去引用的子资源与 ext_resource (实测:地板实验后 ShaderMaterial_mmyc1/tyxef + 3 个 ext_resource 全被清掉, saved_to:"" 也照样落盘)。拿 MCP 做材质实验前先备份,事后用 git diff 逐行核对恢复。

卡通渲染调参(2026-10-15 定的基线取值)

  • shader/toon_ramp_1d.tres(新,替代已删除的 new_gradient_texture_1d.tres): 三级 cel 明暗带,灰阶 0.24 / 0.60 / 1.00,转场各留 0.08 软边。 注意 diffuse_sample() 只取红通道,所以渐变做成彩色也没用。
  • specular_smoothness = 0.08、fresnel_smoothness = 0.12; 原值 10.0 会把高光压到几乎不可见(smoothstep(0.05, 10.05, x))。
  • outline_width = 30、outline_color = Color(0.08, 0.07, 0.1, 1)。
  • toon.tres 的 specular / fresnel RGB 原来都是纯黑 = 两个模块完全失效, 现已改为 specular=(0.12,0.12,0.12,α0.45)、fresnel=(0.4,0.55,0.8,α0.18)。 注意 specular 的 α 只用来推 gloss 指数,亮度只能靠 rgb 调。
  • MCP settings_set 写不了 Color:传 dict 会落成 {"r":..} 嵌套字典(引擎无法绑定), 传 "#rrggbb" 会落成 String。正确做法是手改 project.godot 写成 Color(r,g,b,a)。 int 型也一样:会落成 ={"type": "int","value": 4.0} 的字典块,用完记得手改回 =4。

阴影质量(2026-10-15 定位完毕)

  • DirectionalLight3D.light_angular_distance 是本项目阴影锯齿的元凶。 它开启 PCSS 软阴影:半影半径 = 遮挡距离 × tan(角径),但 Godot 的 PCSS 采样数固定 → 半径一大就变成稀疏采样尖刺,表现为:
    1. 大面积表面(沙发扶手内侧面)上出现成片的「梳齿/蕨叶」黑纹;
    2. 物体投影(白盘子)边缘出现阶梯 + 毛刺。 实测 0.8° 时 artifact 明显,改成 0.0 后两者同时消失。 ⚠️ 别以为角径越大越"柔软自然"——在这个引擎里它只会换来尖刺。
  • 工程设置 rendering/lights_and_shadows/{directional_shadow,positional_shadow}/soft_shadow_filter_quality 默认只有 2(Soft Low),提到 4(Soft High) 实测高频噪点能量降 40%。枚举顺序: 0 Hard, 1 Soft Very Low, 2 Soft Low, 3 Soft Medium, 4 Soft High, 5 Soft Ultra。
  • shadow_blur(0~10,默认 1.0)在 angular=0 时几乎测不出影响,不用调。
  • shadow_bias / shadow_normal_bias 别低于引擎默认 0.1 / 2.0: 归零会让全身自阴影爆发(阴影面积 18484px → 114706px)。原场景的 0.05 / 1.5 偏低。
  • ⚠️ shadow_normal_bias = 0 + shadow_blur = 0 同时出现 = 整幅画面三角颗粒/摩尔纹 (2026-10-15 实测,相邻像素亮度差 0.58 → 12.73,20 倍): normal_bias = 0 让每个阴影贴图纹素都自阴影抖动,而 shadow_blur 就是 PCF 半径, 归零后抖动不再被平滑,于是按贴图纹素频率原样显现(纹素斜投影 → 三角/四边颗粒)。 任意恢复其中一个即可(0.638 / 0.566)。只想锐化就把 blur 取 0、normal_bias 留 2.0。
  • ⚠️ 提高阴影贴图分辨率救不了锯齿:directional_shadow/size 4096 → 8192 在正常配置下只改善 ~8%,在坏配置下反而更糟(12.73 → 17.92), 因为纹素越细抖动频率越高。"锯齿 = 贴图不够大"是错误直觉。
  • 室内场景可把 directional_shadow_max_distance 收到 45(原来 60),近场纹素密度提高 1.3×。

验证"启动时固化"的设置(可复用套路)

[shader_globals]、rendering/lights_and_shadows/*(阴影贴图尺寸、软阴影滤波质量)、 MSAA 等只在引擎启动时读一次,改 project.godot 对运行中的编辑器视口无效。 不要靠编辑器截图验证这类改动,走这条独立进程路线:

  1. 造一个探针场景 tools/_probe.tscn + tools/_probe.gd:extends Node3D, 在 _ready() 里 load("res://world/xxx.tscn").instantiate()、加一台 Camera3D、 等 ~60 帧、await RenderingServer.frame_post_draw 后 get_viewport().get_texture().get_image().save_png(...)、quit()。
  2. DisplayServer.window_set_position(Vector2i(-6000,-6000)) 把窗口挪到屏幕外,不打扰用户。
  3. 用 --resolution 1364x772 + 位置参数指定场景跑: godot --path <proj> --resolution 1364x772 res://tools/_probe.tscn(别加 --headless, dummy 渲染后端出不了图)。
  4. 想在探针里改灯的参数做单变量对照,直接在脚本里改 DirectionalLight3D 的属性再截图, 一次进程就能出多张对照图。
  5. 要像素级对齐编辑器视口:res://tests/test_xxx.gd(@tool + extends McpTestSuite, print 抓不到,得用 FileAccess 写文件)里读 EditorInterface.get_editor_viewport_3d(0).get_camera_3d().global_transform, 再 test_run 执行,就能拿到确切机位/ FOV。本项目的 sofa 机位是 origin(-12.070005, 4.639197, -2.615989),fov 70.01,视口 1364×772。
  6. 注意 main2 的「联机测试」UI 是 autoload(autoload/net.tscn),不在场景树里, 探针要递归隐藏 get_tree().root 下的 CanvasLayer/Control 才遮不掉画面。

坐标系陷阱 + 上游着色器的边界(重要)

Godot 4 的 fragment() 里 NORMAL / TANGENT / BINORMAL 都处于视图空间, 而 NORMAL_MAP 要的是切线空间值、由引擎用 TANGENT/BINORMAL 还原。 把切线空间值直接赋给 NORMAL 会让整个场景的法线变成一个跟着相机转的常量 —— 表现为平视全黑、俯瞰才亮。

但不要因此去改上游文件:cel-shader-base.gdshader 与 includes/normal-map.gdshaderinc 来自 https://github.com/eldskald/godot4-cel-shader, 本项目的策略是逐字跟随上游。上游的法线贴图路径 NORMAL = normal.vector; 确实有上述坐标空间错误,但上游所有变体都是 #define USE_NORMAL_MAP 0, 全仓库从未启用过这条路径(README 也没提),所以是"能编译但结果错"的死代码。 本项目保持 USE_NORMAL_MAP 0,与上游一致;toon.tres 里残留的 shader_parameter/normal_scale 在宏关闭时会被忽略,无副作用。

结论:要动上游 shader 之前,先用 E:/Godot/godot4-cel-shader-main/ 里的干净副本 diff。

正确做法(若将来真要开法线贴图):只写 NORMAL_MAP = 原始切线空间值 + NORMAL_MAP_DEPTH,让引擎合成;不要自己写 NORMAL,也不要预先做 *2-1 重映射 (引擎自己做,蓝通道由引擎重建)。