SnekStudio module for multiplayer / multiuser support
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

147 lines
5.4 KiB

class_name SyncController
extends Node
var module: copyMultiplayer
var model_controller: ModelController
var peer_id: int
var nickname: String
var version: int = -1
var bone_lookup: Array[String]
var blendshape_lookup: Array[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(
new_version: int,
new_bone_lookup: Array[String],
new_blendshape_lookup: Array[String],
filename: String,
) -> void:
# These should be safe to update even if the model doesn't load.
# We just need to stay in sync with the lookup tables.
version = new_version
bone_lookup = new_bone_lookup
blendshape_lookup = new_blendshape_lookup
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
model_name = filename
model = model_controller.get_node_or_null("Model")
skeleton = model_controller._get_model_skeleton()
module.print_log("%s switched to '%s'" % [ get_display_name(), filename ])
func sync_model_animation(
current_version: int,
uncompressed_length: int,
buffer: PackedByteArray,
) -> void:
if version != current_version: return
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_transform16()
# We skipped some bones, so reset the skipped ones to the rest pose.
var all_bones := {}
for bone_name in module.bone_to_lookup:
var bone_idx := skeleton.find_bone(bone_name)
var bone_rest := skeleton.get_bone_rest(bone_idx)
all_bones[bone_name] = { idx = bone_idx, pose = bone_rest }
# Override rest poses with ones from the packet.
var num_bones := stream.read_uint8()
for i in num_bones:
var bone_name := bone_lookup[stream.read_uint8()]
all_bones[bone_name].pose = stream.read_bone_pose()
# Apply bone poses to skeleton.
for bone_name in all_bones:
var bone: Dictionary = all_bones[bone_name]
skeleton.set_bone_pose(bone.idx, bone.pose)
var shape_dict := {}
var num_shapes := stream.read_uint8() # 256 blendshapes (and bones) should be enough, right?
for i in num_shapes:
var shape_name := blendshape_lookup[stream.read_uint8()]
var shape_alpha := stream.read_float16()
shape_dict[shape_name] = shape_alpha
BlendShapes.apply_animations(model, shape_dict)
@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_transform16(model.transform)
# Pre-filter any bones that are in rest pose.
var restless_bones := {}
for bone_name in module.bone_to_lookup:
var bone_idx := skeleton.find_bone(bone_name)
var bone_pose := skeleton.get_bone_pose(bone_idx)
var bone_rest := skeleton.get_bone_rest(bone_idx)
if not bone_pose.is_equal_approx(bone_rest):
restless_bones[bone_name] = bone_pose
write_stream.write_uint8(restless_bones.size())
for bone_name in restless_bones:
write_stream.write_uint8(module.bone_to_lookup[bone_name])
write_stream.write_bone_pose(restless_bones[bone_name])
# TODO: Only write non-default blendshapes. Anything missing = default.
var shape_dict: Dictionary = media_pipe.blend_shape_last_values
write_stream.write_uint8(module.blendshape_to_lookup.size())
for shape_name in module.blendshape_to_lookup:
var shape_alpha: float = shape_dict[shape_name]
write_stream.write_uint8(module.blendshape_to_lookup[shape_name])
write_stream.write_float16(shape_alpha)
# The compression still helps, so we'll keep it for now.
var compressed_buffer := write_stream.slice().compress(FileAccess.COMPRESSION_ZSTD);
# Uncomment this to see packet size. Can we hit < 256 bytes?
# module.set_status("Packet size: %d bytes (%d uncompressed)" % [ compressed_buffer.size(), write_stream.size ])
module.sync_model_animation.rpc(module.version, write_stream.size, compressed_buffer)
write_stream.clear()