修复玩家脸部
This commit is contained in:
+29
-92
@@ -1,20 +1,19 @@
|
||||
@tool
|
||||
extends McpTestSuite
|
||||
## 回归测试:脸部那套「UV 矩形 + 预设 + 画笔」的契约。
|
||||
## 回归测试:脸部那套「贴花 + 预设 + 画笔」的契约。
|
||||
##
|
||||
## body_mesh 只有一个材质 mat_body,脸部不再单独换材质,而是由 shader 顶部的
|
||||
## face_center / face_size 在本体 UV 上圈出表情矩形。**模型重导或改 UV 之后,脸就会画歪、
|
||||
## 被裁掉、甚至糊到肚子上,而且不报任何错** —— 所以这里把「矩形必须落在头部孤岛里」钉死,
|
||||
## 改模型时先让测试红一次。数值本身从 shader 源码里读,方便随手调。
|
||||
## body_mesh 只有一个材质 mat_body,脸是 shader 里贴在身体正面上的一块**贴花**:
|
||||
## 用模型本地坐标 (x, y) 定位、用法线朝前的程度当遮罩,所以和 UV 孤岛怎么排无关,
|
||||
## 也不会因为越界糊到肚子/背上。这里盯住:贴花参数还在、矩形大致落在模型包围盒里
|
||||
## (模型换尺度或换原点时会红)、以及 FaceRig 不许再抢着传这些 uniform。
|
||||
|
||||
const PLAYER_SCENE_PATH := "res://character/player/player.tscn"
|
||||
const SHADER_PATH := "res://character/player/face/face_paint.gdshader"
|
||||
const PRESET_DIR := "res://character/player/face/presets"
|
||||
const BODY_MESH_PATH := "Body/mouse/MouseRig/Skeleton3D/body_mesh"
|
||||
const BODY_MATERIAL_NAME := "mat_body"
|
||||
|
||||
## UV 是浮点,给点容差
|
||||
const EPS := 0.004
|
||||
## 模型本地坐标,给点容差
|
||||
const EPS := 0.05
|
||||
|
||||
|
||||
func suite_name() -> String:
|
||||
@@ -29,11 +28,11 @@ func _shader_code() -> String:
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
## 从 shader 源码里抓 `uniform vec2 名字 = vec2(a, b);` 的两个数。
|
||||
## 从 shader 源码里抓 `名字 = vec2(a, b);` 的两个数(uniform 和 const 都能抓)。
|
||||
func _shader_vec2(code: String, name: String) -> Vector2:
|
||||
var marker := "uniform vec2 %s = vec2(" % name
|
||||
var marker := "%s = vec2(" % name
|
||||
var start := code.find(marker)
|
||||
assert_true(start >= 0, "shader 里找不到 uniform vec2 %s 的默认值" % name)
|
||||
assert_true(start >= 0, "shader 里找不到 %s 的 vec2 默认值" % name)
|
||||
if start < 0:
|
||||
return Vector2(INF, INF)
|
||||
start += marker.length()
|
||||
@@ -63,69 +62,6 @@ func _body_mesh() -> MeshInstance3D:
|
||||
return player.get_node_or_null(BODY_MESH_PATH) as MeshInstance3D
|
||||
|
||||
|
||||
## 找「最高处那块 UV 孤岛」(= 头部)的 UV 范围。脸部矩形必须落在它里面。
|
||||
func _head_island_uv_rect() -> Rect2:
|
||||
var mi := _body_mesh()
|
||||
if mi == null:
|
||||
return Rect2()
|
||||
var mesh := mi.mesh as ArrayMesh
|
||||
if mesh == null or mesh.get_surface_count() == 0:
|
||||
return Rect2()
|
||||
var arrays := mesh.surface_get_arrays(0)
|
||||
var uvs := arrays[Mesh.ARRAY_TEX_UV] as PackedVector2Array
|
||||
var verts := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var indices := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
indices = arrays[Mesh.ARRAY_INDEX]
|
||||
if uvs.size() == 0 or indices.size() == 0:
|
||||
return Rect2()
|
||||
var parent := []
|
||||
parent.resize(uvs.size())
|
||||
for i in uvs.size():
|
||||
parent[i] = i
|
||||
for t in indices.size() / 3:
|
||||
_union(parent, indices[t * 3], indices[t * 3 + 1])
|
||||
_union(parent, indices[t * 3], indices[t * 3 + 2])
|
||||
var groups := {}
|
||||
for i in uvs.size():
|
||||
var root := _find(parent, i)
|
||||
if not groups.has(root):
|
||||
groups[root] = []
|
||||
groups[root].append(i)
|
||||
var best_root := -1
|
||||
var best_top := -INF
|
||||
for root in groups:
|
||||
var members: Array = groups[root]
|
||||
var top := -INF
|
||||
for i in members:
|
||||
top = maxf(top, verts[i].y)
|
||||
if top > best_top:
|
||||
best_top = top
|
||||
best_root = root
|
||||
var members: Array = groups.get(best_root, [])
|
||||
var uv_min := Vector2(INF, INF)
|
||||
var uv_max := Vector2(-INF, -INF)
|
||||
for i in members:
|
||||
var uv: Vector2 = uvs[i]
|
||||
uv_min = Vector2(minf(uv_min.x, uv.x), minf(uv_min.y, uv.y))
|
||||
uv_max = Vector2(maxf(uv_max.x, uv.x), maxf(uv_max.y, uv.y))
|
||||
return Rect2(uv_min, uv_max - uv_min)
|
||||
|
||||
|
||||
func _find(parent: Array, index: int) -> int:
|
||||
while parent[index] != index:
|
||||
parent[index] = parent[parent[index]]
|
||||
index = parent[index]
|
||||
return index
|
||||
|
||||
|
||||
func _union(parent: Array, a: int, b: int) -> void:
|
||||
var root_a := _find(parent, a)
|
||||
var root_b := _find(parent, b)
|
||||
if root_a != root_b:
|
||||
parent[root_b] = root_a
|
||||
|
||||
|
||||
func test_player_has_face_rig_wired() -> void:
|
||||
var player := _player()
|
||||
if player == null:
|
||||
@@ -137,9 +73,9 @@ 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,就会把调好的值盖掉
|
||||
# 贴花参数只写在 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"]:
|
||||
for name in ["face_center", "face_size", "face_flip_y", "face_front_edge"]:
|
||||
assert_true(not script_text.contains("set_shader_parameter(\"%s\"" % name),
|
||||
"face_rig.gd 又在传 %s 了 —— shader 顶部调好的值会被脚本覆盖" % name)
|
||||
|
||||
@@ -166,9 +102,8 @@ func test_body_mesh_has_single_material() -> void:
|
||||
assert_true(standard.albedo_texture != null, "%s 没有 BaseColor 贴图,身体底色会变成白" % BODY_MATERIAL_NAME)
|
||||
|
||||
|
||||
## 脸部矩形必须整块落在头部孤岛里,否则表情会画到身体别的部位上。
|
||||
## 表情矩形(中心 ± 半个大小)必须整块落在头部孤岛里,否则表情会糊到别的部位上。
|
||||
func test_face_rect_inside_head_island() -> void:
|
||||
## 贴花矩形是模型本地坐标,必须落在模型自己的包围盒里,否则表情会飘到体外。
|
||||
func test_face_decal_inside_body_bounds() -> void:
|
||||
var code := _shader_code()
|
||||
if code.is_empty():
|
||||
return
|
||||
@@ -176,33 +111,35 @@ func test_face_rect_inside_head_island() -> void:
|
||||
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:
|
||||
assert_true(size.x > 0.0 and size.y > 0.0, "face_size 必须是正数(要翻转请用 face_flip_y)")
|
||||
var mi := _body_mesh()
|
||||
if mi == null:
|
||||
return
|
||||
var bounds := mi.mesh.get_aabb()
|
||||
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(中心 %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(中心 %s 大小 %s)—— 模型改过 UV 还是值调过头了?"
|
||||
% [rect_max, head, center, size])
|
||||
assert_true(rect_min.x >= bounds.position.x - EPS and rect_min.y >= bounds.position.y - EPS,
|
||||
"贴花左下角 %s 跑到模型包围盒 %s 外面了" % [rect_min, bounds])
|
||||
assert_true(rect_max.x <= bounds.end.x + EPS and rect_max.y <= bounds.end.y + EPS,
|
||||
"贴花右上角 %s 跑到模型包围盒 %s 外面了 —— 模型换尺度/原点后会这样" % [rect_max, bounds])
|
||||
|
||||
|
||||
func test_face_shader_has_expected_uniforms() -> void:
|
||||
var file := FileAccess.open(SHADER_PATH, FileAccess.READ)
|
||||
assert_true(file != null, "%s 加载失败" % SHADER_PATH)
|
||||
if file == null:
|
||||
var code := _shader_code()
|
||||
if code.is_empty():
|
||||
return
|
||||
var code := file.get_as_text()
|
||||
for required in [
|
||||
"body_tex", "body_tint", "surface_roughness", "surface_metallic",
|
||||
"face_center", "face_size", "face_flip_y",
|
||||
"face_center", "face_size", "face_front_edge", "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)
|
||||
assert_true(code.contains("texture(body_tex, UV)"),
|
||||
"shader 必须用 body_tex 采样全身底色,否则脸和身体不是同一块皮肤")
|
||||
assert_true(code.contains("varying vec3 object_position") and code.contains("object_position.xy"),
|
||||
"贴花必须用模型本地坐标定位,否则又会被 UV 孤岛边界卡住")
|
||||
assert_true(code.contains("-normalize(object_normal).z"),
|
||||
"贴花必须按「法线朝前」筛一遍,而且正面是模型本地 -Z,写反了表情会跑到背上")
|
||||
assert_true(code.contains("ROUGHNESS = surface_roughness"), "shader 必须复用 mat_body 的 roughness")
|
||||
assert_true(code.contains("METALLIC = surface_metallic"), "shader 必须复用 mat_body 的 metallic")
|
||||
# alpha 只代表墨线覆盖率,不能写进空间 ALPHA,否则身体面片会被抠出洞
|
||||
|
||||
Reference in New Issue
Block a user