|
| 1 | +import base64 |
| 2 | +import copy |
| 3 | +import hashlib |
| 4 | +from scipy.interpolate import CubicSpline |
| 5 | +from scipy.stats import pearsonr |
| 6 | +import math |
| 7 | + |
| 8 | + |
| 9 | +class Animator: |
| 10 | + def __init__(self, json_string): |
| 11 | + self.name = json_string['name'] |
| 12 | + self.bone_path_hash = json_string['bonePathHash'] |
| 13 | + self.root_frame = json_string['RootFrame'] |
| 14 | + self.mesh_list = json_string['MeshList'] |
| 15 | + self.material_list = json_string['MaterialList'] |
| 16 | + self.texture_list = json_string['TextureList'] |
| 17 | + self.animation_list = json_string['AnimationList'] |
| 18 | + self.mesh_to_materials = dict() |
| 19 | + |
| 20 | + self.textures = dict() |
| 21 | + self.materials = dict() |
| 22 | + |
| 23 | + self.meshes = list() |
| 24 | + self.animations = list() |
| 25 | + |
| 26 | + self.init_material_texture() |
| 27 | + |
| 28 | + def init_material_texture(self): |
| 29 | + for i in self.texture_list: |
| 30 | + texture = dict() |
| 31 | + texture["name"] = i["Name"] |
| 32 | + texture["wd"] = i["m_Width"] |
| 33 | + texture["ht"] = i["m_Height"] |
| 34 | + texture["mc"] = i["m_MipCount"] |
| 35 | + |
| 36 | + data = base64.b64decode(i["Data"]) |
| 37 | + texture["hash"] = hashlib.blake2s(data).hexdigest() |
| 38 | + texture["size"] = len(data) |
| 39 | + self.textures[i["Name"]] = texture |
| 40 | + |
| 41 | + for i in self.material_list: |
| 42 | + if len(i.get("Textures",[])) > 0: |
| 43 | + material = list() |
| 44 | + for textr in i["Textures"]: |
| 45 | + if textr["Name"] in self.textures: |
| 46 | + material.append(self.textures[textr["Name"]]) |
| 47 | + self.materials[i["Name"]] = material |
| 48 | + |
| 49 | + def match_bones_and_animations(self, meshes): |
| 50 | + for jsmesh in self.mesh_list: |
| 51 | + uid = "%s--%s" % (jsmesh["Mesh_AssetFileName"], jsmesh["Mesh_PathID"]) |
| 52 | + mesh = meshes[uid] |
| 53 | + mesh.set_bones(self.bone_path_hash, jsmesh["BoneList"]) |
| 54 | + mates = mesh.set_material(self.materials, jsmesh["SubmeshList"]) |
| 55 | + |
| 56 | + if mesh not in self.mesh_to_materials: self.mesh_to_materials[mesh] = set() |
| 57 | + self.mesh_to_materials[mesh].update(mates) |
| 58 | + self.meshes.append(mesh) |
| 59 | + |
| 60 | + for animation_js in self.animation_list: |
| 61 | + for mesh in self.meshes: |
| 62 | + animation = try_it(mesh, animation_js, self.name, self.mesh_to_materials) |
| 63 | + if animation: |
| 64 | + self.animations.append(animation) |
| 65 | + |
| 66 | + |
| 67 | +class Animation: |
| 68 | + def __init__(self, mesh, json_string, animator_name, materials): |
| 69 | + self.name = json_string["Name"] |
| 70 | + self.sample_rate = json_string.get("SampleRate", -1) |
| 71 | + self.timings = set() |
| 72 | + self.mesh = mesh |
| 73 | + self.mesh_name = mesh.name |
| 74 | + self.animator_name = animator_name |
| 75 | + self.animationjs = json_string |
| 76 | + self.materials = list(materials) |
| 77 | + self.bones = copy.deepcopy(mesh.bones) |
| 78 | + self.bones_id = copy.deepcopy(mesh.bones_id) |
| 79 | + self.bones_interpolate_functions = dict() |
| 80 | + |
| 81 | + self.anim_hash = None |
| 82 | + self.type = "anim" |
| 83 | + self.add_tracks() |
| 84 | + self.chash() |
| 85 | + |
| 86 | + def add_tracks(self): |
| 87 | + def interpolate(track, channle, track_interpolation, resname): |
| 88 | + sorted_x = sorted(track[channle], key=lambda kv: kv["time"]) |
| 89 | + if len(sorted_x) > 0: |
| 90 | + track_interpolation[resname] = ( |
| 91 | + # X |
| 92 | + CubicSpline(range(len(sorted_x)), [i["value"]["X"] for i in sorted_x]), |
| 93 | + # Y |
| 94 | + CubicSpline(range(len(sorted_x)), [i["value"]["Y"] for i in sorted_x]), |
| 95 | + # Z |
| 96 | + CubicSpline(range(len(sorted_x)), [i["value"]["Z"] for i in sorted_x]) |
| 97 | + ) |
| 98 | + |
| 99 | + def ttt(track, channle, res, resname): |
| 100 | + sorted_x = sorted(track[channle], key=lambda kv: kv["time"]) |
| 101 | + if len(sorted_x) > 0: |
| 102 | + res[resname] = ( |
| 103 | + round(sorted_x[0 ]["value"]["X"]), |
| 104 | + round(sorted_x[0 ]["value"]["Y"]), |
| 105 | + round(sorted_x[0 ]["value"]["Z"]), |
| 106 | + |
| 107 | + round(sorted_x[-1]["value"]["X"]), |
| 108 | + round(sorted_x[-1]["value"]["Y"]), |
| 109 | + round(sorted_x[-1]["value"]["Z"]), |
| 110 | + ) |
| 111 | + |
| 112 | + for track in self.animationjs["TrackList"]: |
| 113 | + if track["Path"] == None: |
| 114 | + continue |
| 115 | + if track["sPath"] not in self.bones: |
| 116 | + continue |
| 117 | + |
| 118 | + my_track = {"s": None, "r": None, "t": None} |
| 119 | + track_interpolation = {"s": None, "r": None, "t": None} |
| 120 | + for channle, resname in [("Scalings", "s"), ("Rotations", "r"), ("Translations", "t")]: |
| 121 | + ttt(track, channle, my_track, resname) |
| 122 | + interpolate(track, channle, track_interpolation, resname) |
| 123 | + self.timings.update([one_item.get("time", 0) for one_item in track[channle]]) |
| 124 | + self.bones[track["sPath"]]["tk"] = str(my_track["s"]) + str(my_track["r"]) + str(my_track["t"]) |
| 125 | + self.bones_interpolate_functions[track["sPath"]] = track_interpolation |
| 126 | + |
| 127 | + def compare(self, animation): |
| 128 | + return Animation.compare(self, animation) |
| 129 | + |
| 130 | + @staticmethod |
| 131 | + def compare(animation_a, animation_b): |
| 132 | + if len(animation_a.bones_interpolate_functions) == 0 or len(animation_b.bones_interpolate_functions) == 0: |
| 133 | + print("[-] animations have zero bones_interpolate_functions") |
| 134 | + return 0 |
| 135 | + results = list() |
| 136 | + # only compare the same bones |
| 137 | + for s_path in animation_a.bones_interpolate_functions: |
| 138 | + if s_path not in animation_b.bones_interpolate_functions: |
| 139 | + continue |
| 140 | + # generate 100 values for each dimesion and channel |
| 141 | + generated_range = range(100) |
| 142 | + # let's do Scalings first |
| 143 | + scaling_func_A = animation_a.bones_interpolate_functions[s_path]["s"] |
| 144 | + scaling_func_B = animation_b.bones_interpolate_functions[s_path]["s"] |
| 145 | + if scaling_func_A != None and scaling_func_B != None: |
| 146 | + scaling_generated_A = ( |
| 147 | + scaling_func_A[0](generated_range), |
| 148 | + scaling_func_A[1](generated_range), |
| 149 | + scaling_func_A[2](generated_range) |
| 150 | + ) |
| 151 | + scaling_generated_B = ( |
| 152 | + scaling_func_B[0](generated_range), |
| 153 | + scaling_func_B[1](generated_range), |
| 154 | + scaling_func_B[2](generated_range) |
| 155 | + ) |
| 156 | + else: |
| 157 | + scaling_generated_A = None |
| 158 | + scaling_generated_B = None |
| 159 | + # then Rotations |
| 160 | + rotation_func_A = animation_a.bones_interpolate_functions[s_path]["r"] |
| 161 | + rotation_func_B = animation_b.bones_interpolate_functions[s_path]["r"] |
| 162 | + if rotation_func_A != None and rotation_func_B != None: |
| 163 | + rotation_generated_A = ( |
| 164 | + rotation_func_A[0](generated_range), |
| 165 | + rotation_func_A[1](generated_range), |
| 166 | + rotation_func_A[2](generated_range) |
| 167 | + ) |
| 168 | + rotation_generated_B = ( |
| 169 | + rotation_func_B[0](generated_range), |
| 170 | + rotation_func_B[1](generated_range), |
| 171 | + rotation_func_B[2](generated_range) |
| 172 | + ) |
| 173 | + else: |
| 174 | + rotation_generated_A = None |
| 175 | + rotation_generated_B = None |
| 176 | + # finally Translations |
| 177 | + translation_func_A = animation_a.bones_interpolate_functions[s_path]["t"] |
| 178 | + translation_func_B = animation_b.bones_interpolate_functions[s_path]["t"] |
| 179 | + if translation_func_A != None and translation_func_B != None: |
| 180 | + translation_generated_A = ( |
| 181 | + translation_func_A[0](generated_range), |
| 182 | + translation_func_A[1](generated_range), |
| 183 | + translation_func_A[2](generated_range) |
| 184 | + ) |
| 185 | + translation_generated_B = ( |
| 186 | + translation_func_B[0](generated_range), |
| 187 | + translation_func_B[1](generated_range), |
| 188 | + translation_func_B[2](generated_range) |
| 189 | + ) |
| 190 | + else: |
| 191 | + translation_generated_A = None |
| 192 | + translation_generated_B = None |
| 193 | + # now we can calculate the Pearson correlation coefficient |
| 194 | + # let's do Scalings first |
| 195 | + if scaling_generated_A != None and scaling_generated_B != None: |
| 196 | + scaling_correlation_X = pearsonr(scaling_generated_A[0], scaling_generated_B[0]).statistic |
| 197 | + scaling_correlation_Y = pearsonr(scaling_generated_A[1], scaling_generated_B[1]).statistic |
| 198 | + scaling_correlation_Z = pearsonr(scaling_generated_A[2], scaling_generated_B[2]).statistic |
| 199 | + scaling_correlations = ( |
| 200 | + 0 if math.isnan(scaling_correlation_X) else scaling_correlation_X, |
| 201 | + 0 if math.isnan(scaling_correlation_Y) else scaling_correlation_Y, |
| 202 | + 0 if math.isnan(scaling_correlation_Z) else scaling_correlation_Z, |
| 203 | + ) |
| 204 | + else: |
| 205 | + scaling_correlations = tuple() |
| 206 | + # then, Rotations |
| 207 | + if rotation_generated_A != None and rotation_generated_B != None: |
| 208 | + rotation_correlation_X = pearsonr(rotation_generated_A[0], rotation_generated_B[0]).statistic |
| 209 | + rotation_correlation_Y = pearsonr(rotation_generated_A[1], rotation_generated_B[1]).statistic |
| 210 | + rotation_correlation_Z = pearsonr(rotation_generated_A[2], rotation_generated_B[2]).statistic |
| 211 | + rotation_correlations = ( |
| 212 | + 0 if math.isnan(rotation_correlation_X) else rotation_correlation_X, |
| 213 | + 0 if math.isnan(rotation_correlation_Y) else rotation_correlation_Y, |
| 214 | + 0 if math.isnan(rotation_correlation_Z) else rotation_correlation_Z, |
| 215 | + ) |
| 216 | + else: |
| 217 | + rotation_correlations = tuple() |
| 218 | + # finally, Translations |
| 219 | + if translation_generated_A != None and translation_generated_B != None: |
| 220 | + translation_correlation_X = pearsonr(translation_generated_A[0], translation_generated_B[0]).statistic |
| 221 | + translation_correlation_Y = pearsonr(translation_generated_A[1], translation_generated_B[1]).statistic |
| 222 | + translation_correlation_Z = pearsonr(translation_generated_A[2], translation_generated_B[2]).statistic |
| 223 | + translation_correlations = ( |
| 224 | + 0 if math.isnan(translation_correlation_X) else translation_correlation_X, |
| 225 | + 0 if math.isnan(translation_correlation_Y) else translation_correlation_Y, |
| 226 | + 0 if math.isnan(translation_correlation_Z) else translation_correlation_Z, |
| 227 | + ) |
| 228 | + else: |
| 229 | + translation_correlations = tuple() |
| 230 | + # summarize them |
| 231 | + for i in scaling_correlations + rotation_correlations + translation_correlations: |
| 232 | + # drop the invalid data point |
| 233 | + if i == 0: |
| 234 | + continue |
| 235 | + results.append(abs(i)) |
| 236 | + |
| 237 | + return sum(results) / len(results) if len(results) else 0 |
| 238 | + |
| 239 | + def chash(self): |
| 240 | + def visit_bone(bid): |
| 241 | + bname = self.bones_id[bid] |
| 242 | + subs = list() |
| 243 | + for i in self.bones[bname]["chdr"]: |
| 244 | + subs.append(visit_bone(i)) |
| 245 | + return "(%s):%s" % (self.bones[bname].get("tk", "x"), ",".join(sorted(subs))) |
| 246 | + |
| 247 | + heads = list() |
| 248 | + for bn in self.bones: |
| 249 | + if self.bones[bn]["fath"] == None: |
| 250 | + heads.append(visit_bone(self.bones[bn]["id"])) |
| 251 | + |
| 252 | + bhstr = ",".join(sorted(heads)) |
| 253 | + self.anim_hash = hashlib.blake2s(bhstr.encode('utf-8')).hexdigest() |
| 254 | + self.mesh_bones_animation_hash = hashlib.blake2s((self.mesh.mesh_bones_hash + bhstr).encode('utf-8')).hexdigest() |
| 255 | + |
| 256 | + def to_json(self): |
| 257 | + ret = dict() |
| 258 | + ret["type"] = self.type |
| 259 | + |
| 260 | + if self.anim_hash != None: |
| 261 | + ret['ah'] = self.anim_hash |
| 262 | + |
| 263 | + if self.mesh.bones_hash != None: |
| 264 | + ret['bh'] = self.mesh.bones_hash |
| 265 | + |
| 266 | + if self.mesh.mesh_hash != None: |
| 267 | + ret['mh'] = self.mesh.mesh_hash |
| 268 | + |
| 269 | + if self.mesh.mesh_bones_hash != None: |
| 270 | + ret['mbh'] = self.mesh.mesh_bones_hash |
| 271 | + |
| 272 | + if self.mesh_bones_animation_hash != None: |
| 273 | + ret['mbah'] = self.mesh_bones_animation_hash |
| 274 | + |
| 275 | + ret['n'] = self.name |
| 276 | + ret['sr'] = self.sample_rate |
| 277 | + ret['timings'] = list(self.timings) |
| 278 | + ret['m'] = self.mesh.uid + " " + self.mesh.name |
| 279 | + ret['ator'] = self.animator_name |
| 280 | + ret['matrl'] = self.materials |
| 281 | + return ret |
| 282 | + |
| 283 | + |
| 284 | +def lists_similar_rate(listA, listB): |
| 285 | + listA, listB = set(listA), set(listB) |
| 286 | + |
| 287 | + count = 0 |
| 288 | + for i in listA: |
| 289 | + if i in listB: |
| 290 | + count += 1 |
| 291 | + |
| 292 | + return count * 1.0 / min(len(listB), len(listA)), count |
| 293 | + |
| 294 | + |
| 295 | +def try_it(mesh, js, animator_name, mesh2materials): |
| 296 | + bones = [track["Path"] for track in js["TrackList"] if track["Path"] != None] |
| 297 | + if len(bones) == 0 or len(mesh.bones.keys()) == 0: |
| 298 | + return None |
| 299 | + |
| 300 | + rate, count = lists_similar_rate(bones, mesh.bones.keys()) |
| 301 | + |
| 302 | + if count > 0 : #rate > 0.9 or count < 3: |
| 303 | + for i in js["TrackList"]: |
| 304 | + if i["Path"] == None: |
| 305 | + continue |
| 306 | + |
| 307 | + if "sPath" not in i: |
| 308 | + #if i["Path"].startswith(animator_name+"/"): |
| 309 | + # i["sPath"] = i["Path"][len(animator_name)+1:] |
| 310 | + #else: |
| 311 | + # i["sPath"] = i["Path"] |
| 312 | + i["sPath"] = i["Path"] |
| 313 | + |
| 314 | + #if i["sPath"] not in mesh.bones: |
| 315 | + #print("[-] animation not match", mesh.name, i["sPath"], i["Path"]) |
| 316 | + # return None |
| 317 | + #print("[*] mesh & animation matches!", mesh.name, js["Name"]) |
| 318 | + return Animation(mesh, js, animator_name, mesh2materials.get(mesh, set())) |
| 319 | + else: |
| 320 | + print("[-] animation not match", mesh.name, animator_name, rate, count) |
| 321 | + return None |
| 322 | + |
0 commit comments