The rules have been updated, read them now: Rules!

Offroad Legends GEO/H3D model file, convertor?

Post questions about game models here, or help out others!
User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 1:51 pm

shakotay2 wrote:
Tue Mar 16, 2021 8:47 am
ReVolt wrote:
Tue Mar 16, 2021 1:08 am
btw: reh already gave you an uv address in your other thread
Cool. How do I make it so the UVs are baked into the model? The uvs export as a single mesh, which I don't want.

User avatar
shakotay2
MEGAVETERAN
MEGAVETERAN
Posts: 3326
Joined: Fri Apr 20, 2012 9:24 am
Location: Nexus, searching for Jim Kirk
Has thanked: 908 times
Been thanked: 1819 times

Re: Offroad Legends GEO/H3D model file, convertor?

Post by shakotay2 » Tue Mar 16, 2021 1:57 pm

File/SaveAs mesh
Bigchillghost, Reverse Engineering a Game Model: viewtopic.php?f=29&t=17889
extracting simple models: viewtopic.php?f=29&t=10894
Make_H2O-ForzaHor3-jm9.zip
"You quoted the whole thing, what a mess."

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 3:57 pm

shakotay2 wrote:
Tue Mar 16, 2021 1:57 pm
File/SaveAs mesh
Thanks! Also managed to get the UVs! The way to tell where the UVs are is a pattern, there's a 3(letter) byte at the end of each 4-byte sequence.

mariokart64n
ultra-veteran
ultra-veteran
Posts: 567
Joined: Sun Jun 05, 2005 12:00 pm
Location: Ontario, Canada
Has thanked: 34 times
Been thanked: 197 times

Re: Offroad Legends GEO/H3D model file, convertor?

Post by mariokart64n » Tue Mar 16, 2021 4:16 pm

Image
Below is a python script I wrote for blender that was tested in the latest version (currently 2.92.0)

usage: start blender, goto the script tab then press NEW in the text editor and paste the below script into the editor. To execute the script press ALT+P or press the [>] play button, a file selection box will appear which will allow you to select GEO files to import. Once the script has executed once it can then be accessed through the File > Import Dialog until blender is restarted

Code: Select all

""" ======================================================================

    PythonScript:   [Mobile] Offroad Legends
    Author:         mariokart64n
    Date:           March 16, 2021
    Version:        0.1

    ======================================================================

    ChangeLog:

    2021-03-16
        Script Wrote


    ====================================================================== """

import bpy  # Needed to interface with blender
import struct  # Needed for Binary Reader
import math
from pathlib import Path  # Needed for os stuff
from xml.dom import minidom

useOpenDialog = True


def deleteScene(include=[]):
    if len(include) > 0:
        # Exit and Interactions
        if bpy.context.view_layer.objects.active != None:
            bpy.ops.object.mode_set(mode='OBJECT')

        # Select All
        bpy.ops.object.select_all(action='SELECT')

        # Loop Through Each Selection
        for o in bpy.context.view_layer.objects.selected:
            for t in include:
                if o.type == t:
                    bpy.data.objects.remove(o, do_unlink=True)
                    break

        # De-Select All
        bpy.ops.object.select_all(action='DESELECT')
    return None


def messageBox(message="", title="Message Box", icon='INFO'):
    def draw(self, context): self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None

def doesFileExist(filename):
    file = Path(filename)
    if file.is_file():
        return True
    elif file.is_dir():
        return True
    else:
        return False


def clearListener(len=64):
    for i in range(0, len): print('')



def getFilenamePath(file):  # returns: "g:\subdir1\subdir2\"
    return (str(Path(file).resolve().parent) + "\\")


def getFilenameFile(file):  # returns: "myImage"
    return Path(file).stem


def Bitmaptexture(mat, filename="", name="ShaderNodeTexImage"):
    imageTex = mat.node_tree.nodes.new('ShaderNodeTexImage')
    imageTex.label = name
    try:
        imageTex.image = bpy.data.images.load(
            filepath=filename,
            check_existing=False
        )
        imageTex.image.name = filenameFromPath(filename)
        imageTex.image.colorspace_settings.name = 'sRGB'
    except:
        imageTex.image = bpy.data.images.new(
            name=filename,
            width=8,
            height=8,
            alpha=False,
            float_buffer=False
        )
    return imageTex


class StandardMaterial:
    data = None
    bsdf = None

    def __init__(self, name="Material"):
        # make material
        self.data = bpy.data.materials.new(name=name)
        self.data.use_nodes = True
        self.data.use_backface_culling = True
        self.bsdf = self.data.node_tree.nodes["Principled BSDF"]
        self.bsdf.label = "Standard"
        return None

    def diffuse(self, colour=(0.0, 0.0, 0.0, 0.0), name="Diffuse"):
        rgbaColor = self.data.node_tree.nodes.new('ShaderNodeRGB')
        rgbaColor.label = name
        rgbaColor.outputs[0].default_value = (colour[0], colour[1], colour[2], colour[3])
        if self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], rgbaColor.outputs['Color'])
        return None

    def diffuseMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], imageTex.outputs['Color'])
        return None

    def opacityMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.blend_method = 'BLEND'
            self.data.shadow_method = 'HASHED'
            self.data.show_transparent_back = False
            self.data.node_tree.links.new(self.bsdf.inputs['Alpha'], imageTex.outputs['Alpha'])
        return None

    def normalMap(self, imageNode=None):
        if imageTex != None and self.bsdf != None:
            imageTex.image.colorspace_settings.name = 'Linear'
            normMap = self.data.node_tree.nodes.new('ShaderNodeNormalMap')
            normMap.label = 'ShaderNodeNormalMap'
            self.data.node_tree.links.new(normMap.inputs['Color'], imageTex.outputs['Color'])
            self.data.node_tree.links.new(self.bsdf.inputs['Normal'], normMap.outputs['Normal'])
        return None

    def specularMap(self, imageNode=None, invert=True):
        if imageTex != None and self.bsdf != None:
            if invert:
                invertRGB = self.data.node_tree.nodes.new('ShaderNodeInvert')
                invertRGB.label = 'ShaderNodeInvert'
                self.data.node_tree.links.new(invertRGB.inputs['Color'], imageTex.outputs['Color'])
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], invertRGB.outputs['Color'])
            else:
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], imageTex.outputs['Color'])
        return None


class fopen:
    little_endian = True
    file = ""
    mode = 'rb'
    data = bytearray()
    size = 0
    pos = 0
    isGood = False

    def __init__(self, filename=None, mode='rb', isLittleEndian=True):
        if mode == 'rb':
            if filename != None and Path(filename).is_file():
                self.data = open(filename, mode).read()
                self.size = len(self.data)
                self.pos = 0
                self.mode = mode
                self.file = filename
                self.little_endian = isLittleEndian
                self.isGood = True
        else:
            self.file = filename
            self.mode = mode
            self.data = bytearray()
            self.pos = 0
            self.size = 0
            self.little_endian = isLittleEndian
            self.isGood = False

        return None

    # def __del__(self):
    #    self.flush()

    def resize(self, dataSize=0):
        if dataSize > 0:
            self.data = bytearray(dataSize)
        else:
            self.data = bytearray()
        self.pos = 0
        self.size = dataSize
        self.isGood = False
        return None

    def flush(self):
        print("flush")
        print("file:\t%s" % self.file)
        print("isGood:\t%s" % self.isGood)
        print("size:\t%s" % len(self.data))
        if self.file != "" and not self.isGood and len(self.data) > 0:
            self.isGood = True

            s = open(self.file, 'w+b')
            s.write(self.data)
            s.close()

    def read_and_unpack(self, unpack, size):
        '''
          Charactor, Byte-order
          @,         native, native
          =,         native, standard
          <,         little endian
          >,         big endian
          !,         network

          Format, C-type,         Python-type, Size[byte]
          c,      char,           byte,        1
          b,      signed char,    integer,     1
          B,      unsigned char,  integer,     1
          h,      short,          integer,     2
          H,      unsigned short, integer,     2
          i,      int,            integer,     4
          I,      unsigned int,   integer,     4
          f,      float,          float,       4
          d,      double,         float,       8
        '''
        value = 0
        if self.size > 0 and self.pos + size < self.size:
            value = struct.unpack_from(unpack, self.data, self.pos)[0]
            self.pos += size
        return value

    def pack_and_write(self, pack, size, value):
        if self.pos + size > self.size:
            self.data.extend(b'\x00' * ((self.pos + size) - self.size))
            self.size = self.pos + size
        try:
            struct.pack_into(pack, self.data, self.pos, value)
        except:
            print('Pos:\t%i / %i (buf:%i) [val:%i:%i:%s]' % (self.pos, self.size, len(self.data), value, size, pack))
            pass
        self.pos += size
        return None

    def set_pointer(self, offset):
        self.pos = offset
        return None


def fclose(bitStream):
    bitStream.flush()
    bitStream.isGood = False


def readShort(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'h' if isSigned == 0 else 'H'
    return (bitStream.read_and_unpack(fmt, 2))


def readLong(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'i' if isSigned == 0 else 'I'
    return (bitStream.read_and_unpack(fmt, 4))


def readFloat(bitStream):
    fmt = '>f' if not bitStream.little_endian else '<f'
    return (bitStream.read_and_unpack(fmt, 4))


def mesh_validate (vertices=[], faces=[]):
    #
    # Returns True if mesh is BAD
    #
    # check face index bound
    face_min = 0
    face_max = len(vertices) - 1
    
    for face in faces:
        for side in face:
            if side < face_min or side > face_max:
                print("Face Index Out of Range:\t[%i / %i]" % (side, face_max))
                return True
    return False


def mesh(
    vertices=[],
    faces=[],
    materialIDs=[],
    tverts=[],
    normals=[],
    colours=[],
    materials=[],
    mscale=1.0,
    flipAxis=False,
    obj_name="Object",
    lay_name='',
    position = (0.0, 0.0, 0.0)
    ):
    #
    # This function is pretty, ugly
    # imports the mesh into blender
    #
    # Clear Any Object Selections
    # for o in bpy.context.selected_objects: o.select = False
    bpy.context.view_layer.objects.active = None
    
    # Get Collection (Layers)
    if lay_name != '':
        # make collection
        layer = bpy.data.collections.new(lay_name)
        bpy.context.scene.collection.children.link(layer)
    else:
        layer = bpy.data.collections[bpy.context.view_layer.active_layer_collection.name]

    # make mesh
    msh = bpy.data.meshes.new('Mesh')

    # msh.name = msh.name.replace(".", "_")

    # Apply vertex scaling
    # mscale *= bpy.context.scene.unit_settings.scale_length
    if len(vertices) > 0:
        vertArray = [[float] * 3] * len(vertices)
        if flipAxis:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    -vertices[v][2] * mscale,
                    vertices[v][1] * mscale
                )
        else:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    vertices[v][1] * mscale,
                    vertices[v][2] * mscale
                )

    # assign data from arrays
    msh.from_pydata(vertArray, [], faces)

    # set surface to smooth
    msh.polygons.foreach_set("use_smooth", [True] * len(msh.polygons))

    # Set Normals
    if len(faces) > 0:
        if len(normals) > 0:
            msh.use_auto_smooth = True
            if len(normals) == (len(faces) * 3):
                msh.normals_split_custom_set(normals)
            else:
                normArray = [[float] * 3] * (len(faces) * 3)
                if flipAxis:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 -normals[faces[i][v]][2],
                                 normals[faces[i][v]][1]]
                            )
                else:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 normals[faces[i][v]][1],
                                 normals[faces[i][v]][2]]
                            )
                msh.normals_split_custom_set(normArray)

        # create texture corrdinates
        #print("tverts ", len(tverts))
        # this is just a hack, i just add all the UVs into the same space <<<
        if len(tverts) > 0:
            uvw = msh.uv_layers.new()
            # if len(tverts) == (len(faces) * 3):
            #    for v in range(0, len(faces) * 3):
            #        msh.uv_layers[uvw.name].data[v].uv = tverts[v]
            # else:
            uvwArray = [[float] * 2] * len(tverts[0])
            for i in range(0, len(tverts[0])):
                uvwArray[i] = [0.0, 0.0]

            for v in range(0, len(tverts[0])):
                for i in range(0, len(tverts)):
                    uvwArray[v][0] += tverts[i][v][0]
                    uvwArray[v][1] += 1.0 - tverts[i][v][1]

            for i in range(0, len(faces)):
                for v in range(0, 3):
                    msh.uv_layers[uvw.name].data[(i * 3) + v].uv = (
                        uvwArray[faces[i][v]][0],
                        uvwArray[faces[i][v]][1]
                    )


    # Create Face Maps?
    # msh.face_maps.new()

    # Update Mesh
    msh.update()

    # Check mesh is Valid
    # Without this blender may crash!!! lulz
    # However the check will throw false positives so
    # and additional or a replacement valatiation function
    # would be required
    
    if msh.validate():
        print("Mesh Failed Validation")
        if mesh_validate(vertArray, faces):
            # Erase Mesh
            msh.user_clear()
            bpy.data.meshes.remove(msh)
            print("Mesh Deleted!")
            return None
        

    # Assign Mesh to Object
    obj = bpy.data.objects.new(obj_name, msh)
    obj.location = position
    # obj.name = obj.name.replace(".", "_")

    for i in range(0, len(materials)):

        if len(obj.material_slots) < (i + 1):
            # if there is no slot then we append to create the slot and assign
            obj.data.materials.append(materials[i])
        else:
            # we always want the material in slot[0]
            obj.material_slots[0].material = materials[i]
        # obj.active_material = obj.material_slots[i].material

    if len(materialIDs) == len(faces):
        for i in range(0, len(materialIDs)):
            obj.data.polygons[i].material_index = materialIDs[i]
    elif len(materialIDs) > 0:
        print("Error:\tMaterial Index Out of Range")

    # obj.data.materials.append(material)
    layer.objects.link(obj)

    # Generate a Material
    # img_name = "Test.jpg"  # dummy texture
    # mat_count = len(texmaps)

    # if mat_count == 0 and len(materialIDs) > 0:
    #    for i in range(0, len(materialIDs)):
    #        if (materialIDs[i] + 1) > mat_count: mat_count = materialIDs[i] + 1

    # Assign Material ID's
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    bpy.ops.object.mode_set(mode='OBJECT')
    # materialIDs

    # Redraw Entire Scene
    # bpy.context.scene.update()

    return obj


class h3d_uniform:
    name = ""
    a = 0.0
    b = 0.0
    c = 0.0
    d = 0.0


class h3d_sampler:
    name = ""
    map = ""


class h3d_material:
    name = ""
    sampler = []
    uniform = []


class h3d_geo_block:
    index = 0
    stride = 0
    data = []


class h3d_geo_file:
    fileid = 0  # 0x47443348 'H3DG'
    unk01 = 0
    unk02 = 0
    matrix = ((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))
    unk03 = 0
    vertex_count = 0
    vertices = h3d_geo_block()
    normals = h3d_geo_block()
    texcoord0 = h3d_geo_block()
    texcoord1 = h3d_geo_block()
    face_count = 0
    faces = []

    def __repr__(self):
        return 'VertexCount:\t%i\nFaceCount:\t%i\nUnknowns:\t%i\t%i\t%i' % (
        self.vertex_count, self.face_count, self.unk01, self.unk02, self.unk03)

    def read(self, f=fopen()):
        self.fileid = readLong(f)
        if self.fileid != 0x47443348:
            print("Error:\tInvalid File")
            return False

        self.unk01 = readLong(f)
        self.unk02 = readLong(f)
        self.matrix = (
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f))
        )
        self.unk03 = readLong(f)
        self.vertex_count = readLong(f)

        # Read Vertices
        self.vertices.index = readLong(f)
        self.vertices.stride = readLong(f)

        if self.vertices.stride != 0x0C:
            print("Error:\tInvalid Vertex Stride")
            return False
        comp = int(self.vertices.stride / 4)
        self.vertices.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.vertices.data[x][y] = readFloat(f)

        # Read Normals
        self.normals.index = readLong(f)
        self.normals.stride = readLong(f)
        if self.normals.stride != 0x06:
            print("Error:\tInvalid Normal Stride:\t%i" % self.normals.stride)
            return False

        comp = int(self.normals.stride / 2)
        self.normals.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.normals.data[x][y] = float(readShort(f) / 32767.0)

        # Read UV-0
        self.texcoord0.index = readLong(f)
        self.texcoord0.stride = readLong(f)
        if self.texcoord0.stride != 0x08:
            print("Error:\tInvalid UV0 Stride")
            return False

        comp = int(self.texcoord0.stride / 4)
        self.texcoord0.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord0.data[x][y] = readFloat(f)

        # Read UV-1
        self.texcoord1.index = readLong(f)
        self.texcoord1.stride = readLong(f)
        if self.texcoord1.stride != 0x08:
            print("Error:\tInvalid UV1 Stride")
            return False

        comp = int(self.texcoord1.stride / 4)
        self.texcoord1.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord1.data[x][y] = readFloat(f)

        # Faces
        self.face_count = readLong(f)
        self.faces = [[int] for x in range(self.face_count)]
        for x in range(0, self.face_count):
            self.faces[x] = readLong(f)

        return True


def read_triangle_strip(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    i = 0
    face = [1, 1, 1]
    while i < faceCount:
        faceCW = True
        face[0] = faces[i + facePosition]
        face[1] = faces[i + facePosition + 1]
        i += 2
        while i < faceCount:
            face[2] = faces[i + facePosition]
            if face[2] == 0xFFFF or face[2] == -1: break
            if face[0] != face[2] and face[1] != face[2] and face[2] != face[0]:
                if faceCW:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[1] + faceOffset,
                        face[2] + faceOffset
                    ])
                else:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[2] + faceOffset,
                        face[1] + faceOffset
                    ])
            faceCW = not faceCW
            face = [face[1], face[2], face[0]]
            i += 1
    return None


def read_triangle_list(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    for i in range(0, int(faceCount / 3)):
        faceArray.append([
            faces[(i * 3) + facePosition] + faceOffset,
            faces[(i * 3) + facePosition + 1] + faceOffset,
            faces[(i * 3) + facePosition + 2] + faceOffset
        ])
    return None


def read(file="", mscale=1.0):
    # Check File is present
    if not doesFileExist(file):
        print("Error:\tFailed To Find Geo File")
        return False

    # Strip Paths From fullpath to find sister file
    fpath = getFilenamePath(file)
    fname = getFilenameFile(file)
    h3d_file = fpath + fname + ".h3d"

    # Check if sister file is found
    if not doesFileExist(h3d_file):
        print("Error:\tFailed To Find H3D File")
        return False

    # open GEO file
    f = fopen(file, 'rb')

    # Create GEO Object to store data from Geo file
    geo = h3d_geo_file()

    # Attempt to read GEO file into Geo Class
    read_good = False
    try:
        read_good = geo.read(f)
    except:
        print("Error:\t Failed to Read File")
        return False

    # Check if Geo File Was Read
    if not read_good:
        print("Error:\t Failed to Read File")
        return False

    # Print Geo Class Info
    print(repr(geo))

    # Close Geo File
    fclose(f)

    # Read Sister File, import data
    try:
        h3d = minidom.parse(h3d_file)

        h3d_mat = []
        h3d_mat_index = 0
        h3d_sp = h3d_sampler()
        h3d_un = h3d_uniform()

        for materials in h3d.getElementsByTagName('Materials'):
            for material in materials.getElementsByTagName('Material'):
                h3d_mat.append(h3d_material())

                h3d_mat[h3d_mat_index].name = material.attributes['name'].value

                for sampler in material.getElementsByTagName('Sampler'):
                    h3d_sp = h3d_sampler()
                    try:
                        h3d_sp.name = sampler.attributes['name'].value
                        h3d_sp.map = sampler.attributes['map'].value
                        h3d_mat[h3d_mat_index].sampler.append(h3d_sp)
                    except:
                        pass

                for uniform in material.getElementsByTagName('Uniform'):
                    h3d_un = h3d_uniform()
                    try:
                        h3d_un.name = sampler.attributes['name'].value
                        h3d_un.a = float(sampler.attributes['a'].value)
                        h3d_un.b = float(sampler.attributes['b'].value)
                        h3d_un.c = float(sampler.attributes['c'].value)
                        h3d_un.d = float(sampler.attributes['d'].value)
                        h3d_mat[h3d_mat_index].uniform.append(h3d_un)
                    except:
                        pass
                h3d_mat_index += 1

    except:
        print("Error:\tFailed to Parse XML file")
        return False

    mats = []
    for m in h3d_mat:
        mat = StandardMaterial(m.name)
        for t in m.sampler:
            if t.name == 'albedoMap':
                if '/' in t.map:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map.split('/')[-1:][0]))
                else:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map))
        mats.append(mat.data)

    msh = None
    msh_name = ""
    msh_matn = ""
    msh_tx = 0.0
    msh_ty = 0.0
    msh_tz = 0.0
    msh_batchStart = 0
    msh_batchCount = 0
    msh_vertRStart = 0
    msh_vertREnd = 0
    msh_vertRCount = 0
    mat_index = 0
    mat_name = ""
    vertArray = []
    normArray = []
    uvw0Array = []
    uvw1Array = []
    faceArray = []
    matidArray = []
    for gmesh in h3d.getElementsByTagName('Mesh'):
        try:
            msh_name = gmesh.attributes['name'].value
        except:
            pass
        try:
            msh_matn = gmesh.attributes['material'].value
        except:
            pass
        try:
            msh_tx = float(gmesh.attributes['tx'].value)
        except:
            pass
        try:
            msh_ty = float(gmesh.attributes['ty'].value)
        except:
            pass
        try:
            msh_tz = float(gmesh.attributes['tz'].value)
        except:
            pass
        try:
            msh_batchStart = int(gmesh.attributes['batchStart'].value)
        except:
            pass
        try:
            msh_batchCount = int(gmesh.attributes['batchCount'].value)
        except:
            pass
        try:
            msh_vertRStart = int(gmesh.attributes['vertRStart'].value)
        except:
            pass
        try:
            msh_vertREnd = int(gmesh.attributes['vertREnd'].value)
        except:
            pass

        mat_index = 0
        if '#' in msh_matn:
            mat_name = msh_matn.split('#')[-1:][0]
            for i in range(0, len(h3d_mat)):
                if h3d_mat[i].name == mat_name:
                    mat_index = i
                    break

        matidArray = [mat_index for i in range(int(msh_batchCount / 3))]

        msh_vertRCount = msh_vertREnd - msh_vertRStart + 1
        vertArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        normArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw0Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw1Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])

        for i in range(0, msh_vertRCount):
            vertArray[i] = geo.vertices.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            normArray[i] = geo.normals.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw0Array[i] = geo.texcoord0.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw1Array[i] = geo.texcoord1.data[msh_vertRStart + i]

        faceArray = []
        read_triangle_list(geo.faces, faceArray, msh_batchCount, msh_batchStart, -msh_vertRStart)

        msh = mesh(
            vertices=vertArray,
            tverts=[uvw0Array, uvw1Array],
            normals=normArray,
            faces=faceArray,
            obj_name=msh_name,
            flipAxis=True,
            mscale=mscale,
            materials=mats,
            materialIDs=matidArray,
            position=(msh_tx, -msh_tz, msh_ty)
        )

    return True


# Callback when file(s) are selected
def offroad_legends_imp_callback(fpath="", files=[], clearScene=True, mscale = 1.0):
    if len(files) > 0 and clearScene: deleteScene(['MESH', 'ARMATURE'])
    for file in files:
        read(fpath + file.name, mscale)
    if len(files) > 0:
        messageBox("Done!")
        return True
    else:
        return False


# Wrapper that Invokes FileSelector to open files from blender
def offroad_legends_imp(reload=False):
    # Un-Register Operator
    if reload and hasattr(bpy.types, "IMPORTHELPER_OT_offroad_legends_imp"):  # print(bpy.ops.importhelper.offroad_legends_imp.idname())

        try:
            bpy.types.TOPBAR_MT_file_import.remove(
                bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp').menu_func_import)
        except:
            print("Failed to Unregister2")

        try:
            bpy.utils.unregister_class(bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp'))
        except:
            print("Failed to Unregister1")

    # Define Operator
    class ImportHelper_offroad_legends_imp(bpy.types.Operator):

        # Operator Path
        bl_idname = "importhelper.offroad_legends_imp"
        bl_label = "Select File"

        # Operator Properties
        # filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
        filter_glob: bpy.props.StringProperty(default='*.geo', options={'HIDDEN'}, subtype='FILE_PATH')

        # Variables
        filepath: bpy.props.StringProperty(subtype="FILE_PATH")  # full path of selected item (path+filename)
        filename: bpy.props.StringProperty(subtype="FILE_NAME")  # name of selected item
        directory: bpy.props.StringProperty(subtype="FILE_PATH")  # directory of the selected item
        files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)  # a collection containing all the selected items as filenames

        # Controls
        my_float1: bpy.props.FloatProperty(name="Scale", default=1.0, description="Changes Scale of the imported Mesh")
        my_bool1: bpy.props.BoolProperty(name="Clear Scene", default=True, description="Deletes everything in the scene prior to importing")

        # Runs when this class OPENS
        def invoke(self, context, event):

            # Retrieve Settings
            try: self.filepath = bpy.types.Scene.offroad_legends_imp_filepath
            except: bpy.types.Scene.offroad_legends_imp_filepath = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.directory = bpy.types.Scene.offroad_legends_imp_directory
            except: bpy.types.Scene.offroad_legends_imp_directory = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.my_float1 = bpy.types.Scene.offroad_legends_imp_my_float1
            except: bpy.types.Scene.offroad_legends_imp_my_float1 = bpy.props.FloatProperty(default=1.0)

            try: self.my_bool1 = bpy.types.Scene.offroad_legends_imp_my_bool1
            except: bpy.types.Scene.offroad_legends_imp_my_bool1 = bpy.props.BoolProperty(default=False)


            # Open File Browser
            # Set Properties of the File Browser
            context.window_manager.fileselect_add(self)
            context.area.tag_redraw()

            return {'RUNNING_MODAL'}

        # Runs when this Window is CANCELLED
        def cancel(self, context): print("run bitch")

        # Runs when the class EXITS
        def execute(self, context):

            # Save Settings
            bpy.types.Scene.offroad_legends_imp_filepath = self.filepath
            bpy.types.Scene.offroad_legends_imp_directory = self.directory
            bpy.types.Scene.offroad_legends_imp_my_float1 = self.my_float1
            bpy.types.Scene.offroad_legends_imp_my_bool1 = self.my_bool1

            # Run Callback
            offroad_legends_imp_callback(self.directory + "\\", self.files, self.my_bool1, self.my_float1)

            return {"FINISHED"}

            # Window Settings

        def draw(self, context):

            self.layout.row().label(text="Import Settings")

            self.layout.separator()
            self.layout.row().prop(self, "my_bool1")
            self.layout.row().prop(self, "my_float1")

            self.layout.separator()

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="  Author:", icon='QUESTION')
            col.alignment = 'LEFT'
            col.label(text="mariokart64n")

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="Release:", icon='GRIP')
            col.alignment = 'LEFT'
            col.label(text="March 16, 2021")

        def menu_func_import(self, context):
            self.layout.operator("importhelper.offroad_legends_imp", text="Off Road Legends (*.geo)")

    # Register Operator
    bpy.utils.register_class(ImportHelper_offroad_legends_imp)
    bpy.types.TOPBAR_MT_file_import.append(ImportHelper_offroad_legends_imp.menu_func_import)

    # Call ImportHelper
    bpy.ops.importhelper.offroad_legends_imp('INVOKE_DEFAULT')


if not useOpenDialog:
    deleteScene(['MESH', 'ARMATURE'])  # Clear Scene
    clearListener()  # clears out console
    read(
        "C:\\Users\\Corey\\Downloads\\Cars The Video Game\\carry_chassis.geo"
        )
    messageBox("Done!")
else:
    offroad_legends_imp(True)

Maxscript and other finished work I've done can be found on my DeviantArt account

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 4:48 pm

mariokart64n wrote:
Tue Mar 16, 2021 4:16 pm
Image
Below is a python script I wrote for blender that was tested in the latest version (currently 2.92.0)

usage: start blender, goto the script tab then press NEW in the text editor and paste the below script into the editor. To execute the script press ALT+P or press the [>] play button, a file selection box will appear which will allow you to select GEO files to import. Once the script has executed once it can then be accessed through the File > Import Dialog until blender is restarted

Code: Select all

""" ======================================================================

    PythonScript:   [Mobile] Offroad Legends
    Author:         mariokart64n
    Date:           March 16, 2021
    Version:        0.1

    ======================================================================

    ChangeLog:

    2021-03-16
        Script Wrote


    ====================================================================== """

import bpy  # Needed to interface with blender
import struct  # Needed for Binary Reader
import math
from pathlib import Path  # Needed for os stuff
from xml.dom import minidom

useOpenDialog = True


def deleteScene(include=[]):
    if len(include) > 0:
        # Exit and Interactions
        if bpy.context.view_layer.objects.active != None:
            bpy.ops.object.mode_set(mode='OBJECT')

        # Select All
        bpy.ops.object.select_all(action='SELECT')

        # Loop Through Each Selection
        for o in bpy.context.view_layer.objects.selected:
            for t in include:
                if o.type == t:
                    bpy.data.objects.remove(o, do_unlink=True)
                    break

        # De-Select All
        bpy.ops.object.select_all(action='DESELECT')
    return None


def messageBox(message="", title="Message Box", icon='INFO'):
    def draw(self, context): self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None

def doesFileExist(filename):
    file = Path(filename)
    if file.is_file():
        return True
    elif file.is_dir():
        return True
    else:
        return False


def clearListener(len=64):
    for i in range(0, len): print('')



def getFilenamePath(file):  # returns: "g:\subdir1\subdir2\"
    return (str(Path(file).resolve().parent) + "\\")


def getFilenameFile(file):  # returns: "myImage"
    return Path(file).stem


def Bitmaptexture(mat, filename="", name="ShaderNodeTexImage"):
    imageTex = mat.node_tree.nodes.new('ShaderNodeTexImage')
    imageTex.label = name
    try:
        imageTex.image = bpy.data.images.load(
            filepath=filename,
            check_existing=False
        )
        imageTex.image.name = filenameFromPath(filename)
        imageTex.image.colorspace_settings.name = 'sRGB'
    except:
        imageTex.image = bpy.data.images.new(
            name=filename,
            width=8,
            height=8,
            alpha=False,
            float_buffer=False
        )
    return imageTex


class StandardMaterial:
    data = None
    bsdf = None

    def __init__(self, name="Material"):
        # make material
        self.data = bpy.data.materials.new(name=name)
        self.data.use_nodes = True
        self.data.use_backface_culling = True
        self.bsdf = self.data.node_tree.nodes["Principled BSDF"]
        self.bsdf.label = "Standard"
        return None

    def diffuse(self, colour=(0.0, 0.0, 0.0, 0.0), name="Diffuse"):
        rgbaColor = self.data.node_tree.nodes.new('ShaderNodeRGB')
        rgbaColor.label = name
        rgbaColor.outputs[0].default_value = (colour[0], colour[1], colour[2], colour[3])
        if self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], rgbaColor.outputs['Color'])
        return None

    def diffuseMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], imageTex.outputs['Color'])
        return None

    def opacityMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.blend_method = 'BLEND'
            self.data.shadow_method = 'HASHED'
            self.data.show_transparent_back = False
            self.data.node_tree.links.new(self.bsdf.inputs['Alpha'], imageTex.outputs['Alpha'])
        return None

    def normalMap(self, imageNode=None):
        if imageTex != None and self.bsdf != None:
            imageTex.image.colorspace_settings.name = 'Linear'
            normMap = self.data.node_tree.nodes.new('ShaderNodeNormalMap')
            normMap.label = 'ShaderNodeNormalMap'
            self.data.node_tree.links.new(normMap.inputs['Color'], imageTex.outputs['Color'])
            self.data.node_tree.links.new(self.bsdf.inputs['Normal'], normMap.outputs['Normal'])
        return None

    def specularMap(self, imageNode=None, invert=True):
        if imageTex != None and self.bsdf != None:
            if invert:
                invertRGB = self.data.node_tree.nodes.new('ShaderNodeInvert')
                invertRGB.label = 'ShaderNodeInvert'
                self.data.node_tree.links.new(invertRGB.inputs['Color'], imageTex.outputs['Color'])
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], invertRGB.outputs['Color'])
            else:
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], imageTex.outputs['Color'])
        return None


class fopen:
    little_endian = True
    file = ""
    mode = 'rb'
    data = bytearray()
    size = 0
    pos = 0
    isGood = False

    def __init__(self, filename=None, mode='rb', isLittleEndian=True):
        if mode == 'rb':
            if filename != None and Path(filename).is_file():
                self.data = open(filename, mode).read()
                self.size = len(self.data)
                self.pos = 0
                self.mode = mode
                self.file = filename
                self.little_endian = isLittleEndian
                self.isGood = True
        else:
            self.file = filename
            self.mode = mode
            self.data = bytearray()
            self.pos = 0
            self.size = 0
            self.little_endian = isLittleEndian
            self.isGood = False

        return None

    # def __del__(self):
    #    self.flush()

    def resize(self, dataSize=0):
        if dataSize > 0:
            self.data = bytearray(dataSize)
        else:
            self.data = bytearray()
        self.pos = 0
        self.size = dataSize
        self.isGood = False
        return None

    def flush(self):
        print("flush")
        print("file:\t%s" % self.file)
        print("isGood:\t%s" % self.isGood)
        print("size:\t%s" % len(self.data))
        if self.file != "" and not self.isGood and len(self.data) > 0:
            self.isGood = True

            s = open(self.file, 'w+b')
            s.write(self.data)
            s.close()

    def read_and_unpack(self, unpack, size):
        '''
          Charactor, Byte-order
          @,         native, native
          =,         native, standard
          <,         little endian
          >,         big endian
          !,         network

          Format, C-type,         Python-type, Size[byte]
          c,      char,           byte,        1
          b,      signed char,    integer,     1
          B,      unsigned char,  integer,     1
          h,      short,          integer,     2
          H,      unsigned short, integer,     2
          i,      int,            integer,     4
          I,      unsigned int,   integer,     4
          f,      float,          float,       4
          d,      double,         float,       8
        '''
        value = 0
        if self.size > 0 and self.pos + size < self.size:
            value = struct.unpack_from(unpack, self.data, self.pos)[0]
            self.pos += size
        return value

    def pack_and_write(self, pack, size, value):
        if self.pos + size > self.size:
            self.data.extend(b'\x00' * ((self.pos + size) - self.size))
            self.size = self.pos + size
        try:
            struct.pack_into(pack, self.data, self.pos, value)
        except:
            print('Pos:\t%i / %i (buf:%i) [val:%i:%i:%s]' % (self.pos, self.size, len(self.data), value, size, pack))
            pass
        self.pos += size
        return None

    def set_pointer(self, offset):
        self.pos = offset
        return None


def fclose(bitStream):
    bitStream.flush()
    bitStream.isGood = False


def readShort(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'h' if isSigned == 0 else 'H'
    return (bitStream.read_and_unpack(fmt, 2))


def readLong(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'i' if isSigned == 0 else 'I'
    return (bitStream.read_and_unpack(fmt, 4))


def readFloat(bitStream):
    fmt = '>f' if not bitStream.little_endian else '<f'
    return (bitStream.read_and_unpack(fmt, 4))


def mesh_validate (vertices=[], faces=[]):
    #
    # Returns True if mesh is BAD
    #
    # check face index bound
    face_min = 0
    face_max = len(vertices) - 1
    
    for face in faces:
        for side in face:
            if side < face_min or side > face_max:
                print("Face Index Out of Range:\t[%i / %i]" % (side, face_max))
                return True
    return False


def mesh(
    vertices=[],
    faces=[],
    materialIDs=[],
    tverts=[],
    normals=[],
    colours=[],
    materials=[],
    mscale=1.0,
    flipAxis=False,
    obj_name="Object",
    lay_name='',
    position = (0.0, 0.0, 0.0)
    ):
    #
    # This function is pretty, ugly
    # imports the mesh into blender
    #
    # Clear Any Object Selections
    # for o in bpy.context.selected_objects: o.select = False
    bpy.context.view_layer.objects.active = None
    
    # Get Collection (Layers)
    if lay_name != '':
        # make collection
        layer = bpy.data.collections.new(lay_name)
        bpy.context.scene.collection.children.link(layer)
    else:
        layer = bpy.data.collections[bpy.context.view_layer.active_layer_collection.name]

    # make mesh
    msh = bpy.data.meshes.new('Mesh')

    # msh.name = msh.name.replace(".", "_")

    # Apply vertex scaling
    # mscale *= bpy.context.scene.unit_settings.scale_length
    if len(vertices) > 0:
        vertArray = [[float] * 3] * len(vertices)
        if flipAxis:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    -vertices[v][2] * mscale,
                    vertices[v][1] * mscale
                )
        else:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    vertices[v][1] * mscale,
                    vertices[v][2] * mscale
                )

    # assign data from arrays
    msh.from_pydata(vertArray, [], faces)

    # set surface to smooth
    msh.polygons.foreach_set("use_smooth", [True] * len(msh.polygons))

    # Set Normals
    if len(faces) > 0:
        if len(normals) > 0:
            msh.use_auto_smooth = True
            if len(normals) == (len(faces) * 3):
                msh.normals_split_custom_set(normals)
            else:
                normArray = [[float] * 3] * (len(faces) * 3)
                if flipAxis:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 -normals[faces[i][v]][2],
                                 normals[faces[i][v]][1]]
                            )
                else:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 normals[faces[i][v]][1],
                                 normals[faces[i][v]][2]]
                            )
                msh.normals_split_custom_set(normArray)

        # create texture corrdinates
        #print("tverts ", len(tverts))
        # this is just a hack, i just add all the UVs into the same space <<<
        if len(tverts) > 0:
            uvw = msh.uv_layers.new()
            # if len(tverts) == (len(faces) * 3):
            #    for v in range(0, len(faces) * 3):
            #        msh.uv_layers[uvw.name].data[v].uv = tverts[v]
            # else:
            uvwArray = [[float] * 2] * len(tverts[0])
            for i in range(0, len(tverts[0])):
                uvwArray[i] = [0.0, 0.0]

            for v in range(0, len(tverts[0])):
                for i in range(0, len(tverts)):
                    uvwArray[v][0] += tverts[i][v][0]
                    uvwArray[v][1] += 1.0 - tverts[i][v][1]

            for i in range(0, len(faces)):
                for v in range(0, 3):
                    msh.uv_layers[uvw.name].data[(i * 3) + v].uv = (
                        uvwArray[faces[i][v]][0],
                        uvwArray[faces[i][v]][1]
                    )


    # Create Face Maps?
    # msh.face_maps.new()

    # Update Mesh
    msh.update()

    # Check mesh is Valid
    # Without this blender may crash!!! lulz
    # However the check will throw false positives so
    # and additional or a replacement valatiation function
    # would be required
    
    if msh.validate():
        print("Mesh Failed Validation")
        if mesh_validate(vertArray, faces):
            # Erase Mesh
            msh.user_clear()
            bpy.data.meshes.remove(msh)
            print("Mesh Deleted!")
            return None
        

    # Assign Mesh to Object
    obj = bpy.data.objects.new(obj_name, msh)
    obj.location = position
    # obj.name = obj.name.replace(".", "_")

    for i in range(0, len(materials)):

        if len(obj.material_slots) < (i + 1):
            # if there is no slot then we append to create the slot and assign
            obj.data.materials.append(materials[i])
        else:
            # we always want the material in slot[0]
            obj.material_slots[0].material = materials[i]
        # obj.active_material = obj.material_slots[i].material

    if len(materialIDs) == len(faces):
        for i in range(0, len(materialIDs)):
            obj.data.polygons[i].material_index = materialIDs[i]
    elif len(materialIDs) > 0:
        print("Error:\tMaterial Index Out of Range")

    # obj.data.materials.append(material)
    layer.objects.link(obj)

    # Generate a Material
    # img_name = "Test.jpg"  # dummy texture
    # mat_count = len(texmaps)

    # if mat_count == 0 and len(materialIDs) > 0:
    #    for i in range(0, len(materialIDs)):
    #        if (materialIDs[i] + 1) > mat_count: mat_count = materialIDs[i] + 1

    # Assign Material ID's
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    bpy.ops.object.mode_set(mode='OBJECT')
    # materialIDs

    # Redraw Entire Scene
    # bpy.context.scene.update()

    return obj


class h3d_uniform:
    name = ""
    a = 0.0
    b = 0.0
    c = 0.0
    d = 0.0


class h3d_sampler:
    name = ""
    map = ""


class h3d_material:
    name = ""
    sampler = []
    uniform = []


class h3d_geo_block:
    index = 0
    stride = 0
    data = []


class h3d_geo_file:
    fileid = 0  # 0x47443348 'H3DG'
    unk01 = 0
    unk02 = 0
    matrix = ((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))
    unk03 = 0
    vertex_count = 0
    vertices = h3d_geo_block()
    normals = h3d_geo_block()
    texcoord0 = h3d_geo_block()
    texcoord1 = h3d_geo_block()
    face_count = 0
    faces = []

    def __repr__(self):
        return 'VertexCount:\t%i\nFaceCount:\t%i\nUnknowns:\t%i\t%i\t%i' % (
        self.vertex_count, self.face_count, self.unk01, self.unk02, self.unk03)

    def read(self, f=fopen()):
        self.fileid = readLong(f)
        if self.fileid != 0x47443348:
            print("Error:\tInvalid File")
            return False

        self.unk01 = readLong(f)
        self.unk02 = readLong(f)
        self.matrix = (
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f))
        )
        self.unk03 = readLong(f)
        self.vertex_count = readLong(f)

        # Read Vertices
        self.vertices.index = readLong(f)
        self.vertices.stride = readLong(f)

        if self.vertices.stride != 0x0C:
            print("Error:\tInvalid Vertex Stride")
            return False
        comp = int(self.vertices.stride / 4)
        self.vertices.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.vertices.data[x][y] = readFloat(f)

        # Read Normals
        self.normals.index = readLong(f)
        self.normals.stride = readLong(f)
        if self.normals.stride != 0x06:
            print("Error:\tInvalid Normal Stride:\t%i" % self.normals.stride)
            return False

        comp = int(self.normals.stride / 2)
        self.normals.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.normals.data[x][y] = float(readShort(f) / 32767.0)

        # Read UV-0
        self.texcoord0.index = readLong(f)
        self.texcoord0.stride = readLong(f)
        if self.texcoord0.stride != 0x08:
            print("Error:\tInvalid UV0 Stride")
            return False

        comp = int(self.texcoord0.stride / 4)
        self.texcoord0.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord0.data[x][y] = readFloat(f)

        # Read UV-1
        self.texcoord1.index = readLong(f)
        self.texcoord1.stride = readLong(f)
        if self.texcoord1.stride != 0x08:
            print("Error:\tInvalid UV1 Stride")
            return False

        comp = int(self.texcoord1.stride / 4)
        self.texcoord1.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord1.data[x][y] = readFloat(f)

        # Faces
        self.face_count = readLong(f)
        self.faces = [[int] for x in range(self.face_count)]
        for x in range(0, self.face_count):
            self.faces[x] = readLong(f)

        return True


def read_triangle_strip(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    i = 0
    face = [1, 1, 1]
    while i < faceCount:
        faceCW = True
        face[0] = faces[i + facePosition]
        face[1] = faces[i + facePosition + 1]
        i += 2
        while i < faceCount:
            face[2] = faces[i + facePosition]
            if face[2] == 0xFFFF or face[2] == -1: break
            if face[0] != face[2] and face[1] != face[2] and face[2] != face[0]:
                if faceCW:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[1] + faceOffset,
                        face[2] + faceOffset
                    ])
                else:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[2] + faceOffset,
                        face[1] + faceOffset
                    ])
            faceCW = not faceCW
            face = [face[1], face[2], face[0]]
            i += 1
    return None


def read_triangle_list(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    for i in range(0, int(faceCount / 3)):
        faceArray.append([
            faces[(i * 3) + facePosition] + faceOffset,
            faces[(i * 3) + facePosition + 1] + faceOffset,
            faces[(i * 3) + facePosition + 2] + faceOffset
        ])
    return None


def read(file="", mscale=1.0):
    # Check File is present
    if not doesFileExist(file):
        print("Error:\tFailed To Find Geo File")
        return False

    # Strip Paths From fullpath to find sister file
    fpath = getFilenamePath(file)
    fname = getFilenameFile(file)
    h3d_file = fpath + fname + ".h3d"

    # Check if sister file is found
    if not doesFileExist(h3d_file):
        print("Error:\tFailed To Find H3D File")
        return False

    # open GEO file
    f = fopen(file, 'rb')

    # Create GEO Object to store data from Geo file
    geo = h3d_geo_file()

    # Attempt to read GEO file into Geo Class
    read_good = False
    try:
        read_good = geo.read(f)
    except:
        print("Error:\t Failed to Read File")
        return False

    # Check if Geo File Was Read
    if not read_good:
        print("Error:\t Failed to Read File")
        return False

    # Print Geo Class Info
    print(repr(geo))

    # Close Geo File
    fclose(f)

    # Read Sister File, import data
    try:
        h3d = minidom.parse(h3d_file)

        h3d_mat = []
        h3d_mat_index = 0
        h3d_sp = h3d_sampler()
        h3d_un = h3d_uniform()

        for materials in h3d.getElementsByTagName('Materials'):
            for material in materials.getElementsByTagName('Material'):
                h3d_mat.append(h3d_material())

                h3d_mat[h3d_mat_index].name = material.attributes['name'].value

                for sampler in material.getElementsByTagName('Sampler'):
                    h3d_sp = h3d_sampler()
                    try:
                        h3d_sp.name = sampler.attributes['name'].value
                        h3d_sp.map = sampler.attributes['map'].value
                        h3d_mat[h3d_mat_index].sampler.append(h3d_sp)
                    except:
                        pass

                for uniform in material.getElementsByTagName('Uniform'):
                    h3d_un = h3d_uniform()
                    try:
                        h3d_un.name = sampler.attributes['name'].value
                        h3d_un.a = float(sampler.attributes['a'].value)
                        h3d_un.b = float(sampler.attributes['b'].value)
                        h3d_un.c = float(sampler.attributes['c'].value)
                        h3d_un.d = float(sampler.attributes['d'].value)
                        h3d_mat[h3d_mat_index].uniform.append(h3d_un)
                    except:
                        pass
                h3d_mat_index += 1

    except:
        print("Error:\tFailed to Parse XML file")
        return False

    mats = []
    for m in h3d_mat:
        mat = StandardMaterial(m.name)
        for t in m.sampler:
            if t.name == 'albedoMap':
                if '/' in t.map:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map.split('/')[-1:][0]))
                else:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map))
        mats.append(mat.data)

    msh = None
    msh_name = ""
    msh_matn = ""
    msh_tx = 0.0
    msh_ty = 0.0
    msh_tz = 0.0
    msh_batchStart = 0
    msh_batchCount = 0
    msh_vertRStart = 0
    msh_vertREnd = 0
    msh_vertRCount = 0
    mat_index = 0
    mat_name = ""
    vertArray = []
    normArray = []
    uvw0Array = []
    uvw1Array = []
    faceArray = []
    matidArray = []
    for gmesh in h3d.getElementsByTagName('Mesh'):
        try:
            msh_name = gmesh.attributes['name'].value
        except:
            pass
        try:
            msh_matn = gmesh.attributes['material'].value
        except:
            pass
        try:
            msh_tx = float(gmesh.attributes['tx'].value)
        except:
            pass
        try:
            msh_ty = float(gmesh.attributes['ty'].value)
        except:
            pass
        try:
            msh_tz = float(gmesh.attributes['tz'].value)
        except:
            pass
        try:
            msh_batchStart = int(gmesh.attributes['batchStart'].value)
        except:
            pass
        try:
            msh_batchCount = int(gmesh.attributes['batchCount'].value)
        except:
            pass
        try:
            msh_vertRStart = int(gmesh.attributes['vertRStart'].value)
        except:
            pass
        try:
            msh_vertREnd = int(gmesh.attributes['vertREnd'].value)
        except:
            pass

        mat_index = 0
        if '#' in msh_matn:
            mat_name = msh_matn.split('#')[-1:][0]
            for i in range(0, len(h3d_mat)):
                if h3d_mat[i].name == mat_name:
                    mat_index = i
                    break

        matidArray = [mat_index for i in range(int(msh_batchCount / 3))]

        msh_vertRCount = msh_vertREnd - msh_vertRStart + 1
        vertArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        normArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw0Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw1Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])

        for i in range(0, msh_vertRCount):
            vertArray[i] = geo.vertices.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            normArray[i] = geo.normals.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw0Array[i] = geo.texcoord0.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw1Array[i] = geo.texcoord1.data[msh_vertRStart + i]

        faceArray = []
        read_triangle_list(geo.faces, faceArray, msh_batchCount, msh_batchStart, -msh_vertRStart)

        msh = mesh(
            vertices=vertArray,
            tverts=[uvw0Array, uvw1Array],
            normals=normArray,
            faces=faceArray,
            obj_name=msh_name,
            flipAxis=True,
            mscale=mscale,
            materials=mats,
            materialIDs=matidArray,
            position=(msh_tx, -msh_tz, msh_ty)
        )

    return True


# Callback when file(s) are selected
def offroad_legends_imp_callback(fpath="", files=[], clearScene=True, mscale = 1.0):
    if len(files) > 0 and clearScene: deleteScene(['MESH', 'ARMATURE'])
    for file in files:
        read(fpath + file.name, mscale)
    if len(files) > 0:
        messageBox("Done!")
        return True
    else:
        return False


# Wrapper that Invokes FileSelector to open files from blender
def offroad_legends_imp(reload=False):
    # Un-Register Operator
    if reload and hasattr(bpy.types, "IMPORTHELPER_OT_offroad_legends_imp"):  # print(bpy.ops.importhelper.offroad_legends_imp.idname())

        try:
            bpy.types.TOPBAR_MT_file_import.remove(
                bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp').menu_func_import)
        except:
            print("Failed to Unregister2")

        try:
            bpy.utils.unregister_class(bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp'))
        except:
            print("Failed to Unregister1")

    # Define Operator
    class ImportHelper_offroad_legends_imp(bpy.types.Operator):

        # Operator Path
        bl_idname = "importhelper.offroad_legends_imp"
        bl_label = "Select File"

        # Operator Properties
        # filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
        filter_glob: bpy.props.StringProperty(default='*.geo', options={'HIDDEN'}, subtype='FILE_PATH')

        # Variables
        filepath: bpy.props.StringProperty(subtype="FILE_PATH")  # full path of selected item (path+filename)
        filename: bpy.props.StringProperty(subtype="FILE_NAME")  # name of selected item
        directory: bpy.props.StringProperty(subtype="FILE_PATH")  # directory of the selected item
        files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)  # a collection containing all the selected items as filenames

        # Controls
        my_float1: bpy.props.FloatProperty(name="Scale", default=1.0, description="Changes Scale of the imported Mesh")
        my_bool1: bpy.props.BoolProperty(name="Clear Scene", default=True, description="Deletes everything in the scene prior to importing")

        # Runs when this class OPENS
        def invoke(self, context, event):

            # Retrieve Settings
            try: self.filepath = bpy.types.Scene.offroad_legends_imp_filepath
            except: bpy.types.Scene.offroad_legends_imp_filepath = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.directory = bpy.types.Scene.offroad_legends_imp_directory
            except: bpy.types.Scene.offroad_legends_imp_directory = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.my_float1 = bpy.types.Scene.offroad_legends_imp_my_float1
            except: bpy.types.Scene.offroad_legends_imp_my_float1 = bpy.props.FloatProperty(default=1.0)

            try: self.my_bool1 = bpy.types.Scene.offroad_legends_imp_my_bool1
            except: bpy.types.Scene.offroad_legends_imp_my_bool1 = bpy.props.BoolProperty(default=False)


            # Open File Browser
            # Set Properties of the File Browser
            context.window_manager.fileselect_add(self)
            context.area.tag_redraw()

            return {'RUNNING_MODAL'}

        # Runs when this Window is CANCELLED
        def cancel(self, context): print("run bitch")

        # Runs when the class EXITS
        def execute(self, context):

            # Save Settings
            bpy.types.Scene.offroad_legends_imp_filepath = self.filepath
            bpy.types.Scene.offroad_legends_imp_directory = self.directory
            bpy.types.Scene.offroad_legends_imp_my_float1 = self.my_float1
            bpy.types.Scene.offroad_legends_imp_my_bool1 = self.my_bool1

            # Run Callback
            offroad_legends_imp_callback(self.directory + "\\", self.files, self.my_bool1, self.my_float1)

            return {"FINISHED"}

            # Window Settings

        def draw(self, context):

            self.layout.row().label(text="Import Settings")

            self.layout.separator()
            self.layout.row().prop(self, "my_bool1")
            self.layout.row().prop(self, "my_float1")

            self.layout.separator()

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="  Author:", icon='QUESTION')
            col.alignment = 'LEFT'
            col.label(text="mariokart64n")

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="Release:", icon='GRIP')
            col.alignment = 'LEFT'
            col.label(text="March 16, 2021")

        def menu_func_import(self, context):
            self.layout.operator("importhelper.offroad_legends_imp", text="Off Road Legends (*.geo)")

    # Register Operator
    bpy.utils.register_class(ImportHelper_offroad_legends_imp)
    bpy.types.TOPBAR_MT_file_import.append(ImportHelper_offroad_legends_imp.menu_func_import)

    # Call ImportHelper
    bpy.ops.importhelper.offroad_legends_imp('INVOKE_DEFAULT')


if not useOpenDialog:
    deleteScene(['MESH', 'ARMATURE'])  # Clear Scene
    clearListener()  # clears out console
    read(
        "C:\\Users\\Corey\\Downloads\\Cars The Video Game\\carry_chassis.geo"
        )
    messageBox("Done!")
else:
    offroad_legends_imp(True)

WOW! AMAZING WORK! This work perfectly! Even with UVs and correct position!
Also, can you make it so the script can read the XML of the car? The XML of the cars has wheel positions. (carry.xml)
It's a shame it can't read the data from Off The Road, that has some exclusive cars. I'll see if it works with Offroad Legends 2 though.

mariokart64n
ultra-veteran
ultra-veteran
Posts: 567
Joined: Sun Jun 05, 2005 12:00 pm
Location: Ontario, Canada
Has thanked: 34 times
Been thanked: 197 times

Re: Offroad Legends GEO/H3D model file, convertor?

Post by mariokart64n » Tue Mar 16, 2021 4:56 pm

I did not notice any carry.xml file so I can't help you
Maxscript and other finished work I've done can be found on my DeviantArt account

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 4:57 pm

mariokart64n wrote:
Tue Mar 16, 2021 4:56 pm
I did not notice any carry.xml file so I can't help you
Oops, I forgot to get the XML of carry.
Here

Code: Select all

<?xml version="1.0"?>
<VEHICLE>
  <CHASSIS>
    <SHAPE type="box" mass="600.0">
      <SIZE x="1.6" y="0.8" z="2.6"/>
      <POS x="0" y="0.25" z="0.9"/>
    </SHAPE>
    <SHAPE type="box" mass="25.0">
      <SIZE x="1.7" y="0.7" z="1.1"/>
      <POS x="0" y="0.85" z="0.20"/>
    </SHAPE>
    
    <SHAPE type="box" mass="25.0">
      <SIZE x="1.6" y="0.1" z="2.3"/>
      <POS x="0" y="0.15" z="-1.40"/>
    </SHAPE>
 

    <SHAPE type="box" mass="25.0">
      <SIZE x="0.15" y="1.1" z="2.3"/>
      <POS x="0.55" y="0.5" z="-1.40"/>
    </SHAPE>
    <SHAPE type="box" mass="25.0">
      <SIZE x="0.15" y="1.1" z="2.3"/>
      <POS x="-0.55" y="0.5" z="-1.40"/>
    </SHAPE>
    <SHAPE type="box" mass="25.0">
      <SIZE x="1.2" y="0.9" z="0.4"/>
      <POS x="0.0" y="0.40" z="-2.46"/>
    </SHAPE>
    
  </CHASSIS>
  <WHEELS>
    <WHEEL>
      <CONNECTIONPOINT x="-0.86" y="0.65" z="1.6"/>
      <SUSPENSION_RESTLENGTH value="1.1"/>
      <SUSPENSIONTRAVEL value="0.1"/>
      <STIFFNESS value="20.0"/>
      <DAMPINGCOMPRESSION value ="2.0"/>
      <DAMPINGRELAXATION value ="1.8"/>
      <STEERED value="1"/>
      <DRIVERATIO value="0.4"/>
      <RADIUS value="0.38"/>
    </WHEEL>
    <WHEEL>
      <CONNECTIONPOINT x="0.86" y="0.65" z="1.6"/>
      <SUSPENSION_RESTLENGTH value="1.1"/>
      <SUSPENSIONTRAVEL value="0.1"/>
      <STIFFNESS value="20.0"/>
      <DAMPINGCOMPRESSION value ="2.0"/>
      <DAMPINGRELAXATION value ="1.8"/>
      <STEERED value="1"/>
      <DRIVERATIO value="0.4"/>
      <RADIUS value="0.38"/>
    </WHEEL>
    <WHEEL>
      <CONNECTIONPOINT x="-0.86" y="0.65" z="-1.495"/>
      <SUSPENSION_RESTLENGTH value="1.1"/>
      <SUSPENSIONTRAVEL value="0.1"/>
      <STIFFNESS value="20.0"/>
      <DAMPINGCOMPRESSION value ="2.0"/>
      <DAMPINGRELAXATION value ="1.5"/>
      <STEERED value="0"/>
      <DRIVERATIO value="0.1"/>
      <RADIUS value="0.38"/>
    </WHEEL>
    <WHEEL>
      <CONNECTIONPOINT x="0.85" y="0.65" z="-1.495"/>
      <SUSPENSION_RESTLENGTH value="1.1"/>
      <SUSPENSIONTRAVEL value="0.1"/>
      <STIFFNESS value="20.0"/>
      <DAMPINGCOMPRESSION value ="2.0"/>
      <DAMPINGRELAXATION value ="1.5"/>
      <STEERED value="0"/>
      <DRIVERATIO value="0.1"/>
      <RADIUS value="0.38"/>
    </WHEEL>
  </WHEELS>
  <FRICTION side="1.0" forward="1.25" air="16.0" rolling="0.005"/>
  <ENGINE idleRpm="650.0" maxRpm="6500.0" friction="1.0" mass="0.57">
    <TORQUETABLE>
      <ELEM rpm="0.0" torque="0.0"/>
      <ELEM rpm="1000.0" torque="395.0"/>
      <ELEM rpm="2000.0" torque="375.0"/>
      <ELEM rpm="3000.0" torque="420.0"/>
      <ELEM rpm="4000.0" torque="445.0"/>
      <ELEM rpm="5000.0" torque="380.0"/>
      <ELEM rpm="6000.0" torque="345.0"/>
      <ELEM rpm="7000.0" torque="323.0"/>
      <ELEM rpm="8000.0" torque="280.0"/>
    </TORQUETABLE>
    <GEARBOX numGears="5" effectiveness="0.85" diffratio="4.2">
      <RATIO value="0.0"/>
      <RATIO value="2.73"/>
      <RATIO value="1.92"/>
      <RATIO value="1.38"/>
      <RATIO value="0.99"/>
      <RATIO value="0.79"/>
    </GEARBOX>
  </ENGINE>


</VEHICLE>

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 5:14 pm

ReVolt wrote:
Tue Mar 16, 2021 4:48 pm
mariokart64n wrote:
Tue Mar 16, 2021 4:16 pm
Image
Below is a python script I wrote for blender that was tested in the latest version (currently 2.92.0)

usage: start blender, goto the script tab then press NEW in the text editor and paste the below script into the editor. To execute the script press ALT+P or press the [>] play button, a file selection box will appear which will allow you to select GEO files to import. Once the script has executed once it can then be accessed through the File > Import Dialog until blender is restarted

Code: Select all

""" ======================================================================

    PythonScript:   [Mobile] Offroad Legends
    Author:         mariokart64n
    Date:           March 16, 2021
    Version:        0.1

    ======================================================================

    ChangeLog:

    2021-03-16
        Script Wrote


    ====================================================================== """

import bpy  # Needed to interface with blender
import struct  # Needed for Binary Reader
import math
from pathlib import Path  # Needed for os stuff
from xml.dom import minidom

useOpenDialog = True


def deleteScene(include=[]):
    if len(include) > 0:
        # Exit and Interactions
        if bpy.context.view_layer.objects.active != None:
            bpy.ops.object.mode_set(mode='OBJECT')

        # Select All
        bpy.ops.object.select_all(action='SELECT')

        # Loop Through Each Selection
        for o in bpy.context.view_layer.objects.selected:
            for t in include:
                if o.type == t:
                    bpy.data.objects.remove(o, do_unlink=True)
                    break

        # De-Select All
        bpy.ops.object.select_all(action='DESELECT')
    return None


def messageBox(message="", title="Message Box", icon='INFO'):
    def draw(self, context): self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None

def doesFileExist(filename):
    file = Path(filename)
    if file.is_file():
        return True
    elif file.is_dir():
        return True
    else:
        return False


def clearListener(len=64):
    for i in range(0, len): print('')



def getFilenamePath(file):  # returns: "g:\subdir1\subdir2\"
    return (str(Path(file).resolve().parent) + "\\")


def getFilenameFile(file):  # returns: "myImage"
    return Path(file).stem


def Bitmaptexture(mat, filename="", name="ShaderNodeTexImage"):
    imageTex = mat.node_tree.nodes.new('ShaderNodeTexImage')
    imageTex.label = name
    try:
        imageTex.image = bpy.data.images.load(
            filepath=filename,
            check_existing=False
        )
        imageTex.image.name = filenameFromPath(filename)
        imageTex.image.colorspace_settings.name = 'sRGB'
    except:
        imageTex.image = bpy.data.images.new(
            name=filename,
            width=8,
            height=8,
            alpha=False,
            float_buffer=False
        )
    return imageTex


class StandardMaterial:
    data = None
    bsdf = None

    def __init__(self, name="Material"):
        # make material
        self.data = bpy.data.materials.new(name=name)
        self.data.use_nodes = True
        self.data.use_backface_culling = True
        self.bsdf = self.data.node_tree.nodes["Principled BSDF"]
        self.bsdf.label = "Standard"
        return None

    def diffuse(self, colour=(0.0, 0.0, 0.0, 0.0), name="Diffuse"):
        rgbaColor = self.data.node_tree.nodes.new('ShaderNodeRGB')
        rgbaColor.label = name
        rgbaColor.outputs[0].default_value = (colour[0], colour[1], colour[2], colour[3])
        if self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], rgbaColor.outputs['Color'])
        return None

    def diffuseMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], imageTex.outputs['Color'])
        return None

    def opacityMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.blend_method = 'BLEND'
            self.data.shadow_method = 'HASHED'
            self.data.show_transparent_back = False
            self.data.node_tree.links.new(self.bsdf.inputs['Alpha'], imageTex.outputs['Alpha'])
        return None

    def normalMap(self, imageNode=None):
        if imageTex != None and self.bsdf != None:
            imageTex.image.colorspace_settings.name = 'Linear'
            normMap = self.data.node_tree.nodes.new('ShaderNodeNormalMap')
            normMap.label = 'ShaderNodeNormalMap'
            self.data.node_tree.links.new(normMap.inputs['Color'], imageTex.outputs['Color'])
            self.data.node_tree.links.new(self.bsdf.inputs['Normal'], normMap.outputs['Normal'])
        return None

    def specularMap(self, imageNode=None, invert=True):
        if imageTex != None and self.bsdf != None:
            if invert:
                invertRGB = self.data.node_tree.nodes.new('ShaderNodeInvert')
                invertRGB.label = 'ShaderNodeInvert'
                self.data.node_tree.links.new(invertRGB.inputs['Color'], imageTex.outputs['Color'])
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], invertRGB.outputs['Color'])
            else:
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], imageTex.outputs['Color'])
        return None


class fopen:
    little_endian = True
    file = ""
    mode = 'rb'
    data = bytearray()
    size = 0
    pos = 0
    isGood = False

    def __init__(self, filename=None, mode='rb', isLittleEndian=True):
        if mode == 'rb':
            if filename != None and Path(filename).is_file():
                self.data = open(filename, mode).read()
                self.size = len(self.data)
                self.pos = 0
                self.mode = mode
                self.file = filename
                self.little_endian = isLittleEndian
                self.isGood = True
        else:
            self.file = filename
            self.mode = mode
            self.data = bytearray()
            self.pos = 0
            self.size = 0
            self.little_endian = isLittleEndian
            self.isGood = False

        return None

    # def __del__(self):
    #    self.flush()

    def resize(self, dataSize=0):
        if dataSize > 0:
            self.data = bytearray(dataSize)
        else:
            self.data = bytearray()
        self.pos = 0
        self.size = dataSize
        self.isGood = False
        return None

    def flush(self):
        print("flush")
        print("file:\t%s" % self.file)
        print("isGood:\t%s" % self.isGood)
        print("size:\t%s" % len(self.data))
        if self.file != "" and not self.isGood and len(self.data) > 0:
            self.isGood = True

            s = open(self.file, 'w+b')
            s.write(self.data)
            s.close()

    def read_and_unpack(self, unpack, size):
        '''
          Charactor, Byte-order
          @,         native, native
          =,         native, standard
          <,         little endian
          >,         big endian
          !,         network

          Format, C-type,         Python-type, Size[byte]
          c,      char,           byte,        1
          b,      signed char,    integer,     1
          B,      unsigned char,  integer,     1
          h,      short,          integer,     2
          H,      unsigned short, integer,     2
          i,      int,            integer,     4
          I,      unsigned int,   integer,     4
          f,      float,          float,       4
          d,      double,         float,       8
        '''
        value = 0
        if self.size > 0 and self.pos + size < self.size:
            value = struct.unpack_from(unpack, self.data, self.pos)[0]
            self.pos += size
        return value

    def pack_and_write(self, pack, size, value):
        if self.pos + size > self.size:
            self.data.extend(b'\x00' * ((self.pos + size) - self.size))
            self.size = self.pos + size
        try:
            struct.pack_into(pack, self.data, self.pos, value)
        except:
            print('Pos:\t%i / %i (buf:%i) [val:%i:%i:%s]' % (self.pos, self.size, len(self.data), value, size, pack))
            pass
        self.pos += size
        return None

    def set_pointer(self, offset):
        self.pos = offset
        return None


def fclose(bitStream):
    bitStream.flush()
    bitStream.isGood = False


def readShort(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'h' if isSigned == 0 else 'H'
    return (bitStream.read_and_unpack(fmt, 2))


def readLong(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'i' if isSigned == 0 else 'I'
    return (bitStream.read_and_unpack(fmt, 4))


def readFloat(bitStream):
    fmt = '>f' if not bitStream.little_endian else '<f'
    return (bitStream.read_and_unpack(fmt, 4))


def mesh_validate (vertices=[], faces=[]):
    #
    # Returns True if mesh is BAD
    #
    # check face index bound
    face_min = 0
    face_max = len(vertices) - 1
    
    for face in faces:
        for side in face:
            if side < face_min or side > face_max:
                print("Face Index Out of Range:\t[%i / %i]" % (side, face_max))
                return True
    return False


def mesh(
    vertices=[],
    faces=[],
    materialIDs=[],
    tverts=[],
    normals=[],
    colours=[],
    materials=[],
    mscale=1.0,
    flipAxis=False,
    obj_name="Object",
    lay_name='',
    position = (0.0, 0.0, 0.0)
    ):
    #
    # This function is pretty, ugly
    # imports the mesh into blender
    #
    # Clear Any Object Selections
    # for o in bpy.context.selected_objects: o.select = False
    bpy.context.view_layer.objects.active = None
    
    # Get Collection (Layers)
    if lay_name != '':
        # make collection
        layer = bpy.data.collections.new(lay_name)
        bpy.context.scene.collection.children.link(layer)
    else:
        layer = bpy.data.collections[bpy.context.view_layer.active_layer_collection.name]

    # make mesh
    msh = bpy.data.meshes.new('Mesh')

    # msh.name = msh.name.replace(".", "_")

    # Apply vertex scaling
    # mscale *= bpy.context.scene.unit_settings.scale_length
    if len(vertices) > 0:
        vertArray = [[float] * 3] * len(vertices)
        if flipAxis:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    -vertices[v][2] * mscale,
                    vertices[v][1] * mscale
                )
        else:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    vertices[v][1] * mscale,
                    vertices[v][2] * mscale
                )

    # assign data from arrays
    msh.from_pydata(vertArray, [], faces)

    # set surface to smooth
    msh.polygons.foreach_set("use_smooth", [True] * len(msh.polygons))

    # Set Normals
    if len(faces) > 0:
        if len(normals) > 0:
            msh.use_auto_smooth = True
            if len(normals) == (len(faces) * 3):
                msh.normals_split_custom_set(normals)
            else:
                normArray = [[float] * 3] * (len(faces) * 3)
                if flipAxis:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 -normals[faces[i][v]][2],
                                 normals[faces[i][v]][1]]
                            )
                else:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 normals[faces[i][v]][1],
                                 normals[faces[i][v]][2]]
                            )
                msh.normals_split_custom_set(normArray)

        # create texture corrdinates
        #print("tverts ", len(tverts))
        # this is just a hack, i just add all the UVs into the same space <<<
        if len(tverts) > 0:
            uvw = msh.uv_layers.new()
            # if len(tverts) == (len(faces) * 3):
            #    for v in range(0, len(faces) * 3):
            #        msh.uv_layers[uvw.name].data[v].uv = tverts[v]
            # else:
            uvwArray = [[float] * 2] * len(tverts[0])
            for i in range(0, len(tverts[0])):
                uvwArray[i] = [0.0, 0.0]

            for v in range(0, len(tverts[0])):
                for i in range(0, len(tverts)):
                    uvwArray[v][0] += tverts[i][v][0]
                    uvwArray[v][1] += 1.0 - tverts[i][v][1]

            for i in range(0, len(faces)):
                for v in range(0, 3):
                    msh.uv_layers[uvw.name].data[(i * 3) + v].uv = (
                        uvwArray[faces[i][v]][0],
                        uvwArray[faces[i][v]][1]
                    )


    # Create Face Maps?
    # msh.face_maps.new()

    # Update Mesh
    msh.update()

    # Check mesh is Valid
    # Without this blender may crash!!! lulz
    # However the check will throw false positives so
    # and additional or a replacement valatiation function
    # would be required
    
    if msh.validate():
        print("Mesh Failed Validation")
        if mesh_validate(vertArray, faces):
            # Erase Mesh
            msh.user_clear()
            bpy.data.meshes.remove(msh)
            print("Mesh Deleted!")
            return None
        

    # Assign Mesh to Object
    obj = bpy.data.objects.new(obj_name, msh)
    obj.location = position
    # obj.name = obj.name.replace(".", "_")

    for i in range(0, len(materials)):

        if len(obj.material_slots) < (i + 1):
            # if there is no slot then we append to create the slot and assign
            obj.data.materials.append(materials[i])
        else:
            # we always want the material in slot[0]
            obj.material_slots[0].material = materials[i]
        # obj.active_material = obj.material_slots[i].material

    if len(materialIDs) == len(faces):
        for i in range(0, len(materialIDs)):
            obj.data.polygons[i].material_index = materialIDs[i]
    elif len(materialIDs) > 0:
        print("Error:\tMaterial Index Out of Range")

    # obj.data.materials.append(material)
    layer.objects.link(obj)

    # Generate a Material
    # img_name = "Test.jpg"  # dummy texture
    # mat_count = len(texmaps)

    # if mat_count == 0 and len(materialIDs) > 0:
    #    for i in range(0, len(materialIDs)):
    #        if (materialIDs[i] + 1) > mat_count: mat_count = materialIDs[i] + 1

    # Assign Material ID's
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    bpy.ops.object.mode_set(mode='OBJECT')
    # materialIDs

    # Redraw Entire Scene
    # bpy.context.scene.update()

    return obj


class h3d_uniform:
    name = ""
    a = 0.0
    b = 0.0
    c = 0.0
    d = 0.0


class h3d_sampler:
    name = ""
    map = ""


class h3d_material:
    name = ""
    sampler = []
    uniform = []


class h3d_geo_block:
    index = 0
    stride = 0
    data = []


class h3d_geo_file:
    fileid = 0  # 0x47443348 'H3DG'
    unk01 = 0
    unk02 = 0
    matrix = ((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))
    unk03 = 0
    vertex_count = 0
    vertices = h3d_geo_block()
    normals = h3d_geo_block()
    texcoord0 = h3d_geo_block()
    texcoord1 = h3d_geo_block()
    face_count = 0
    faces = []

    def __repr__(self):
        return 'VertexCount:\t%i\nFaceCount:\t%i\nUnknowns:\t%i\t%i\t%i' % (
        self.vertex_count, self.face_count, self.unk01, self.unk02, self.unk03)

    def read(self, f=fopen()):
        self.fileid = readLong(f)
        if self.fileid != 0x47443348:
            print("Error:\tInvalid File")
            return False

        self.unk01 = readLong(f)
        self.unk02 = readLong(f)
        self.matrix = (
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f))
        )
        self.unk03 = readLong(f)
        self.vertex_count = readLong(f)

        # Read Vertices
        self.vertices.index = readLong(f)
        self.vertices.stride = readLong(f)

        if self.vertices.stride != 0x0C:
            print("Error:\tInvalid Vertex Stride")
            return False
        comp = int(self.vertices.stride / 4)
        self.vertices.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.vertices.data[x][y] = readFloat(f)

        # Read Normals
        self.normals.index = readLong(f)
        self.normals.stride = readLong(f)
        if self.normals.stride != 0x06:
            print("Error:\tInvalid Normal Stride:\t%i" % self.normals.stride)
            return False

        comp = int(self.normals.stride / 2)
        self.normals.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.normals.data[x][y] = float(readShort(f) / 32767.0)

        # Read UV-0
        self.texcoord0.index = readLong(f)
        self.texcoord0.stride = readLong(f)
        if self.texcoord0.stride != 0x08:
            print("Error:\tInvalid UV0 Stride")
            return False

        comp = int(self.texcoord0.stride / 4)
        self.texcoord0.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord0.data[x][y] = readFloat(f)

        # Read UV-1
        self.texcoord1.index = readLong(f)
        self.texcoord1.stride = readLong(f)
        if self.texcoord1.stride != 0x08:
            print("Error:\tInvalid UV1 Stride")
            return False

        comp = int(self.texcoord1.stride / 4)
        self.texcoord1.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord1.data[x][y] = readFloat(f)

        # Faces
        self.face_count = readLong(f)
        self.faces = [[int] for x in range(self.face_count)]
        for x in range(0, self.face_count):
            self.faces[x] = readLong(f)

        return True


def read_triangle_strip(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    i = 0
    face = [1, 1, 1]
    while i < faceCount:
        faceCW = True
        face[0] = faces[i + facePosition]
        face[1] = faces[i + facePosition + 1]
        i += 2
        while i < faceCount:
            face[2] = faces[i + facePosition]
            if face[2] == 0xFFFF or face[2] == -1: break
            if face[0] != face[2] and face[1] != face[2] and face[2] != face[0]:
                if faceCW:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[1] + faceOffset,
                        face[2] + faceOffset
                    ])
                else:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[2] + faceOffset,
                        face[1] + faceOffset
                    ])
            faceCW = not faceCW
            face = [face[1], face[2], face[0]]
            i += 1
    return None


def read_triangle_list(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    for i in range(0, int(faceCount / 3)):
        faceArray.append([
            faces[(i * 3) + facePosition] + faceOffset,
            faces[(i * 3) + facePosition + 1] + faceOffset,
            faces[(i * 3) + facePosition + 2] + faceOffset
        ])
    return None


def read(file="", mscale=1.0):
    # Check File is present
    if not doesFileExist(file):
        print("Error:\tFailed To Find Geo File")
        return False

    # Strip Paths From fullpath to find sister file
    fpath = getFilenamePath(file)
    fname = getFilenameFile(file)
    h3d_file = fpath + fname + ".h3d"

    # Check if sister file is found
    if not doesFileExist(h3d_file):
        print("Error:\tFailed To Find H3D File")
        return False

    # open GEO file
    f = fopen(file, 'rb')

    # Create GEO Object to store data from Geo file
    geo = h3d_geo_file()

    # Attempt to read GEO file into Geo Class
    read_good = False
    try:
        read_good = geo.read(f)
    except:
        print("Error:\t Failed to Read File")
        return False

    # Check if Geo File Was Read
    if not read_good:
        print("Error:\t Failed to Read File")
        return False

    # Print Geo Class Info
    print(repr(geo))

    # Close Geo File
    fclose(f)

    # Read Sister File, import data
    try:
        h3d = minidom.parse(h3d_file)

        h3d_mat = []
        h3d_mat_index = 0
        h3d_sp = h3d_sampler()
        h3d_un = h3d_uniform()

        for materials in h3d.getElementsByTagName('Materials'):
            for material in materials.getElementsByTagName('Material'):
                h3d_mat.append(h3d_material())

                h3d_mat[h3d_mat_index].name = material.attributes['name'].value

                for sampler in material.getElementsByTagName('Sampler'):
                    h3d_sp = h3d_sampler()
                    try:
                        h3d_sp.name = sampler.attributes['name'].value
                        h3d_sp.map = sampler.attributes['map'].value
                        h3d_mat[h3d_mat_index].sampler.append(h3d_sp)
                    except:
                        pass

                for uniform in material.getElementsByTagName('Uniform'):
                    h3d_un = h3d_uniform()
                    try:
                        h3d_un.name = sampler.attributes['name'].value
                        h3d_un.a = float(sampler.attributes['a'].value)
                        h3d_un.b = float(sampler.attributes['b'].value)
                        h3d_un.c = float(sampler.attributes['c'].value)
                        h3d_un.d = float(sampler.attributes['d'].value)
                        h3d_mat[h3d_mat_index].uniform.append(h3d_un)
                    except:
                        pass
                h3d_mat_index += 1

    except:
        print("Error:\tFailed to Parse XML file")
        return False

    mats = []
    for m in h3d_mat:
        mat = StandardMaterial(m.name)
        for t in m.sampler:
            if t.name == 'albedoMap':
                if '/' in t.map:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map.split('/')[-1:][0]))
                else:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map))
        mats.append(mat.data)

    msh = None
    msh_name = ""
    msh_matn = ""
    msh_tx = 0.0
    msh_ty = 0.0
    msh_tz = 0.0
    msh_batchStart = 0
    msh_batchCount = 0
    msh_vertRStart = 0
    msh_vertREnd = 0
    msh_vertRCount = 0
    mat_index = 0
    mat_name = ""
    vertArray = []
    normArray = []
    uvw0Array = []
    uvw1Array = []
    faceArray = []
    matidArray = []
    for gmesh in h3d.getElementsByTagName('Mesh'):
        try:
            msh_name = gmesh.attributes['name'].value
        except:
            pass
        try:
            msh_matn = gmesh.attributes['material'].value
        except:
            pass
        try:
            msh_tx = float(gmesh.attributes['tx'].value)
        except:
            pass
        try:
            msh_ty = float(gmesh.attributes['ty'].value)
        except:
            pass
        try:
            msh_tz = float(gmesh.attributes['tz'].value)
        except:
            pass
        try:
            msh_batchStart = int(gmesh.attributes['batchStart'].value)
        except:
            pass
        try:
            msh_batchCount = int(gmesh.attributes['batchCount'].value)
        except:
            pass
        try:
            msh_vertRStart = int(gmesh.attributes['vertRStart'].value)
        except:
            pass
        try:
            msh_vertREnd = int(gmesh.attributes['vertREnd'].value)
        except:
            pass

        mat_index = 0
        if '#' in msh_matn:
            mat_name = msh_matn.split('#')[-1:][0]
            for i in range(0, len(h3d_mat)):
                if h3d_mat[i].name == mat_name:
                    mat_index = i
                    break

        matidArray = [mat_index for i in range(int(msh_batchCount / 3))]

        msh_vertRCount = msh_vertREnd - msh_vertRStart + 1
        vertArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        normArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw0Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw1Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])

        for i in range(0, msh_vertRCount):
            vertArray[i] = geo.vertices.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            normArray[i] = geo.normals.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw0Array[i] = geo.texcoord0.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw1Array[i] = geo.texcoord1.data[msh_vertRStart + i]

        faceArray = []
        read_triangle_list(geo.faces, faceArray, msh_batchCount, msh_batchStart, -msh_vertRStart)

        msh = mesh(
            vertices=vertArray,
            tverts=[uvw0Array, uvw1Array],
            normals=normArray,
            faces=faceArray,
            obj_name=msh_name,
            flipAxis=True,
            mscale=mscale,
            materials=mats,
            materialIDs=matidArray,
            position=(msh_tx, -msh_tz, msh_ty)
        )

    return True


# Callback when file(s) are selected
def offroad_legends_imp_callback(fpath="", files=[], clearScene=True, mscale = 1.0):
    if len(files) > 0 and clearScene: deleteScene(['MESH', 'ARMATURE'])
    for file in files:
        read(fpath + file.name, mscale)
    if len(files) > 0:
        messageBox("Done!")
        return True
    else:
        return False


# Wrapper that Invokes FileSelector to open files from blender
def offroad_legends_imp(reload=False):
    # Un-Register Operator
    if reload and hasattr(bpy.types, "IMPORTHELPER_OT_offroad_legends_imp"):  # print(bpy.ops.importhelper.offroad_legends_imp.idname())

        try:
            bpy.types.TOPBAR_MT_file_import.remove(
                bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp').menu_func_import)
        except:
            print("Failed to Unregister2")

        try:
            bpy.utils.unregister_class(bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp'))
        except:
            print("Failed to Unregister1")

    # Define Operator
    class ImportHelper_offroad_legends_imp(bpy.types.Operator):

        # Operator Path
        bl_idname = "importhelper.offroad_legends_imp"
        bl_label = "Select File"

        # Operator Properties
        # filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
        filter_glob: bpy.props.StringProperty(default='*.geo', options={'HIDDEN'}, subtype='FILE_PATH')

        # Variables
        filepath: bpy.props.StringProperty(subtype="FILE_PATH")  # full path of selected item (path+filename)
        filename: bpy.props.StringProperty(subtype="FILE_NAME")  # name of selected item
        directory: bpy.props.StringProperty(subtype="FILE_PATH")  # directory of the selected item
        files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)  # a collection containing all the selected items as filenames

        # Controls
        my_float1: bpy.props.FloatProperty(name="Scale", default=1.0, description="Changes Scale of the imported Mesh")
        my_bool1: bpy.props.BoolProperty(name="Clear Scene", default=True, description="Deletes everything in the scene prior to importing")

        # Runs when this class OPENS
        def invoke(self, context, event):

            # Retrieve Settings
            try: self.filepath = bpy.types.Scene.offroad_legends_imp_filepath
            except: bpy.types.Scene.offroad_legends_imp_filepath = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.directory = bpy.types.Scene.offroad_legends_imp_directory
            except: bpy.types.Scene.offroad_legends_imp_directory = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.my_float1 = bpy.types.Scene.offroad_legends_imp_my_float1
            except: bpy.types.Scene.offroad_legends_imp_my_float1 = bpy.props.FloatProperty(default=1.0)

            try: self.my_bool1 = bpy.types.Scene.offroad_legends_imp_my_bool1
            except: bpy.types.Scene.offroad_legends_imp_my_bool1 = bpy.props.BoolProperty(default=False)


            # Open File Browser
            # Set Properties of the File Browser
            context.window_manager.fileselect_add(self)
            context.area.tag_redraw()

            return {'RUNNING_MODAL'}

        # Runs when this Window is CANCELLED
        def cancel(self, context): print("run bitch")

        # Runs when the class EXITS
        def execute(self, context):

            # Save Settings
            bpy.types.Scene.offroad_legends_imp_filepath = self.filepath
            bpy.types.Scene.offroad_legends_imp_directory = self.directory
            bpy.types.Scene.offroad_legends_imp_my_float1 = self.my_float1
            bpy.types.Scene.offroad_legends_imp_my_bool1 = self.my_bool1

            # Run Callback
            offroad_legends_imp_callback(self.directory + "\\", self.files, self.my_bool1, self.my_float1)

            return {"FINISHED"}

            # Window Settings

        def draw(self, context):

            self.layout.row().label(text="Import Settings")

            self.layout.separator()
            self.layout.row().prop(self, "my_bool1")
            self.layout.row().prop(self, "my_float1")

            self.layout.separator()

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="  Author:", icon='QUESTION')
            col.alignment = 'LEFT'
            col.label(text="mariokart64n")

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="Release:", icon='GRIP')
            col.alignment = 'LEFT'
            col.label(text="March 16, 2021")

        def menu_func_import(self, context):
            self.layout.operator("importhelper.offroad_legends_imp", text="Off Road Legends (*.geo)")

    # Register Operator
    bpy.utils.register_class(ImportHelper_offroad_legends_imp)
    bpy.types.TOPBAR_MT_file_import.append(ImportHelper_offroad_legends_imp.menu_func_import)

    # Call ImportHelper
    bpy.ops.importhelper.offroad_legends_imp('INVOKE_DEFAULT')


if not useOpenDialog:
    deleteScene(['MESH', 'ARMATURE'])  # Clear Scene
    clearListener()  # clears out console
    read(
        "C:\\Users\\Corey\\Downloads\\Cars The Video Game\\carry_chassis.geo"
        )
    messageBox("Done!")
else:
    offroad_legends_imp(True)

WOW! AMAZING WORK! This work perfectly! Even with UVs and correct position!
Also, can you make it so the script can read the XML of the car? The XML of the cars has wheel positions. (carry.xml)
It's a shame it can't read the data from Off The Road, that has some exclusive cars. I'll see if it works with Offroad Legends 2 though.
Hmm, it's not working with Offroad Legends 2 models. Here's the new Carry from that game,
GEO: https://www.mediafire.com/file/2vbpspby ... s.geo/file
H3D: https://www.mediafire.com/file/hca3t835 ... s.h3d/file

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 6:15 pm

Also here's a list of vehicles that don't load correctly (Offroad Legends 1)
FireStar - Won't even load at all, just says list index out of range.

Reaper (Xperia) - Pieces are out of place.

GoldStar - Only loads front bumper, same error as FireStar.

Hunter - Same error as FireStar.

Jeep - Just says "Done!" and nothing imports.

Karma - Pieces are out of place.

MAZ - Pieces are out of place.

Rainbow - Pieces are out of place.

Smokey - Pieces are out of place.

Too tired to upload all the cars as individual parts, here's a zip of all the broken vehicles.
https://www.mediafire.com/file/0fsz9n9a ... s.zip/file
..Yeah, was excited about this tool, but now a bit disappointed. Oh well, will probably have to stick with the hex2obj method. Using Android files for these, too.
(Also sorry for double-posting again. I felt like this post was too large to fit into my previous post.)

mariokart64n
ultra-veteran
ultra-veteran
Posts: 567
Joined: Sun Jun 05, 2005 12:00 pm
Location: Ontario, Canada
Has thanked: 34 times
Been thanked: 197 times

Re: Offroad Legends GEO/H3D model file, convertor?

Post by mariokart64n » Tue Mar 16, 2021 7:13 pm

traceback error was related to the materials, so i just deleted that section... seems to work on those files now..

Code: Select all

""" ======================================================================

    PythonScript:   [Mobile] Offroad Legends
    Author:         mariokart64n
    Date:           March 16, 2021
    Version:        0.1

    ======================================================================

    ChangeLog:

    2021-03-16
        Script Wrote


    ====================================================================== """

import bpy  # Needed to interface with blender
import struct  # Needed for Binary Reader
import math
from pathlib import Path  # Needed for os stuff
from xml.dom import minidom

useOpenDialog = True


def deleteScene(include=[]):
    if len(include) > 0:
        # Exit and Interactions
        if bpy.context.view_layer.objects.active != None:
            bpy.ops.object.mode_set(mode='OBJECT')

        # Select All
        bpy.ops.object.select_all(action='SELECT')

        # Loop Through Each Selection
        for o in bpy.context.view_layer.objects.selected:
            for t in include:
                if o.type == t:
                    bpy.data.objects.remove(o, do_unlink=True)
                    break

        # De-Select All
        bpy.ops.object.select_all(action='DESELECT')
    return None


def messageBox(message="", title="Message Box", icon='INFO'):
    def draw(self, context): self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None

def doesFileExist(filename):
    file = Path(filename)
    if file.is_file():
        return True
    elif file.is_dir():
        return True
    else:
        return False


def clearListener(len=64):
    for i in range(0, len): print('')



def getFilenamePath(file):  # returns: "g:\subdir1\subdir2\"
    return (str(Path(file).resolve().parent) + "\\")


def getFilenameFile(file):  # returns: "myImage"
    return Path(file).stem


def Bitmaptexture(mat, filename="", name="ShaderNodeTexImage"):
    imageTex = mat.node_tree.nodes.new('ShaderNodeTexImage')
    imageTex.label = name
    try:
        imageTex.image = bpy.data.images.load(
            filepath=filename,
            check_existing=False
        )
        imageTex.image.name = filenameFromPath(filename)
        imageTex.image.colorspace_settings.name = 'sRGB'
    except:
        imageTex.image = bpy.data.images.new(
            name=filename,
            width=8,
            height=8,
            alpha=False,
            float_buffer=False
        )
    return imageTex


class StandardMaterial:
    data = None
    bsdf = None

    def __init__(self, name="Material"):
        # make material
        self.data = bpy.data.materials.new(name=name)
        self.data.use_nodes = True
        self.data.use_backface_culling = True
        self.bsdf = self.data.node_tree.nodes["Principled BSDF"]
        self.bsdf.label = "Standard"
        return None

    def diffuse(self, colour=(0.0, 0.0, 0.0, 0.0), name="Diffuse"):
        rgbaColor = self.data.node_tree.nodes.new('ShaderNodeRGB')
        rgbaColor.label = name
        rgbaColor.outputs[0].default_value = (colour[0], colour[1], colour[2], colour[3])
        if self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], rgbaColor.outputs['Color'])
        return None

    def diffuseMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], imageTex.outputs['Color'])
        return None

    def opacityMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.blend_method = 'BLEND'
            self.data.shadow_method = 'HASHED'
            self.data.show_transparent_back = False
            self.data.node_tree.links.new(self.bsdf.inputs['Alpha'], imageTex.outputs['Alpha'])
        return None

    def normalMap(self, imageNode=None):
        if imageTex != None and self.bsdf != None:
            imageTex.image.colorspace_settings.name = 'Linear'
            normMap = self.data.node_tree.nodes.new('ShaderNodeNormalMap')
            normMap.label = 'ShaderNodeNormalMap'
            self.data.node_tree.links.new(normMap.inputs['Color'], imageTex.outputs['Color'])
            self.data.node_tree.links.new(self.bsdf.inputs['Normal'], normMap.outputs['Normal'])
        return None

    def specularMap(self, imageNode=None, invert=True):
        if imageTex != None and self.bsdf != None:
            if invert:
                invertRGB = self.data.node_tree.nodes.new('ShaderNodeInvert')
                invertRGB.label = 'ShaderNodeInvert'
                self.data.node_tree.links.new(invertRGB.inputs['Color'], imageTex.outputs['Color'])
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], invertRGB.outputs['Color'])
            else:
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], imageTex.outputs['Color'])
        return None


class fopen:
    little_endian = True
    file = ""
    mode = 'rb'
    data = bytearray()
    size = 0
    pos = 0
    isGood = False

    def __init__(self, filename=None, mode='rb', isLittleEndian=True):
        if mode == 'rb':
            if filename != None and Path(filename).is_file():
                self.data = open(filename, mode).read()
                self.size = len(self.data)
                self.pos = 0
                self.mode = mode
                self.file = filename
                self.little_endian = isLittleEndian
                self.isGood = True
        else:
            self.file = filename
            self.mode = mode
            self.data = bytearray()
            self.pos = 0
            self.size = 0
            self.little_endian = isLittleEndian
            self.isGood = False

        return None

    # def __del__(self):
    #    self.flush()

    def resize(self, dataSize=0):
        if dataSize > 0:
            self.data = bytearray(dataSize)
        else:
            self.data = bytearray()
        self.pos = 0
        self.size = dataSize
        self.isGood = False
        return None

    def flush(self):
        print("flush")
        print("file:\t%s" % self.file)
        print("isGood:\t%s" % self.isGood)
        print("size:\t%s" % len(self.data))
        if self.file != "" and not self.isGood and len(self.data) > 0:
            self.isGood = True

            s = open(self.file, 'w+b')
            s.write(self.data)
            s.close()

    def read_and_unpack(self, unpack, size):
        '''
          Charactor, Byte-order
          @,         native, native
          =,         native, standard
          <,         little endian
          >,         big endian
          !,         network

          Format, C-type,         Python-type, Size[byte]
          c,      char,           byte,        1
          b,      signed char,    integer,     1
          B,      unsigned char,  integer,     1
          h,      short,          integer,     2
          H,      unsigned short, integer,     2
          i,      int,            integer,     4
          I,      unsigned int,   integer,     4
          f,      float,          float,       4
          d,      double,         float,       8
        '''
        value = 0
        if self.size > 0 and self.pos + size < self.size:
            value = struct.unpack_from(unpack, self.data, self.pos)[0]
            self.pos += size
        return value

    def pack_and_write(self, pack, size, value):
        if self.pos + size > self.size:
            self.data.extend(b'\x00' * ((self.pos + size) - self.size))
            self.size = self.pos + size
        try:
            struct.pack_into(pack, self.data, self.pos, value)
        except:
            print('Pos:\t%i / %i (buf:%i) [val:%i:%i:%s]' % (self.pos, self.size, len(self.data), value, size, pack))
            pass
        self.pos += size
        return None

    def set_pointer(self, offset):
        self.pos = offset
        return None


def fclose(bitStream):
    bitStream.flush()
    bitStream.isGood = False


def readShort(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'h' if isSigned == 0 else 'H'
    return (bitStream.read_and_unpack(fmt, 2))


def readLong(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'i' if isSigned == 0 else 'I'
    return (bitStream.read_and_unpack(fmt, 4))


def readFloat(bitStream):
    fmt = '>f' if not bitStream.little_endian else '<f'
    return (bitStream.read_and_unpack(fmt, 4))


def mesh_validate (vertices=[], faces=[]):
    #
    # Returns True if mesh is BAD
    #
    # check face index bound
    face_min = 0
    face_max = len(vertices) - 1
    
    for face in faces:
        for side in face:
            if side < face_min or side > face_max:
                print("Face Index Out of Range:\t[%i / %i]" % (side, face_max))
                return True
    return False


def mesh(
    vertices=[],
    faces=[],
    materialIDs=[],
    tverts=[],
    normals=[],
    colours=[],
    materials=[],
    mscale=1.0,
    flipAxis=False,
    obj_name="Object",
    lay_name='',
    position = (0.0, 0.0, 0.0)
    ):
    #
    # This function is pretty, ugly
    # imports the mesh into blender
    #
    # Clear Any Object Selections
    # for o in bpy.context.selected_objects: o.select = False
    bpy.context.view_layer.objects.active = None
    
    # Get Collection (Layers)
    if lay_name != '':
        # make collection
        layer = bpy.data.collections.new(lay_name)
        bpy.context.scene.collection.children.link(layer)
    else:
        layer = bpy.data.collections[bpy.context.view_layer.active_layer_collection.name]

    # make mesh
    msh = bpy.data.meshes.new('Mesh')

    # msh.name = msh.name.replace(".", "_")

    # Apply vertex scaling
    # mscale *= bpy.context.scene.unit_settings.scale_length
    if len(vertices) > 0:
        vertArray = [[float] * 3] * len(vertices)
        if flipAxis:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    -vertices[v][2] * mscale,
                    vertices[v][1] * mscale
                )
        else:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    vertices[v][1] * mscale,
                    vertices[v][2] * mscale
                )

    # assign data from arrays
    msh.from_pydata(vertArray, [], faces)

    # set surface to smooth
    msh.polygons.foreach_set("use_smooth", [True] * len(msh.polygons))

    # Set Normals
    if len(faces) > 0:
        if len(normals) > 0:
            msh.use_auto_smooth = True
            if len(normals) == (len(faces) * 3):
                msh.normals_split_custom_set(normals)
            else:
                normArray = [[float] * 3] * (len(faces) * 3)
                if flipAxis:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 -normals[faces[i][v]][2],
                                 normals[faces[i][v]][1]]
                            )
                else:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 normals[faces[i][v]][1],
                                 normals[faces[i][v]][2]]
                            )
                msh.normals_split_custom_set(normArray)

        # create texture corrdinates
        #print("tverts ", len(tverts))
        # this is just a hack, i just add all the UVs into the same space <<<
        if len(tverts) > 0:
            uvw = msh.uv_layers.new()
            # if len(tverts) == (len(faces) * 3):
            #    for v in range(0, len(faces) * 3):
            #        msh.uv_layers[uvw.name].data[v].uv = tverts[v]
            # else:
            uvwArray = [[float] * 2] * len(tverts[0])
            for i in range(0, len(tverts[0])):
                uvwArray[i] = [0.0, 0.0]

            for v in range(0, len(tverts[0])):
                for i in range(0, len(tverts)):
                    uvwArray[v][0] += tverts[i][v][0]
                    uvwArray[v][1] += 1.0 - tverts[i][v][1]

            for i in range(0, len(faces)):
                for v in range(0, 3):
                    msh.uv_layers[uvw.name].data[(i * 3) + v].uv = (
                        uvwArray[faces[i][v]][0],
                        uvwArray[faces[i][v]][1]
                    )


    # Create Face Maps?
    # msh.face_maps.new()

    # Update Mesh
    msh.update()

    # Check mesh is Valid
    # Without this blender may crash!!! lulz
    # However the check will throw false positives so
    # and additional or a replacement valatiation function
    # would be required
    
    if msh.validate():
        print("Mesh Failed Validation")
        if mesh_validate(vertArray, faces):
            # Erase Mesh
            msh.user_clear()
            bpy.data.meshes.remove(msh)
            print("Mesh Deleted!")
            return None
        

    # Assign Mesh to Object
    obj = bpy.data.objects.new(obj_name, msh)
    obj.location = position
    # obj.name = obj.name.replace(".", "_")

    for i in range(0, len(materials)):

        if len(obj.material_slots) < (i + 1):
            # if there is no slot then we append to create the slot and assign
            obj.data.materials.append(materials[i])
        else:
            # we always want the material in slot[0]
            obj.material_slots[0].material = materials[i]
        # obj.active_material = obj.material_slots[i].material

    if len(materialIDs) == len(obj.data.polygons):
        if len(materialIDs) > 0:
            for i in range(0, len(materialIDs)):
                obj.data.polygons[i].material_index = materialIDs[i]
    elif len(materialIDs) > 0:
        print("Error:\tMaterial Index Out of Range")

    # obj.data.materials.append(material)
    layer.objects.link(obj)

    # Generate a Material
    # img_name = "Test.jpg"  # dummy texture
    # mat_count = len(texmaps)

    # if mat_count == 0 and len(materialIDs) > 0:
    #    for i in range(0, len(materialIDs)):
    #        if (materialIDs[i] + 1) > mat_count: mat_count = materialIDs[i] + 1

    # Assign Material ID's
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    bpy.ops.object.mode_set(mode='OBJECT')
    # materialIDs

    # Redraw Entire Scene
    # bpy.context.scene.update()

    return obj


class h3d_uniform:
    name = ""
    a = 0.0
    b = 0.0
    c = 0.0
    d = 0.0


class h3d_sampler:
    name = ""
    map = ""


class h3d_material:
    name = ""
    sampler = []
    uniform = []


class h3d_geo_block:
    index = 0
    stride = 0
    data = []


class h3d_geo_file:
    fileid = 0  # 0x47443348 'H3DG'
    unk01 = 0
    unk02 = 0
    matrix = ((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))
    unk03 = 0
    vertex_count = 0
    vertices = h3d_geo_block()
    normals = h3d_geo_block()
    texcoord0 = h3d_geo_block()
    texcoord1 = h3d_geo_block()
    face_count = 0
    faces = []

    def __repr__(self):
        return 'VertexCount:\t%i\nFaceCount:\t%i\nUnknowns:\t%i\t%i\t%i' % (
        self.vertex_count, self.face_count, self.unk01, self.unk02, self.unk03)

    def read(self, f=fopen()):
        self.fileid = readLong(f)
        if self.fileid != 0x47443348:
            print("Error:\tInvalid File")
            return False

        self.unk01 = readLong(f)
        self.unk02 = readLong(f)
        self.matrix = (
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f))
        )
        self.unk03 = readLong(f)
        self.vertex_count = readLong(f)

        # Read Vertices
        self.vertices.index = readLong(f)
        self.vertices.stride = readLong(f)

        if self.vertices.stride != 0x0C:
            print("Error:\tInvalid Vertex Stride")
            return False
        comp = int(self.vertices.stride / 4)
        self.vertices.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.vertices.data[x][y] = readFloat(f)

        # Read Normals
        self.normals.index = readLong(f)
        self.normals.stride = readLong(f)
        if self.normals.stride != 0x06:
            print("Error:\tInvalid Normal Stride:\t%i" % self.normals.stride)
            return False

        comp = int(self.normals.stride / 2)
        self.normals.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.normals.data[x][y] = float(readShort(f) / 32767.0)

        # Read UV-0
        self.texcoord0.index = readLong(f)
        self.texcoord0.stride = readLong(f)
        if self.texcoord0.stride != 0x08:
            print("Error:\tInvalid UV0 Stride")
            return False

        comp = int(self.texcoord0.stride / 4)
        self.texcoord0.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord0.data[x][y] = readFloat(f)

        # Read UV-1
        self.texcoord1.index = readLong(f)
        self.texcoord1.stride = readLong(f)
        if self.texcoord1.stride != 0x08:
            print("Error:\tInvalid UV1 Stride")
            return False

        comp = int(self.texcoord1.stride / 4)
        self.texcoord1.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord1.data[x][y] = readFloat(f)

        # Faces
        self.face_count = readLong(f)
        self.faces = [[int] for x in range(self.face_count)]
        for x in range(0, self.face_count):
            self.faces[x] = readLong(f)

        return True


def read_triangle_strip(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    i = 0
    face = [1, 1, 1]
    while i < faceCount:
        faceCW = True
        face[0] = faces[i + facePosition]
        face[1] = faces[i + facePosition + 1]
        i += 2
        while i < faceCount:
            face[2] = faces[i + facePosition]
            if face[2] == 0xFFFF or face[2] == -1: break
            if face[0] != face[2] and face[1] != face[2] and face[2] != face[0]:
                if faceCW:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[1] + faceOffset,
                        face[2] + faceOffset
                    ])
                else:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[2] + faceOffset,
                        face[1] + faceOffset
                    ])
            faceCW = not faceCW
            face = [face[1], face[2], face[0]]
            i += 1
    return None


def read_triangle_list(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    for i in range(0, int(faceCount / 3)):
        faceArray.append([
            faces[(i * 3) + facePosition] + faceOffset,
            faces[(i * 3) + facePosition + 1] + faceOffset,
            faces[(i * 3) + facePosition + 2] + faceOffset
        ])
    return None


def read(file="", mscale=1.0):
    # Check File is present
    if not doesFileExist(file):
        print("Error:\tFailed To Find Geo File")
        return False

    # Strip Paths From fullpath to find sister file
    fpath = getFilenamePath(file)
    fname = getFilenameFile(file)
    h3d_file = fpath + fname + ".h3d"

    # Check if sister file is found
    if not doesFileExist(h3d_file):
        print("Error:\tFailed To Find H3D File")
        return False

    # open GEO file
    f = fopen(file, 'rb')

    # Create GEO Object to store data from Geo file
    geo = h3d_geo_file()

    # Attempt to read GEO file into Geo Class
    read_good = False
    try:
        read_good = geo.read(f)
    except:
        print("Error:\t Failed to Read File")
        return False

    # Check if Geo File Was Read
    if not read_good:
        print("Error:\t Failed to Read File")
        return False

    # Print Geo Class Info
    print(repr(geo))

    # Close Geo File
    fclose(f)

    # Read Sister File, import data
    try:
        h3d = minidom.parse(h3d_file)

        h3d_mat = []
        h3d_mat_index = 0
        h3d_sp = h3d_sampler()
        h3d_un = h3d_uniform()

        for materials in h3d.getElementsByTagName('Materials'):
            for material in materials.getElementsByTagName('Material'):
                h3d_mat.append(h3d_material())

                h3d_mat[h3d_mat_index].name = material.attributes['name'].value

                for sampler in material.getElementsByTagName('Sampler'):
                    h3d_sp = h3d_sampler()
                    try:
                        h3d_sp.name = sampler.attributes['name'].value
                        h3d_sp.map = sampler.attributes['map'].value
                        h3d_mat[h3d_mat_index].sampler.append(h3d_sp)
                    except:
                        pass

                for uniform in material.getElementsByTagName('Uniform'):
                    h3d_un = h3d_uniform()
                    try:
                        h3d_un.name = sampler.attributes['name'].value
                        h3d_un.a = float(sampler.attributes['a'].value)
                        h3d_un.b = float(sampler.attributes['b'].value)
                        h3d_un.c = float(sampler.attributes['c'].value)
                        h3d_un.d = float(sampler.attributes['d'].value)
                        h3d_mat[h3d_mat_index].uniform.append(h3d_un)
                    except:
                        pass
                h3d_mat_index += 1

    except:
        print("Error:\tFailed to Parse XML file")
        return False

    mats = []
    for m in h3d_mat:
        mat = StandardMaterial(m.name)
        for t in m.sampler:
            if t.name == 'albedoMap':
                if '/' in t.map:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map.split('/')[-1:][0]))
                else:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map))
        mats.append(mat.data)

    msh = None
    msh_name = ""
    msh_matn = ""
    msh_tx = 0.0
    msh_ty = 0.0
    msh_tz = 0.0
    msh_batchStart = 0
    msh_batchCount = 0
    msh_vertRStart = 0
    msh_vertREnd = 0
    msh_vertRCount = 0
    mat_index = 0
    mat_name = ""
    vertArray = []
    normArray = []
    uvw0Array = []
    uvw1Array = []
    faceArray = []
    matidArray = []
    for gmesh in h3d.getElementsByTagName('Mesh'):
        try:
            msh_name = gmesh.attributes['name'].value
        except:
            pass
        try:
            msh_matn = gmesh.attributes['material'].value
        except:
            pass
        try:
            msh_tx = float(gmesh.attributes['tx'].value)
        except:
            pass
        try:
            msh_ty = float(gmesh.attributes['ty'].value)
        except:
            pass
        try:
            msh_tz = float(gmesh.attributes['tz'].value)
        except:
            pass
        try:
            msh_batchStart = int(gmesh.attributes['batchStart'].value)
        except:
            pass
        try:
            msh_batchCount = int(gmesh.attributes['batchCount'].value)
        except:
            pass
        try:
            msh_vertRStart = int(gmesh.attributes['vertRStart'].value)
        except:
            pass
        try:
            msh_vertREnd = int(gmesh.attributes['vertREnd'].value)
        except:
            pass

        mat_index = 0
        if '#' in msh_matn:
            mat_name = msh_matn.split('#')[-1:][0]
            for i in range(0, len(h3d_mat)):
                if h3d_mat[i].name == mat_name:
                    mat_index = i
                    break

        matidArray = [mat_index for i in range(int(msh_batchCount / 3))]

        msh_vertRCount = msh_vertREnd - msh_vertRStart + 1
        vertArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        normArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw0Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw1Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])

        for i in range(0, msh_vertRCount):
            vertArray[i] = geo.vertices.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            normArray[i] = geo.normals.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw0Array[i] = geo.texcoord0.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw1Array[i] = geo.texcoord1.data[msh_vertRStart + i]

        faceArray = []
        read_triangle_list(geo.faces, faceArray, msh_batchCount, msh_batchStart, -msh_vertRStart)

        msh = mesh(
            vertices=vertArray,
            tverts=[uvw0Array, uvw1Array],
            normals=normArray,
            faces=faceArray,
            obj_name=msh_name,
            flipAxis=True,
            mscale=mscale,
            materials=mats,
            materialIDs=matidArray,
            position=(msh_tx, -msh_tz, msh_ty)
        )

    return True


# Callback when file(s) are selected
def offroad_legends_imp_callback(fpath="", files=[], clearScene=True, mscale = 1.0):
    if len(files) > 0 and clearScene: deleteScene(['MESH', 'ARMATURE'])
    for file in files:
        read(fpath + file.name, mscale)
    if len(files) > 0:
        messageBox("Done!")
        return True
    else:
        return False


# Wrapper that Invokes FileSelector to open files from blender
def offroad_legends_imp(reload=False):
    # Un-Register Operator
    if reload and hasattr(bpy.types, "IMPORTHELPER_OT_offroad_legends_imp"):  # print(bpy.ops.importhelper.offroad_legends_imp.idname())

        try:
            bpy.types.TOPBAR_MT_file_import.remove(
                bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp').menu_func_import)
        except:
            print("Failed to Unregister2")

        try:
            bpy.utils.unregister_class(bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp'))
        except:
            print("Failed to Unregister1")

    # Define Operator
    class ImportHelper_offroad_legends_imp(bpy.types.Operator):

        # Operator Path
        bl_idname = "importhelper.offroad_legends_imp"
        bl_label = "Select File"

        # Operator Properties
        # filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
        filter_glob: bpy.props.StringProperty(default='*.geo', options={'HIDDEN'}, subtype='FILE_PATH')

        # Variables
        filepath: bpy.props.StringProperty(subtype="FILE_PATH")  # full path of selected item (path+filename)
        filename: bpy.props.StringProperty(subtype="FILE_NAME")  # name of selected item
        directory: bpy.props.StringProperty(subtype="FILE_PATH")  # directory of the selected item
        files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)  # a collection containing all the selected items as filenames

        # Controls
        my_float1: bpy.props.FloatProperty(name="Scale", default=1.0, description="Changes Scale of the imported Mesh")
        my_bool1: bpy.props.BoolProperty(name="Clear Scene", default=True, description="Deletes everything in the scene prior to importing")

        # Runs when this class OPENS
        def invoke(self, context, event):

            # Retrieve Settings
            try: self.filepath = bpy.types.Scene.offroad_legends_imp_filepath
            except: bpy.types.Scene.offroad_legends_imp_filepath = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.directory = bpy.types.Scene.offroad_legends_imp_directory
            except: bpy.types.Scene.offroad_legends_imp_directory = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.my_float1 = bpy.types.Scene.offroad_legends_imp_my_float1
            except: bpy.types.Scene.offroad_legends_imp_my_float1 = bpy.props.FloatProperty(default=1.0)

            try: self.my_bool1 = bpy.types.Scene.offroad_legends_imp_my_bool1
            except: bpy.types.Scene.offroad_legends_imp_my_bool1 = bpy.props.BoolProperty(default=False)


            # Open File Browser
            # Set Properties of the File Browser
            context.window_manager.fileselect_add(self)
            context.area.tag_redraw()

            return {'RUNNING_MODAL'}

        # Runs when this Window is CANCELLED
        def cancel(self, context): print("run bitch")

        # Runs when the class EXITS
        def execute(self, context):

            # Save Settings
            bpy.types.Scene.offroad_legends_imp_filepath = self.filepath
            bpy.types.Scene.offroad_legends_imp_directory = self.directory
            bpy.types.Scene.offroad_legends_imp_my_float1 = self.my_float1
            bpy.types.Scene.offroad_legends_imp_my_bool1 = self.my_bool1

            # Run Callback
            offroad_legends_imp_callback(self.directory + "\\", self.files, self.my_bool1, self.my_float1)

            return {"FINISHED"}

            # Window Settings

        def draw(self, context):

            self.layout.row().label(text="Import Settings")

            self.layout.separator()
            self.layout.row().prop(self, "my_bool1")
            self.layout.row().prop(self, "my_float1")

            self.layout.separator()

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="  Author:", icon='QUESTION')
            col.alignment = 'LEFT'
            col.label(text="mariokart64n")

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="Release:", icon='GRIP')
            col.alignment = 'LEFT'
            col.label(text="March 16, 2021")

        def menu_func_import(self, context):
            self.layout.operator("importhelper.offroad_legends_imp", text="Off Road Legends (*.geo)")

    # Register Operator
    bpy.utils.register_class(ImportHelper_offroad_legends_imp)
    bpy.types.TOPBAR_MT_file_import.append(ImportHelper_offroad_legends_imp.menu_func_import)

    # Call ImportHelper
    bpy.ops.importhelper.offroad_legends_imp('INVOKE_DEFAULT')


if not useOpenDialog:
    deleteScene(['MESH', 'ARMATURE'])  # Clear Scene
    clearListener()  # clears out console
    read(
        "C:\\Users\\Corey\\Downloads\\Cars The Video Game\\OL_Brokencars\\BrokenCars\\goldstar\\monster_wheel.geo"
        )
    messageBox("Done!")
else:
    offroad_legends_imp(True)

Maxscript and other finished work I've done can be found on my DeviantArt account

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 7:57 pm

mariokart64n wrote:
Tue Mar 16, 2021 7:13 pm
traceback error was related to the materials, so i just deleted that section... seems to work on those files now..

Code: Select all

""" ======================================================================

    PythonScript:   [Mobile] Offroad Legends
    Author:         mariokart64n
    Date:           March 16, 2021
    Version:        0.1

    ======================================================================

    ChangeLog:

    2021-03-16
        Script Wrote


    ====================================================================== """

import bpy  # Needed to interface with blender
import struct  # Needed for Binary Reader
import math
from pathlib import Path  # Needed for os stuff
from xml.dom import minidom

useOpenDialog = True


def deleteScene(include=[]):
    if len(include) > 0:
        # Exit and Interactions
        if bpy.context.view_layer.objects.active != None:
            bpy.ops.object.mode_set(mode='OBJECT')

        # Select All
        bpy.ops.object.select_all(action='SELECT')

        # Loop Through Each Selection
        for o in bpy.context.view_layer.objects.selected:
            for t in include:
                if o.type == t:
                    bpy.data.objects.remove(o, do_unlink=True)
                    break

        # De-Select All
        bpy.ops.object.select_all(action='DESELECT')
    return None


def messageBox(message="", title="Message Box", icon='INFO'):
    def draw(self, context): self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None

def doesFileExist(filename):
    file = Path(filename)
    if file.is_file():
        return True
    elif file.is_dir():
        return True
    else:
        return False


def clearListener(len=64):
    for i in range(0, len): print('')



def getFilenamePath(file):  # returns: "g:\subdir1\subdir2\"
    return (str(Path(file).resolve().parent) + "\\")


def getFilenameFile(file):  # returns: "myImage"
    return Path(file).stem


def Bitmaptexture(mat, filename="", name="ShaderNodeTexImage"):
    imageTex = mat.node_tree.nodes.new('ShaderNodeTexImage')
    imageTex.label = name
    try:
        imageTex.image = bpy.data.images.load(
            filepath=filename,
            check_existing=False
        )
        imageTex.image.name = filenameFromPath(filename)
        imageTex.image.colorspace_settings.name = 'sRGB'
    except:
        imageTex.image = bpy.data.images.new(
            name=filename,
            width=8,
            height=8,
            alpha=False,
            float_buffer=False
        )
    return imageTex


class StandardMaterial:
    data = None
    bsdf = None

    def __init__(self, name="Material"):
        # make material
        self.data = bpy.data.materials.new(name=name)
        self.data.use_nodes = True
        self.data.use_backface_culling = True
        self.bsdf = self.data.node_tree.nodes["Principled BSDF"]
        self.bsdf.label = "Standard"
        return None

    def diffuse(self, colour=(0.0, 0.0, 0.0, 0.0), name="Diffuse"):
        rgbaColor = self.data.node_tree.nodes.new('ShaderNodeRGB')
        rgbaColor.label = name
        rgbaColor.outputs[0].default_value = (colour[0], colour[1], colour[2], colour[3])
        if self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], rgbaColor.outputs['Color'])
        return None

    def diffuseMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], imageTex.outputs['Color'])
        return None

    def opacityMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.blend_method = 'BLEND'
            self.data.shadow_method = 'HASHED'
            self.data.show_transparent_back = False
            self.data.node_tree.links.new(self.bsdf.inputs['Alpha'], imageTex.outputs['Alpha'])
        return None

    def normalMap(self, imageNode=None):
        if imageTex != None and self.bsdf != None:
            imageTex.image.colorspace_settings.name = 'Linear'
            normMap = self.data.node_tree.nodes.new('ShaderNodeNormalMap')
            normMap.label = 'ShaderNodeNormalMap'
            self.data.node_tree.links.new(normMap.inputs['Color'], imageTex.outputs['Color'])
            self.data.node_tree.links.new(self.bsdf.inputs['Normal'], normMap.outputs['Normal'])
        return None

    def specularMap(self, imageNode=None, invert=True):
        if imageTex != None and self.bsdf != None:
            if invert:
                invertRGB = self.data.node_tree.nodes.new('ShaderNodeInvert')
                invertRGB.label = 'ShaderNodeInvert'
                self.data.node_tree.links.new(invertRGB.inputs['Color'], imageTex.outputs['Color'])
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], invertRGB.outputs['Color'])
            else:
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], imageTex.outputs['Color'])
        return None


class fopen:
    little_endian = True
    file = ""
    mode = 'rb'
    data = bytearray()
    size = 0
    pos = 0
    isGood = False

    def __init__(self, filename=None, mode='rb', isLittleEndian=True):
        if mode == 'rb':
            if filename != None and Path(filename).is_file():
                self.data = open(filename, mode).read()
                self.size = len(self.data)
                self.pos = 0
                self.mode = mode
                self.file = filename
                self.little_endian = isLittleEndian
                self.isGood = True
        else:
            self.file = filename
            self.mode = mode
            self.data = bytearray()
            self.pos = 0
            self.size = 0
            self.little_endian = isLittleEndian
            self.isGood = False

        return None

    # def __del__(self):
    #    self.flush()

    def resize(self, dataSize=0):
        if dataSize > 0:
            self.data = bytearray(dataSize)
        else:
            self.data = bytearray()
        self.pos = 0
        self.size = dataSize
        self.isGood = False
        return None

    def flush(self):
        print("flush")
        print("file:\t%s" % self.file)
        print("isGood:\t%s" % self.isGood)
        print("size:\t%s" % len(self.data))
        if self.file != "" and not self.isGood and len(self.data) > 0:
            self.isGood = True

            s = open(self.file, 'w+b')
            s.write(self.data)
            s.close()

    def read_and_unpack(self, unpack, size):
        '''
          Charactor, Byte-order
          @,         native, native
          =,         native, standard
          <,         little endian
          >,         big endian
          !,         network

          Format, C-type,         Python-type, Size[byte]
          c,      char,           byte,        1
          b,      signed char,    integer,     1
          B,      unsigned char,  integer,     1
          h,      short,          integer,     2
          H,      unsigned short, integer,     2
          i,      int,            integer,     4
          I,      unsigned int,   integer,     4
          f,      float,          float,       4
          d,      double,         float,       8
        '''
        value = 0
        if self.size > 0 and self.pos + size < self.size:
            value = struct.unpack_from(unpack, self.data, self.pos)[0]
            self.pos += size
        return value

    def pack_and_write(self, pack, size, value):
        if self.pos + size > self.size:
            self.data.extend(b'\x00' * ((self.pos + size) - self.size))
            self.size = self.pos + size
        try:
            struct.pack_into(pack, self.data, self.pos, value)
        except:
            print('Pos:\t%i / %i (buf:%i) [val:%i:%i:%s]' % (self.pos, self.size, len(self.data), value, size, pack))
            pass
        self.pos += size
        return None

    def set_pointer(self, offset):
        self.pos = offset
        return None


def fclose(bitStream):
    bitStream.flush()
    bitStream.isGood = False


def readShort(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'h' if isSigned == 0 else 'H'
    return (bitStream.read_and_unpack(fmt, 2))


def readLong(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'i' if isSigned == 0 else 'I'
    return (bitStream.read_and_unpack(fmt, 4))


def readFloat(bitStream):
    fmt = '>f' if not bitStream.little_endian else '<f'
    return (bitStream.read_and_unpack(fmt, 4))


def mesh_validate (vertices=[], faces=[]):
    #
    # Returns True if mesh is BAD
    #
    # check face index bound
    face_min = 0
    face_max = len(vertices) - 1
    
    for face in faces:
        for side in face:
            if side < face_min or side > face_max:
                print("Face Index Out of Range:\t[%i / %i]" % (side, face_max))
                return True
    return False


def mesh(
    vertices=[],
    faces=[],
    materialIDs=[],
    tverts=[],
    normals=[],
    colours=[],
    materials=[],
    mscale=1.0,
    flipAxis=False,
    obj_name="Object",
    lay_name='',
    position = (0.0, 0.0, 0.0)
    ):
    #
    # This function is pretty, ugly
    # imports the mesh into blender
    #
    # Clear Any Object Selections
    # for o in bpy.context.selected_objects: o.select = False
    bpy.context.view_layer.objects.active = None
    
    # Get Collection (Layers)
    if lay_name != '':
        # make collection
        layer = bpy.data.collections.new(lay_name)
        bpy.context.scene.collection.children.link(layer)
    else:
        layer = bpy.data.collections[bpy.context.view_layer.active_layer_collection.name]

    # make mesh
    msh = bpy.data.meshes.new('Mesh')

    # msh.name = msh.name.replace(".", "_")

    # Apply vertex scaling
    # mscale *= bpy.context.scene.unit_settings.scale_length
    if len(vertices) > 0:
        vertArray = [[float] * 3] * len(vertices)
        if flipAxis:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    -vertices[v][2] * mscale,
                    vertices[v][1] * mscale
                )
        else:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    vertices[v][1] * mscale,
                    vertices[v][2] * mscale
                )

    # assign data from arrays
    msh.from_pydata(vertArray, [], faces)

    # set surface to smooth
    msh.polygons.foreach_set("use_smooth", [True] * len(msh.polygons))

    # Set Normals
    if len(faces) > 0:
        if len(normals) > 0:
            msh.use_auto_smooth = True
            if len(normals) == (len(faces) * 3):
                msh.normals_split_custom_set(normals)
            else:
                normArray = [[float] * 3] * (len(faces) * 3)
                if flipAxis:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 -normals[faces[i][v]][2],
                                 normals[faces[i][v]][1]]
                            )
                else:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 normals[faces[i][v]][1],
                                 normals[faces[i][v]][2]]
                            )
                msh.normals_split_custom_set(normArray)

        # create texture corrdinates
        #print("tverts ", len(tverts))
        # this is just a hack, i just add all the UVs into the same space <<<
        if len(tverts) > 0:
            uvw = msh.uv_layers.new()
            # if len(tverts) == (len(faces) * 3):
            #    for v in range(0, len(faces) * 3):
            #        msh.uv_layers[uvw.name].data[v].uv = tverts[v]
            # else:
            uvwArray = [[float] * 2] * len(tverts[0])
            for i in range(0, len(tverts[0])):
                uvwArray[i] = [0.0, 0.0]

            for v in range(0, len(tverts[0])):
                for i in range(0, len(tverts)):
                    uvwArray[v][0] += tverts[i][v][0]
                    uvwArray[v][1] += 1.0 - tverts[i][v][1]

            for i in range(0, len(faces)):
                for v in range(0, 3):
                    msh.uv_layers[uvw.name].data[(i * 3) + v].uv = (
                        uvwArray[faces[i][v]][0],
                        uvwArray[faces[i][v]][1]
                    )


    # Create Face Maps?
    # msh.face_maps.new()

    # Update Mesh
    msh.update()

    # Check mesh is Valid
    # Without this blender may crash!!! lulz
    # However the check will throw false positives so
    # and additional or a replacement valatiation function
    # would be required
    
    if msh.validate():
        print("Mesh Failed Validation")
        if mesh_validate(vertArray, faces):
            # Erase Mesh
            msh.user_clear()
            bpy.data.meshes.remove(msh)
            print("Mesh Deleted!")
            return None
        

    # Assign Mesh to Object
    obj = bpy.data.objects.new(obj_name, msh)
    obj.location = position
    # obj.name = obj.name.replace(".", "_")

    for i in range(0, len(materials)):

        if len(obj.material_slots) < (i + 1):
            # if there is no slot then we append to create the slot and assign
            obj.data.materials.append(materials[i])
        else:
            # we always want the material in slot[0]
            obj.material_slots[0].material = materials[i]
        # obj.active_material = obj.material_slots[i].material

    if len(materialIDs) == len(obj.data.polygons):
        if len(materialIDs) > 0:
            for i in range(0, len(materialIDs)):
                obj.data.polygons[i].material_index = materialIDs[i]
    elif len(materialIDs) > 0:
        print("Error:\tMaterial Index Out of Range")

    # obj.data.materials.append(material)
    layer.objects.link(obj)

    # Generate a Material
    # img_name = "Test.jpg"  # dummy texture
    # mat_count = len(texmaps)

    # if mat_count == 0 and len(materialIDs) > 0:
    #    for i in range(0, len(materialIDs)):
    #        if (materialIDs[i] + 1) > mat_count: mat_count = materialIDs[i] + 1

    # Assign Material ID's
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    bpy.ops.object.mode_set(mode='OBJECT')
    # materialIDs

    # Redraw Entire Scene
    # bpy.context.scene.update()

    return obj


class h3d_uniform:
    name = ""
    a = 0.0
    b = 0.0
    c = 0.0
    d = 0.0


class h3d_sampler:
    name = ""
    map = ""


class h3d_material:
    name = ""
    sampler = []
    uniform = []


class h3d_geo_block:
    index = 0
    stride = 0
    data = []


class h3d_geo_file:
    fileid = 0  # 0x47443348 'H3DG'
    unk01 = 0
    unk02 = 0
    matrix = ((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))
    unk03 = 0
    vertex_count = 0
    vertices = h3d_geo_block()
    normals = h3d_geo_block()
    texcoord0 = h3d_geo_block()
    texcoord1 = h3d_geo_block()
    face_count = 0
    faces = []

    def __repr__(self):
        return 'VertexCount:\t%i\nFaceCount:\t%i\nUnknowns:\t%i\t%i\t%i' % (
        self.vertex_count, self.face_count, self.unk01, self.unk02, self.unk03)

    def read(self, f=fopen()):
        self.fileid = readLong(f)
        if self.fileid != 0x47443348:
            print("Error:\tInvalid File")
            return False

        self.unk01 = readLong(f)
        self.unk02 = readLong(f)
        self.matrix = (
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f)),
            (readFloat(f), readFloat(f), readFloat(f), readFloat(f))
        )
        self.unk03 = readLong(f)
        self.vertex_count = readLong(f)

        # Read Vertices
        self.vertices.index = readLong(f)
        self.vertices.stride = readLong(f)

        if self.vertices.stride != 0x0C:
            print("Error:\tInvalid Vertex Stride")
            return False
        comp = int(self.vertices.stride / 4)
        self.vertices.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.vertices.data[x][y] = readFloat(f)

        # Read Normals
        self.normals.index = readLong(f)
        self.normals.stride = readLong(f)
        if self.normals.stride != 0x06:
            print("Error:\tInvalid Normal Stride:\t%i" % self.normals.stride)
            return False

        comp = int(self.normals.stride / 2)
        self.normals.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.normals.data[x][y] = float(readShort(f) / 32767.0)

        # Read UV-0
        self.texcoord0.index = readLong(f)
        self.texcoord0.stride = readLong(f)
        if self.texcoord0.stride != 0x08:
            print("Error:\tInvalid UV0 Stride")
            return False

        comp = int(self.texcoord0.stride / 4)
        self.texcoord0.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord0.data[x][y] = readFloat(f)

        # Read UV-1
        self.texcoord1.index = readLong(f)
        self.texcoord1.stride = readLong(f)
        if self.texcoord1.stride != 0x08:
            print("Error:\tInvalid UV1 Stride")
            return False

        comp = int(self.texcoord1.stride / 4)
        self.texcoord1.data = ([[[float] for x in range(comp)] for y in range(self.vertex_count)])
        for x in range(0, self.vertex_count):
            for y in range(0, comp):
                self.texcoord1.data[x][y] = readFloat(f)

        # Faces
        self.face_count = readLong(f)
        self.faces = [[int] for x in range(self.face_count)]
        for x in range(0, self.face_count):
            self.faces[x] = readLong(f)

        return True


def read_triangle_strip(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    i = 0
    face = [1, 1, 1]
    while i < faceCount:
        faceCW = True
        face[0] = faces[i + facePosition]
        face[1] = faces[i + facePosition + 1]
        i += 2
        while i < faceCount:
            face[2] = faces[i + facePosition]
            if face[2] == 0xFFFF or face[2] == -1: break
            if face[0] != face[2] and face[1] != face[2] and face[2] != face[0]:
                if faceCW:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[1] + faceOffset,
                        face[2] + faceOffset
                    ])
                else:
                    faceArray.append([
                        face[0] + faceOffset,
                        face[2] + faceOffset,
                        face[1] + faceOffset
                    ])
            faceCW = not faceCW
            face = [face[1], face[2], face[0]]
            i += 1
    return None


def read_triangle_list(faces=[], faceArray=[], faceCount=0, facePosition=0, faceOffset=0):
    for i in range(0, int(faceCount / 3)):
        faceArray.append([
            faces[(i * 3) + facePosition] + faceOffset,
            faces[(i * 3) + facePosition + 1] + faceOffset,
            faces[(i * 3) + facePosition + 2] + faceOffset
        ])
    return None


def read(file="", mscale=1.0):
    # Check File is present
    if not doesFileExist(file):
        print("Error:\tFailed To Find Geo File")
        return False

    # Strip Paths From fullpath to find sister file
    fpath = getFilenamePath(file)
    fname = getFilenameFile(file)
    h3d_file = fpath + fname + ".h3d"

    # Check if sister file is found
    if not doesFileExist(h3d_file):
        print("Error:\tFailed To Find H3D File")
        return False

    # open GEO file
    f = fopen(file, 'rb')

    # Create GEO Object to store data from Geo file
    geo = h3d_geo_file()

    # Attempt to read GEO file into Geo Class
    read_good = False
    try:
        read_good = geo.read(f)
    except:
        print("Error:\t Failed to Read File")
        return False

    # Check if Geo File Was Read
    if not read_good:
        print("Error:\t Failed to Read File")
        return False

    # Print Geo Class Info
    print(repr(geo))

    # Close Geo File
    fclose(f)

    # Read Sister File, import data
    try:
        h3d = minidom.parse(h3d_file)

        h3d_mat = []
        h3d_mat_index = 0
        h3d_sp = h3d_sampler()
        h3d_un = h3d_uniform()

        for materials in h3d.getElementsByTagName('Materials'):
            for material in materials.getElementsByTagName('Material'):
                h3d_mat.append(h3d_material())

                h3d_mat[h3d_mat_index].name = material.attributes['name'].value

                for sampler in material.getElementsByTagName('Sampler'):
                    h3d_sp = h3d_sampler()
                    try:
                        h3d_sp.name = sampler.attributes['name'].value
                        h3d_sp.map = sampler.attributes['map'].value
                        h3d_mat[h3d_mat_index].sampler.append(h3d_sp)
                    except:
                        pass

                for uniform in material.getElementsByTagName('Uniform'):
                    h3d_un = h3d_uniform()
                    try:
                        h3d_un.name = sampler.attributes['name'].value
                        h3d_un.a = float(sampler.attributes['a'].value)
                        h3d_un.b = float(sampler.attributes['b'].value)
                        h3d_un.c = float(sampler.attributes['c'].value)
                        h3d_un.d = float(sampler.attributes['d'].value)
                        h3d_mat[h3d_mat_index].uniform.append(h3d_un)
                    except:
                        pass
                h3d_mat_index += 1

    except:
        print("Error:\tFailed to Parse XML file")
        return False

    mats = []
    for m in h3d_mat:
        mat = StandardMaterial(m.name)
        for t in m.sampler:
            if t.name == 'albedoMap':
                if '/' in t.map:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map.split('/')[-1:][0]))
                else:
                    mat.diffuseMap(Bitmaptexture(mat.data, filename=t.map))
        mats.append(mat.data)

    msh = None
    msh_name = ""
    msh_matn = ""
    msh_tx = 0.0
    msh_ty = 0.0
    msh_tz = 0.0
    msh_batchStart = 0
    msh_batchCount = 0
    msh_vertRStart = 0
    msh_vertREnd = 0
    msh_vertRCount = 0
    mat_index = 0
    mat_name = ""
    vertArray = []
    normArray = []
    uvw0Array = []
    uvw1Array = []
    faceArray = []
    matidArray = []
    for gmesh in h3d.getElementsByTagName('Mesh'):
        try:
            msh_name = gmesh.attributes['name'].value
        except:
            pass
        try:
            msh_matn = gmesh.attributes['material'].value
        except:
            pass
        try:
            msh_tx = float(gmesh.attributes['tx'].value)
        except:
            pass
        try:
            msh_ty = float(gmesh.attributes['ty'].value)
        except:
            pass
        try:
            msh_tz = float(gmesh.attributes['tz'].value)
        except:
            pass
        try:
            msh_batchStart = int(gmesh.attributes['batchStart'].value)
        except:
            pass
        try:
            msh_batchCount = int(gmesh.attributes['batchCount'].value)
        except:
            pass
        try:
            msh_vertRStart = int(gmesh.attributes['vertRStart'].value)
        except:
            pass
        try:
            msh_vertREnd = int(gmesh.attributes['vertREnd'].value)
        except:
            pass

        mat_index = 0
        if '#' in msh_matn:
            mat_name = msh_matn.split('#')[-1:][0]
            for i in range(0, len(h3d_mat)):
                if h3d_mat[i].name == mat_name:
                    mat_index = i
                    break

        matidArray = [mat_index for i in range(int(msh_batchCount / 3))]

        msh_vertRCount = msh_vertREnd - msh_vertRStart + 1
        vertArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        normArray = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw0Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])
        uvw1Array = ([[[float] for x in range(3)] for y in range(msh_vertRCount)])

        for i in range(0, msh_vertRCount):
            vertArray[i] = geo.vertices.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            normArray[i] = geo.normals.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw0Array[i] = geo.texcoord0.data[msh_vertRStart + i]

        for i in range(0, msh_vertRCount):
            uvw1Array[i] = geo.texcoord1.data[msh_vertRStart + i]

        faceArray = []
        read_triangle_list(geo.faces, faceArray, msh_batchCount, msh_batchStart, -msh_vertRStart)

        msh = mesh(
            vertices=vertArray,
            tverts=[uvw0Array, uvw1Array],
            normals=normArray,
            faces=faceArray,
            obj_name=msh_name,
            flipAxis=True,
            mscale=mscale,
            materials=mats,
            materialIDs=matidArray,
            position=(msh_tx, -msh_tz, msh_ty)
        )

    return True


# Callback when file(s) are selected
def offroad_legends_imp_callback(fpath="", files=[], clearScene=True, mscale = 1.0):
    if len(files) > 0 and clearScene: deleteScene(['MESH', 'ARMATURE'])
    for file in files:
        read(fpath + file.name, mscale)
    if len(files) > 0:
        messageBox("Done!")
        return True
    else:
        return False


# Wrapper that Invokes FileSelector to open files from blender
def offroad_legends_imp(reload=False):
    # Un-Register Operator
    if reload and hasattr(bpy.types, "IMPORTHELPER_OT_offroad_legends_imp"):  # print(bpy.ops.importhelper.offroad_legends_imp.idname())

        try:
            bpy.types.TOPBAR_MT_file_import.remove(
                bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp').menu_func_import)
        except:
            print("Failed to Unregister2")

        try:
            bpy.utils.unregister_class(bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_offroad_legends_imp'))
        except:
            print("Failed to Unregister1")

    # Define Operator
    class ImportHelper_offroad_legends_imp(bpy.types.Operator):

        # Operator Path
        bl_idname = "importhelper.offroad_legends_imp"
        bl_label = "Select File"

        # Operator Properties
        # filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
        filter_glob: bpy.props.StringProperty(default='*.geo', options={'HIDDEN'}, subtype='FILE_PATH')

        # Variables
        filepath: bpy.props.StringProperty(subtype="FILE_PATH")  # full path of selected item (path+filename)
        filename: bpy.props.StringProperty(subtype="FILE_NAME")  # name of selected item
        directory: bpy.props.StringProperty(subtype="FILE_PATH")  # directory of the selected item
        files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)  # a collection containing all the selected items as filenames

        # Controls
        my_float1: bpy.props.FloatProperty(name="Scale", default=1.0, description="Changes Scale of the imported Mesh")
        my_bool1: bpy.props.BoolProperty(name="Clear Scene", default=True, description="Deletes everything in the scene prior to importing")

        # Runs when this class OPENS
        def invoke(self, context, event):

            # Retrieve Settings
            try: self.filepath = bpy.types.Scene.offroad_legends_imp_filepath
            except: bpy.types.Scene.offroad_legends_imp_filepath = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.directory = bpy.types.Scene.offroad_legends_imp_directory
            except: bpy.types.Scene.offroad_legends_imp_directory = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.my_float1 = bpy.types.Scene.offroad_legends_imp_my_float1
            except: bpy.types.Scene.offroad_legends_imp_my_float1 = bpy.props.FloatProperty(default=1.0)

            try: self.my_bool1 = bpy.types.Scene.offroad_legends_imp_my_bool1
            except: bpy.types.Scene.offroad_legends_imp_my_bool1 = bpy.props.BoolProperty(default=False)


            # Open File Browser
            # Set Properties of the File Browser
            context.window_manager.fileselect_add(self)
            context.area.tag_redraw()

            return {'RUNNING_MODAL'}

        # Runs when this Window is CANCELLED
        def cancel(self, context): print("run bitch")

        # Runs when the class EXITS
        def execute(self, context):

            # Save Settings
            bpy.types.Scene.offroad_legends_imp_filepath = self.filepath
            bpy.types.Scene.offroad_legends_imp_directory = self.directory
            bpy.types.Scene.offroad_legends_imp_my_float1 = self.my_float1
            bpy.types.Scene.offroad_legends_imp_my_bool1 = self.my_bool1

            # Run Callback
            offroad_legends_imp_callback(self.directory + "\\", self.files, self.my_bool1, self.my_float1)

            return {"FINISHED"}

            # Window Settings

        def draw(self, context):

            self.layout.row().label(text="Import Settings")

            self.layout.separator()
            self.layout.row().prop(self, "my_bool1")
            self.layout.row().prop(self, "my_float1")

            self.layout.separator()

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="  Author:", icon='QUESTION')
            col.alignment = 'LEFT'
            col.label(text="mariokart64n")

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="Release:", icon='GRIP')
            col.alignment = 'LEFT'
            col.label(text="March 16, 2021")

        def menu_func_import(self, context):
            self.layout.operator("importhelper.offroad_legends_imp", text="Off Road Legends (*.geo)")

    # Register Operator
    bpy.utils.register_class(ImportHelper_offroad_legends_imp)
    bpy.types.TOPBAR_MT_file_import.append(ImportHelper_offroad_legends_imp.menu_func_import)

    # Call ImportHelper
    bpy.ops.importhelper.offroad_legends_imp('INVOKE_DEFAULT')


if not useOpenDialog:
    deleteScene(['MESH', 'ARMATURE'])  # Clear Scene
    clearListener()  # clears out console
    read(
        "C:\\Users\\Corey\\Downloads\\Cars The Video Game\\OL_Brokencars\\BrokenCars\\goldstar\\monster_wheel.geo"
        )
    messageBox("Done!")
else:
    offroad_legends_imp(True)

That fixes most of the vehicles, but the others still have problems. The vehicles with the pieces out of place, you can fix that by placing the chassis to 0, 0, 0. But we need to find out the root of that. As for Jeep (Wheely), it just refuses to load. It just says "Done!".

Karpati
ultra-veteran
ultra-veteran
Posts: 359
Joined: Thu Dec 07, 2006 11:25 pm
Has thanked: 7 times
Been thanked: 71 times

Re: Offroad Legends GEO/H3D model file, convertor?

Post by Karpati » Tue Mar 16, 2021 8:01 pm

mariokart64n,

This game uses the Horde 3D engine:
http://horde3d.org/docs/manual.html

I have used the .geo file format information about 6 years ago in my program.

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 9:08 pm

Karpati wrote:
Tue Mar 16, 2021 8:01 pm
mariokart64n,

This game uses the Horde 3D engine:
http://horde3d.org/docs/manual.html

I have used the .geo file format information about 6 years ago in my program.
Which program?

User avatar
shakotay2
MEGAVETERAN
MEGAVETERAN
Posts: 3326
Joined: Fri Apr 20, 2012 9:24 am
Location: Nexus, searching for Jim Kirk
Has thanked: 908 times
Been thanked: 1819 times

Re: Offroad Legends GEO/H3D model file, convertor?

Post by shakotay2 » Tue Mar 16, 2021 9:20 pm

ReVolt wrote:
Tue Mar 16, 2021 7:57 pm
As for Jeep (Wheely), it just refuses to load. It just says "Done!".
If it's only this one try
.
Jeep_chassis.png
(But guess you know meanwhile how to get uvs' address.)
You do not have the required permissions to view the files attached to this post.
Bigchillghost, Reverse Engineering a Game Model: viewtopic.php?f=29&t=17889
extracting simple models: viewtopic.php?f=29&t=10894
Make_H2O-ForzaHor3-jm9.zip
"You quoted the whole thing, what a mess."

User avatar
ReVolt
veteran
Posts: 107
Joined: Tue Jun 16, 2020 2:21 am
Location: My hard-drive
Has thanked: 10 times
Been thanked: 1 time
Contact:

Re: Offroad Legends GEO/H3D model file, convertor?

Post by ReVolt » Tue Mar 16, 2021 9:33 pm

shakotay2 wrote:
Tue Mar 16, 2021 9:20 pm
ReVolt wrote:
Tue Mar 16, 2021 7:57 pm
As for Jeep (Wheely), it just refuses to load. It just says "Done!".
If it's only this one try
.
Jeep_chassis.png
(But guess you know meanwhile how to get uvs' address.)
The script also doesn't work with Offroad Legends 2. By that, I mean all the cars, they won't load. (also I do know how to get uv address now)
Well, sampling time!
The new carry:
GEO: https://www.mediafire.com/file/2vbpspby ... s.geo/file
H3D: https://www.mediafire.com/file/hca3t835 ... s.h3d/file
Edit: Also we still need to figure out the position of the wheels. I tried using the XML from the files but got this... (Also the Y and Z is swapped.)
Image

Post Reply