Files
2026-10-16 16:01:18 +08:00

213 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# agame 项目长期笔记
Godot 4.7.2(Forward+,Jolt Physics,D3D12)。**可用编辑器(2026-10-16 实测):
`E:\Godot_v4.7.2-stable_mono_win64\Godot_v4.7.2-stable_mono_win64\Godot_v4.7.2-stable_mono_win64.exe`
(mono 版;同目录另有 `..._console.exe`)。**
⚠️ 旧记录里的 `D:\_yvmou\Godot\...` **已失效 —— D 盘根本没有 `_yvmou` 目录**,别再照抄。
## 代码约定
- **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` 重映射
(引擎自己做,蓝通道由引擎重建)。
## 玩家尺寸 / 相机高度(2026-10-16 查证)
全在 `character/player/player.tscn`,「大小」是三层缩放叠加:
1. **`Player` 根节点整体 scale = 0.096096**(第 38 行)—— 主开关,模型+碰撞+相机一起缩放。
2. **`Body/mouse` 模型自身 scale = 0.18240134**(第 87 行,X 为负 = 180° 翻转朝向);
实际模型缩放 = 0.096096 × 0.1824 ≈ 0.0175。只想改模型不动碰撞就改这里。
3. **碰撞胶囊(局部)** `CapsuleShape3D: radius 0.54541016 / height 1.4063476`(第 33-35 行),
`Collision` 节点 y = 0.7031738(= height/2,贴地)。
- ⚠️ **相机高度与缩放联动,改一个必须同步改另一个**:站立视线高 = `CameraRig.position.y`
(当前 1.6,第 46 行),世界高度 = 1.6 × 0.096096 ≈ **0.154 m**;而 `Camera` 节点自身
scale = **10.4062605 = 1/0.096096**(第 49 行),唯一作用就是**抵消父级的 0.096 缩放**,
让相机在世界空间回到 1:1(否则 near/FOV 异常)。动了 `Player` 的 scale,这个倒数必须一起改。
- **无头探针实测(2026-10-16,真实实例化 `player.tscn`)**:`Player.global_scale = 0.096096`;
模型 `Body/mouse.global_scale = 0.017528`;**视觉包围盒 = 0.158 × 0.150 × 0.135 m(约 15cm)**;
胶囊世界尺寸 = 半径 5.24cm / 总高 13.5cm;**相机在脚底上方 0.1116 m**。
⚠️ 相机净高度 ≠ 1.6 × 0.096 = 0.154 —— `Camera` 自身局部还带 `y = −0.439`,
实际 = (1.6 − 0.439) × 0.096096 ≈ **0.1116 m**(`DebugPanel._camera_height()` 用
`camera.global_position.y − player.global_position.y` 读数就是这个值)。
- ⚠️⚠️ **2026-10-16 定位并修复的重大 bug:`world/main.gd` 的玩家生成会把 Player 根节点的缩放静默抹掉。**
旧代码在 `spawn_function` 里写 `player.global_position = player_spawn_pos.global_position`,
但该节点**此刻还没入树**(spawner 之后才 add_child)。`Node3D.get_global_transform()` 未入树时
直接 `ERR_FAIL_COND_V(!is_inside_tree(), Transform3D())` 返回**单位矩阵**,于是
`set_global_transform()` 把 Player 的 basis 整体换成单位阵 —— **0.096096 那层缩放没了**。
实测后果(与游戏内 DebugPanel 逐位吻合):玩家模型 **1.561 m**、第一人称相机 **1.1610 m**,
面板显示「相机高度 1.161 m / 5.80 身高」;控制台每生成一个玩家刷一条
`ERROR: Condition "!is_inside_tree()" is true.`。
修法:`player.position = player_spawn_pos.position`(Main 与 PlayerSpawnPos 同级、Main 无变换,
局部坐标即世界坐标)。**通用规则:未入树的节点只能写局部 position/transform。**
- 全项目**只有** `player.tscn` 一个 CharacterBody3D;`main2.tscn` 的 `World` 根节点无缩放(1:1),
所以 15cm 就是游戏内真实绝对尺寸。玩家是**约 15cm 高的小老鼠**,有意为之(见
`player_motor.gd` / `debug_panel.gd` 注释里的「小人国玩法,身高约 0.2 m」基准;
走速 2.419 m/s ≈ 12 身长/秒就是按 0.2 m 身高定标的 —— 缩放被抹掉时手感同样全错)。
- 相机自身 `scale = 10.4062605 = 1/0.096096`,作者意图是**抵消父级的 0.096**;实测
`Camera3D` 的全局缩放两种玩家缩放下都是 1.0,所以 `InteractionRay.target_position = (0,0.25,0)`
在世界里恒为 0.25 m —— **不随玩家缩放变化,改缩放时不用动它**。
- `ui/character_select.tscn`(选人界面)的预览模型只挂了 `0.18240133` **一层**缩放
(没有 Player 那层 0.096),模型 ≈ 1.56 m,机位 `PreviewCamera` 在 `(0,0.62,1.5)` 看向 `(0,0.45,0)`
也是按这个尺度配的。它是**进游戏前的展示界面**,与游戏内 15cm 天然不同尺(要一致就再乘 0.096
并把机位挪到 ~0.15 m 高)。
- **渲染/骨骼实测(2026-10-16)**:`main2.tscn` 的 `woman` AABB 高 **1.703 m**(世界是 1:1 真人尺度);
绕开 `main.gd` 直接实例化 `player.tscn` 时,玩家渲染出来约 15 cm、`Skeleton3D` 骨骼竖直跨度
0.099 m。⚠️ 这一组数据只对「直接实例化」成立 —— 走真实生成路径时缩放会被上面那条 bug 抹掉。
- ⚠️ **探针技巧**:`player.gd` 的 `_ready()` 会把**整只模型** `Body/mouse` 设为 `visible = false`,
探针里只把下面的 `MeshInstance3D` 设成 `true` **没用**(父节点还藏着),
必须显式 `player.get_node("Body/mouse").visible = true`。这个坑踩了三轮才排掉。
- ⚠️ 量 skinned mesh 时不能只看 `mi.global_transform * mi.get_aabb()`:那是 **mesh 资源**的
包围盒,**不含骨骼变换**,位置/尺寸都可能偏(实测玩家 mesh AABB 落在 Player 原点**下方**,
而真实骨骼在原点**上方**)。要真实数据就查 `Skeleton3D.get_bone_global_pose()`,或者直接渲染截图量像素。
- 蹲伏高度由 `player_look.gd` 的 `crouch_height_ratio = 0.6` 从 `CameraRig.position.y` 推导,
改 rig 的 Y 会自动跟随,不用改脚本。
- `world/main.tscn` 根节点 `Main`(Node3D)无缩放,`MultiplayerSpawner.spawn_path = ".."`,
所以 Player 的 0.096 即最终世界缩放。(`PlayerSpawnPos` 在 y=4.8,出生会先坠落。)