角色和动画

This commit is contained in:
2026-10-02 09:04:37 +08:00
parent 27a550f61b
commit 5173b45066
32 changed files with 351 additions and 2904 deletions
+16
View File
@@ -0,0 +1,16 @@
# 2026-10-02
## 审查:Godot 4.7「SubViewport 面部手绘 + 骨骼驱动」方案
用户贴来一份第三方方案(SubViewport 当可绘制面部贴图 + SkeletonModifier3D 把骨骼 Z 转成 shader 参数),问可行性。只做评审,未改任何代码。
结论:**原理可行**(SubViewport-as-texture + shader 混合是标准做法),但原文代码有 4 处硬伤:
1. shader `render_mode ... skip_vertex_transform` 却没写 vertex() 手动变换 → 模型位置错。删掉该 render_mode。
2. `PaintSurface._input` 在**后台 SubViewport 里收不到鼠标事件**(Viewport 之间不派发输入)→ 必须放进 `SubViewportContainer`,或手动 `push_input`。
3. `SkeletonModifier3D` **没有 `skeleton` 属性**,只有 `get_skeleton()`(已查 4.5 文档确认)。
4. `save_paint` 没 `await RenderingServer.frame_post_draw` → 存到空帧。
标定类问题:`get_bone_pose_position()` 是相对父骨的姿态,Z 轴受父骨朝向影响,GLTF 导入后与 Blender 不一致,z_min/z_max 必须实测标定;`face_index` 只做了亮度乘,没真正切换表情图层。
与本项目相关的判断:角色是 `character-male-e.glb`,需先确认有独立面部材质槽 + 展平 UV;多人下 SubViewport/材质必须每实例一份,远端玩家的画要按 PNG 字节流同步。
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

@@ -1,41 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cq3awe0wjqsih"
path.s3tc="res://.godot/imported/colormap.png-4bd9a802f0fbdd1bc388c729ce936ac0.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://character/player/assets/Textures/colormap.png"
dest_files=["res://.godot/imported/colormap.png-4bd9a802f0fbdd1bc388c729ce936ac0.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

@@ -1,41 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://chawqtd3fcydp"
path.s3tc="res://.godot/imported/texture_01.png-e765476e37c3bd9342318549a3523e3a.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://character/player/assets/Textures/texture_01.png"
dest_files=["res://.godot/imported/texture_01.png-e765476e37c3bd9342318549a3523e3a.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="animation_library"
importer_version=1
type="AnimationLibrary"
uid="uid://stsads1ivf5g"
path="res://.godot/imported/crouch.glb-e5c926449e187b681f5015d7207822d2.res"
[deps]
source_file="res://character/player/assets/animations/crouch.glb"
dest_files=["res://.godot/imported/crouch.glb-e5c926449e187b681f5015d7207822d2.res"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="animation_library"
importer_version=1
type="AnimationLibrary"
uid="uid://ddnmtmswkjx6g"
path="res://.godot/imported/fall.glb-49c9490de76a63bebdee88239a1f0f9e.res"
[deps]
source_file="res://character/player/assets/animations/fall.glb"
dest_files=["res://.godot/imported/fall.glb-49c9490de76a63bebdee88239a1f0f9e.res"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="animation_library"
importer_version=1
type="AnimationLibrary"
uid="uid://ddkl5cqdd5usx"
path="res://.godot/imported/idle.glb-2d0157de17feb1a431c92000d6d714d9.res"
[deps]
source_file="res://character/player/assets/animations/idle.glb"
dest_files=["res://.godot/imported/idle.glb-2d0157de17feb1a431c92000d6d714d9.res"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="animation_library"
importer_version=1
type="AnimationLibrary"
uid="uid://esw60hwb2cty"
path="res://.godot/imported/jump.glb-26f2d5decb10ac3d815d0fc041029ce3.res"
[deps]
source_file="res://character/player/assets/animations/jump.glb"
dest_files=["res://.godot/imported/jump.glb-26f2d5decb10ac3d815d0fc041029ce3.res"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="animation_library"
importer_version=1
type="AnimationLibrary"
uid="uid://rqhclfyiingo"
path="res://.godot/imported/run.glb-cfcc84d875800bb53941cb49d5cb93b3.res"
[deps]
source_file="res://character/player/assets/animations/run.glb"
dest_files=["res://.godot/imported/run.glb-cfcc84d875800bb53941cb49d5cb93b3.res"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.
@@ -1,15 +1,15 @@
[remap]
importer="scene"
importer="animation_library"
importer_version=1
type="PackedScene"
uid="uid://ccf670c6bkob8"
path="res://.godot/imported/character-male-e.glb-9d9c3cbc43d44547b0be72ff662d6ebd.scn"
type="AnimationLibrary"
uid="uid://dy8a178vlbsof"
path="res://.godot/imported/sneak.glb-94e9451c07d186fd632d29f778666d4f.res"
[deps]
source_file="res://character/player/assets/character-male-e.glb"
dest_files=["res://.godot/imported/character-male-e.glb-9d9c3cbc43d44547b0be72ff662d6ebd.scn"]
source_file="res://character/player/assets/animations/sneak.glb"
dest_files=["res://.godot/imported/sneak.glb-94e9451c07d186fd632d29f778666d4f.res"]
[params]
@@ -40,6 +40,6 @@ materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=1
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=0
gltf/texture_map_mode=1
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="animation_library"
importer_version=1
type="AnimationLibrary"
uid="uid://db83440o2cyr7"
path="res://.godot/imported/walk.glb-0021c25d27c990fe38e60a06db5fd410.res"
[deps]
source_file="res://character/player/assets/animations/walk.glb"
dest_files=["res://.godot/imported/walk.glb-0021c25d27c990fe38e60a06db5fd410.res"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dyr1r3xy4k6yr"
path="res://.godot/imported/mouse.glb-bb5a2e095042167ba6c85c244cff8451.scn"
uid="uid://css1vk3u8gt7o"
path="res://.godot/imported/mouse.glb-4ea50106b9c7f752fe9b9922707b87db.scn"
[deps]
source_file="res://character/mouse.glb"
dest_files=["res://.godot/imported/mouse.glb-bb5a2e095042167ba6c85c244cff8451.scn"]
source_file="res://character/player/assets/mouse.glb"
dest_files=["res://.godot/imported/mouse.glb-4ea50106b9c7f752fe9b9922707b87db.scn"]
[params]

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -2,8 +2,8 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bkskciyppsmhi"
path.s3tc="res://.godot/imported/mouse_mouse_lambert1_BaseColor.png-3ad3fdf9b364375fc113d826837b1d99.s3tc.ctex"
uid="uid://demw0p70x7tf"
path.s3tc="res://.godot/imported/mouse_mouse_lambert1_BaseColor.png-df62a96328566f97654d49386d6c6e11.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
@@ -14,8 +14,8 @@ generator_parameters={
[deps]
source_file="res://character/mouse_mouse_lambert1_BaseColor.png"
dest_files=["res://.godot/imported/mouse_mouse_lambert1_BaseColor.png-3ad3fdf9b364375fc113d826837b1d99.s3tc.ctex"]
source_file="res://character/player/assets/mouse_mouse_lambert1_BaseColor.png"
dest_files=["res://.godot/imported/mouse_mouse_lambert1_BaseColor.png-df62a96328566f97654d49386d6c6e11.s3tc.ctex"]
[params]

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

@@ -2,8 +2,8 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://dc7vkn6i6ddbr"
path.s3tc="res://.godot/imported/mouse_表情测试.png-77e2e39d7db722ce2d95ba6a8099623b.s3tc.ctex"
uid="uid://dpf8vtls80ko7"
path.s3tc="res://.godot/imported/mouse_表情测试.png-d03396f0afb7f56f6567aea06cc6e030.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
@@ -14,8 +14,8 @@ generator_parameters={
[deps]
source_file="res://character/mouse_表情测试.png"
dest_files=["res://.godot/imported/mouse_表情测试.png-77e2e39d7db722ce2d95ba6a8099623b.s3tc.ctex"]
source_file="res://character/player/assets/mouse_表情测试.png"
dest_files=["res://.godot/imported/mouse_表情测试.png-d03396f0afb7f56f6567aea06cc6e030.s3tc.ctex"]
[params]
File diff suppressed because one or more lines are too long
+6 -32
View File
@@ -1,36 +1,19 @@
extends Node
class_name PlayerAnimator
## 角色动作状态机 —— 只负责「把运动状态翻译成播放哪段动画」,完全不碰移动。
##
## 挂在 Player/Body 下,与具体模型解耦:换角色模型只要在检视面板重新指定
## animation_player(或让它自动找),代码里没有任何硬编码的节点路径。
##
## 对外接口只有一个:set_motion()。方向、加速度、重力、输入全归控制器管;
## 本组件只读入一个「运动快照」,输出到 AnimationPlayer。反向不回写任何东西。
##
## 为什么状态与倍率必须是可复制属性:
## 远端玩家物理与输入都关着,手里只有 position 和 Body.rotation,
## 算不出水平速度,也分不清是上升还是下落。所以权威端把结论复制过去,
## 远端只在 setter 里回放。
## 对应的同步条目(Sync.root_path = "..",即 Player 节点):
## Body/PlayerAnimator:anim_state
## Body/PlayerAnimator:anim_speed
## 角色动作状态。权威端每物理帧算一次,再靠 Sync 复制给远端回放。
enum AnimState { IDLE, WALK, SPRINT, JUMP, FALL }
enum AnimState { IDLE, WALK, CROUCH, RUN, FALL, SNEAK, JUMP }
## 状态 → 片段名。取自 character-male-e.glb 的动作库,换动作库改这里。
const ANIM_CLIPS: Dictionary = {
AnimState.IDLE: "idle",
AnimState.WALK: "walk",
AnimState.SPRINT: "sprint",
AnimState.CROUCH: "crouch",
AnimState.RUN: "run",
AnimState.SNEAK: "SNEAK",
AnimState.JUMP: "jump",
AnimState.FALL: "fall",
}
## 需要循环的状态。glb 里每个片段的 loop_mode 都是 NONE,
## 不补循环的话 idle/walk/sprint 播一遍就定在最后一帧。
const ANIM_LOOPED: Array[int] = [AnimState.IDLE, AnimState.WALK, AnimState.SPRINT]
## 需要循环的状态。
const ANIM_LOOPED: Array[int] = [AnimState.IDLE, AnimState.WALK, AnimState.RUN]
@export_group("接线")
## 要驱动的 AnimationPlayer。留空则在本节点的兄弟子树里自动找一个。
@@ -73,8 +56,6 @@ var anim_speed: float = 1.0:
func _ready() -> void:
if animation_player == null:
animation_player = _auto_find_animation_player()
if animation_player == null:
push_warning("%s 没绑定 AnimationPlayer,角色不会有动画" % get_path())
return
@@ -138,13 +119,6 @@ func _play(clip: String, blend: float) -> void:
animation_player.play(clip, blend)
## 兜底:在同一层视觉节点里找一个 AnimationPlayer。只在没接线时生效。
func _auto_find_animation_player() -> AnimationPlayer:
var parent := get_parent()
if parent == null:
return null
return parent.find_child("AnimationPlayer", true, false) as AnimationPlayer
func _patch_loop_modes() -> void:
for state in ANIM_LOOPED:
+13
View File
@@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://44re8f0ce0kc"]
[ext_resource type="Material" path="res://addons/csg_blockout/res/materials/mat_grid_light.tres" id="1_lbw0t"]
[node name="World" type="Node3D" unique_id=332495265]
[node name="CSGBox3D" type="CSGBox3D" parent="." unique_id=768581787]
transform = Transform3D(100, 0, 0, 0, 1, 0, 0, 0, 100, 0, 0, 0)
material = ExtResource("1_lbw0t")
[node name="CSGBox3D2" type="CSGBox3D" parent="." unique_id=198061082]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
material = ExtResource("1_lbw0t")