1
This commit is contained in:
@@ -296,6 +296,22 @@ FaceRig 加同名 `@export`(默认 `true`),并把 uniform 推给 shader。
|
||||
|
||||
`test_run` 32/32。
|
||||
|
||||
### 改成「shader 顶部调参」而不是 UI 滑块
|
||||
|
||||
用户先问能不能在角色选择界面加滑块移动脸部,我给完方案后他决定不做界面,改成在 `face_paint.gdshader`
|
||||
顶部放几个简单明了的设置。于是把 `face_uv_min` / `face_uv_size` 换成更直观的 **`face_center`(中心)+ `face_size`(宽高)**,
|
||||
和 `face_flip_y` 一起集中到 shader 顶部的「调表情就改这一段」,每行都写了「往下挪就减小 .y」这类人话注释。
|
||||
|
||||
关键点:**FaceRig 必须停止传这三个 uniform**,否则脚本里的值会把 shader 里调好的值盖掉(等于白改)。
|
||||
删掉了 `face_uv_min` / `face_uv_size` / `face_uv_flip_y` 三个 @export 和三行 set_shader_parameter,
|
||||
并在 `test_player_has_face_rig_wired` 里加了一条断言:`face_rig.gd` 源码里不许再出现
|
||||
`set_shader_parameter("face_center"/"face_size"/"face_flip_y"`。
|
||||
|
||||
测试改成从 shader 源码里解析 `face_center` / `face_size`(`_shader_vec2()` 辅助函数),
|
||||
再判定「中心 ± 半径」是否落在头部孤岛内 —— 这样用户随手调值不会误报,只有溢出孤岛才红。
|
||||
默认值就是原来的覆盖范围(中心 0.8455/0.839,大小 0.301/0.314),画面不变。
|
||||
`test_run` 32/32,角色选择场景实机跑无 shader/脚本报错。
|
||||
|
||||
## 执行用户提供的 export_split.py 命令
|
||||
|
||||
用户给出 Blender 5.1 与 `_rig_tools/export_split.py` 路径。我检查到脚本读取 `bpy.data.objects["MouseRig"]`,因此命令原样缺少 blend 源文件;实际运行时补传已有主工程 `mouse.blend`,其余仍是指定脚本。
|
||||
|
||||
@@ -57,8 +57,11 @@
|
||||
- 表情遮罩用 `step()` 相乘而不是 `if`:采样留在统一控制流里,免得 mipmap 导数在分支里未定义。
|
||||
- **仍然不能写空间 `ALPHA`**:alpha 只代表墨线覆盖率,写进 ALPHA 会让面片变洞。
|
||||
- `face_atlas_transparent.png` 是我们自己的素材(从已删除的 `mouse_表情测试.png` 去蓝底、反混合出来的 1024² 副本),保留原图坐标排布;内置 ID `builtin:face_01/02` 对应各自的图集 rect。**原始表情图已随模型删除,这个副本现在是唯一来源,别删。**
|
||||
- **脸部矩形 = 整块头部 UV 孤岛 (0.695, 0.682) 尺寸 (0.301, 0.314)** —— 用户确认「就是整块头部」。**头部 UV 的 v 越大越靠上,而贴图第 0 行在上边,所以必须开 `face_uv_flip_y`(默认 true),否则表情上下颠倒**(用户实测确认过)。想往下挪:把 `face_uv_min.y` 钉在孤岛底边 0.682、改小 `face_uv_size.y`;**min.y 不能低于 0.682**,否则矩形会伸进下方那段孤岛(u 0.695~0.935 / v 0.293~0.674,是胸腹),表情会同时画到肚子和前胸上。
|
||||
- `face_uv_min` / `face_uv_size` / `face_uv_flip_y` 在 `player.tscn` 和 `character_select.tscn` 里**各有一份**(两个 FaceRig 节点),检视器里改只影响当前那个;要两边一致就改 `face_rig.gd` 里的默认值。`face_uv_size` 是 `Vector2`。测试 `test_face_uv_rect_inside_head_island` 会按 min/max 归一化后再判定,允许负边长当翻转用。
|
||||
- **表情位置/大小/翻转只写在 `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 里。
|
||||
- `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,按一笔/动作同步一次。
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,23 @@
|
||||
shader_type spatial;
|
||||
// 全身 shader:body_mesh 现在只有一个材质 mat_body,所以身体底色照常取它的 BaseColor,
|
||||
// 脸部**不再单独换材质**,而是在同一个 shader 里,只在指定 UV 矩形内把表情和手绘叠上去。
|
||||
// 这样脸和头部走的是同一套材质、同一套光照,不会再出现独立面片的色块边界。
|
||||
// 全身 shader:body_mesh 只有一个材质 mat_body —— 身体底色照常取它的 BaseColor,
|
||||
// 脸部不再换材质,只在这一个 shader 里把表情和手绘叠到指定 UV 矩形上。
|
||||
// 脸和周围走的是同一套材质、同一套光照,所以不会再出现独立面片的色块边界。
|
||||
//
|
||||
// 贴图 alpha 只表示「墨线覆盖率」,绝不写进空间 ALPHA —— body_mesh 的面片必须保持不透明。
|
||||
// 贴图 alpha 只代表「墨线覆盖率」,绝不写进空间 ALPHA —— body_mesh 的面片必须保持不透明。
|
||||
|
||||
// ================= 调表情就改这一段 =================
|
||||
// 头部那一段 UV 孤岛的实测范围:u 0.695~0.996(左右),v 0.682~0.996(上下,v 越大越靠头顶)。
|
||||
// 下面三个值就是「表情画在哪 / 多大 / 正不正」,FaceRig 不会覆盖它们,改完存盘即可。
|
||||
//
|
||||
// 表情中心点:**想往下挪就减小 .y**,往左挪就减小 .x
|
||||
uniform vec2 face_center = vec2(0.8455, 0.639);
|
||||
// 表情大小:.x 是左右宽度,.y 是上下高度(想只盖住半张脸就把 .y 改小)
|
||||
uniform vec2 face_size = vec2(0.301, 0.314);
|
||||
// 上下颠倒了就改成 false(头部 UV 的 v 朝上、贴图第 0 行在上边,所以默认要翻)
|
||||
uniform bool face_flip_y = true;
|
||||
// ====================================================
|
||||
//
|
||||
// 中心 ± 半个大小必须整个落在上面那段 UV 孤岛里,否则表情会糊到肚子 / 背上去。
|
||||
|
||||
uniform sampler2D body_tex : source_color, filter_linear, repeat_disable,
|
||||
hint_default_white;
|
||||
@@ -11,14 +25,6 @@ uniform vec4 body_tint : source_color = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
uniform float surface_roughness : hint_range(0.0, 1.0) = 0.65;
|
||||
uniform float surface_metallic : hint_range(0.0, 1.0) = 0.0;
|
||||
|
||||
// 脸部在 body_mesh 的 UV 上占的矩形。**模型重导 / 改 UV 之后必须复核这里**:
|
||||
// 它要整块落在头部那块 UV 孤岛里面,否则表情会画到别的部位上。
|
||||
uniform vec2 face_uv_min = vec2(0.695, 0.682);
|
||||
uniform vec2 face_uv_size = vec2(0.301, 0.314);
|
||||
// 头部 UV 的 v 是「越往上越大」,而贴图第 0 行在上边,所以默认要把 v 翻过来,
|
||||
// 否则表情上下颠倒。
|
||||
uniform bool face_uv_flip_y = true;
|
||||
|
||||
// 表情底图:内置表情是保留原排布、已去底的透明图集;本地表情是整张透明 PNG。
|
||||
uniform sampler2D base_tex : source_color, filter_linear, repeat_disable,
|
||||
hint_default_transparent;
|
||||
@@ -32,8 +38,8 @@ void fragment() {
|
||||
// 全身底色都来自 mat_body 的 BaseColor,表情只是盖在脸上
|
||||
vec3 albedo = texture(body_tex, UV).rgb * body_tint.rgb;
|
||||
|
||||
vec2 face_uv = (UV - face_uv_min) / face_uv_size;
|
||||
if (face_uv_flip_y) {
|
||||
vec2 face_uv = (UV - (face_center - face_size * 0.5)) / face_size;
|
||||
if (face_flip_y) {
|
||||
face_uv.y = 1.0 - face_uv.y;
|
||||
}
|
||||
// 用 step 而不是 if:采样留在统一控制流里,免得 mipmap 的导数在分支里未定义
|
||||
|
||||
@@ -30,17 +30,13 @@ const FACE_SYNC_INITIAL_DELAY := 1.0
|
||||
|
||||
@export_group("画布")
|
||||
@export var canvas_size: int = 512
|
||||
## 与两个原始贴图的背景/头部底色完全相同:RGB(137,179,255)。
|
||||
## 与模型身体贴图的底色一致(FaceRig 启动时会从 mat_body 的 BaseColor 左上角自动读出来)。
|
||||
## 只给画布和缩略图垫底用,不参与 3D 着色。
|
||||
@export var skin_color: Color = Color(0.537255, 0.701961, 1.0, 1.0)
|
||||
|
||||
@export_group("脸部 UV")
|
||||
## 脸部矩形在 body_mesh 的 UV 上占的范围(实测自当前模型:头部孤岛是 u 0.695~0.996 / v 0.682~0.996)。
|
||||
## 它必须整块落在头部那块 UV 孤岛里;模型重导后 UV 一变,表情就会画歪或跑到别的部位上。
|
||||
@export var face_uv_min: Vector2 = Vector2(0.695, 0.682)
|
||||
## 脸部矩形的 UV 边长
|
||||
@export var face_uv_size: Vector2 = Vector2(0.301, 0.314)
|
||||
## 头部 UV 的 v 越往上越大,而贴图第 0 行在上边 —— 不翻就在脸上上下颠倒
|
||||
@export var face_uv_flip_y: bool = true
|
||||
## 表情画在哪 / 多大 / 正不正,全部写在 face_paint.gdshader 顶部的「调表情就改这一段」里
|
||||
## (face_center / face_size / face_flip_y)。**FaceRig 故意不传这三个 uniform**,
|
||||
## 免得脚本里的值把 shader 里调好的值盖掉。
|
||||
|
||||
var mesh: MeshInstance3D
|
||||
var material: ShaderMaterial
|
||||
@@ -88,9 +84,6 @@ func _ready() -> void:
|
||||
material.set_shader_parameter("body_tint", body_material.albedo_color)
|
||||
material.set_shader_parameter("surface_roughness", body_material.roughness)
|
||||
material.set_shader_parameter("surface_metallic", body_material.metallic)
|
||||
material.set_shader_parameter("face_uv_min", face_uv_min)
|
||||
material.set_shader_parameter("face_uv_size", face_uv_size)
|
||||
material.set_shader_parameter("face_uv_flip_y", face_uv_flip_y)
|
||||
material.set_shader_parameter("paint_tex", painter.texture)
|
||||
mesh.set_surface_override_material(surface_index, material)
|
||||
|
||||
|
||||
+50
-16
@@ -2,9 +2,10 @@
|
||||
extends McpTestSuite
|
||||
## 回归测试:脸部那套「UV 矩形 + 预设 + 画笔」的契约。
|
||||
##
|
||||
## body_mesh 现在只有一个材质 mat_body,脸部不再单独换材质,而是靠 face_uv_min / face_uv_size
|
||||
## 在这块 UV 上圈出脸部矩形。**模型重导或改 UV 之后,脸就会画歪、被裁掉、甚至画到别的部位上,
|
||||
## 而且不报任何错** —— 所以这里把数值钉死,改模型时先让测试红一次。
|
||||
## body_mesh 只有一个材质 mat_body,脸部不再单独换材质,而是由 shader 顶部的
|
||||
## face_center / face_size 在本体 UV 上圈出表情矩形。**模型重导或改 UV 之后,脸就会画歪、
|
||||
## 被裁掉、甚至糊到肚子上,而且不报任何错** —— 所以这里把「矩形必须落在头部孤岛里」钉死,
|
||||
## 改模型时先让测试红一次。数值本身从 shader 源码里读,方便随手调。
|
||||
|
||||
const PLAYER_SCENE_PATH := "res://character/player/player.tscn"
|
||||
const SHADER_PATH := "res://character/player/face/face_paint.gdshader"
|
||||
@@ -12,10 +13,7 @@ const PRESET_DIR := "res://character/player/face/presets"
|
||||
const BODY_MESH_PATH := "Body/mouse/MouseRig/Skeleton3D/body_mesh"
|
||||
const BODY_MATERIAL_NAME := "mat_body"
|
||||
|
||||
## FaceRig / shader 里脸部矩形的默认值;必须和 shader 的 uniform 默认值一致
|
||||
const FACE_UV_MIN := Vector2(0.695, 0.682)
|
||||
const FACE_UV_SIZE := Vector2(0.301, 0.314)
|
||||
## UV 是浮点,两边各写一份,给点容差
|
||||
## UV 是浮点,给点容差
|
||||
const EPS := 0.004
|
||||
|
||||
|
||||
@@ -23,6 +21,31 @@ func suite_name() -> String:
|
||||
return "face"
|
||||
|
||||
|
||||
func _shader_code() -> String:
|
||||
var file := FileAccess.open(SHADER_PATH, FileAccess.READ)
|
||||
assert_true(file != null, "%s 加载失败" % SHADER_PATH)
|
||||
if file == null:
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
## 从 shader 源码里抓 `uniform vec2 名字 = vec2(a, b);` 的两个数。
|
||||
func _shader_vec2(code: String, name: String) -> Vector2:
|
||||
var marker := "uniform vec2 %s = vec2(" % name
|
||||
var start := code.find(marker)
|
||||
assert_true(start >= 0, "shader 里找不到 uniform vec2 %s 的默认值" % name)
|
||||
if start < 0:
|
||||
return Vector2(INF, INF)
|
||||
start += marker.length()
|
||||
var stop := code.find(")", start)
|
||||
if stop < 0:
|
||||
return Vector2(INF, INF)
|
||||
var parts := code.substr(start, stop - start).split(",")
|
||||
if parts.size() != 2:
|
||||
return Vector2(INF, INF)
|
||||
return Vector2(float(parts[0].strip_edges()), float(parts[1].strip_edges()))
|
||||
|
||||
|
||||
func _player() -> Node:
|
||||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||||
assert_true(packed != null, "%s 未能加载" % PLAYER_SCENE_PATH)
|
||||
@@ -114,6 +137,11 @@ func test_player_has_face_rig_wired() -> void:
|
||||
assert_eq(rig.surface_index, 0, "body_mesh 只剩一个材质,FaceRig 必须覆盖 surface 0")
|
||||
assert_true(rig.get_node_or_null(rig.mesh_path) is MeshInstance3D,
|
||||
"FaceRig.mesh_path(%s)解析不到 MeshInstance3D —— 换模型后路径要跟着改" % rig.mesh_path)
|
||||
# 表情位置/大小只写在 shader 里;脚本一旦也传这三个 uniform,就会把调好的值盖掉
|
||||
var script_text := FileAccess.open("res://character/player/face/face_rig.gd", FileAccess.READ).get_as_text()
|
||||
for name in ["face_center", "face_size", "face_flip_y"]:
|
||||
assert_true(not script_text.contains("set_shader_parameter(\"%s\"" % name),
|
||||
"face_rig.gd 又在传 %s 了 —— shader 顶部调好的值会被脚本覆盖" % name)
|
||||
|
||||
|
||||
func test_body_mesh_has_single_material() -> void:
|
||||
@@ -139,20 +167,26 @@ func test_body_mesh_has_single_material() -> void:
|
||||
|
||||
|
||||
## 脸部矩形必须整块落在头部孤岛里,否则表情会画到身体别的部位上。
|
||||
func test_face_uv_rect_inside_head_island() -> void:
|
||||
## 表情矩形(中心 ± 半个大小)必须整块落在头部孤岛里,否则表情会糊到别的部位上。
|
||||
func test_face_rect_inside_head_island() -> void:
|
||||
var code := _shader_code()
|
||||
if code.is_empty():
|
||||
return
|
||||
var center := _shader_vec2(code, "face_center")
|
||||
var size := _shader_vec2(code, "face_size")
|
||||
if not center.is_finite() or not size.is_finite():
|
||||
return
|
||||
var head := _head_island_uv_rect()
|
||||
assert_true(head.size.x > 0.0 and head.size.y > 0.0, "没算出头部 UV 孤岛 —— 模型结构变了?")
|
||||
if head.size.x <= 0.0:
|
||||
return
|
||||
# 允许负边长(负数 = 把该轴翻过来),所以先归一化成 min/max
|
||||
var rect_min := Vector2(minf(FACE_UV_MIN.x, FACE_UV_MIN.x + FACE_UV_SIZE.x),
|
||||
minf(FACE_UV_MIN.y, FACE_UV_MIN.y + FACE_UV_SIZE.y))
|
||||
var rect_max := Vector2(maxf(FACE_UV_MIN.x, FACE_UV_MIN.x + FACE_UV_SIZE.x),
|
||||
maxf(FACE_UV_MIN.y, FACE_UV_MIN.y + FACE_UV_SIZE.y))
|
||||
var rect_min := center - size * 0.5
|
||||
var rect_max := center + size * 0.5
|
||||
assert_true(rect_min.x >= head.position.x - EPS and rect_min.y >= head.position.y - EPS,
|
||||
"脸部矩形起点 %s 跑出了头部 UV 孤岛 %s" % [rect_min, head])
|
||||
"表情矩形左下角 %s 跑出了头部 UV 孤岛 %s(中心 %s 大小 %s)" % [rect_min, head, center, size])
|
||||
assert_true(rect_max.x <= head.end.x + EPS and rect_max.y <= head.end.y + EPS,
|
||||
"脸部矩形终点 %s 超出了头部 UV 孤岛 %s —— 模型改过 UV?" % [rect_max, head])
|
||||
"表情矩形右上角 %s 超出了头部 UV 孤岛 %s(中心 %s 大小 %s)—— 模型改过 UV 还是值调过头了?"
|
||||
% [rect_max, head, center, size])
|
||||
|
||||
|
||||
func test_face_shader_has_expected_uniforms() -> void:
|
||||
@@ -163,7 +197,7 @@ func test_face_shader_has_expected_uniforms() -> void:
|
||||
var code := file.get_as_text()
|
||||
for required in [
|
||||
"body_tex", "body_tint", "surface_roughness", "surface_metallic",
|
||||
"face_uv_min", "face_uv_size", "face_uv_flip_y",
|
||||
"face_center", "face_size", "face_flip_y",
|
||||
"base_tex", "base_uv_min", "base_uv_size", "paint_tex",
|
||||
]:
|
||||
assert_true(code.contains("uniform ") and code.contains(required), "shader 少了 uniform %s" % required)
|
||||
|
||||
Reference in New Issue
Block a user