@ -6,6 +6,11 @@ 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
@ -32,7 +37,18 @@ func change_nickname(new_nickname: String) -> void:
nickname = new_nickname
## Attempts to change the model of this player.
func change_model ( filename : String ) - > void :
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
@ -43,34 +59,52 @@ func change_model(filename: String) -> void:
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 :
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_transform32 ( )
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 : = { }
# 256 blendshapes (and bones) should be enough, right?
var num_shapes : = stream . read_uint8 ( )
var num_shapes : = stream . read_uint8 ( ) # 256 blendshapes (and bones) should be enough, right?
for i in num_shapes :
var shape_name : = stream . read_string ( )
var shape_alpha : = stream . read_float16 ( )
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 )
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.
@ -81,34 +115,33 @@ static func send_model_animation(module: copyMultiplayer) -> void:
var media_pipe = module . get_node ( " ../MediaPipeController " )
if ( not model ) or ( not skeleton ) or ( not media_pipe ) : return
write_stream . write_transform32 ( model . transform )
write_stream . write_transform16 ( model . transform )
# TODO: Do not write full strings. Use a lookup table!
# TODO: Only write non-default blendshapes. Anything missing = default.
# 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 ( shape_dict . size ( ) )
for shape_name in shape_dict :
write_stream . write_uint8 ( module . blend shape_to_lookup . size ( ) )
for shape_name in module . blend shape_to_lookup :
var shape_alpha : float = shape_dict [ shape_name ]
write_stream . write_string ( shape_name )
write_stream . write_uint8 ( module . blendshape_to_lookup [ 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.
# The compression still helps, so we'll keep it for now.
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 )
# 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 ( )