diff --git a/anm.py b/anm.py index 166b17e..415650f 100644 --- a/anm.py +++ b/anm.py @@ -1,3 +1,4 @@ +from lib.gbs import * from lib.skn_anm import * import os @@ -9,9 +10,51 @@ def read_anm(filepath) -> AnmAnim: return r +def read_skn(filepath) -> AnmSkin: + with open(filepath, "rb") as fp: + init_filetrack(os.fstat(fp.fileno()).st_size) + r = AnmFile.parse(fp, AnmSkin) + print("Read %s%% of file" % stat_track()) + return r + + +def rotate_vec3_by_quaternion(vec: Vec3, quat: Quat): + u = Vec3(quat.x, quat.y, quat.z) + s = quat.w + return 2 * Vec3.dot(u, vec) * u + (s*s - Vec3.dot(u, u)) * vec + 2.0 * s * cross(u, vec) + + def main(): - anm = read_anm("/home/tasty/Projects/gck-map-extract-objects/anm_skn/verm_fly.anm") - pprint(anm) + skin = read_skn("/home/tasty/Projects/gck-map-extract-objects/anm_skn/rp_l0.skn") + anm = read_anm("/home/tasty/Projects/gck-map-extract-objects/anm_skn/rp_run_scan1.anm") + #anm = read_anm("/home/tasty/Projects/gck-map-extract-objects/anm_skn/kb_roar2.anm") + #pprint(skin) + print("----------------------------------------------------------------") + print("----------------------------------------------------------------") + print("----------------------------------------------------------------") + print("----------------------------------------------------------------") + print("----------------------------------------------------------------") + # pprint(anm) + animation = AnmAnimation.create(skin, anm) + + gbs = GbsData() + gbs.read("/home/tasty/Projects/gck-map-extract-objects/all_gbs/rp_l0.gbs") + animation.eval(1000, gbs.vertices, 0x4c, 0) + + vbase = 0 + for binder in animation.binders: + for bindings in binder.bindings: + cluster = bindings.cluster + node = bindings.node + for cluster_vertex_i in range(len(cluster.vertices)): + cluster_vertex = cluster.vertices[cluster_vertex_i] + cluster_vertex += node.transform.affine.translation + cluster_vertex = rotate_vec3_by_quaternion(cluster_vertex, node.transform.affine.quat) + gbs.vertices[vbase] = cluster_vertex + vbase += 1 + print(vbase) + + gbs.save_obj("/home/tasty/Projects/gck-map-extract-objects/mc_eval_0.obj") if __name__ == "__main__": diff --git a/id_to_gbs.py b/id_to_gbs.py new file mode 100644 index 0000000..c21e4af --- /dev/null +++ b/id_to_gbs.py @@ -0,0 +1,141 @@ +import os +import json + +decoded_dir = "/home/tasty/Nextcloud/docs/giants_private/binlnk (compiler for xxbin files)/all/" + + +def _all_gbs(): + all_gbs = {} + objdata = {} + objsets = {} + files = os.listdir(decoded_dir) + for file in files: + fullpath = decoded_dir + file + print("reading %s" % fullpath) + with open(fullpath) as fp: + currblock = None + curr_objset = None + curr_obj = None + lod_found = False + + audio_wav = None + audio_dist = None + for line in fp.readlines(): + line = line.strip() + if not line: + continue + if line == "[objdata]": + currblock = line + continue + if line == "[objset]": + currblock = line + continue + if line == "[object]": + currblock = line + continue + if line.startswith("[") and line.endswith("]"): + currblock = None + continue + + if currblock == "[objdata]": + objdata_id, gbs_name = line.split(" ") + objdata_id = int(objdata_id) + if objdata_id in objdata: + # raise Exception("%s was already in objdata ?! %s" % (objdata_id, objdata[objdata_id])) + pass + gbs_name = gbs_name.replace("\"", "") + print("OBJDATA[%s] = %s" % (objdata_id, gbs_name)) + objdata[objdata_id] = gbs_name + + if currblock == "[objset]": + line_attrs = line.split(" ") + if line_attrs[0] == "ID": + curr_objset = int(line_attrs[1]) + lod_found = False + if line_attrs[0] == "LOD" and not lod_found: + lod_found = True + objset_objdata = int(line_attrs[1]) + print("OBJSETS[%s] = %s" % (curr_objset, objset_objdata)) + objsets[curr_objset] = objset_objdata + + if currblock == "[object]": + line_attrs = line.split(" ") + if line_attrs[0] == "ID": + curr_obj = int(line_attrs[1]) + if line_attrs[0] == "OS": + obj_objset = int(line_attrs[1]) + if line_attrs[0] == "AmbientStreamLoop": + audio_wav = line_attrs[2].strip("\"") + audio_dist = float(line_attrs[4]) + + if line_attrs[0] == "Done": + try: + print("OBJ[%s] = OBJSETS[%s] = OBJDATA[%s] = %s" % ( + curr_obj, obj_objset, objsets[obj_objset], objdata[objsets[obj_objset]])) + all_gbs[curr_obj] = {"model": objdata[objsets[obj_objset]]} + if audio_wav: + all_gbs[curr_obj]["audio"] = audio_wav + all_gbs[curr_obj]["audiodist"] = audio_dist + except KeyError: + print("ERR: could not find OBJSET %s" % obj_objset) + continue + audio_wav = None + audio_dist = None + return all_gbs + + +def map_txt_to_json(map_txt_path): + all_objs = [] + curr_obj = None + with open(map_txt_path) as fp: + for line in fp.readlines(): + line = line.strip() + if not line: + continue + + line_attrs = line.split(" ") + + if line_attrs[0] == "ObjectRef6": + if curr_obj: + all_objs.append(curr_obj) + curr_obj = { + "id": int(line_attrs[1]), + "x": float(line_attrs[2]), + "y": float(line_attrs[3]), + "z": float(line_attrs[4]), + "angle": float(line_attrs[5]), + "angle_2": float(line_attrs[6]), + "angle_3": float(line_attrs[7]), + "scale": 1 + } + + if line_attrs[0] == "ObjectRef": + if curr_obj: + all_objs.append(curr_obj) + curr_obj = { + "id": int(line_attrs[1]), + "x": float(line_attrs[2]), + "y": float(line_attrs[3]), + "z": float(line_attrs[4]), + "angle": float(line_attrs[5]), + "angle_2": 0, + "angle_3": 0, + "scale": 1 + } + if line_attrs[0] == "Scale": + curr_obj["scale"] = float(line_attrs[1]) + with open(map_txt_path+".json", "w") as fp: + json.dump(all_objs, fp) + return all_objs + + +def create_id_to_gbs_json(): + g = _all_gbs() + with open("id_to_gbs.json", "w") as fp: + json.dump(g, fp) + + +if __name__ == '__main__': + create_id_to_gbs_json() + # m = map_txt_to_json("/home/tasty/Projects/Giants/assets/terrains/square_one_1/w_M_3Way_Tigs - Threeway - Square One.bin.txt") + # print(m) diff --git a/lib/fileutils.py b/lib/fileutils.py index d78de7f..65c08b3 100644 --- a/lib/fileutils.py +++ b/lib/fileutils.py @@ -14,6 +14,22 @@ def init_filetrack(size): file_track = [0] * size +def show_filetrack(): + i = 0 + curr = 1 + curr_len = 0 + while i < len(file_track): + if file_track[i] != curr: + if curr == 0: + print("%s starts at %s, ends at %s for %s (%s) bytes (%s ints)" % (curr, i-curr_len, i, curr_len, hex(curr_len), curr_len/4)) + curr = file_track[i] + curr_len = 1 + else: + curr_len += 1 + i += 1 + print(file_track) + + def advance_track(offset, size): if not file_track: return diff --git a/lib/skn_anm.py b/lib/skn_anm.py index d33f381..2083157 100644 --- a/lib/skn_anm.py +++ b/lib/skn_anm.py @@ -1,6 +1,6 @@ from lib.fileutils import * from dataclasses import dataclass, field -from typing import List, Union +from typing import List, Union, Optional import math import prettyprinter from prettyprinter import pprint @@ -38,6 +38,38 @@ ANM_NODE_FLAG_USR_ANIMATED = (1 << 5) ANM_NODE_FLAG_ANIMATED = ((1 << 6) - 1) NAM_STRING_KEY = 0 NAM_HANDLE_KEY = 1 +ANM_CLUSTER_ARRAY_SIZE = 128 +ANM_BINDING_ARRAY_SIZE = ANM_CLUSTER_ARRAY_SIZE + +ANM_XFORM_TYPE_NONE = 0 +ANM_XFORM_TYPE_SAMPLE = 1 +ANM_XFORM_TYPE_PRS = 2 +ANM_XFORM_TYPE_LOOKAT = 3 + +ANM_KEYS_ORT_ILLEGAL = 0 +ANM_KEYS_ORT_CONSTANT = 1 +ANM_KEYS_ORT_CYCLE = 2 +ANM_KEYS_ORT_LOOP = 3 # This is cycle with continuity. +ANM_KEYS_ORT_OSCILLATE = 4 +ANM_KEYS_ORT_LINEAR = 5 +ANM_KEYS_ORT_IDENTITY = 6 +ANM_KEYS_ORT_RELATIVE = 7 + +GEO_MAT_ROTATION = (1 << 0) +GEO_MAT_SCALE = (1 << 1) +GEO_MAT_SHEAR = (1 << 2) +GEO_MAT_SIGN = (1 << 3) +GEO_MAT_TRANSLATION = (1 << 4) +GEO_MAT_PROJECTION = (1 << 5) + +GEO_MAT_ORTHO = (GEO_MAT_ROTATION | GEO_MAT_SIGN) +GEO_MAT_ORTHO_AFFINE = (GEO_MAT_ORTHO | GEO_MAT_TRANSLATION) + +GEO_MAT_LINEAR = (GEO_MAT_ORTHO | GEO_MAT_SCALE | GEO_MAT_SHEAR) +GEO_MAT_AFFINE = (GEO_MAT_LINEAR | GEO_MAT_TRANSLATION) +GEO_MAT_GENERAL = (GEO_MAT_AFFINE | GEO_MAT_PROJECTION) + +QUAT_COS_EPSILON = 0.000001 class ANM_KeyType: @@ -47,6 +79,7 @@ class ANM_KeyType: if cls.__dict__[d] == i: return d return None + ANM_KEYS_TYPE_NULL = 0 ANM_KEYS_TYPE_REAL_TCB = 1 ANM_KEYS_TYPE_REAL_BEZ = 2 @@ -60,6 +93,17 @@ class ANM_KeyType: ANM_KEYS_TYPE_AFFINE_SAM = 10 +class ANM_KeyFlags: + @classmethod + def get_name(cls, i): + for d in cls.__dict__: + if cls.__dict__[d] == i: + return d + return None + + ANM_KEYS_FLAGS_CLOSED = (1 << 0) + ANM_KEYS_FLAGS_ORT_DISABLE = (1 << 1) + @dataclass class Vec3: x: float @@ -69,6 +113,10 @@ class Vec3: def distance(self, other: 'Vec3') -> float: return math.sqrt(math.pow(self.x - other.x, 2) + math.pow(self.y - other.y, 2) + math.pow(self.z - other.z, 2)) + @staticmethod + def dot(a: "Vec3", b: "Vec3") -> float: + return a.x * b.x + a.y * b.y + a.z * b.z + @classmethod @with_offset def create(cls, b): @@ -77,6 +125,36 @@ class Vec3: z = read_float_and_print("Vec3->z", b) return cls(x=x, y=y, z=z) + def __add__(self, other): + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other): + return self + -other + + def __radd__(self, other): + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __mul__(self, scalar): + return Vec3(self.x * scalar, self.y * scalar, self.z * scalar) + + def __rmul__(self, scalar): + return Vec3(self.x * scalar, self.y * scalar, self.z * scalar) + + def __neg__(self): + return Vec3(-self.x, -self.y, -self.z) + + def __pos__(self): + return Vec3(self.x, self.y, self.z) + + def __xor__(self, other): + cx = self.y * other.z - self.z * other.y + cy = self.z * other.x - self.x * other.z + cz = self.x * other.y - self.y * other.x + return Vec3(cx, cy, cz) + + def cross(self, other): + return self ^ other + @dataclass class Vec3Indexed(Vec3): @@ -120,6 +198,40 @@ class Vec4(Vec3): w = read_float_and_print("Vec4->w", b) return cls(x=x, y=y, z=z, w=w) + @staticmethod + def squad(p: "Quat", a: "Quat", b: "Quat", q: "Quat", t: float) -> "Quat": # GEO_QuatSquad + k = 2 * (1 - t) * t + pq = Quat.slerp(p, q, t) + ab = Quat.slerp(a, b, t) + return Quat.slerp(pq, ab, k) + + @staticmethod + def slerp(p: "Quat", q: "Quat", t: float) -> "Quat": # GEO_QuatSlerp + cosom = p.x * q.x + p.y * q.y + p.z * q.z + p.w * q.w + if 1 + cosom > QUAT_COS_EPSILON: + # usual case + if 1 - cosom > QUAT_COS_EPSILON: + # usual case + omega = math.acos(cosom) + oosinom = 1 / math.sin(omega) + sclp = math.sin((1 - t) * omega) * oosinom + sclq = math.sin(t * omega) * oosinom + else: + # ends very close -- just lerp + sclp = 1 - t + sclq = t + return Quat( + sclp * p.x + sclq * q.x, + sclp * p.y + sclq * q.y, + sclp * p.z + sclq * q.z, + sclp * p.w + sclq * q.w, + ) + else: + return q + + +Quat = Vec4 # alias + @dataclass class AnmKey: @@ -127,20 +239,276 @@ class AnmKey: @classmethod @with_offset - def create(cls, b): - time = read_byte_and_print("ANM_Key->Time", b) + def create(cls, b, key_type: ANM_KeyType): + time = read_int_and_print("ANM_Key->Time", b) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_REAL_TCB or key_type == ANM_KeyType.ANM_KEYS_TYPE_VEC3_TCB or key_type == ANM_KeyType.ANM_KEYS_TYPE_QUAT_TCB: + tens = read_float_and_print("ANM_Key->Tens", b) + cont = read_float_and_print("ANM_Key->Cont", b) + bias = read_float_and_print("ANM_Key->Bias", b) + easein = read_float_and_print("ANM_Key->EaseIn", b) + easeout = read_float_and_print("ANM_Key->EaseOut", b) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_REAL_TCB: + outtan = read_float_and_print("ANM_Key->OutTan", b) + intan = read_float_and_print("ANM_Key->InTan", b) + val = read_float_and_print("ANM_Key->Val", b) + return AnmKeyRealTCB(time, tens, cont, bias, easein, easeout, outtan, intan, val) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_VEC3_TCB: + outtan = Vec3.create(b) + intan = Vec3.create(b) + val = Vec3.create(b) + return AnmKeyVec3TCB(time, tens, cont, bias, easein, easeout, outtan, intan, val) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_QUAT_TCB: + outtan = Vec4.create(b) + intan = Vec4.create(b) + val = Vec4.create(b) + return AnmKeyQuatTCB(time, tens, cont, bias, easein, easeout, outtan, intan, val) + elif key_type == ANM_KeyType.ANM_KEYS_TYPE_REAL_BEZ or key_type == ANM_KeyType.ANM_KEYS_TYPE_VEC3_BEZ or key_type == ANM_KeyType.ANM_KEYS_TYPE_QUAT_BEZ: + flags = read_float_and_print("ANM_Key->Flags", b) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_REAL_BEZ: + outtan = read_float_and_print("ANM_Key->OutTan", b) + intan = read_float_and_print("ANM_Key->InTan", b) + val = read_float_and_print("ANM_Key->Val", b) + return AnmKeyRealBez(time, flags, outtan, intan, val) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_VEC3_BEZ: + outtan = Vec3.create(b) + intan = Vec3.create(b) + val = Vec3.create(b) + return AnmKeyVec3Bez(time, flags, outtan, intan, val) + if key_type == ANM_KeyType.ANM_KEYS_TYPE_QUAT_BEZ: + outtan = Vec4.create(b) + intan = Vec4.create(b) + val = Vec4.create(b) + return AnmKeyQuatBez(time, flags, outtan, intan, val) + + else: + print("WARNING: key_type: %s (%s)" % (key_type, ANM_KeyType.get_name(key_type))) return cls(time=time) +@dataclass +class AnmKeyBez(AnmKey): + flags: int + + ANM_BEZKEY_SMOOTH = 0 + ANM_BEZKEY_LINEAR = 1 + ANM_BEZKEY_STEP = 2 + ANM_BEZKEY_FAST = 3 + ANM_BEZKEY_SLOW = 4 + ANM_BEZKEY_USER = 5 + + ANM_BEZKEY_NUMTYPEBITS = 3 + ANM_BEZKEY_INTYPESHIFT = 7 + ANM_BEZKEY_OUTTYPESHIFT = (ANM_BEZKEY_INTYPESHIFT + ANM_BEZKEY_NUMTYPEBITS) + ANM_BEZKEY_TYPEMASK = 7 + + @staticmethod + def ANM_GET_OUT_TAN_TYPE(flags: int): + return int((flags >> AnmKeyBez.ANM_BEZKEY_OUTTYPESHIFT) & AnmKeyBez.ANM_BEZKEY_TYPEMASK) + + @staticmethod + def ANM_GET_IN_TAN_TYPE(flags: int): + return int((flags >> AnmKeyBez.ANM_BEZKEY_INTYPESHIFT) & AnmKeyBez.ANM_BEZKEY_TYPEMASK) + + @staticmethod + def ANM_DU(key_info: "AnmKeyInfo") -> int: + du = key_info.key1.time - key_info.key0.time + if du < 0: + du = key_info.key1.time - key_info.start_time + key_info.end_time - key_info.key0.time + return du + + @staticmethod + def bez_interpolate(bez_info: "ANM_BezInterpolateInfo"): # ANM_BezInterpolate + key_info = bez_info.info + key0: AnmKeyBez = key_info.key0 + key1: AnmKeyBez = key_info.key1 + t2 = key_info.t * key_info.t + out_type = AnmKeyBez.ANM_GET_OUT_TAN_TYPE(key0.flags) + in_type = AnmKeyBez.ANM_GET_IN_TAN_TYPE(key1.flags) + + if out_type == AnmKeyBez.ANM_BEZKEY_STEP or in_type == AnmKeyBez.ANM_BEZKEY_STEP: + for el in range(bez_info.elements): + if key_info.t == 1: + bez_info.val[el] = bez_info.val1[el] + else: + bez_info.val[el] = bez_info.val0[el] + return + + du = AnmKeyBez.ANM_DU(key_info) / 3 + for el in range(bez_info.elements): + out = bez_info.tan0[el] * du + bez_info.val0[el] + _in = bez_info.tan1[el] * du + bez_info.val1[el] + bez_info.val[el] = ((key_info.omt * bez_info.val0[el] + (3 * key_info.t) * out) * key_info.omt + + (3 * t2) * _in) * key_info.omt + key_info.t * t2 * bez_info.val1[el] + +@dataclass +class ANM_BezInterpolateInfo: + info: "AnmKeyInfo" + val0: List[float] + tan0: List[float] + tan1: List[float] + val1: List[float] + val: List[float] + elements: int + + +@dataclass +class AnmKeyRealBez(AnmKeyBez): + outtan: float + intan: float + val: float + + @staticmethod + def interpolate(vec: float, info: "AnmKeyInfo") -> float: # ANM_InterpRealBEZ + key0: AnmKeyRealBez = info.key0 + key1: AnmKeyRealBez = info.key1 + + bez_info = ANM_BezInterpolateInfo(info, [key0.val], [key0.outtan], [key1.intan], [key1.val], [vec], 1) + AnmKeyBez.bez_interpolate(bez_info) + return bez_info.val[0] + + +@dataclass +class AnmKeyVec3Bez(AnmKeyBez): + outtan: Vec3 + intan: Vec3 + val: Vec3 + + @staticmethod + def interpolate(vec: Vec3, info: "AnmKeyInfo") -> Vec3: # ANM_InterpVec3BEZ + key0: AnmKeyVec3Bez = info.key0 + key1: AnmKeyVec3Bez = info.key1 + + bez_info = ANM_BezInterpolateInfo(info, [key0.val.x, key0.val.y, key0.val.z], + [key0.outtan.x, key0.outtan.y, key0.outtan.z], + [key1.intan.x, key1.intan.y, key1.intan.z], + [key1.val.x, key1.val.y, key1.val.z], + [vec.x, vec.y, vec.z], 3) + AnmKeyBez.bez_interpolate(bez_info) + return Vec3(bez_info.val[0], bez_info.val[1], bez_info.val[2]) + + +@dataclass +class AnmKeyQuatBez(AnmKeyBez): + outtan: Quat + intan: Quat + val: Quat + + +@dataclass +class AnmKeyTCB(AnmKey): + tens: float + cont: float + bias: float + easein: float + easeout: float + + @staticmethod + def compute_hermit_basis(u: float) -> tuple: # ANM_TCBComputeHermiteBasis + u2 = u*u + u3 = u*u*u + a = 2*u3 - 3*u2 + v0 = 1 + a + v1 = -a + v2 = u - 2*u2 + u3 + v3 = -u2 + u3 + return v0, v1, v2, v3 + + @staticmethod + def ease(u: float, a: float, b: float) -> float: # ANM_TCBEase + s = a + b + if u == 0 or u == 1: + return u + if s == 0: + return u + if s > 1: + a = a / s + b = b / s + k = 1 / (2 - a - b) + if u < a: + return (k / a) * u * u + elif u < 1 - b: + return k * (2 * u - a) + else: + u = 1 - u + return 1 - (k / b) * u * u + + +@dataclass +class AnmKeyRealTCB(AnmKeyTCB): + outtan: float + intan: float + val: float + + @staticmethod + def interpolate(info: "AnmKeyInfo") -> float: # ANM_InterpRealTCB + key0: AnmKeyRealTCB = info.key0 + key1: AnmKeyRealTCB = info.key1 + t = AnmKeyTCB.ease(info.t, key0.easeout, key1.easein) + v = AnmKeyTCB.compute_hermit_basis(t) + return key0.val * v[0] + key1.val * v[1] + key0.outtan * v[2] + key1.intan * v[3] + + +@dataclass +class AnmKeyVec3TCB(AnmKeyTCB): + outtan: Vec3 + intan: Vec3 + val: Vec3 + + @staticmethod + def interpolate(info: "AnmKeyInfo") -> Vec3: # ANM_InterpVec3TCB + key0: AnmKeyVec3TCB = info.key0 + key1: AnmKeyVec3TCB = info.key1 + t = AnmKeyTCB.ease(info.t, key0.easeout, key1.easein) + v = AnmKeyTCB.compute_hermit_basis(t) + + return Vec3( + key0.val.x * v[0] + key1.val.x * v[1] + key0.outtan.x * v[2] + key1.intan.x * v[3], + key0.val.y * v[0] + key1.val.y * v[1] + key0.outtan.y * v[2] + key1.intan.y * v[3], + key0.val.z * v[0] + key1.val.z * v[1] + key0.outtan.z * v[2] + key1.intan.z * v[3], + ) + + +@dataclass +class AnmKeyQuatTCB(AnmKeyTCB): + outtan: Vec4 + intan: Vec4 + val: Vec4 + + @staticmethod + def interpolate(info: "AnmKeyInfo") -> Vec4: # ANM_InterpQuatTCB + key0: AnmKeyQuatTCB = info.key0 + key1: AnmKeyQuatTCB = info.key1 + t = AnmKeyTCB.ease(info.t, key0.easeout, key1.easein) + return Quat.squad(key0.val, key0.outtan, key1.intan, key1.val, t) + + +@dataclass +class AnmKeyInfo: + start_time: int + end_time: int + key0: AnmKey + key1: AnmKey + time: int + t: float + omt: float + + @dataclass class AnmKeys: keys_type: int ort_before: int ort_after: int flags: int - p_interpolate: int + p_interpolate: int # pointer to interpolate + # if ANM_XFORM_TYPE_PRS: + # if posKey: Vec3Interpolate + # if rotKey: QuatInterpolate + # if sclKey: Vec3Interpolate + # if ANM_XFORM_TYPE_LOOKAT: + # if rolKey: RealInterpolate + num_keys: int - keys: list + keys: List[AnmKey] = field(repr=False) @classmethod @with_offset @@ -157,16 +525,91 @@ class AnmKeys: _old = b.tell() b.seek(p_keys) for _ in range(num_keys): - keys.append(AnmKey.create(b)) + keys.append(AnmKey.create(b, keys_type)) b.seek(_old) return cls(keys_type=keys_type, ort_before=ort_before, ort_after=ort_after, flags=flags, p_interpolate=p_interpolate, num_keys=num_keys, keys=keys) + def vec3_interpolate(self, keyinfo: AnmKeyInfo) -> Vec3: + if self.keys_type in [ANM_KeyType.ANM_KEYS_TYPE_REAL_TCB, ANM_KeyType.ANM_KEYS_TYPE_VEC3_TCB, ANM_KeyType.ANM_KEYS_TYPE_QUAT_TCB]: + return AnmKeyVec3TCB.interpolate(keyinfo) + elif self.keys_type in [ANM_KeyType.ANM_KEYS_TYPE_REAL_BEZ, ANM_KeyType.ANM_KEYS_TYPE_VEC3_BEZ, ANM_KeyType.ANM_KEYS_TYPE_QUAT_BEZ]: + return AnmKeyVec3Bez.interpolate(keyinfo) + + def quat_interpolate(self, keyinfo: AnmKeyInfo) -> Quat: + if self.keys_type in [ANM_KeyType.ANM_KEYS_TYPE_REAL_TCB, ANM_KeyType.ANM_KEYS_TYPE_VEC3_TCB, ANM_KeyType.ANM_KEYS_TYPE_QUAT_TCB]: + return AnmKeyQuatTCB.interpolate(keyinfo) + elif self.keys_type in [ANM_KeyType.ANM_KEYS_TYPE_REAL_BEZ, ANM_KeyType.ANM_KEYS_TYPE_VEC3_BEZ, ANM_KeyType.ANM_KEYS_TYPE_QUAT_BEZ]: + return AnmKeyQuatBez.interpolate(keyinfo) + + def find_info(self, anm_start_time, anm_end_time, time) -> AnmKeyInfo: # translation of ANM_KeyFind + begin = 0 + index = end = self.num_keys + n = 0 + + start_time = self.keys[0].time + end_time = self.keys[self.num_keys-1].time + total_time = end_time - start_time + if ~self.flags & ANM_KeyFlags.ANM_KEYS_FLAGS_ORT_DISABLE and total_time: + if time < start_time: + ort_before = self.ort_before + if ort_before in [ANM_KEYS_ORT_LOOP, ANM_KEYS_ORT_CYCLE]: + lapse = -(time - end_time) + time = end_time - (lapse % total_time) + if ort_before == ANM_KEYS_ORT_OSCILLATE: + lapse = -(time - end_time) + time = end_time - (lapse % total_time) + if int(lapse / total_time) & 1: + time = end_time - (time - start_time) + if time > end_time: + ort_after = self.ort_after + if ort_after in [ANM_KEYS_ORT_LOOP, ANM_KEYS_ORT_CYCLE]: + lapse = (time - start_time) + time = start_time + (lapse % total_time) + if ort_after == ANM_KEYS_ORT_OSCILLATE: + lapse = (time - start_time) + time = start_time + (lapse % total_time) + if int(lapse / total_time) & 1: + time = end_time - (time - start_time) + index = self.num_keys + + # not sure about the while loop + n = begin + ((end - begin) >> 1) + while index != n: + n = begin + ((end - begin) >> 1) + index -= 1 + test = time - self.keys[index].time + if test: + if test < 0: + end = index + else: + begin = index + else: + break + + key0 = self.keys[index] + wrap = key0.time > time + if wrap: + key1 = key0 + key0 = self.keys[self.num_keys - 1] + t = 1 + else: + wrap = index == self.num_keys - 1 + if wrap: + key1 = self.keys[0] + t = 0 + else: + key1 = self.keys[index + 1] + t = (time - key0.time) / (key1.time - key0.time) + omt = 1 - t + keyinfo = AnmKeyInfo(start_time=anm_start_time, end_time=anm_end_time, key0=key0, key1=key1, time=time, t=t, omt=omt) + return keyinfo + @dataclass class AnmObj: custom: int = field(repr=False) - skin: int = field(repr=False) + skin: Optional["AnmSkin"] # = field(repr=False) # will be set later by AnmSkin itself zero_cluster: int total_number_vertices: int num_clusters: int @@ -189,7 +632,7 @@ class AnmObj: clusters.append(cluster) b.seek(_old1) - r = cls(custom=custom, skin=skin, zero_cluster=zero, total_number_vertices=total_vertices, + r = cls(custom=custom, skin=None, zero_cluster=zero, total_number_vertices=total_vertices, num_clusters=num_clusters, clusters=clusters) for cluster in clusters: cluster.obj = r @@ -199,7 +642,7 @@ class AnmObj: @dataclass class AnmCluster: custom: int = field(repr=False) - obj: Union[AnmObj, None] = field(repr=False) + obj: Union[AnmObj, None] # = field(repr=False) handle: int bounding_box: List[Vec3] = field(repr=False) # size 2 num_vertices: int @@ -228,6 +671,17 @@ class AnmCluster: return cls(custom=custom, obj=obj, handle=handle, bounding_box=bounding_box, num_vertices=num_vertices, vertices=vertices) + def apply_mat(self, mat: "GeoMat") -> List[Vec3]: # ANM_ClusterApplyMatSingleVertex + ret: List[Vec3] = [] + for vertex in self.vertices: + x = mat.s.m[0][0] * vertex.x + mat.s.m[0][1] * vertex.y + mat.s.m[0][2] * vertex.z + mat.s.m[0][3] + y = mat.s.m[1][0] * vertex.x + mat.s.m[1][1] * vertex.y + mat.s.m[1][2] * vertex.z + mat.s.m[1][3] + z = mat.s.m[2][0] * vertex.x + mat.s.m[2][1] * vertex.y + mat.s.m[2][2] * vertex.z + mat.s.m[2][3] + + n = Vec3(x, y, z) + ret.append(n) + return ret + class NamDictionnary: @staticmethod @@ -288,7 +742,8 @@ class NamDictionnary: while True: if key_index >= totalmappings: break - b.seek(p_lut + key_index * 4) # NAM_Dictionnary.KeyContexts[KEY].LUT[INDEX] -> LA je suis au MAP_Mapping* == NAM_Name* + b.seek( + p_lut + key_index * 4) # NAM_Dictionnary.KeyContexts[KEY].LUT[INDEX] -> LA je suis au MAP_Mapping* == NAM_Name* debug_print("MAP_Mapping*") addr_begin_mapping = read_int_and_print("MAP_Mapping*", b) # MAP_MapIndex* -> X_UINT16* if addr_begin_mapping == 0: @@ -309,7 +764,7 @@ class NamDictionnary: debug_print("MAP_MapIndex at %s" % b.tell()) index = read_short_and_print("MAP_MapIndex value", b) - b.seek(addr_begin_mapping+index) + b.seek(addr_begin_mapping + index) debug_print("STRINGDATA* OU HANDLEDATA* at %s" % b.tell()) # pdata = read_int_and_print("Data*", b) # b.seek(pdata) @@ -327,6 +782,12 @@ class NamDictionnary: if key == NAM_HANDLE_KEY: read_short_and_print("RefCnt", b) # NAM_NameRefCnt == X_UINT16 handle = read_short_and_print("Handle", b) # NAM_NameHandle == X_UINT16 + + # MAYBE BREAKS SOMETHING: 23/07/2021 + # maybevalue = read_int_and_print("TEST", b) + # r[key_index]["obj"] = NamDictionnary.create(AnmNode, b, offset=maybevalue) + # r[key_index]["obj1"] = AnmNode.create(b, offset=maybevalue) + r[key_index]["Handle"] = handle key_index += 1 debug_print("Key_index=%s" % key_index) @@ -335,15 +796,27 @@ class NamDictionnary: @dataclass class GeoMat4x4: - m: List[float] + m: List[List[float]] @classmethod @with_offset def create(cls, b): m = [] for _ in range(4): + row = [] for _j in range(4): - m.append(read_float(b)) + row.append(read_float(b)) + m.append(row) + return cls(m=m) + + @classmethod + def create_new(cls): + m = [] + for _ in range(4): + row = [] + for _j in range(4): + row.append(0) + m.append(row) return cls(m=m) @@ -359,10 +832,138 @@ class GeoMat: s = GeoMat4x4.create(b) return cls(mat_class=mat_class, s=s) + def multiply(self, other: "GeoMat") -> "GeoMat": # translation of S_GEO_MatMultiply + ret = GeoMat4x4.create_new() + if self.mat_class & GEO_MAT_PROJECTION or other.mat_class & GEO_MAT_PROJECTION: + for i in range(4): + for j in range(4): + val = self.s.m[i][0] * other.s.m[0][j] + \ + self.s.m[i][1] * other.s.m[1][j] + \ + self.s.m[i][2] * other.s.m[2][j] + \ + self.s.m[i][3] * other.s.m[3][j] + ret.m[i][j] = val + else: + if self.mat_class & GEO_MAT_LINEAR and other.mat_class & GEO_MAT_LINEAR: + for i in range(3): + for j in range(3): + val = self.s.m[i][0] * other.s.m[0][j] + \ + self.s.m[i][1] * other.s.m[1][j] + \ + self.s.m[i][2] * other.s.m[2][j] + ret.m[i][j] = val + else: + rot_src = self if self.mat_class & GEO_MAT_LINEAR else other + for i in range(3 * 4): + ret.m[0][i] = rot_src.s.m[0][i] + if other.mat_class & GEO_MAT_TRANSLATION: + for j in range(3): + ret.m[j][3] = self.s.m[j][0] * other.s.m[0][3] + \ + self.s.m[j][1] * other.s.m[1][3] + \ + self.s.m[j][2] * other.s.m[2][3] + \ + self.s.m[j][3] + else: + ret.m[0][3] = self.s.m[0][3] + ret.m[1][3] = self.s.m[1][3] + ret.m[2][3] = self.s.m[2][3] + ret.m[3][0] = ret.m[3][1] = ret.m[3][2] = 0 + ret.m[3][3] = 1 + cls = ((self.mat_class | other.mat_class) & ~GEO_MAT_SIGN) | ((self.mat_class ^ other.mat_class) & GEO_MAT_SIGN) + if self.mat_class & GEO_MAT_SCALE and other.mat_class & GEO_MAT_ROTATION: + cls |= GEO_MAT_SHEAR + return GeoMat(mat_class=cls, s=ret) + + @classmethod + def quat(cls, quat: "Vec4") -> "GeoMat": + x = quat.x + y = quat.y + z = quat.z + w = quat.w + + xs = x + x + ys = y + y + zs = z + z + + wx = w * xs + wy = w * ys + wz = w * zs + xx = x * xs + xy = x * ys + xz = x * zs + yy = y * ys + yz = y * zs + zz = z * zs + + ret = GeoMat4x4.create_new() + ret.m[0][0] = 1 - (yy + zz) + ret.m[0][1] = xy - wz + ret.m[0][2] = xz + wy + + ret.m[1][0] = xy + wz + ret.m[1][1] = 1 - (xx + zz) + ret.m[1][2] = yz - wx + + ret.m[2][0] = xz - wy + ret.m[2][1] = yz + wx + ret.m[2][2] = 1 - (xx + yy) + + ret.m[3][0] = ret.m[3][1] = ret.m[3][2] = ret.m[0][3] = ret.m[1][3] = ret.m[2][3] = 0 + ret.m[3][3] = 1 + + c = GEO_MAT_ROTATION + return cls(mat_class=c, s=ret) + + @classmethod + def identity(cls): + m = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + m = GeoMat4x4(m=m) + return cls(mat_class=0, s=m) + + def affine(self, affine: "GeoAffine") -> "GeoMat": # GEO_MatAffine + if affine.affine_class & GEO_MAT_ROTATION: + mat = self.quat(affine.quat) + else: + mat = GeoMat.identity() + + if affine.affine_class & GEO_MAT_TRANSLATION: + mat.s.m[0][3] = affine.translation.x + mat.s.m[1][3] = affine.translation.y + mat.s.m[2][3] = affine.translation.z + if affine.affine_class & (GEO_MAT_SCALE | GEO_MAT_SIGN): + if affine.affine_class & GEO_MAT_SIGN: + mat.s.m[0][0] *= -affine.scale.x + mat.s.m[1][0] *= -affine.scale.x + mat.s.m[2][0] *= -affine.scale.x + + mat.s.m[0][1] *= -affine.scale.y + mat.s.m[1][1] *= -affine.scale.y + mat.s.m[2][1] *= -affine.scale.y + + mat.s.m[0][2] *= -affine.scale.z + mat.s.m[1][2] *= -affine.scale.z + mat.s.m[2][2] *= -affine.scale.z + else: + mat.s.m[0][0] *= affine.scale.x + mat.s.m[1][0] *= affine.scale.x + mat.s.m[2][0] *= affine.scale.x + + mat.s.m[0][1] *= affine.scale.y + mat.s.m[1][1] *= affine.scale.y + mat.s.m[2][1] *= affine.scale.y + + mat.s.m[0][2] *= affine.scale.z + mat.s.m[1][2] *= affine.scale.z + mat.s.m[2][2] *= affine.scale.z + mat.mat_class = affine.affine_class + return mat + @dataclass class GeoAffine: - transform: Vec3 + translation: Vec3 quat: Vec4 scale: Vec3 affine_class: int @@ -374,7 +975,7 @@ class GeoAffine: quat = Vec4.create(b) scale = Vec3.create(b) affine_class = read_int(b) - return cls(transform=trans, quat=quat, scale=scale, affine_class=affine_class) + return cls(translation=trans, quat=quat, scale=scale, affine_class=affine_class) @dataclass @@ -396,17 +997,17 @@ class AnmNode: node_type: int x_form_type: int flags: int - p_anim: int - p_parent: int + anim: Optional["AnmAnim"] # = field(repr=False) + parent: Optional["AnmNode"] p_target: int p_usercall: int p_usercontext: int stamp: int - geo_mat: GeoMat - transform: AnmTransform - pos_keys: Union[AnmKeys, int] - rot_keys: Union[AnmKeys, int] - scl_keys: Union[AnmKeys, int] + geo_mat: GeoMat = field(repr=False) + transform: AnmTransform = field(repr=False) + pos_keys: Union[AnmKeys, int] = field(repr=False) + rot_or_aff_keys: Union[AnmKeys, int] = field(repr=False) + scl_keys: Union[AnmKeys, int] = field(repr=False) @classmethod @with_offset @@ -417,6 +1018,10 @@ class AnmNode: flags = read_int_and_print("ANM_Node->Flags", b) p_anim = read_int_and_print("ANM_Node->pAnim", b) p_parent = read_int_and_print("ANM_Node->pParent", b) + if p_parent: + parent = AnmNode.create(b, offset=p_parent) + else: + parent = None p_target = read_int_and_print("ANM_Node->pTarget", b) p_usercall = read_int_and_print("ANM_Node->pUserCall", b) p_usercontext = read_int_and_print("ANM_Node->pUserContext", b) @@ -424,23 +1029,145 @@ class AnmNode: geo_mat = GeoMat.create(b) transform = AnmTransform.create(b) pos_keys = read_int_and_print("ANM_Node->pPosKeys", b) - rot_keys = read_int_and_print("ANM_Node->pRotKeys", b) + rot_or_aff_keys = read_int_and_print("ANM_Node->pRotKeys", b) scl_keys = read_int_and_print("ANM_Node->pSclKeys", b) if pos_keys > 0: pos_keys = AnmKeys.create(b, offset=pos_keys) - if rot_keys > 0: - rot_keys = AnmKeys.create(b, offset=rot_keys) + if rot_or_aff_keys > 0: + rot_or_aff_keys = AnmKeys.create(b, offset=rot_or_aff_keys) if scl_keys > 0: scl_keys = AnmKeys.create(b, offset=scl_keys) - return cls(custom=custom, node_type=node_type, x_form_type=x_form_type, flags=flags, p_anim=p_anim, - p_parent=p_parent, p_target=p_target, p_usercall=p_usercall, p_usercontext=p_usercontext, - stamp=stamp, geo_mat=geo_mat, transform=transform, pos_keys=pos_keys, rot_keys=rot_keys, + return cls(custom=custom, node_type=node_type, x_form_type=x_form_type, flags=flags, anim=None, + parent=parent, p_target=p_target, p_usercall=p_usercall, p_usercontext=p_usercontext, + stamp=stamp, geo_mat=geo_mat, transform=transform, pos_keys=pos_keys, rot_or_aff_keys=rot_or_aff_keys, scl_keys=scl_keys) + def get_mat(self, time: int, stamp): + affine = self.get_affine(time, stamp) + if not self.parent: + return self.geo_mat.affine(affine) + else: + return self.geo_mat.affine(affine).multiply(self.parent.get_mat(time, stamp)) + + def get_affine(self, time: int, stamp) -> GeoAffine: # ANM_NodeGetAffine, Stamp is used for LOOKAT + if self.transform.time != time: + start_time = self.anim.start_time + end_time = self.anim.end_time + assert (self.x_form_type == ANM_XFORM_TYPE_PRS) # todo: process others + if self.pos_keys: + keyinfo = self.pos_keys.find_info(start_time, end_time, time) + self.transform.affine.translation = AnmKeys.vec3_interpolate(keyinfo) + self.transform.affine.affine_class |= GEO_MAT_TRANSLATION + if self.rot_or_aff_keys: + keyinfo = self.rot_or_aff_keys.find_info(start_time, end_time, time) + self.transform.affine.quat = AnmKeys.quat_interpolate(keyinfo) + self.transform.affine.affine_class |= GEO_MAT_ROTATION + if self.scl_keys: + keyinfo = self.scl_keys.find_info(start_time, end_time, time) + self.transform.affine.scale = AnmKeys.vec3_interpolate(keyinfo) + if self.transform.affine.affine_class & GEO_MAT_SIGN: + self.transform.affine.scale.x *= -1 + self.transform.affine.scale.y *= -1 + self.transform.affine.scale.z *= -1 + self.transform.affine.affine_class |= GEO_MAT_SCALE + self.transform.time = time + return self.transform.affine + + +@dataclass +class AnmBinding: + cluster: Optional[AnmCluster] + node: Optional[AnmNode] = field(repr=False) + + +def dictionnary_find_node_by_handle(dictionnary: dict, handle: int, node_dict: dict): + small_node = None + for k, v in dictionnary.items(): + if not isinstance(v, dict) or "Handle" not in v: + raise Exception("dictionnary_find_by_handle: dict does not have handles") + if v["Handle"] == handle: + small_node = v + if not small_node: + raise Exception("dictionnary_find_by_handle: could not find handle %s in dict" % handle) + else: + name = small_node["Name"] + if name in node_dict: + return node_dict[name] + else: + raise Exception("dictionnary_find_by_handle: could not find name %s in node_dict" % name) + + +@dataclass +class AnmBinder: + dictionnary: dict + obj: "AnmObj" + NumberOfBindings: int + bindings: List["AnmBinding"] + + @classmethod + def create(cls, anim: "AnmAnim", obj: "AnmObj"): + dictionnary = anim.default_obj_dictionnary + bindings = [] + for i in range(obj.num_clusters): + bindings.append(AnmBinding(None, None)) + + if obj.num_clusters > 0: + num_bindings = obj.num_clusters + for i in range(num_bindings): + binding = bindings[i] + binding.cluster = obj.clusters[i] + binding.node = dictionnary_find_node_by_handle(obj.skin.node_dictionnary, binding.cluster.handle, anim.default_obj_dictionnary) + else: + num_bindings = 0 + + return cls(dictionnary=dictionnary, obj=obj, NumberOfBindings=num_bindings, bindings=bindings) + + def eval_vertices(self, time: int, vertices: List[Vec3], stride: int, vertexbase: int, stamp): # ANM_BinderEvalVertices + for binding in self.bindings: + if binding.node: + mat = binding.node.get_mat(time, stamp) + debug_print("Applying mat to cluster") + binding.cluster.apply_mat(mat) + + +@dataclass +class AnmAnimation: + dictionnary: dict + anim: "AnmAnim" + skin: "AnmSkin" + GrandTotalNumberOfVertices: int + NumberOfBinders: int + binders: List["AnmBinder"] + + @classmethod + def create(cls, skin: "AnmSkin", anim: "AnmAnim"): + binders = [] + num_objs = len(skin.default_obj_dictionnary) + grand_total_num_vertices = 0 + num_binders = num_objs + for i in range(num_objs): + key: str = list(skin.default_obj_dictionnary)[i] + obj: AnmObj = skin.default_obj_dictionnary[key] + binder = AnmBinder.create(anim, obj) + binders.append(binder) + grand_total_num_vertices += obj.total_number_vertices + + dictionnary = binders[0].dictionnary + + return cls(dictionnary=dictionnary, anim=anim, skin=skin, GrandTotalNumberOfVertices=grand_total_num_vertices, + NumberOfBinders=num_binders, binders=binders) + + def eval(self, time: int, vertices: List[Vec3], stride: int, stamp): # ANM_AnimationEvalVertices + vbase = 0 + for binder in self.binders: + debug_print("Evaluating binder") + binder.eval_vertices(time, vertices, stride, vbase, stamp) + vbase += binder.obj.total_number_vertices + @dataclass class AnmAnim: @@ -496,12 +1223,30 @@ class AnmAnim: treedict = NamDictionnary.create(AnmNode, b, depth=2, offset=treedict) defaultobjdict = NamDictionnary.create(AnmNode, b, offset=defaultobjdict) + for obj_name in treedict["Default Tree"]: + # memory hack: prevent keeping in memory similar objects + obj = treedict["Default Tree"][obj_name] + for defobj_name in defaultobjdict: + if obj_name == defobj_name: + defaultobjdict[obj_name] = obj - return cls(custom=custom, fileversion=version, image=image, memgroup=memgroup, start_time=start_time, - end_time=end_time, ticks_per_frame=ticks_per_frame, framerate=framerate, tree_dictionnary=treedict, - default_obj_dictionnary=defaultobjdict, num_nodes=num_nodes, extra_stamp=stamp, extra_flags=flags, - extra_time1=time1, extra_stamp1=stamp1, extra_time2=time2, extra_stamp2=stamp2, extra_factor=factor, - extra_other_anim=otheranim, name=name) + r = cls(custom=custom, fileversion=version, image=image, memgroup=memgroup, start_time=start_time, + end_time=end_time, ticks_per_frame=ticks_per_frame, framerate=framerate, tree_dictionnary=treedict, + default_obj_dictionnary=defaultobjdict, num_nodes=num_nodes, extra_stamp=stamp, extra_flags=flags, + extra_time1=time1, extra_stamp1=stamp1, extra_time2=time2, extra_stamp2=stamp2, extra_factor=factor, + extra_other_anim=otheranim, name=name) + r.fix_links() + return r + + def fix_node_anim_recursive(self, node: AnmNode): + node.anim = self + if node.parent: + self.fix_node_anim_recursive(node.parent) + + def fix_links(self): + for node_name in self.default_obj_dictionnary: + node: AnmNode = self.default_obj_dictionnary[node_name] + self.fix_node_anim_recursive(node) @dataclass @@ -520,7 +1265,6 @@ class AnmSkin: @staticmethod def link(nodedict, defaultobjtree): - m = {} for node_index in nodedict: node = nodedict[node_index] @@ -549,18 +1293,37 @@ class AnmSkin: treedict = NamDictionnary.create(AnmObj, b, depth=2, offset=tree) defaultobjdict = NamDictionnary.create(AnmObj, b, offset=defaultobj) - AnmSkin.link(nodedict, defaultobjdict) - return cls(custom=custom, file_version=version, image=image, memgroup=memgroup, node_dictionnary=nodedict, + for obj_name in treedict["Default Tree"]: + # memory hack: prevent keeping in memory similar objects + obj = treedict["Default Tree"][obj_name] + for defobj_name in defaultobjdict: + if obj_name == defobj_name: + defaultobjdict[obj_name] = obj + + pprint([treedict, defaultobjdict]) + + # AnmSkin.link(nodedict, defaultobjdict) + skin = cls(custom=custom, file_version=version, image=image, memgroup=memgroup, node_dictionnary=nodedict, tree_dictionnary=treedict, default_obj_dictionnary=defaultobjdict, total_number_trees=num_trees, total_number_objs=num_objs, total_number_clusters=num_clusters, name=name) + skin.fix_links() + return skin + + def fix_links(self): + for k in self.default_obj_dictionnary: + obj = self.default_obj_dictionnary[k] + obj.skin = self + @dataclass class AnmFile: @staticmethod def parse(b, cls): + # MAP_Image _ = read_int_and_print("size", b) memgroupoffset = read_int_and_print("memgroupoffset", b) + b.seek(memgroupoffset) read_int_and_print("\tNode", b) read_int_and_print("\tSize", b) diff --git a/map2obj.py b/map2obj.py new file mode 100644 index 0000000..cf4fc20 --- /dev/null +++ b/map2obj.py @@ -0,0 +1,32 @@ +from giantslib.game.map import Map + + +def main(): + basedir = "/home/tasty/Projects/gck-map-extract-objects/intro_island/" + map_path = basedir + "intro_island.zip" + m = Map(map_path) + m.save_heightmap(basedir + "heightmap.png") + m.save_colormap(basedir + "lightmap.png") + print(m.minheight, m.maxheight, m.stretch) + return + + indexes = [] + for tri in m.triangles: + index = m.vertices.index(tri) + indexes.append(index) + print(indexes) + assert(len(indexes) % 3 == 0) + + assert(max(indexes) < len(m.vertices)) + with open("/home/tasty/Projects/Giants/assets/terrains/test.obj", "w") as fp: + for v in m.vertices: + fp.write("v %s %s %s\n" % (v[0], v[1], v[2])) + + t = 0 + while t < len(indexes): + fp.write("f %s/%s %s/%s %s/%s\n" % (indexes[t]+1, indexes[t]+1, indexes[t+1]+1, indexes[t+1]+1, indexes[t+2]+1, indexes[t+2]+1)) + t += 3 + + +if __name__ == '__main__': + main() diff --git a/obj2gbs.py b/obj2gbs.py index 323dab4..b983220 100644 --- a/obj2gbs.py +++ b/obj2gbs.py @@ -16,6 +16,7 @@ GBSFlagMaxLit = (1 << 31) def check(gbs_file, materials): + return with open("data.json") as fp: d = json.load(fp) @@ -109,6 +110,8 @@ class OBJObject: self.faces: List[OBJFace] = [] self.name = "root" self.material: Union[None, OBJMaterial] = None + self.vref_start = 0 + self.vref_count = 0 class OBJFace: @@ -126,14 +129,19 @@ def obj_read_materials(matlib_file) -> List[OBJMaterial]: line = line.strip() arr = line.split(" ") if arr[0] == "newmtl": + if len(arr) <= 1: + curr_mat = None + continue mat = OBJMaterial() materials.append(mat) mat.name = arr[1].rstrip() curr_mat = mat if arr[0] == "map_Ka" or arr[0] == "map_Kd": - matname_without_ext = "".join(arr[1:]).split("/")[-1] - matname_without_ext = "".join(matname_without_ext.split(".")[0:-1]) - curr_mat.texture = matname_without_ext + if curr_mat: + matname_without_ext = "".join(arr[1:]).split("/")[-1] + matname_without_ext = "".join(matname_without_ext.split(".")[0:-1]) + curr_mat.texture = matname_without_ext + # print("Set %s to %s" % (curr_mat.texture, curr_mat.name)) return materials @@ -205,6 +213,7 @@ class GbsData: normals: List[Vec3] = [] objects: List[OBJObject] = [] root_obj = OBJObject() + max_objs: List[MaxObj] = [] objects.append(root_obj) last_material = None @@ -258,22 +267,45 @@ class GbsData: f.index_normals.append(v3_normal_index) current_object.faces.append(f) if arr[0] == "o": - obj_name = arr[1].rstrip() + obj_line = arr[-1].rstrip() + try: + end = obj_line.index("_#_") + except ValueError: + end = None + + obj_name = obj_line[0:end] o = OBJObject() o.name = obj_name o.material = last_material + if end: + meta = obj_line[end+3:].split("_") + o.vref_start = int(meta[0]) + o.vref_count = int(meta[1]) + objects.append(o) if len(current_object.faces) == 0: objects.remove(current_object) + current_object = o - if arr[0] == "usemtl": + if arr[0] == "usemtl" and len(arr) > 1: mtl_name = arr[1].rstrip() - mtl = [mat for mat in materials if mat.name == mtl_name][0] - current_object.material = mtl - last_material = mtl + if mtl_name: + mtl = [mat for mat in materials if mat.name == mtl_name][0] + current_object.material = mtl + last_material = mtl if arr[0] == "mtllib": matlib_file = arr[1].rstrip() - materials = obj_read_materials("%s/%s" % (os.path.dirname(obj_file), matlib_file)) + obj_mat = "%s/%s" % (os.path.dirname(obj_file), matlib_file) + print(obj_mat) + materials = obj_read_materials(obj_mat) + if arr[0] == "#" and arr[1] == "maxobj": + max_obj = MaxObj() + max_obj.vstart = int(arr[2]) + max_obj.vcount = int(arr[3]) + max_obj.nstart = int(arr[4]) + max_obj.ncount = int(arr[5]) + max_obj.noffset = int(arr[6]) + max_objs.append(max_obj) num_faces = sum([len(o.faces) for o in objects]) print("%s vertices, %s uvs, %s normals, %s objects, %s materials, %s faces" % (len(vertices), len(uvs), len(normals), len(objects), len(materials), num_faces)) @@ -339,15 +371,27 @@ class GbsData: data.put_float(v.v * -1) # max objects - data.put_long(1) # 1 big object - data.put_long(0) # vstart - data.put_long(len_vertices) # vcount - data.put_long(0) # nstart - data.put_long(0) # ncount - data.put_long(0) # noffset ??? + print("There are %s max objects" % len(max_objs)) + if not max_objs: + data.put_long(1) # 1 big object + data.put_long(0) # vstart + data.put_long(len_vertices) # vcount + data.put_long(0) # nstart + data.put_long(0) # ncount + data.put_long(0) # noffset ??? + else: + data.put_long(len(max_objs)) + for i in range(len(max_objs)): + max_obj = max_objs[i] + data.put_long(max_obj.vstart) + data.put_long(max_obj.vcount) + data.put_long(max_obj.nstart) + data.put_long(max_obj.ncount) + data.put_long(max_obj.noffset) # start write subobjects data.put_long(len(objects)) + print("THERE ARE %s subobjects" % len(objects)) for obj in objects: data.put_string_size(obj.name, 32) data.put_long(0) # max obj index @@ -360,8 +404,8 @@ class GbsData: data.put_short(face.index_uvs[1] - 1) data.put_short(face.index_uvs[2] - 1) - data.put_long(0) # verticeref_start - data.put_long(nverts) # verticeref_count + data.put_long(obj.vref_start) # verticeref_start + data.put_long(nverts if obj.vref_count == 0 else obj.vref_count) # verticeref_count if options & GBSFlagUVs: data.put_string_size(obj.material.texture, 32) # texture data.put_string_size(obj.material.texture, 32) # bump @@ -391,6 +435,7 @@ def convert_obj(path): output = "%s/%s.gbs" % (os.path.dirname(os.path.abspath(path)), os.path.basename(path)) print("Done! Output: %s" % output) + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("path") diff --git a/skn.py b/skn.py index ac8c13c..6ef69e5 100644 --- a/skn.py +++ b/skn.py @@ -1,5 +1,8 @@ from lib.skn_anm import * from lib.gbs import * +import struct +import os +import shutil def read_skn(filepath) -> AnmSkin: @@ -27,8 +30,9 @@ def test(): print("%s: dist=%s" % (mini_dist_v, mini_dist_v.distance(my_vertex))) -def print_vertices(): - skn = read_skn("/home/tasty/Projects/gck-map-extract-objects/test/rp_l0.apatch.gzp.skn") +def print_vertices(file): + skn = read_skn(file) + pprint(skn) for obj_name in skn.default_obj_dictionnary: obj = skn.default_obj_dictionnary[obj_name] for cluster in obj.clusters: @@ -54,13 +58,12 @@ def link_gbs_skn(): vbase += obj.total_number_vertices -def main(): - skn = read_skn("/home/tasty/Projects/gck-map-extract-objects/test/rp_l0.apatch.gzp.skn") +def main(file): + skn = read_skn(file) pprint(skn) vbase = 0 for tree in skn.tree_dictionnary: for obj_name in skn.tree_dictionnary[tree]: - print(obj_name) obj = skn.tree_dictionnary[tree][obj_name] for cluster in obj.clusters: if cluster.num_vertices <= 0: @@ -74,12 +77,96 @@ def main(): vbase = 0 for obj_name, obj in skn.default_obj_dictionnary.items(): for cluster in obj.clusters: - # fp.write("name %s %s %s %s %s %s %s\n" % (cluster.bone_name.strip().replace(" ", ""), cluster.bounding_box[0].x, cluster.bounding_box[0].y, cluster.bounding_box[0].z, cluster.bounding_box[1].x, cluster.bounding_box[1].y, cluster.bounding_box[1].z)) - for vertex in cluster.vertices: - fp.write("index %s\n" % (vertex.index + vbase)) - # fp.write("offset %s %s %s\n" % (vertex.x, vertex.y, vertex.z)) + ## 81: part of hair + ## 12: left eye + ## 9/13: leg or arm + ## 15/17 is a boob + if cluster.handle == 15: + # fp.write("name %s %s %s %s %s %s %s\n" % (cluster.bone_name.strip().replace(" ", ""), cluster.bounding_box[0].x, cluster.bounding_box[0].y, cluster.bounding_box[0].z, cluster.bounding_box[1].x, cluster.bounding_box[1].y, cluster.bounding_box[1].z)) + for vertex in cluster.vertices: + # fp.write("index %s\n" % (vertex.index + vbase)) + fp.write("%s %s %s 0 0 0\n" % (vertex.x, vertex.y, vertex.z)) vbase += obj.total_number_vertices +def replace_vertex(x1, y1, z1, x2, y2, z2, skn_file): + final = skn_file+".repl" + if not os.path.exists(final): + shutil.copy(skn_file, final) + + with open(final, "rb") as fp: + fp.seek(0) + bytesarr = bytearray(fp.read()) + + bfind = x1+y1+z1 + brepl = x2+y2+z2 + bytesarr_new = bytesarr.replace(bfind, brepl) + assert(bytesarr_new != bytesarr) + with open(final, "ab+") as fp: + fp.seek(0) + fp.write(bytesarr_new) + + +def replace_vertices(file_input, skn_file): + with open(skn_file, "rb") as fp: + orig_bytes = bytearray(fp.read()) + + with open(file_input, "r") as fp: + lines = fp.readlines() + to_search = [] + for line in lines: + line = line.strip() + if not line or line.startswith("#"): + continue + a = line.split(" ") + print(line) + assert(len(a) == 6) + x1 = struct.pack("