From 2ac43ba7df3b2c3dc40c6b87c2bc57c4b02ffa42 Mon Sep 17 00:00:00 2001 From: hop311 Date: Sun, 5 May 2024 18:36:21 +0100 Subject: Add GDScript XAC and XSM loaders --- game/src/Game/Model/XSMLoader.gd | 363 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 game/src/Game/Model/XSMLoader.gd (limited to 'game/src/Game/Model/XSMLoader.gd') diff --git a/game/src/Game/Model/XSMLoader.gd b/game/src/Game/Model/XSMLoader.gd new file mode 100644 index 0000000..332bbec --- /dev/null +++ b/game/src/Game/Model/XSMLoader.gd @@ -0,0 +1,363 @@ +class_name XSMLoader + +# Keys: source_file (String) +# Values: loaded animation (Animation) or LOAD_FAILED_MARKER (StringName) +static var xsm_cache : Dictionary + +const LOAD_FAILED_MARKER : StringName = &"XSM LOAD FAILED" + +static func get_xsm_animation(source_file : String) -> Animation: + var cached : Variant = xsm_cache.get(source_file) + if not cached: + cached = _load_xsm_animation(source_file) + if cached: + xsm_cache[source_file] = cached + else: + xsm_cache[source_file] = LOAD_FAILED_MARKER + push_error("Failed to get XSM model \"", source_file, "\" (current load failed)") + return null + + if not cached is Animation: + push_error("Failed to get XSM model \"", source_file, "\" (previous load failed)") + return null + + return cached + +const SKELETON_PATH : String = "./skeleton:%s" + +static func _load_xsm_animation(source_file : String) -> Animation: + var source_path : String = GameSingleton.lookup_file_path(source_file) + var file : FileAccess = FileAccess.open(source_path, FileAccess.READ) + if file == null: + push_error("Failed to load XSM ", source_file, " from looked up path ", source_path) + return null + + readHeader(file) + + var metadataChunk : MetadataChunk = null + var boneAnimationChunks : Array[BoneAnimationChunk] = [] + + while file.get_position() < file.get_length(): + var type : int = FileAccessUtils.read_int32(file) + var length : int = FileAccessUtils.read_int32(file) + var version : int = FileAccessUtils.read_int32(file) + + match type: + 0xC9: #Metadata + metadataChunk = readMetadataChunk(file) + 0xCA: #Bone Animation + boneAnimationChunks.push_back(readBoneAnimationChunk(file, metadataChunk.use_quat_16())) + _: + push_error(">> INVALID XSM CHUNK TYPE %X" % type) + break + + var animLength : float = 0.0 + var anim : Animation = Animation.new() + for anim_Chunk : BoneAnimationChunk in boneAnimationChunks: + for submotion : SkeletalSubMotion in anim_Chunk.SkeletalSubMotions: + # NOTE: godot uses ':' to specify properties, so we replace such characters with '_' + var skeleton_path : String = SKELETON_PATH % FileAccessUtils.replace_chars(submotion.nodeName) + + if submotion.numPosKeys > 0: + var id : int = anim.add_track(Animation.TYPE_POSITION_3D) + anim.track_set_path(id, skeleton_path) + for key : PosKey in submotion.PosKeys: + anim.position_track_insert_key(id, key.fTime, key.pos) + if key.fTime > animLength: + animLength = key.fTime + else: # EXPERIMENTAL: see if setting posePos fixes idle3 + var id : int = anim.add_track(Animation.TYPE_POSITION_3D) + anim.track_set_path(id, skeleton_path) + anim.position_track_insert_key(id, 0, submotion.posePos) + + if submotion.numRotKeys > 0: + var id : int = anim.add_track(Animation.TYPE_ROTATION_3D) + anim.track_set_path(id, skeleton_path) + for key : RotKey in submotion.RotKeys: + anim.rotation_track_insert_key(id, key.fTime, key.rot) + if key.fTime > animLength: + animLength = key.fTime + else: # EXPERIMENTAL: see if setting posePos fixes idle3 + var id : int = anim.add_track(Animation.TYPE_ROTATION_3D) + anim.track_set_path(id, skeleton_path) + anim.rotation_track_insert_key(id, 0, submotion.poseRot) + + if submotion.numScaleKeys > 0: + var id : int = anim.add_track(Animation.TYPE_SCALE_3D) + anim.track_set_path(id, skeleton_path) + for key : ScaleKey in submotion.ScaleKeys: + anim.scale_track_insert_key(id, key.fTime, key.scale) + if key.fTime > animLength: + animLength = key.fTime + + # TODO: submotion.numScaleRotKeys + + anim.length = animLength + anim.loop_mode = Animation.LOOP_LINEAR + + xsm_cache[source_file] = anim + return anim + +static func readHeader(file : FileAccess) -> void: + var magic_bytes : PackedByteArray = [file.get_8(), file.get_8(), file.get_8(), file.get_8()] + var magic : String = magic_bytes.get_string_from_ascii() + var version : String = "%d.%d" % [file.get_8(), file.get_8()] + var bBigEndian : bool = file.get_8() + var pad : int = file.get_8() + #print(magic, ", version: ", version, ", bigEndian: ", bBigEndian, " pad: ", pad) + +# NOTE: the "pad" variable is actually very important! +# It seems to have something to do with whether paradox uses int16 or int32 +# for quaternions (it's "pad" or version number, can't tell) + +static func readMetadataChunk(file : FileAccess) -> MetadataChunk: + return MetadataChunk.new( + file.get_float(), file.get_float(), FileAccessUtils.read_int32(file), + file.get_8(), file.get_8(), file.get_16(), + FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file) + ) + +class MetadataChunk: + var unused : float + var fMaxAcceptableError : float + var fps : int # int32 + var exporterMajorVersion : int # byte + var exporterMinorVersion : int # byte + var pad : int # 2x byte + var sourceApp : String + var origFileName : String + var exportDate : String + var motionName : String + + func _init( + unused : float, + fMaxAcceptableError : float, + fps : int, + exporterMajorVersion : int, + exporterMinorVersion : int, + pad : int, + sourceApp : String, + origFileName : String, + exportDate : String, + motionName : String + ) -> void: + self.unused = unused + self.fMaxAcceptableError = fMaxAcceptableError + self.fps = fps + self.exporterMajorVersion = exporterMajorVersion + self.exporterMinorVersion = exporterMinorVersion + self.pad = pad + self.sourceApp = sourceApp + self.origFileName = origFileName + self.exportDate = exportDate + self.motionName = motionName + + func debugPrint() -> void: + print("FileName: %s, sourceApp: %s, exportDate: %s, ExporterV:%d.%d" % + [origFileName, sourceApp, exportDate, exporterMajorVersion, exporterMinorVersion]) + print("MotionName: %s, fps: %d, MaxError: %s, Use 16-bit int Quaternions?:%s" % + [motionName, fps, fMaxAcceptableError, use_quat_16()]) + + func use_quat_16() -> bool: + return pad == 0x0 + +static func readPosKey(file : FileAccess) -> PosKey: + return PosKey.new(FileAccessUtils.read_pos(file), file.get_float()) + +class PosKey: + var pos : Vector3 + var fTime : float + + func _init(pos : Vector3, fTime : float) -> void: + self.pos = pos + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tPos:%s, time:%s" % [pos, fTime]) + +static func readRotKey(file : FileAccess, use_quat16 : bool) -> RotKey: + return RotKey.new(FileAccessUtils.read_quat(file, use_quat16), file.get_float()) + +class RotKey: + var rot : Quaternion + var fTime : float + + func _init(rot : Quaternion, fTime : float) -> void: + self.rot = rot + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tRot:%s, time:%s" % [rot, fTime]) + +static func readScaleKey(file : FileAccess) -> ScaleKey: + return ScaleKey.new(FileAccessUtils.read_vec3(file), file.get_float()) + +class ScaleKey: + var scale : Vector3 + var fTime : float + + func _init(scale : Vector3, fTime : float) -> void: + self.scale = scale + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tScale:%s, time:%s" % [scale, fTime]) + +static func readScaleRotKey(file : FileAccess, use_quat16 : bool) -> ScaleRotKey: + return ScaleRotKey.new(FileAccessUtils.read_quat(file, use_quat16), file.get_float()) + +class ScaleRotKey: + var rot : Quaternion + var fTime : float + + func _init(rot : Quaternion, fTime : float) -> void: + self.rot = rot + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tScaleRot:%s, time:%s" % [rot, fTime]) + +static func readSkeletalSubMotion(file : FileAccess, use_quat16 : bool) -> SkeletalSubMotion: + var a : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + var b : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + var c : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + var d : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + + var e : Vector3 = FileAccessUtils.read_pos(file) + var f : Vector3 = FileAccessUtils.read_vec3(file) + var g : Vector3 = FileAccessUtils.read_pos(file) + var h : Vector3 = FileAccessUtils.read_vec3(file) + + var p : int = FileAccessUtils.read_int32(file) + var j : int = FileAccessUtils.read_int32(file) + var k : int = FileAccessUtils.read_int32(file) + var l : int = FileAccessUtils.read_int32(file) + + var m : float = file.get_float() + var n : String = FileAccessUtils.read_xac_str(file) + + var submotion : SkeletalSubMotion = SkeletalSubMotion.new( + a, b, c, d, # quats + e, f, g, h, # vec3 + p, j, k, l, # ints + m, n + ) + var poskeys : Array[PosKey] = [] + var rotkeys : Array[RotKey] = [] + var scalekeys : Array[ScaleKey] = [] + var scalerotkeys : Array[ScaleRotKey] = [] + #FIXME: Did paradox store the number of pos keys as a float instead of int? + + for i : int in submotion.numPosKeys: + poskeys.push_back(readPosKey(file)) + for i : int in submotion.numRotKeys: + rotkeys.push_back(readRotKey(file, use_quat16)) + for i : int in submotion.numScaleKeys: + scalekeys.push_back(readScaleKey(file)) + for i : int in submotion.numScaleRotKeys: + scalerotkeys.push_back(readScaleRotKey(file, use_quat16)) + submotion.setPosKeys(poskeys) + submotion.setRotKeys(rotkeys) + submotion.setScaleKeys(scalekeys) + submotion.setScaleRotKeys(scalerotkeys) + return submotion + +class SkeletalSubMotion: + var poseRot : Quaternion + var bindPoseRot : Quaternion + var poseScaleRot : Quaternion + var bindPoseScaleRot : Quaternion + var posePos : Vector3 + var poseScale : Vector3 + var bindPosePos : Vector3 + var bindPoseScale : Vector3 + var numPosKeys : int # int32 + var numRotKeys : int # int32 + var numScaleKeys : int # int32 + var numScaleRotKeys : int # int32 + var fMaxError : float + var nodeName : String + + var PosKeys : Array[PosKey] + var RotKeys : Array[RotKey] + var ScaleKeys : Array[ScaleKey] + var ScaleRotKeys : Array[ScaleRotKey] + + func _init( + poseRot : Quaternion, + bindPoseRot : Quaternion, + poseScaleRot : Quaternion, + bindPoseScaleRot : Quaternion, + posePos : Vector3, + poseScale : Vector3, + bindPosePos : Vector3, + bindPoseScale : Vector3, + numPosKeys : int, + numRotKeys : int, + numScaleKeys : int, + numScaleRotKeys : int, + fMaxError : float, + nodeName : String + ) -> void: + self.poseRot = poseRot + self.bindPoseRot = bindPoseRot + self.poseScaleRot = poseScaleRot + self.bindPoseScaleRot = bindPoseScaleRot + self.posePos = posePos + self.poseScale = poseScale + self.bindPosePos = bindPosePos + self.bindPoseScale = bindPoseScale + self.numPosKeys = numPosKeys + self.numRotKeys = numRotKeys + self.numScaleKeys = numScaleKeys + self.numScaleRotKeys = numScaleRotKeys + self.fMaxError = fMaxError + self.nodeName = nodeName + + func setPosKeys(PosKeys : Array[PosKey]) -> void: + self.PosKeys = PosKeys + + func setRotKeys(RotKeys : Array[RotKey]) -> void: + self.RotKeys = RotKeys + + func setScaleKeys(ScaleKeys : Array[ScaleKey]) -> void: + self.ScaleKeys = ScaleKeys + + func setScaleRotKeys(ScaleRotKeys : Array[ScaleRotKey]) -> void: + self.ScaleRotKeys = ScaleRotKeys + + func debugPrint() -> void: + print("Node: %s, #PosKeys %d, #RotKeys %d, #ScaleKeys %d, #ScaleRotKeys %d" % [nodeName, numPosKeys, numRotKeys, numScaleKeys, numScaleRotKeys]) + print("\tposeScaleRot %s,\tbindPoseScaleRot %s,\tposeScale %s,\tbindPoseScale %s" % [poseScaleRot, bindPoseScaleRot, poseScale, bindPoseScale]) + for key : PosKey in PosKeys: + key.debugPrint() + for key : RotKey in RotKeys: + key.debugPrint() + for key : ScaleKey in ScaleKeys: + key.debugPrint() + for key : ScaleRotKey in ScaleRotKeys: + key.debugPrint() + +static func readBoneAnimationChunk(file : FileAccess, use_quat16 : bool) -> BoneAnimationChunk: + var numSubMotions : int = FileAccessUtils.read_int32(file) + var animChunk : BoneAnimationChunk = BoneAnimationChunk.new(numSubMotions) + var submotions : Array[SkeletalSubMotion] = [] + for i : int in animChunk.numSubMotions: + submotions.push_back(readSkeletalSubMotion(file, use_quat16)) + animChunk.setSkeletalSubMotions(submotions) + return animChunk + +class BoneAnimationChunk: + var numSubMotions : int # int32 + var SkeletalSubMotions : Array[SkeletalSubMotion] + + func _init(numSubMotions : int) -> void: + self.numSubMotions = numSubMotions + + func setSkeletalSubMotions(SkeletalSubMotions : Array[SkeletalSubMotion]) -> void: + self.SkeletalSubMotions = SkeletalSubMotions + + func debugPrint() -> void: + print("Number of Submotions: %d" % numSubMotions) + for submotion : SkeletalSubMotion in SkeletalSubMotions: + submotion.debugPrint() -- cgit v1.2.3-56-ga3b1