修复玩家脸部

This commit is contained in:
2026-10-03 17:26:42 +08:00
parent 974ca67c41
commit bc90e6d7b2
9 changed files with 190 additions and 121 deletions
+17 -6
View File
@@ -52,16 +52,19 @@
- `face_paint.gdshader`:将 face surface 的 UV remap 到 0~1,再映射到内置图集 rect 或本地 PNG;贴图 alpha 只代表墨线覆盖率,混到 `skin_color` 上,**不写空间 `ALPHA`**(否则脸片变洞)。
- `mouse_mouse_lambert1_BaseColor.png` 和 `mouse_表情测试.png` 的背景都精确为 RGB(137,179,255) / `#89B3FF`;表情图源 alpha 全 255,不能直接当透明度。
- **最终做法:全身一个 shader,脸部靠 UV 矩形圈出来**。`face_paint.gdshader` 用 `body_tex`(= `mat_body` 的 BaseColor)采样全身底色,再在 `face_uv_min` / `face_uv_size` 圈出的矩形里,把表情贴图(内置图集或本地 PNG)和玩家手绘按 alpha 叠上去。透明处保持身体底色,所以**脸和周围不可能是两块不同的皮肤**。`FaceRig` 把 mat_body 的 `albedo_texture` / `albedo_color` / `roughness` / `metallic` 抄进 shader,并用 `set_surface_override_material(0, mat)`(surface 0,不再是 1)。
- **最终做法:全身一个 shader,脸是「贴在身体正面的贴花」**。`face_paint.gdshader` 用 `body_tex`(= `mat_body` 的 BaseColor)采样全身底色,再在 `face_center` / `face_size` 圈出的矩形里,把表情贴图(内置图集或本地 PNG)和玩家手绘按 alpha 叠上去。透明处保持身体底色,所以**脸和周围不可能是两块不同的皮肤**。`FaceRig` 把 mat_body 的 `albedo_texture` / `albedo_color` / `roughness` / `metallic` 抄进 shader,并用 `set_surface_override_material(0, mat)`(surface 0,不再是 1)。
- **贴花用模型本地坐标,不用 UV**:`varying vec3 object_position` / `object_normal` 在 `vertex()` 里取 `VERTEX` / `NORMAL`,`fragment()` 里用 `object_position.xy` 定位。**这样和 UV 孤岛怎么排无关** —— 之前用 UV 矩形时,矩形一越界就会把表情糊到肚子/背上(头部孤岛和胸腹孤岛只差 0.008),换贴花后这类问题从根上没了。
- **模型自己的正面是本地 -Z**:`character_select.tscn` 的 `Model` 节点和 `player.tscn` 的 `mouse` 节点都把它转了 180°。所以正面遮罩写 `-normalize(object_normal).z`,**写反了表情会跑到背上**(踩过)。背面同 (x,y) 的点也会落进矩形,所以这个遮罩是必须的。
- 不再需要 `skin_color` 当 shader 输入:`FaceRig` 反过来从身体贴图左上角取色写进 `skin_color`,只给画布和缩略图垫底用(导入贴图是压缩的,`get_pixel` 前要先 `decompress()`,否则报 "Can't get_pixel() on compressed image")。
- 表情遮罩用 `step()` 相乘而不是 `if`:采样留在统一控制流里,免得 mipmap 导数在分支里未定义。
- **仍然不能写空间 `ALPHA`**:alpha 只代表墨线覆盖率,写进 ALPHA 会让面片变洞。
- `face_atlas_transparent.png` 是我们自己的素材(从已删除的 `mouse_表情测试.png` 去蓝底、反混合出来的 1024² 副本),保留原图坐标排布;内置 ID `builtin:face_01/02` 对应各自的图集 rect。**原始表情图已随模型删除,这个副本现在是唯一来源,别删。**
- **表情位置/大小/翻转只写在 `face_paint.gdshader` 顶部那段「调表情就改这一段」里**:`face_center`(中心,UV 单位,**往下挪就减小 .y**)、`face_size`(.x 宽 .y 高)、`face_flip_y`(默认 true)。
**FaceRig 故意不传这三个 uniform** —— 早期版本从 `@export` 传过去,会把 shader 里调好的值盖掉;现在 `face_rig.gd` 里没有对应的 export,测试里有一条断言禁止再传。
- 头部孤岛实测:u 0.695~0.996(左右)、v 0.682~0.996(上下,**v 越大越靠头顶**)。贴图第 0 行在上边,所以默认必须 `face_flip_y = true`,否则表情上下颠倒(用户实测确认过)。
- **矩形中心 ± 半个大小不能溢出头部孤岛**:正下方还有一段孤岛(u 0.695~0.935 / v 0.293~0.674),量下来是胸腹那一带,溢出去表情就会糊到肚子上。`tests/test_face.gd` 的 `test_face_rect_inside_head_island` 会直接从 shader 源码里读 `face_center` / `face_size` 再判定,**所以随手调这两个值不会被测试误报,只有溢出才会红**。
- 两个 FaceRig 节点(`player.tscn` 的 `Player/FaceRig`、`character_select.tscn` 的预览 `FaceRig`)现在都不带表情矩形参数了 —— 值只有一份,在 shader 里。
- **表情位置/大小/翻转只写在 `face_paint.gdshader` 顶部那段「调表情就改这一段」里**:`face_center`(中心,**模型本地坐标**,模型大约 x -3~3、y -3~3,**往下挪就减小 .y**)、`face_size`(.x 宽 .y 高,默认 4.0×4.0)、`face_front_edge`(法线朝前的阈值,调大只留最正面)、`face_flip_y`(默认 true)。
**FaceRig 故意不传这几个 uniform** —— 早期版本从 `@export` 传过去,会把 shader 里调好的值盖掉;现在 `face_rig.gd` 里没有对应的 export,测试里有一条断言禁止再传。
- 贴图第 0 行在上边,而头部 UV 的 v 朝上,所以默认必须 `face_flip_y = true`,否则表情上下颠倒(用户实测确认过)。
- 内置表情在各自图集 tile 里的墨线占比:`face_01` 68.9%×74.4%、`face_02` 83.9%×65.4%,且都居中 —— 所以 `face_size` 要按「墨线只占 tile 的七成」来估:4.0 的矩形给出约 2.8 单位的墨线宽度。
- `test_face_decal_inside_body_bounds` 校验贴花矩形落在模型包围盒里(模型换尺度/原点会红);`test_face_shader_has_expected_uniforms` 里钉住 `-normalize(object_normal).z` 和 `object_position.xy`,防止有人改回 UV 方案或写反正面。
- 两个 FaceRig 节点(`player.tscn` 的 `Player/FaceRig`、`character_select.tscn` 的预览 `FaceRig`)都不带表情参数 —— 值只有一份,在 shader 里。
- `FaceRig` 保存 `user://face_preset.json` 中的 expression ID 和 `user://face_paint.png` 暂存笔画;已保存的自绘表情用 `user://face_library/index.json` + 各自透明 PNG 持久化,角色选择页动态显示内置 atlas 表情和本地表情。
- 联机当前表情同步:内置表情发送 ID;`user:` 本地表情发送当前 PNG 字节;笔画层另外发送。**不广播整份本地表情库**。
- `FacePainter`:直接改 RGBA8 字节缓冲。画 = source-over alpha 混合,擦 = 把目标 alpha 乘掉;512² 一次 1MB,按一笔/动作同步一次。
@@ -81,6 +84,14 @@
## Godot MCP 踩坑
- **看画面**:`Read` 工具读用户发的图片文件会报 `Current model does not support reading images`,
但 `editor_screenshot(source="game")` 返回的游戏画面能正常看 —— 排查画面问题直接自己截图对照,
比让用户描述快。没有画面时也可以用像素统计(PNG 手写解码 + 粗 ASCII 图)反推布局。
- **用户在编辑器里开着的文件会覆盖磁盘改动**:改完 `face_paint.gdshader` 后,磁盘上出现过
「我的新注释 + 旧的 `+normal.z` + 我从未写过的 `face_center.y = 0.6`」这种混合体 ——
用户在 Godot 里开着这个 shader、缓冲区是旧版本,他一保存就把我后来的改动冲掉了,而且没有任何提示。
所以改 `.gdshader` / `.tscn` 之后要在回复里提醒对方先关掉/重开那个文件再保存。
- **数值标量经 MCP 会被字符串化**:`node_set_property` 传 `0.15` 能写进去,但回显是 `"0.15"`;
而 `batch_execute` 里包 `{"command": "set_property", "params": {...}}` 直接报
`INTERNAL_ERROR … handler returned malformed result`(和旧版本文档里写的正好相反,以实测为准)。