# 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.2~16.6、z −24.9~32.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`。