commit 41cfa7e22d43a87220bc3ecd144a0f06868ce8ae Author: Hipstercat Date: Thu Apr 1 13:09:23 2021 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9bf75c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv/ +.idea + __pycache__ diff --git a/gbs_plugin/__init__.py b/gbs_plugin/__init__.py new file mode 100644 index 0000000..d05e1d2 --- /dev/null +++ b/gbs_plugin/__init__.py @@ -0,0 +1,90 @@ +# addon information +import bpy +from bpy.types import Operator, AddonPreferences +from bpy_extras.io_utils import ImportHelper +from bpy.props import StringProperty +from .importer import imp_gbs +import time + + + +bl_info = { + "name": "Import Giants GBS format", + "author": "Amazed#0001", + "version": (1, 0), + "blender": (2, 80, 0), + "location": "File > Import > Giants GBS (.gbs)", + "description": "Import static objects from Giants GBS files.", + "category": "Import-Export", +} + +class GBSPreferences(AddonPreferences): + # this must match the addon name, use '__package__' + # when defining this in a submodule of a python package. + bl_idname = __name__ + + game_path = StringProperty( + name="Your Giants game folder", + description="This is needed to extract textures", + subtype='DIR_PATH' + ) + + def draw(self, context): + layout = self.layout + layout.label(text="GBS plugin preferences") + layout.prop(self, "game_path") + + +# main code +class GBSImporter(bpy.types.Operator, ImportHelper): + bl_idname = "gbs_importer.gbs" + bl_description = "Import Giants model files (.gbs)" + bl_label = "Giants GBS Importer" + bl_options = {'REGISTER', 'UNDO'} + + filename_ext = ".gbs" + filter_glob: StringProperty( + default="*.gbs", + options={'HIDDEN'}, + ) + + filter_glob: StringProperty( + default="*.gbs", + options={'HIDDEN'}, + ) + + def execute(self, context): + user_preferences = context.preferences + addon_prefs = user_preferences.addons[__name__].preferences + + time_start = time.time() + + game_path = addon_prefs.game_path + + if not game_path: + raise Exception("Please set your Giants folder in the add-on preferences") + imp_gbs(self.filepath, game_path) + print("Elapsed time: %.2fs" % (time.time() - time_start)) + return {'FINISHED'} + +classes = ( + GBSPreferences, + GBSImporter +) + +def menu_func(self, context): + self.layout.operator(GBSImporter.bl_idname, text="Giants GBS (.gbs)") + + +def register(): + for c in classes: + bpy.utils.register_class(c) + + bpy.types.TOPBAR_MT_file_import.append(menu_func) + + +def unregister(): + bpy.types.TOPBAR_MT_file_import.remove(menu_func) + + for c in classes: + bpy.utils.unregister_class(c) \ No newline at end of file diff --git a/gbs_plugin/importer.py b/gbs_plugin/importer.py new file mode 100644 index 0000000..7a6feed --- /dev/null +++ b/gbs_plugin/importer.py @@ -0,0 +1,63 @@ +from .shared import * + + +def imp_gbs(gbspath, giantspath): + path = gbspath + game_folder = giantspath + gbs = GbsData() + gbs.read(path) + name = os.path.splitext(os.path.basename(path))[0] + + map_collection = bpy.data.collections.new(name) + bpy.context.scene.collection.children.link(map_collection) + + verts = [] + for index in gbs.indexed_vertices: + v = gbs.vertices[index] + vert = (v.x, v.y, v.z) + verts.append(vert) + + for subobj in gbs.SubObjs: + fool = 0 + uvs = [] + + mesh = bpy.data.meshes.new(subobj.objname) + obj = bpy.data.objects.new(mesh.name, mesh) + faces = [] + + curr_index = 0 + print(subobj.tridata) + while curr_index < len(subobj.tridata) - 1: + num_tris = subobj.tridata[curr_index] + for tri_num in range(num_tris): + curr_face = [] + for k in range(3): + vert_index = subobj.tridata[curr_index + 1 + tri_num * 3 + k] + curr_face.append(vert_index) + + u = gbs.vertuv[vert_index].u + v = gbs.vertuv[vert_index].v + uv = (u, v) + uvs.append(uv) + + faces.append(curr_face) + curr_index = num_tris * 3 + 1 + + mesh.from_pydata(verts, [], faces) + map_collection.objects.link(obj) + + # Uvs + uv_layer = mesh.uv_layers.new(name="UV") + mesh.uv_layers.active = uv_layer + + for i in range(len(uvs)): + uv_layer.data[i].uv = uvs[i] + + tmpfilename = find_and_extract_texture(subobj.texname, name, game_folder) + mat = material_from_file(tmpfilename, subobj.texname) + if obj.data.materials: + # assign to 1st material slot + obj.data.materials[0] = mat + else: + # no slots + obj.data.materials.append(mat) \ No newline at end of file diff --git a/gbs_plugin/shared.py b/gbs_plugin/shared.py new file mode 100644 index 0000000..8a5f4cb --- /dev/null +++ b/gbs_plugin/shared.py @@ -0,0 +1,474 @@ +import struct +from typing import List +import math +import os +import tempfile +import bpy + +GBS_VERSION = 0xaa0100be +GBSFlagNormals = 0x0001 +GBSFlagUVs = 0x0002 +GBSFlagRGBs = 0x0004 +GBSFlagCalcNormals = 0x0008 +GBSFlagMaxLit = (1 << 31) + + +def read_int(fp): + return struct.unpack(" str: + s = "" + c = read_byte(fp) + while c != 0x00: + s += chr(c) + c = read_byte(fp) + return s + + +def decompress(compressed_bytes, original_size: int): + i = 0 + j = 0 + dec_byte = 0 + dec_bits = 8 + buff_start = 0xFEE + + res = bytearray(original_size) + + if original_size == 0: + return res + + while j < original_size: + if dec_bits == 8: + dec_byte = compressed_bytes[i] + i += 1 + dec_bits = 0 + if (dec_byte >> dec_bits & 1) == 0: + dec_pos = ((compressed_bytes[i] + ( + (compressed_bytes[i + 1] & 0xF0) << 4) - buff_start - j) & 0xFFF) - 0x1000 + j + dec_len = (compressed_bytes[i + 1] & 0xF) + 3 + i += 2 + while dec_len > 0: + if dec_pos >= 0: + res[j] = res[dec_pos] + else: + res[j] = 32 + j += 1 + dec_pos += 1 + dec_len -= 1 + else: + res[j] = compressed_bytes[i] + i += 1 + j += 1 + dec_bits += 1 + return res + + +class Vec2: + def __init__(self, x=0.0, y=0.0): + self.x = x + self.y = y + + def __str__(self): + return "[%s,%s]" % (self.x, self.y) + + +class Vec3: + def __init__(self, x=0.0, y=0.0, z=0.0): + self.x = x + self.y = y + self.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 + + def normalize(self): + w = math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) + if w == 0: + return Vec3() + else: + return Vec3(self.x / w, self.y / w, self.z / w) + + def __str__(self): + return "[%s,%s,%s]" % (self.x, self.y, self.z) + + +class VecRGB: + def __init__(self, r=0, g=0, b=0): + self.r = r + self.g = g + self.b = b + + def __str__(self): + return "[%s,%s,%s]" % (self.r, self.g, self.b) + + +def cross(v1: Vec3, v2: Vec3): + return v1.cross(v2) + + +def resize(li: List, t, num): + li.clear() + for i in range(num): + li.append(t()) + + +class FileMaxObj: + def __init__(self): + self.vstart: int = 0 + self.vcount: int = 0 + self.nstart: int = 0 + self.ncount: int = 0 + self.noffset: int = 0 + + +class MaxObj(FileMaxObj): + def __init__(self): + super().__init__() + self.fstart: int = 0 + self.fcount: int = 0 + self.sostart: int = 0 + self.socount: int = 0 + + +class SubObject: + def __init__(self): + self.objname: str = "" + self.maxobjindex: int = 0 + self.ntris: int = 0 # count of tridata (including preceding 'count' short) + self.totaltris: int = 0 + self.tridata: List[int] = [] # unsigned short + self.verticeref_start: int = 0 + self.verticeref_count: int = 0 + self.texname: str = "" + self.bumptexture: str = "" + self.falloff: float = 0 + self.blend: float = 0 + self.flags: int = 0 + self.emissive: int = 0 + self.ambient: int = 0 + self.diffuse: int = 0 + self.specular: int = 0 + self.power: int = 0 + + +class UV: + def __init__(self, u: float = 0, v: float = 0): + self.u: float = u + self.v: float = v + + +class GbsData: + def __init__(self): + self.name = "" + self.optionsflags: int = 0 + self.nndefs: int = 0 + self.num_normals: int = 0 + self.normals: List[int] = [] # word + self.indexed_normals: List[int] = [] # unsigned short + self.num_vertices: int = 0 + self.vertices: List[Vec3] = [] + self.nsobjs: int = 0 + self.nmobjs: int = 0 + self.indexed_vertices: List[int] = [] # unsigned short + self.vertrgb: List[VecRGB] = [] + self.nverts: int = 0 + self.vertuv: List[UV] = [] + self.MaxObjs: List[MaxObj] = [] + self.SubObjs: List[SubObject] = [] + + def read(self, file): + debug = True + self.name = os.path.basename(file) + with open(file, "rb") as fp: + version_header = read_int(fp) + if version_header != GBS_VERSION: + raise Exception("File does not appear to be a GBS file.") + + self.optionsflags = read_int(fp) + self.num_vertices = read_int(fp) + resize(self.vertices, Vec3, self.num_vertices) + for i in range(self.num_vertices): + self.vertices[i].x = read_float(fp) + self.vertices[i].y = read_float(fp) + self.vertices[i].z = read_float(fp) + + if self.optionsflags & GBSFlagNormals: + self.nndefs = read_int(fp) + self.num_normals = read_int(fp) + resize(self.normals, int, self.num_normals) + for i in range(self.num_normals): + self.normals[i] = read_short(fp) + + self.nverts = read_int(fp) + if debug: print( + "NVERTS=%s, NUMVERTICES=%s, options=%s" % (self.nverts, self.num_vertices, self.optionsflags)) + resize(self.indexed_vertices, int, self.nverts) + for i in range(self.nverts): + self.indexed_vertices[i] = read_short(fp) + if debug: print(self.indexed_vertices) + + if self.optionsflags & GBSFlagNormals: + resize(self.indexed_normals, int, self.nverts) + for i in range(self.nverts): + self.indexed_normals[i] = read_short(fp) + + if self.optionsflags & GBSFlagUVs: + resize(self.vertuv, UV, self.nverts) + if debug: print("Read %s UV" % self.nverts) + for i in range(self.nverts): + self.vertuv[i].u = read_float(fp) + self.vertuv[i].v = read_float(fp) * -1 + + if self.optionsflags & GBSFlagRGBs: + resize(self.vertrgb, VecRGB, self.nverts) + if debug: print("Read %s RGB" % self.nverts) + for i in range(self.nverts): + self.vertrgb[i].r = read_byte(fp) + self.vertrgb[i].g = read_byte(fp) + self.vertrgb[i].b = read_byte(fp) + + # Get number of objects + self.nmobjs = read_int(fp) + if debug: print("NMOBJS = %s" % self.nmobjs) + resize(self.MaxObjs, MaxObj, self.nmobjs) + for maxobj in self.MaxObjs: + print("Processing new maxobj") + fileMaxObj = FileMaxObj() + fileMaxObj.vstart = read_int(fp) + fileMaxObj.vcount = read_int(fp) + fileMaxObj.nstart = read_int(fp) + fileMaxObj.ncount = read_int(fp) + fileMaxObj.noffset = read_int(fp) + print("vstart=%s vcount=%s nstart=%s ncount=%s noffset=%s" % ( + fileMaxObj.vstart, fileMaxObj.vcount, fileMaxObj.nstart, fileMaxObj.ncount, fileMaxObj.noffset)) + maxobj.vstart = fileMaxObj.vstart + maxobj.vcount = fileMaxObj.vcount + maxobj.nstart = fileMaxObj.nstart + maxobj.ncount = fileMaxObj.ncount + maxobj.noffset = fileMaxObj.noffset + maxobj.fstart = 0 + maxobj.fcount = 0 + maxobj.sostart = 0 + maxobj.socount = 0 + + # verify max obj + nmcount = 0 + for maxobj in self.MaxObjs: + nmcount += maxobj.vcount + assert nmcount == self.num_vertices + + self.nsobjs = read_int(fp) + print("Subobject count: %s" % self.nsobjs) + resize(self.SubObjs, SubObject, self.nsobjs) + num_faces = 0 + len_tridata = 0 + for ns in range(self.nsobjs): + if debug: print("Processing subobj %s" % ns) + object = self.SubObjs[ns] + object.objname = read_string(fp, 32) + object.maxobjindex = read_int(fp) + object.totaltris = read_int(fp) + if debug: print("read %s totaltris" % object.totaltris) + num_faces += object.totaltris + object.ntris = read_int(fp) + + assert ((object.ntris - 1) / 3 == object.totaltris) + + resize(object.tridata, int, object.ntris + 1) + if debug: print("read gbs: totaltris: %s, tridata is %s long, ntris: %s" % ( + object.totaltris, len(object.tridata), object.ntris)) + len_tridata += object.ntris + 1 + for i in range(object.ntris): + object.tridata[i] = read_short(fp) + + # if debug: print("tridata: %s" % object.tridata) + + object.verticeref_start = read_int(fp) + object.verticeref_count = read_int(fp) + if self.optionsflags & GBSFlagUVs: + object.texname = read_string(fp, 32) + object.bumptexture = read_string(fp, 32) + + object.falloff = read_float(fp) + if self.optionsflags & GBSFlagRGBs: + object.blend = read_float(fp) + + object.flags = read_int(fp) + object.emissive = read_int(fp) + object.ambient = read_int(fp) + object.diffuse = read_int(fp) + object.specular = read_int(fp) + object.power = read_float(fp) + if debug: print( + "%s (%s) falloff: %s, blend: %s, flags:%s, emissive: %s, ambiant: %s, diffuse: %s, specular: %s, power: %s" % ( + object.objname, object.texname, object.falloff, object.blend, object.flags, object.emissive, + object.ambient, object.diffuse, object.specular, object.power)) + + maxobj = self.MaxObjs[object.maxobjindex] + maxobj.fcount += object.totaltris + if not maxobj.socount: + maxobj.socount = ns + + maxobj.socount += 1 + if debug: print("read num_faces: %s, len_tridata: %s" % (num_faces, len_tridata)) + + @staticmethod + def evaluate_tridata(tridata, tri_idx, count): + if count == 0: + count = tridata[0] + if count == 0: + return False + tri_idx = 0 + v1 = tridata[tri_idx + 1] + v2 = tridata[tri_idx + 2] + v3 = tridata[tri_idx + 3] + tri_idx += 3 + + count -= 1 + if count < 0: + count = 0xffff # max unsigned short + if count == 0: + tridata = tridata[tridata[0] * 3 + 1:] + return tridata, tri_idx, count, v1, v2, v3 + + def generate_normals(self): + normals: List[Vec3] = [] + resize(normals, Vec3, self.num_vertices) + for subobj_i in range(len(self.SubObjs)): + subobj = self.SubObjs[subobj_i] + tridata = subobj.tridata + values = self.evaluate_tridata(tridata, -1, 0) + while values: + tridata, tri_idx, count, v1, v2, v3 = values + try: + p = cross(self.vertices[self.indexed_vertices[v2]] - self.vertices[self.indexed_vertices[v1]], + self.vertices[self.indexed_vertices[v3]] - self.vertices[self.indexed_vertices[v1]]) + except Exception as e: + raise e + normals[self.indexed_vertices[v1]] += p + normals[self.indexed_vertices[v2]] += p + normals[self.indexed_vertices[v3]] += p + values = self.evaluate_tridata(tridata, tri_idx, count) + + for i in range(len(normals)): + normals[i] = normals[i].normalize() + return normals + + +def find_and_extract_texture(texture_filename, objname, giantspath): + game_folder = giantspath + game_folder = game_folder + "/Bin" + + for gzp_filename in os.listdir(game_folder): + if not gzp_filename.endswith(".gzp"): + continue + + gzp_fp = open(game_folder + "/" + gzp_filename, "rb") + + checksum = read_int(gzp_fp) + if checksum != 0x6608F101: + raise Exception("Invalid GZP checksum") + + meta_info_offset = read_int(gzp_fp) + + gzp_fp.seek(meta_info_offset) + unk = read_int(gzp_fp) + entries_count = read_int(gzp_fp) + + if entries_count == 0: + continue + + for index in range(entries_count): + compressed_size = read_int(gzp_fp) + original_size = read_int(gzp_fp) + file_time = read_int(gzp_fp) + content_offset = read_int(gzp_fp) + 16 + compression = read_byte(gzp_fp) # compression: 1 if compressed else 0 + name_length = read_byte(gzp_fp) + name = read_bytes(gzp_fp, name_length).decode("utf8").strip('\x00') + + texture_name = name.lower() + if not texture_name.endswith(".tga"): + continue + + texture_name = texture_name[0:-4] + if texture_name != texture_filename.lower(): + continue + + curr_pos = gzp_fp.tell() + gzp_fp.seek(content_offset) + buffer = gzp_fp.read(compressed_size) + gzp_fp.seek(curr_pos) + + if compression == 1: + buffer = decompress(buffer, original_size) + + fd, filename = tempfile.mkstemp(suffix=".tga", prefix=objname, dir=None, text=False) + with open(filename, "wb") as entry_fp: + entry_fp.write(buffer) + + return filename + return None + + +def material_from_file(filename, matname): + image = bpy.data.images.load(filename, check_existing=True) + texture = bpy.data.textures.new(matname, type='IMAGE') + texture.image = image + + mat = bpy.data.materials.new(matname) + mat.preview_render_type = 'FLAT' + mat.use_nodes = True + bsdf = mat.node_tree.nodes["Principled BSDF"] + texImage = mat.node_tree.nodes.new('ShaderNodeTexImage') + texImage.image = image + mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) + return mat