开始菜单、玩家选择、脸部绘制
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
shader_type spatial;
|
||||
// 面部 = 两层贴图的合成:底图(预设脸)+ 笔画层(玩家自绘),后者带 alpha 盖在前面。
|
||||
// 两层都用「面部孤岛重映射后的 UV」采样,所以一张方形小图就能正好铺满整张脸。
|
||||
//
|
||||
// 为什么不是直接把贴图换掉:画布只装笔画、不装底图,橡皮擦掉笔画后露出来的是预设脸,
|
||||
// 而不是把脸擦成黑洞。这也是它能一边切预设一边保留手绘笔画的原因。
|
||||
|
||||
// 面部 UV 孤岛(实测自 mouse.glb 的 body_mesh surface 1 "face"):
|
||||
// u 0.142~0.356,v 0.038~0.238。v 往下外扩到 0.031 是为了取成正方形,避免拉伸。
|
||||
uniform vec2 face_uv_min = vec2(0.142, 0.031);
|
||||
uniform float face_uv_size : hint_range(0.01, 1.0) = 0.214;
|
||||
|
||||
// 预设脸。没接上时给张白图,至少不会一片黑
|
||||
uniform sampler2D base_tex : source_color, filter_linear, repeat_disable,
|
||||
hint_default_white;
|
||||
// 玩家笔画层(一个透明底的 SubViewport)。没接上时给张全透明图,等于没画
|
||||
uniform sampler2D paint_tex : source_color, filter_linear, repeat_disable,
|
||||
hint_default_transparent;
|
||||
|
||||
void fragment() {
|
||||
vec2 uv = (UV - face_uv_min) / face_uv_size;
|
||||
vec4 base = texture(base_tex, uv);
|
||||
vec4 paint = texture(paint_tex, uv);
|
||||
|
||||
ALBEDO = mix(base.rgb, paint.rgb, paint.a);
|
||||
// 顶掉 GLB 里 lambert1 那套手感,不然脸会比身体亮一块
|
||||
ROUGHNESS = 1.0;
|
||||
METALLIC = 0.0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://boka801kewxls
|
||||
@@ -0,0 +1,190 @@
|
||||
class_name FacePainter
|
||||
extends Node
|
||||
## 面部画笔:直接往一张 Image 的字节缓冲上盖笔刷印章。
|
||||
##
|
||||
## 为什么不用 Control._draw():Godot 4 的 canvas 已经没有「擦除」混合模式了
|
||||
## (CanvasItemMaterial 只有 mix / add / sub / mul / premult,也没有 draw_set_blend_mode),
|
||||
## 橡皮没法靠 blend_mode 实现。自己管像素反而更直接:
|
||||
## 画 = 标准的 source-over alpha 混合;擦 = 把目标的 alpha 乘掉;存档 = 就是存这张图。
|
||||
|
||||
var image: Image
|
||||
var texture: ImageTexture
|
||||
var canvas_size: int = 512
|
||||
|
||||
## 当前笔刷。绘画面板改这几个值,下一笔生效。
|
||||
var brush_color: Color = Color.BLACK
|
||||
var brush_width: float = 12.0
|
||||
var erasing: bool = false
|
||||
|
||||
## 像素缓冲(RGBA8)。所有绘制都改它,一笔画完再一次性推给显卡。
|
||||
var _buf: PackedByteArray
|
||||
## 圆形笔刷的覆盖度,边长 _stamp_d
|
||||
var _cov: PackedFloat32Array
|
||||
var _stamp_d: int = 0
|
||||
var _stamp_rgb: Vector3 = Vector3.ZERO
|
||||
var _last_stamp: Vector2 = Vector2.INF
|
||||
|
||||
|
||||
func _init(size: int = 512) -> void:
|
||||
canvas_size = size
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_buf = PackedByteArray()
|
||||
_buf.resize(canvas_size * canvas_size * 4)
|
||||
_sync()
|
||||
|
||||
|
||||
# ---------------- 画笔接口 ----------------
|
||||
|
||||
func begin_stroke(pos: Vector2) -> void:
|
||||
_rebuild_stamp()
|
||||
_last_stamp = Vector2.INF
|
||||
_stamp_at(pos)
|
||||
_last_stamp = pos
|
||||
_sync()
|
||||
|
||||
|
||||
func extend_stroke(pos: Vector2) -> void:
|
||||
if not _last_stamp.is_finite():
|
||||
_stamp_at(pos)
|
||||
_last_stamp = pos
|
||||
_sync()
|
||||
return
|
||||
var spacing := maxf(1.0, brush_width * 0.25)
|
||||
var dist := _last_stamp.distance_to(pos)
|
||||
if dist < spacing:
|
||||
return
|
||||
# 鼠标动得快时两点之间得补章,否则线是一串断点
|
||||
var steps := int(ceil(dist / spacing))
|
||||
for i in range(1, steps + 1):
|
||||
_stamp_at(_last_stamp.lerp(pos, float(i) / float(steps)))
|
||||
_last_stamp = pos
|
||||
_sync()
|
||||
|
||||
|
||||
func end_stroke() -> void:
|
||||
_last_stamp = Vector2.INF
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_buf.fill(0)
|
||||
_last_stamp = Vector2.INF
|
||||
_sync()
|
||||
|
||||
|
||||
## 拍一张当前笔画层的快照。绘画面板打开时存一份,点「取消」时还原
|
||||
func snapshot() -> PackedByteArray:
|
||||
return _buf.duplicate()
|
||||
|
||||
|
||||
## 还原到快照。尺寸对不上就忽略(换过画布大小)
|
||||
func restore(data: PackedByteArray) -> void:
|
||||
if data.size() != _buf.size():
|
||||
return
|
||||
_buf = data.duplicate()
|
||||
_last_stamp = Vector2.INF
|
||||
_sync()
|
||||
|
||||
|
||||
# ---------------- 存档 ----------------
|
||||
|
||||
## 笔画层存成一张透明底 PNG
|
||||
func save_png(path: String) -> void:
|
||||
_sync()
|
||||
image.save_png(path)
|
||||
|
||||
|
||||
## 读回笔画层。尺寸对不上就当没读到(换了画布大小的话)
|
||||
func load_png(path: String) -> void:
|
||||
if not FileAccess.file_exists(path):
|
||||
return
|
||||
set_image(Image.load_from_file(path))
|
||||
|
||||
|
||||
## 从一张 Image 直接载入笔画层 —— 联机收到别人的脸时走这条,不用落盘
|
||||
func set_image(img: Image) -> void:
|
||||
if img == null:
|
||||
return
|
||||
if img.get_width() != canvas_size or img.get_height() != canvas_size:
|
||||
push_warning("笔画层尺寸是 %s,画布是 %d,忽略" % [img.get_size(), canvas_size])
|
||||
return
|
||||
img.convert(Image.FORMAT_RGBA8)
|
||||
_buf = img.get_data()
|
||||
_last_stamp = Vector2.INF
|
||||
_sync()
|
||||
|
||||
|
||||
## 当前笔画层的 PNG 字节 —— 联机同步用,不落盘
|
||||
func to_png_bytes() -> PackedByteArray:
|
||||
_sync()
|
||||
return image.save_png_to_buffer()
|
||||
|
||||
|
||||
# ---------------- 内部 ----------------
|
||||
|
||||
## 把字节缓冲变成 Image 再推给显卡。一笔画完调一次 —— 512² 一次就是 1MB,别每盖一个印章都来
|
||||
func _sync() -> void:
|
||||
image = Image.create_from_data(canvas_size, canvas_size, false, Image.FORMAT_RGBA8, _buf)
|
||||
if texture == null:
|
||||
texture = ImageTexture.create_from_image(image)
|
||||
else:
|
||||
texture.update(image)
|
||||
|
||||
|
||||
## 按当前粗细和颜色重造笔刷。边缘留 1 像素羽化 —— 硬边的线很难看。
|
||||
func _rebuild_stamp() -> void:
|
||||
var r := maxi(2, int(round(brush_width * 0.5)))
|
||||
var d := r * 2
|
||||
_stamp_d = d
|
||||
_stamp_rgb = Vector3(brush_color.r, brush_color.g, brush_color.b)
|
||||
_cov.resize(d * d)
|
||||
for y in d:
|
||||
for x in d:
|
||||
var dx := float(x) - float(r) + 0.5
|
||||
var dy := float(y) - float(r) + 0.5
|
||||
var dist := sqrt(dx * dx + dy * dy) / float(r)
|
||||
_cov[y * d + x] = clampf((1.0 - dist) * float(r), 0.0, 1.0)
|
||||
|
||||
|
||||
## 在 pos 处盖一个印章。越界像素直接跳过,所以画到边上也不会出错。
|
||||
func _stamp_at(pos: Vector2) -> void:
|
||||
var d := _stamp_d
|
||||
var r := d / 2
|
||||
var cx := int(round(pos.x)) - r
|
||||
var cy := int(round(pos.y)) - r
|
||||
var sr := _stamp_rgb.x * 255.0
|
||||
var sg := _stamp_rgb.y * 255.0
|
||||
var sb := _stamp_rgb.z * 255.0
|
||||
|
||||
for sy in d:
|
||||
var ty := cy + sy
|
||||
if ty < 0 or ty >= canvas_size:
|
||||
continue
|
||||
var row := ty * canvas_size * 4
|
||||
var srow := sy * d
|
||||
for sx in d:
|
||||
var tx := cx + sx
|
||||
if tx < 0 or tx >= canvas_size:
|
||||
continue
|
||||
var cov := _cov[srow + sx]
|
||||
if cov <= 0.001:
|
||||
continue
|
||||
var o := row + tx * 4
|
||||
|
||||
if erasing:
|
||||
# 擦:把 alpha 按覆盖度乘掉。RGB 留着不管 —— shader 只看 alpha
|
||||
_buf[o + 3] = int(float(_buf[o + 3]) * (1.0 - cov))
|
||||
continue
|
||||
|
||||
var sa := cov
|
||||
var da := float(_buf[o + 3]) / 255.0
|
||||
var na := sa + da * (1.0 - sa)
|
||||
if na <= 0.0001:
|
||||
continue
|
||||
var ws := sa / na
|
||||
var wd := da * (1.0 - sa) / na
|
||||
_buf[o] = int(clampf(sr * ws + float(_buf[o]) * wd, 0.0, 255.0))
|
||||
_buf[o + 1] = int(clampf(sg * ws + float(_buf[o + 1]) * wd, 0.0, 255.0))
|
||||
_buf[o + 2] = int(clampf(sb * ws + float(_buf[o + 2]) * wd, 0.0, 255.0))
|
||||
_buf[o + 3] = int(clampf(na * 255.0, 0.0, 255.0))
|
||||
@@ -0,0 +1 @@
|
||||
uid://5fbgd46kg2hj
|
||||
@@ -0,0 +1,162 @@
|
||||
class_name FaceRig
|
||||
extends Node
|
||||
## 角色的脸:把 mesh 上那个「face」surface 换成自定义材质,
|
||||
## 材质把「预设底图」和「玩家自绘的笔画层」合成起来。
|
||||
##
|
||||
## 笔画层是一张常驻的 Image(不是 SubViewport)—— 这样即使绘画面板没打开,
|
||||
## 游戏内的脸也一直是玩家画的那张。绘画面板只是把 painter.texture 显示出来,
|
||||
## 把鼠标坐标换算好之后喂给 painter。
|
||||
|
||||
const SHADER_PATH := "res://character/player/face/face_paint.gdshader"
|
||||
## 存档:选的是哪张预设
|
||||
const SAVE_PRESET := "user://face_preset.json"
|
||||
## 存档:笔画层(透明底 PNG)
|
||||
const SAVE_PAINT := "user://face_paint.png"
|
||||
|
||||
## 联机同步:非本机玩家主动去要脸的次数与间隔。
|
||||
## 各端玩家节点出现的先后顺序不保证,头几次请求可能打空,所以要重试。
|
||||
const FACE_SYNC_TRIES := 5
|
||||
const FACE_SYNC_INTERVAL := 0.8
|
||||
## 第一次要之前先等一会儿。实测:主机生成玩家比客户端早几帧,立刻要会打在一个
|
||||
## 还没建出来的节点上(Godot 报 "Node not found: Main/<id>/FaceRig"),白刷一堆红字。
|
||||
const FACE_SYNC_INITIAL_DELAY := 1.0
|
||||
|
||||
@export_group("接线")
|
||||
## 面部所在的 MeshInstance3D 的路径(相对本节点)。默认就是 mouse.glb 里的 body_mesh。
|
||||
## 存路径而不是节点引用:节点引用是场景实例化时由 Godot 解析的,实测这里解析不到(拿到 null),
|
||||
## 自己在 _ready() 里 get_node_or_null 才稳。
|
||||
@export var mesh_path: NodePath = ^"../Body/mouse/MouseRig/Skeleton3D/body_mesh"
|
||||
## 面部是第几个 surface。mouse.glb 里是 1,名字就叫 face
|
||||
@export var surface_index: int = 1
|
||||
## 没有存档时用哪张脸
|
||||
@export var default_preset: Texture2D
|
||||
|
||||
@export_group("画布")
|
||||
## 画布边长(像素)。玩家画的笔画坐标就在这个空间里
|
||||
@export var canvas_size: int = 512
|
||||
|
||||
@export_group("面部 UV")
|
||||
## 面部孤岛在贴图上的起点(实测自模型)。模型重导后 UV 变了就改这里
|
||||
@export var face_uv_min: Vector2 = Vector2(0.142, 0.031)
|
||||
## 孤岛的边长(UV 单位)
|
||||
@export var face_uv_size: float = 0.214
|
||||
|
||||
## 当前预设的 res:// 路径。存档靠它记住「你选的是哪张脸」
|
||||
var preset_path: String = ""
|
||||
## 联机时有没有收到过这张脸主人的数据(收到了就不用再重试)
|
||||
var _got_face := false
|
||||
|
||||
var mesh: MeshInstance3D
|
||||
var material: ShaderMaterial
|
||||
var painter: FacePainter
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mesh = get_node_or_null(mesh_path) as MeshInstance3D
|
||||
if mesh == null:
|
||||
push_warning("%s 找不到面部 mesh(%s),脸不会有变化" % [get_path(), mesh_path])
|
||||
return
|
||||
|
||||
painter = FacePainter.new(canvas_size)
|
||||
painter.name = "FacePainter"
|
||||
add_child(painter)
|
||||
|
||||
material = ShaderMaterial.new()
|
||||
material.shader = load(SHADER_PATH)
|
||||
material.set_shader_parameter("face_uv_min", face_uv_min)
|
||||
material.set_shader_parameter("face_uv_size", face_uv_size)
|
||||
material.set_shader_parameter("paint_tex", painter.texture)
|
||||
mesh.set_surface_override_material(surface_index, material)
|
||||
|
||||
apply_saved()
|
||||
|
||||
# 联机时非本机玩家去要脸。单机(OfflineMultiplayerPeer)不用同步
|
||||
var peer := multiplayer.multiplayer_peer
|
||||
if peer != null and not (peer is OfflineMultiplayerPeer) and not is_multiplayer_authority():
|
||||
_pull_remote_face()
|
||||
|
||||
|
||||
# ---------------- 对外接口 ----------------
|
||||
|
||||
## 换成某张预设脸。只换底图,玩家画的笔画保留 —— 所以可以边挑预设边留着笔迹。
|
||||
func set_preset(tex: Texture2D, path: String = "") -> void:
|
||||
if material != null:
|
||||
material.set_shader_parameter("base_tex", tex)
|
||||
if not path.is_empty():
|
||||
preset_path = path
|
||||
|
||||
|
||||
func clear_paint() -> void:
|
||||
if painter != null:
|
||||
painter.clear()
|
||||
|
||||
|
||||
## 存:预设路径 + 笔画层。下次进游戏自动读回来
|
||||
func save() -> void:
|
||||
if painter == null:
|
||||
return
|
||||
painter.save_png(SAVE_PAINT)
|
||||
var f := FileAccess.open(SAVE_PRESET, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_warning("脸部预设存不下来:%s" % SAVE_PRESET)
|
||||
return
|
||||
f.store_string(JSON.stringify({"preset": preset_path}))
|
||||
|
||||
|
||||
## 读存档;没有(或存的预设已经不在了)就退回 default_preset
|
||||
func apply_saved() -> void:
|
||||
var preset: Texture2D = default_preset
|
||||
var path := default_preset.resource_path if default_preset != null else ""
|
||||
|
||||
if FileAccess.file_exists(SAVE_PRESET):
|
||||
var f := FileAccess.open(SAVE_PRESET, FileAccess.READ)
|
||||
var data: Variant = JSON.parse_string(f.get_as_text()) if f != null else null
|
||||
if data is Dictionary:
|
||||
var saved := str(data.get("preset", ""))
|
||||
if not saved.is_empty() and ResourceLoader.exists(saved):
|
||||
preset = load(saved)
|
||||
path = saved
|
||||
|
||||
set_preset(preset, path)
|
||||
if painter != null:
|
||||
painter.load_png(SAVE_PAINT)
|
||||
|
||||
|
||||
# ---------------- 联机同步 ----------------
|
||||
# 用「谁缺谁来要」而不是「出生时推」:各端玩家节点出现的先后顺序不保证,
|
||||
# 推的话很可能推给一个还不存在的节点;而请求总是从已经存在的那一端发出去的。
|
||||
|
||||
## 非本机玩家在 _ready 里调,去要这张脸主人的那一份。带重试,因为对端节点可能还没建好。
|
||||
func _pull_remote_face() -> void:
|
||||
await get_tree().create_timer(FACE_SYNC_INITIAL_DELAY).timeout
|
||||
for i in FACE_SYNC_TRIES:
|
||||
if not is_inside_tree() or _got_face:
|
||||
return
|
||||
_rpc_request_face.rpc_id(get_multiplayer_authority())
|
||||
await get_tree().create_timer(FACE_SYNC_INTERVAL).timeout
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _rpc_request_face() -> void:
|
||||
# 只有这张脸的主人回应 —— 别人的脸不该由我来发
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
_rpc_send_face.rpc_id(
|
||||
multiplayer.get_remote_sender_id(),
|
||||
preset_path,
|
||||
painter.to_png_bytes() if painter != null else PackedByteArray(),
|
||||
)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _rpc_send_face(preset: String, png: PackedByteArray) -> void:
|
||||
# 只认这张脸主人的回应;本机自己的脸不接受覆盖
|
||||
if is_multiplayer_authority() or multiplayer.get_remote_sender_id() != get_multiplayer_authority():
|
||||
return
|
||||
_got_face = true
|
||||
if not preset.is_empty() and ResourceLoader.exists(preset):
|
||||
set_preset(load(preset), preset)
|
||||
if png.size() > 0 and painter != null:
|
||||
var img := Image.new()
|
||||
if img.load_png_from_buffer(png) == OK:
|
||||
painter.set_image(img)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6l2fl5jgvr1h
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b2yfeejwww5oi"
|
||||
path="res://.godot/imported/face_00_blank.png-258a9c2a268c4896542abf099fbd9d15.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://character/player/face/presets/face_00_blank.png"
|
||||
dest_files=["res://.godot/imported/face_00_blank.png-258a9c2a268c4896542abf099fbd9d15.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
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=false
|
||||
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=1
|
||||
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://5wd8ao2i3lcq"
|
||||
path="res://.godot/imported/face_01.png-cc81c7ed8009dc49f59ab7f3b569ca21.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://character/player/face/presets/face_01.png"
|
||||
dest_files=["res://.godot/imported/face_01.png-cc81c7ed8009dc49f59ab7f3b569ca21.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
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=false
|
||||
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=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bup0pyw24lsft"
|
||||
path="res://.godot/imported/face_02.png-914865f7dedf5c93ed2fd1b1b037f243.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://character/player/face/presets/face_02.png"
|
||||
dest_files=["res://.godot/imported/face_02.png-914865f7dedf5c93ed2fd1b1b037f243.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
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=false
|
||||
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=1
|
||||
@@ -11,6 +11,8 @@
|
||||
[ext_resource type="AnimationLibrary" uid="uid://rqhclfyiingo" path="res://character/player/assets/animations/run.glb" id="7_muf2e"]
|
||||
[ext_resource type="AnimationLibrary" uid="uid://dy8a178vlbsof" path="res://character/player/assets/animations/sneak.glb" id="8_jo6ux"]
|
||||
[ext_resource type="AnimationLibrary" uid="uid://db83440o2cyr7" path="res://character/player/assets/animations/walk.glb" id="9_fkbv7"]
|
||||
[ext_resource type="Script" uid="uid://c6l2fl5jgvr1h" path="res://character/player/face/face_rig.gd" id="12_lqp71"]
|
||||
[ext_resource type="Texture2D" path="res://character/player/face/presets/face_01.png" id="13_face01"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_rkbax"]
|
||||
radius = 0.4
|
||||
@@ -65,3 +67,8 @@ modulate = Color(1, 0.95, 0.6, 1)
|
||||
text = "按 E 挂上滑索"
|
||||
font_size = 48
|
||||
outline_size = 8
|
||||
|
||||
[node name="FaceRig" type="Node" parent="." unique_id=792293233]
|
||||
script = ExtResource("12_lqp71")
|
||||
mesh_path = NodePath("../Body/mouse/MouseRig/Skeleton3D/body_mesh")
|
||||
default_preset = ExtResource("13_face01")
|
||||
|
||||
Reference in New Issue
Block a user