|
|
|
class_name SyncController
|
|
|
|
extends Node
|
|
|
|
|
|
|
|
var module: copyMultiplayer
|
|
|
|
var model_controller: ModelController
|
|
|
|
var peer_id: int
|
|
|
|
|
|
|
|
var nickname: String
|
|
|
|
var model_name: String
|
|
|
|
var model: Node
|
|
|
|
var skeleton: Skeleton3D
|
|
|
|
|
|
|
|
# Reusable buffer to write data for synchronizing models.
|
|
|
|
static var write_stream: StreamBuffer = StreamBuffer.with_capacity(2048)
|
|
|
|
|
|
|
|
# Allows us to use the "apply_animations" function to apply blendshapes to a model.
|
|
|
|
static var BlendShapes: Script = load("res://Mods/MediaPipe/MediaPipeController_BlendShapes.gd")
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
module = get_parent().get_parent()
|
|
|
|
model_controller = get_parent()
|
|
|
|
peer_id = model_controller.name.to_int()
|
|
|
|
|
|
|
|
func get_display_name() -> String:
|
|
|
|
if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ]
|
|
|
|
else: return "Player (%d)" % peer_id
|
|
|
|
|
|
|
|
func change_nickname(new_nickname: String) -> void:
|
|
|
|
new_nickname = new_nickname.strip_edges()
|
|
|
|
if new_nickname == "": return # Ignore empty nicknames.
|
|
|
|
module.print_log("%s is now known as '%s'" % [ get_display_name(), new_nickname ])
|
|
|
|
nickname = new_nickname
|
|
|
|
|
|
|
|
## Attempts to change the model of this player.
|
|
|
|
func change_model(filename: String) -> void:
|
|
|
|
if not filename.is_valid_filename():
|
|
|
|
module.print_log("ERROR: '%s' is not a valid file name!" % filename)
|
|
|
|
return
|
|
|
|
var full_path := module.cache.path_join(filename)
|
|
|
|
if not FileAccess.file_exists(full_path):
|
|
|
|
module.print_log("%s wanted to switch to '%s', but it doesn't exist, skipping" % [ get_display_name(), filename ])
|
|
|
|
return
|
|
|
|
if not model_controller.load_vrm(full_path):
|
|
|
|
module.print_log("ERROR: Model '%s' could not be loaded!" % filename)
|
|
|
|
return
|
|
|
|
module.print_log("%s switched to '%s'" % [ get_display_name(), filename ])
|
|
|
|
model_name = filename
|
|
|
|
model = model_controller.get_node_or_null("Model")
|
|
|
|
skeleton = model_controller._get_model_skeleton()
|
|
|
|
|
|
|
|
func sync_model_animation(uncompressed_length: int, buffer: PackedByteArray) -> void:
|
|
|
|
if (not model) or (not skeleton): return
|
|
|
|
|
|
|
|
var uncompressed_buffer := buffer.decompress(uncompressed_length, FileAccess.COMPRESSION_ZSTD);
|
|
|
|
var stream := StreamBuffer.from_buffer(uncompressed_buffer)
|
|
|
|
model.transform = stream.read_transform32()
|
|
|
|
|
|
|
|
var shape_dict := {}
|
|
|
|
# 256 blendshapes (and bones) should be enough, right?
|
|
|
|
var num_shapes := stream.read_uint8()
|
|
|
|
for i in num_shapes:
|
|
|
|
var shape_name := stream.read_string()
|
|
|
|
var shape_alpha := stream.read_float16()
|
|
|
|
shape_dict[shape_name] = shape_alpha
|
|
|
|
BlendShapes.apply_animations(model, shape_dict)
|
|
|
|
|
|
|
|
var num_bones := stream.read_uint8()
|
|
|
|
for i in num_bones:
|
|
|
|
var bone_name := stream.read_string()
|
|
|
|
var bone_pose := stream.read_bone_pose()
|
|
|
|
var bone_idx := skeleton.find_bone(bone_name)
|
|
|
|
if bone_idx != -1: skeleton.set_bone_pose(bone_idx, bone_pose)
|
|
|
|
|
|
|
|
@warning_ignore("shadowed_variable")
|
|
|
|
static func send_model_animation(module: copyMultiplayer) -> void:
|
|
|
|
# Check if there's other players we're connected to.
|
|
|
|
if module.multiplayer.get_peers().size() == 0: return
|
|
|
|
|
|
|
|
var model := module.get_model()
|
|
|
|
var skeleton := module.get_skeleton()
|
|
|
|
var media_pipe = module.get_node("../MediaPipeController")
|
|
|
|
if (not model) or (not skeleton) or (not media_pipe): return
|
|
|
|
|
|
|
|
write_stream.write_transform32(model.transform)
|
|
|
|
|
|
|
|
# TODO: Do not write full strings. Use a lookup table!
|
|
|
|
# TODO: Only write non-default blendshapes. Anything missing = default.
|
|
|
|
|
|
|
|
var shape_dict: Dictionary = media_pipe.blend_shape_last_values
|
|
|
|
write_stream.write_uint8(shape_dict.size())
|
|
|
|
for shape_name in shape_dict:
|
|
|
|
var shape_alpha: float = shape_dict[shape_name]
|
|
|
|
write_stream.write_string(shape_name)
|
|
|
|
write_stream.write_float16(shape_alpha)
|
|
|
|
|
|
|
|
var bone_poses = {}
|
|
|
|
for bone_name in module.tracked_bones:
|
|
|
|
var bone_idx = skeleton.find_bone(bone_name)
|
|
|
|
if bone_idx == -1: continue
|
|
|
|
var bone_pose = skeleton.get_bone_pose(bone_idx)
|
|
|
|
bone_poses[bone_name] = bone_pose
|
|
|
|
|
|
|
|
write_stream.write_uint8(bone_poses.size())
|
|
|
|
for bone_name in bone_poses:
|
|
|
|
var bone_pose: Transform3D = bone_poses[bone_name]
|
|
|
|
write_stream.write_string(bone_name)
|
|
|
|
write_stream.write_bone_pose(bone_pose)
|
|
|
|
|
|
|
|
# TODO: Ideally, compression won't be needed once we remove strings.
|
|
|
|
var compressed_buffer := write_stream.slice().compress(FileAccess.COMPRESSION_ZSTD);
|
|
|
|
# DEBUG: Uncomment this to see packet size (ideally < 1024).
|
|
|
|
# module.set_status("Packet size: %d (%d uncompressed)" % [compressed_buffer.size(), write_stream.size])
|
|
|
|
module.sync_model_animation.rpc(write_stream.size, compressed_buffer)
|
|
|
|
write_stream.clear()
|