Skip to content

Commit 3b5764a

Browse files
committed
initial commit
0 parents  commit 3b5764a

11 files changed

Lines changed: 719 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
.vscode/
3+
__pycache__
4+

examples/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
com.kiloo.subwaysurf/
3+
net.robber.running/
4+

examples/com.kiloo.subwaysurf.zip

34.9 MB
Binary file not shown.

examples/net.robber.running.zip

28.1 MB
Binary file not shown.

main.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import argparse
2+
import sys
3+
import os
4+
from three_scan import three_scan
5+
6+
7+
def main():
8+
parser = argparse.ArgumentParser(prog="3DScan",
9+
formatter_class=argparse.RawTextHelpFormatter)
10+
11+
parser.add_argument("-i", "--input", dest="input", metavar="path", action='append', required=True,
12+
help="path of input models."
13+
"Example usage: --input game_a --input game_b")
14+
parser.add_argument("-a", "--animation", dest="animation", metavar="bool", type=bool, required=False, default=False,
15+
help="compare Animation or not."
16+
" The default value is False")
17+
parser.add_argument("-o", "--output", dest="output", metavar="path", type=str, default="result.json",
18+
help="path of output result JSON file."
19+
"If --animation flag is present, an additional animation comparision result will be produced.")
20+
21+
args = parser.parse_args()
22+
input_path = args.input
23+
animation_flag = args.animation
24+
output_path = args.output
25+
26+
# test input
27+
if not (len(input_path) == 1 or len(input_path) == 2):
28+
print("[main] error: invalid input path", file=sys.stderr)
29+
os._exit(-1)
30+
else:
31+
for i in input_path:
32+
if not os.path.exists(i):
33+
print("[main] error: invalid input path", file=sys.stderr)
34+
os._exit(-1)
35+
if not os.path.isdir(i):
36+
print(f"[main] error: input {i} should be a directory", file=sys.stderr)
37+
38+
if animation_flag == True:
39+
if len(input_path) != 2:
40+
print("[main] error: --input should be used twice", file=sys.stderr)
41+
os._exit(-1)
42+
three_scan.scan_and_compare(input_path, output_path)
43+
else:
44+
if len(input_path) != 1:
45+
print("[main] warning: --input shall be used only once, addition flag is ignored", file=sys.stderr)
46+
three_scan.scan(input_path[0], output_path)
47+
48+
49+
if __name__ == "__main__":
50+
main()
51+

three_scan/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .lib import Animation
2+
from .lib import Animator
3+
from .lib import Mesh
4+
from .three_scan import *

three_scan/lib/Animation.py

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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

Comments
 (0)