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

14 KiB
Raw Blame History

2026-10-15

玩家状态管理迁移到 LimboAI LimboHSM

  • 需求:玩家场景的状态管理完全用 LimboHSM 实现(addons/limboai v1.8.1,GDExtension 版, 无需启用 editor plugin,ClassDB 里没有 HSMPlayer —— LimboHSM/LimboState 本身就是 Node)。
  • 方案讨论:BT vs HSM 选型 → 玩家是人操作的(决策已由人完成),用 HSM 建模身体状态; BT 留给将来的 NPC/autopilot。
  • 新增 character/player/hsm/(11 个脚本):
    • player_hsm.gd(根,转换表 Grounded⇄Airborne:airborne/landed 事件)
    • grounded.gd(Grounded 子机:12 条转换 + _enter 按当下输入挑初始子状态)
    • airborne.gd(Jump→Fall on falling;_enter 按 vy>0 挑 Jump 否则 Fall)
    • ground_state.gd(地面叶子基类:统一判 try_jump/踏空 → airborne,再走 _think())
    • idle/walk/sprint/crouch/sneak.gd(写 motor.target_speed/crouching/sprinting + 动画)
    • jump.gd/fall.gd(落地判定 is_on_floor and vy<=0,带 vy 门槛防起跳帧弹回)
  • 改动:
    • player.gd:加 class_name Player;挂 HSM;_ready 末尾 hsm.initialize(self) + hsm.set_active(true)(仅权威端);管线第一位 hsm.update(delta);animator 改调 set_gait。
    • player_motor.gd:删掉从输入推断 sprint/crouch/target 的逻辑,暴露 target_speed(状态写)、has_move_input()、try_jump()(起跳在状态判定处就地执行, 同帧转 Jump);crouching/sprinting 变成状态写入的事实镜像(Look/DebugPanel 继续用)。
    • player_animator.gd:set_motion 拆成 play_state(state)(状态 _enter 调)+ set_gait(velocity)(每帧刷倍率);删 _resolve_state 和 idle_threshold。 anim_state/anim_speed 同步属性保持不变(远端靠 Sync 复制驱动)。
    • player.tscn:HSM 节点子树(LimboHSM root + Grounded{Idle,Walk,Sprint,Crouch,Sneak} + Airborne{Jump,Fall}),根机器 update_mode = 2(MANUAL,编辑器里写入 int 失败, 手改 tscn 后 force_reload 场景生效)。
  • 行为变化(有意为之):行走/站立切换改按输入判定(旧版按速度阈值 0.2 m/s), 顶墙时播 walk 而非 idle;其余(蹲伏优先于疾跑、跳跃缓冲、落地按输入挑状态)与旧版一致。
  • 验证:全部脚本 --check-only 通过;无头探针场景模拟输入跑完整状态链 (Idle→Walk→Sprint→Sneak→Crouch→Idle→Walk→Jump→落地Walk→Idle),数值与理论吻合。

二次重写:小人国定标(同日)

  • 用户明确:不保旧手感、可重写;小人国玩法,角色身高约 0.2 m;旧代码可删。
  • 定标(全部固化进 player_motor.gd 默认值,删掉了 tscn 里的 walk/sprint 覆盖): walk 2.419(≈12 身长/s)、sprint 3.624(≈18 身长/s)、crouch 1.21(走速一半)、 accel 22(0.11s 到全速)、air_accel 5.9(地面 1/4)、jump 1.5 / gravity_scale 0.5 (跳高 0.23 m ≈ 1.15 身高、滞空 0.61 s)不变。DebugPanel body_height 0.15 → 0.2。
  • 修掉一个真 bug:场景里 walk 被调到 2.419 后,动画器基准速度还是 0.4286, walk/run 动画一直被 clamp 在 1.6× 上限。重构:删掉 animator 的 4 个 *_ref_speed 导出, play_state(state, ref_speed) 由 HSM 状态传入基准速度(状态知道目标速度是多少), set_gait 用存下的 _ref_speed 算倍率 —— 两处对表的坑没了。
  • 探针复测:三个位移动画全速倍率均 1.00,转换链不变。

踩坑(复用注意)

  1. 无头 --check-only 对新 class_name 报「Could not find type」: 全局类缓存 .godot/global_script_class_cache.cfg 没刷新。让编辑器跑一次 EditorFileSystem.scan()(godot_ai MCP 的 filesystem scan)即解;批量循环检查时 同一批里前后结果不一致也是这个原因。
  2. godot-ai MCP node_set_property 写 enum int 属性失败 (WRONG_TYPE: Cannot write String to a int property,value 传 JSON number 也一样)。 变通:MCP scene_save 后直接手改 .tscn 补属性行,再 scene_open force_reload=true。
  3. MCP 场景节点路径要含场景根:/Player/HSM,不是 /HSM。
  4. LimboHSM 嵌套用法:子状态机的 _enter(脚本 override)先于其 initial_state 激活执行, 所以可以在 _enter 里 set_initial_state() 动态挑入口子状态(落地按输入挑、空中按 vy 挑)。
  5. 转换触发的环境判定要放叶子状态,不要放父机 _update:父机先于子状态执行, 在父机里 dispatch 转换后,本轮级联还可能调到已退出的旧子状态(叶子自己判、dispatch 后 立即 return,没有这个窗口)。
  6. 无头模拟按键:Input.action_press 只改动作状态、不发输入事件(Motor 的跳跃缓冲在 _unhandled_input 收事件,探不到);要用 Input.parse_input_event(InputEventAction)。

main2 卡通渲染 / 世界环境 / 光照 调优(达到可交付观感)

目标:把 world/main2.tscn 的卡通着色器参数 + WorldEnvironment + 光照调到"商业化"水平。 改了 5 个文件:project.godot、world/main2.tscn、shader/toon.tres、 新增 shader/toon_ramp_1d.tres、删除 shader/new_gradient_texture_1d.tres。

动画诊断出的三个致命问题

  1. 描边等于不存在。cel-shader-base 的 next_pass 链、outline.gdshader 都正常, 但 outline_width = 2.0 在这个 Godot 版本里只对应约 0.1px,肉眼不可能看见。 实测标定:像素宽度 ≈ outline_width × 0.057(1364px 视口)。已改为 30(≈1.7px)。
  2. toon.tres 的 specular / fresnel RGB 都是纯黑 —— 两个模块虽然连着,贡献恒为 0。
  3. 漫反射渐变是"黑→硬边→白"的二值 ramp,导致场景只有一层死板明暗。

最关键的一条坑(务必记住)

编辑器只在启动时注册 [shader_globals]。 外部(含 MCP settings_set)改 project.godot 的全局着色器参数,正在跑的编辑器视口完全不变。 我就是因为这个,一开始把 outline_width 从 2 调到 10 再到 60,截图却毫无变化, 最后才定位到根因。验证手段:写一个 res://tests/test_*.gd(必须 extends McpTestSuite), 在 test_* 方法里直接 RenderingServer.global_shader_parameter_set(...), 再用 MCP test_run 执行 —— 能在编辑器进程内把值推进渲染器。

同时用这个办法做了隔离实验,排除了 4 个错误猜想:雾、SSAO、泛光、菲涅尔都不是 墙面噪点的来源;真正的噪点是截图传输压缩(480px 截图墙面完全干净,1364px 才有颗粒)。 另外顺手证实 POSITION 顶点覆盖是生效的(写 p.y += p.w * 0.8 面板会整体下移), 所以描边 pass 本身没问题,别去改上游 outline.gdshader。

最终取值

  • 着色器全局(project.godot [shader_globals]): diffuse_curve = toon_ramp_1d.tres、specular_smoothness = 0.08、 fresnel_smoothness = 0.12、outline_width = 30、 outline_color = Color(0.08, 0.07, 0.1, 1)。
  • 新 ramp(shader/toon_ramp_1d.tres,GradientTexture1D/256): 0.00–0.30 → 0.24;0.38–0.62 → 0.60;0.70–1.00 → 1.00,两级转场各 0.08 软边。
  • 材质(shader/toon.tres,以及 main2 里地板的独立 ShaderMaterial): specular = (0.12,0.12,0.12,0.45)、fresnel = (0.4,0.55,0.8,0.18)。
  • Environment:天空换成项目现成的 world/sky/cartoon_sky.tres(与 character_select / ocean_demo 一致);ambient_light_source = 3(Sky) + 冷色补光 (0.3,0.38,0.55)、sky_contribution 0.45、energy 0.5;ACES 色调映射(tonemap_mode=3, 这个版本枚举确实是 LINEAR0/REINHARDT1/FILMIC2/ACES3/AGX4);SSAO 轻度(intensity 1.0、 detail 0.3、light_affect 0);Glow 阈值抬到 2.2 防止白面爆光;雾 0.004 + aerial 0.3; adjustment contrast 1.12 / saturation 1.08。
  • 光照:平行光改暖色 (1,0.925,0.83)、energy 0.95、light_angular_distance 0.8 软阴影、 bias 0.05 / n_bias 1.5、max_distance 60;sconce 聚光灯 energy 3.2 / range 12 / angle 55 / 开启阴影。顺手开了 anti_aliasing/quality/msaa_3d = 2(4x,描边抗锯齿)。

排查时踩的 MCP 细节

  • MCP settings_set 写不了 Color:传 {"r":..,"g":..} 会落成嵌套字典、 传 "#141219" 会落成 String,两者都不是引擎要的 Color。 正解是手改 project.godot 写成 Color(r,g,b,a)。改完用无头探针脚本 (extends SceneTree + ProjectSettings.get_setting() 打印 typeof(inner))复验。
  • settings_set 会把整个 ProjectSettings 从内存写回磁盘, 所以它写完之后我手改过的 Color(...) 会被覆盖成 String —— 必须最后一步再手改。
  • editor_screenshot 的 view_target 会移动用户的编辑器相机;max_resolution=0 才是原生分辨率。

追加:远处地面"发白"的根因 = fresnel 掠射角饱和(务必记住)

用户反馈:main2 里走廊/餐厅方向的地面(z≈15~25)呈一片灰白偏冷的平面,近处木地板正常。

  • 排查路径:floor1.glb 只有 1 个 mesh / 1 个材质(用 Python 解 GLB 的 JSON chunk 确认), AABB 覆盖 x −18.216.6、z −24.932.9 —— 餐厅地面就是这块地板,不是缺几何、 也不是贴图问题(BaseColor 是纯木纹)。地毯不泛白是因为它的固有色深,把叠加量盖住了。
  • 根因在 shader/includes/base-cel-shader.gdshaderinc 的 fresnel_light():
    fresnel_dot = 1.0 - dot(normal, view);          // 水平面越远越掠射 → 越接近 1
    fresnel_threshold = pow(1.0 - fresnel_strength, dot(light, normal));  // ≈0.84
    ... * smoothstep(threshold ± fresnel_smoothness/2, fresnel_dot)
    
    实测:5 m 处 fresnel_dot≈0.69(=0,无贡献),20 m 处≈0.92(=1,满强度)。 于是远处地面被整片点亮,叠加的是 fresnel_color = (0.4,0.55,0.8) 蓝白, 木地板 albedo(≈0.34,0.24,0.2) + 叠光(≈0.40,0.51,0.66) → 就是用户看到的灰白。
  • 是上一轮调优引入的:fresnel 原来 RGB=(0,0,0)(贡献恒 0,所以从没显形), 上一轮改成 (0.4,0.55,0.8) 后才出现。大平面(地板/天花板)上放菲涅尔轮廓光必然有这副作用。
  • 修法(择一):fresnel.a 调到 0.05 以下;或把 fresnel_color 压暗到接近物色; 或把 fresnel_smoothness(全局 uniform)调小收紧过渡带。当前已按用户要求 恢复为 (0.4,0.55,0.8,0.18),泛白会重现,等用户定夺。

追加:MCP material_manage(apply_to_node) 会写盘并删除孤儿资源

  • 用 MCP apply_to_node 给 model/floor1/pPlane1 临时套一个纯红 unshaded 材质做隔离实验, 之后 scene_open force_reload 材质没有恢复 —— 因为该操作直接改了磁盘上的 .tscn (返回值里的 saved_to: "" 不代表没落盘)。
  • 更严重:Godot 重新序列化时把失去引用的资源全删了 —— 地板配套的 ShaderMaterial_mmyc1(cel + outline)、ShaderMaterial_tyxef, 以及 3 个 ext_resource(outline.gdshader/cel-shader-base.gdshader/地板 BaseColor 贴图) 一并消失,同时把等于默认值的属性行省略(如 glow_blend_mode = 1)。
  • 教训:拿 MCP 改材质做实验前先 git stash 或备份 .tscn;事后用 git diff 对照逐行恢复。 恢复时注意用调优后的值(不是 HEAD 的原值)。

阴影锯齿专项(main2 沙发/盘子)

  • 现象:沙发扶手上大片「梳齿/蕨叶」状黑纹 + 盘子影子边缘阶梯状锯齿。
  • 逐个隔离(改 shadow_enabled / 单变量逐项)后确认:都来自 DirectionalLight3D。
  • 根因 = light_angular_distance = 0.8。它开启 PCSS 软阴影,半影半径随距离放大, 但 Godot 的采样数有限 → 出现稀疏采样尖刺(就是那片蕨叶纹)。 归零后(0.0)两个 artifact 同时消失。
  • 次要项:工程设置 soft_shadow_filter_quality 默认是 2(Soft Low), 提到 4(Soft High) 后高频噪点能量 1.149 → 0.687(-40%),强跳变 756 → 630。
  • shadow_blur(1.0/1.5/2.5/4.0)实测几乎无影响(0.669~0.687),已删掉该覆盖行。
  • shadow_bias/shadow_normal_bias 归零会导致全身自阴影爆发(阴影面积 18484 → 114706), 必须用引擎默认 0.1 / 2.0(原场景的 0.05 / 1.5 偏低)。
  • 最终:light_angular_distance = 0.0、删掉 bias/blur 覆盖行、 directional_shadow_max_distance 60 → 45、project.godot 加 lights_and_shadows/{directional_shadow,positional_shadow}/soft_shadow_filter_quality=4。
  • 前后对比图存在 docs/shadow_aliasing_fix.png。

追加:用户把 blur 归零后「全画面三角形颗粒」

用户手动把 shadow_blur = 0,同时把 shadow_normal_bias 也设成了 0, 结果整幅画面(沙发、盘子、地板全部)盖满斜向的三角颗粒 / 摩尔纹。

在独立进程里做了 2×2 对照(soft_shadow_filter_quality = 4, 指标 = 区域内相邻像素亮度差的平均,越低越干净):

组合 4096 贴图 8192 贴图
blur 0 + normal_bias 0(用户当时) 12.730 17.923
blur 0 + normal_bias 2 0.638 0.611
blur 1 + normal_bias 0 0.566 0.522
blur 1 + normal_bias 2(推荐) 0.577 0.532
blur 0.6 + normal_bias 2 + max_dist 25 0.546 0.537

结论:

  • 必须「blur=0 且 normal_bias=0」同时成立才会爆(12.7)。任意改回一个就回到 ~0.6。 机制:normal_bias = 0 会在每个阴影贴图纹素上产生自阴影判定抖动, 而 shadow_blur 正是那个 PCF 半径 —— 它被归零后抖动不再被平滑, 于是以贴图纹素的频率原样显现成三角颗粒(贴图纹素斜投影到受光面就是四边形/三角)。
  • 加大阴影贴图分辨率没用,反而更糟(12.730 → 17.923)。 分辨率越高的贴图纹素越细密 → 抖动频率越高 → 颗粒越密越明显。 所以"锯齿是阴影贴图不够大"是错的直觉。
  • 想要更锐的阴影:shadow_blur 可以取 0(normal_bias 保持 2.0 就干净,0.638)。
  • 已把编辑器的 shadow_normal_bias 恢复到 2.0、shadow_blur 恢复到 1.0 (用 MCP node_set_property,实测它只改编辑器内存、不写盘)。
  • 对比图存在 docs/shadow_normal_bias_fix.png。