开始菜单、玩家选择、脸部绘制
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
@tool
|
||||
extends McpTestSuite
|
||||
## 回归测试:面部那套「UV 孤岛 + 预设 + 画笔」的契约。
|
||||
##
|
||||
## 脸是 body_mesh 的 surface 1,UV 是一个孤岛;shader 靠 face_uv_min / face_uv_size 把孤岛
|
||||
## 重映射到 0~1,再拿这个 UV 去采样预设和笔画层。**模型重导后 UV 一变,脸就会画歪或者被裁掉,
|
||||
## 而且不报任何错** —— 所以这里把数值钉死,改模型时先让测试红一次。
|
||||
|
||||
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"
|
||||
|
||||
## FaceRig 里 face_uv_min / face_uv_size 的默认值,必须和 shader 的 uniform 默认值一致
|
||||
const FACE_UV_MIN := Vector2(0.142, 0.031)
|
||||
const FACE_UV_SIZE := 0.214
|
||||
## UV 是浮点,两边各写一份,给点容差
|
||||
const EPS := 0.003
|
||||
|
||||
|
||||
func suite_name() -> String:
|
||||
return "face"
|
||||
|
||||
|
||||
func _player() -> Node:
|
||||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||||
assert_true(packed != null, "%s 未能加载" % PLAYER_SCENE_PATH)
|
||||
if packed == null:
|
||||
return null
|
||||
var player := packed.instantiate()
|
||||
track(player)
|
||||
return player
|
||||
|
||||
|
||||
func _body_mesh() -> MeshInstance3D:
|
||||
var player := _player()
|
||||
if player == null:
|
||||
return null
|
||||
return player.get_node_or_null(BODY_MESH_PATH) as MeshInstance3D
|
||||
|
||||
|
||||
func test_player_has_face_rig_wired() -> void:
|
||||
var player := _player()
|
||||
if player == null:
|
||||
return
|
||||
var rig := player.get_node_or_null("FaceRig") as FaceRig
|
||||
assert_true(rig != null, "Player 下缺少 FaceRig 节点")
|
||||
if rig == null:
|
||||
return
|
||||
assert_eq(rig.surface_index, 1, "面部是 surface 1(名字叫 face),索引写错就画到身体上了")
|
||||
assert_true(rig.get_node_or_null(rig.mesh_path) is MeshInstance3D,
|
||||
"FaceRig.mesh_path(%s)解析不到 MeshInstance3D —— 换模型后路径要跟着改" % rig.mesh_path)
|
||||
|
||||
|
||||
func test_face_surface_exists() -> void:
|
||||
var mi := _body_mesh()
|
||||
assert_true(mi != null, "找不到 %s" % BODY_MESH_PATH)
|
||||
if mi == null:
|
||||
return
|
||||
var m := mi.mesh as ArrayMesh
|
||||
assert_true(m != null, "body_mesh 的 mesh 不是 ArrayMesh")
|
||||
if m == null:
|
||||
return
|
||||
assert_gt(m.get_surface_count(), 1, "body_mesh 至少要有两个 surface(0 身体 / 1 脸)")
|
||||
if m.get_surface_count() < 2:
|
||||
return
|
||||
assert_eq(m.surface_get_name(1), "face", "surface 1 不叫 face 了 —— 确认过再改 surface_index")
|
||||
|
||||
|
||||
func test_face_uv_island_matches_shader() -> void:
|
||||
var mi := _body_mesh()
|
||||
if mi == null:
|
||||
return
|
||||
var m := mi.mesh as ArrayMesh
|
||||
if m == null or m.get_surface_count() < 2:
|
||||
return
|
||||
var uv: PackedVector2Array = m.surface_get_arrays(1)[Mesh.ARRAY_TEX_UV]
|
||||
var mn := Vector2(INF, INF)
|
||||
var mx := Vector2(-INF, -INF)
|
||||
for v in uv:
|
||||
mn = mn.min(v)
|
||||
mx = mx.max(v)
|
||||
var far := FACE_UV_MIN + Vector2(FACE_UV_SIZE, FACE_UV_SIZE)
|
||||
# 孤岛必须整个落在 shader 重映射用的那个方框里,否则脸会错位 / 被裁掉
|
||||
assert_true(mn.x >= FACE_UV_MIN.x - EPS and mn.y >= FACE_UV_MIN.y - EPS,
|
||||
"面部 UV 起点 %s 跑出了 face_uv_min %s —— 模型重导后 UV 变了?" % [mn, FACE_UV_MIN])
|
||||
assert_true(mx.x <= far.x + EPS and mx.y <= far.y + EPS,
|
||||
"面部 UV 终点 %s 超出了 face_uv_min + face_uv_size = %s" % [mx, far])
|
||||
|
||||
|
||||
func test_face_shader_has_expected_uniforms() -> void:
|
||||
var sh: Shader = load(SHADER_PATH)
|
||||
assert_true(sh != null, "%s 加载失败" % SHADER_PATH)
|
||||
if sh == null:
|
||||
return
|
||||
var names: Array[String] = []
|
||||
for p in sh.get_shader_uniform_list():
|
||||
names.append(str(p.get("name")))
|
||||
for required in ["face_uv_min", "face_uv_size", "base_tex", "paint_tex"]:
|
||||
assert_true(names.has(required), "shader 少了 uniform %s,实际为 %s" % [required, names])
|
||||
|
||||
|
||||
## 联机同步靠这三个方法名,改名了要一起改 —— 改了就不同步,而且不报错
|
||||
func test_face_sync_methods_exist() -> void:
|
||||
var script := load("res://character/player/face/face_rig.gd") as GDScript
|
||||
assert_true(script != null, "face_rig.gd 加载失败")
|
||||
if script == null:
|
||||
return
|
||||
var names: Array[String] = []
|
||||
for m in script.get_script_method_list():
|
||||
names.append(str(m.get("name")))
|
||||
for required in ["_pull_remote_face", "_rpc_request_face", "_rpc_send_face"]:
|
||||
assert_true(names.has(required), "face_rig.gd 少了 %s,联机时脸不会同步" % required)
|
||||
|
||||
|
||||
func test_presets_are_square() -> void:
|
||||
var dir := DirAccess.open(PRESET_DIR)
|
||||
assert_true(dir != null, "打不开预设目录 %s" % PRESET_DIR)
|
||||
if dir == null:
|
||||
return
|
||||
var found := 0
|
||||
for f in dir.get_files():
|
||||
if not f.ends_with(".png"):
|
||||
continue
|
||||
found += 1
|
||||
var tex: Texture2D = load(PRESET_DIR + "/" + f)
|
||||
assert_true(tex != null, "预设 %s 加载失败" % f)
|
||||
if tex == null:
|
||||
continue
|
||||
var s := tex.get_size()
|
||||
assert_eq(int(s.x), int(s.y), "预设 %s 不是正方形(%s),贴到脸上会拉伸" % [f, s])
|
||||
assert_gt(found, 0, "%s 里一张预设都没有" % PRESET_DIR)
|
||||
@@ -0,0 +1 @@
|
||||
uid://brw0ntpgcol2o
|
||||
@@ -187,6 +187,21 @@ func test_animation_track_paths_resolve() -> void:
|
||||
)
|
||||
|
||||
|
||||
## 入口场景必须是开始菜单 —— 否则启动就直接落在世界里,还顶着 Net 那个调试面板。
|
||||
## 这条防的是「有人把 run/main_scene 改回世界」。
|
||||
func test_main_scene_is_the_menu() -> void:
|
||||
var configured := str(ProjectSettings.get_setting("application/run/main_scene", ""))
|
||||
assert_true(not configured.is_empty(), "project.godot 没设 application/run/main_scene")
|
||||
if configured.is_empty():
|
||||
return
|
||||
# 存的是 uid 也可能是 res:// 路径,两种都认
|
||||
var path := configured
|
||||
if configured.begins_with("uid://"):
|
||||
path = ResourceUID.get_id_path(ResourceUID.text_to_id(configured))
|
||||
assert_true(path.ends_with("ui/main_menu.tscn"),
|
||||
"入口场景是 %s,不是开始菜单 —— 启动会直接进世界" % path)
|
||||
|
||||
|
||||
## 片段在 AnimationPlayer 里按「库名/片段名」存(导入时每个 glb 各自成一个库),
|
||||
## 短名 has_animation() / get_animation() 一律查不到 —— 拿短名断言会假红,必须按库扫一遍。
|
||||
func _find_clip(anim_player: AnimationPlayer, clip: String) -> Animation:
|
||||
|
||||
Reference in New Issue
Block a user