Important information: this site is currently scheduled to go offline indefinitely by end of the year.
Offroad Legends GEO/H3D model file, convertor?
- shakotay2
- MEGAVETERAN
- Posts: 4291
- Joined: Fri Apr 20, 2012 9:24 am
- Location: Nexus, searching for Jim Kirk
- Has thanked: 1151 times
- Been thanked: 2244 times
Re: Offroad Legends GEO/H3D model file, convertor?
File/SaveAs mesh
Tuts: a) Bigchillghost, viewtopic.php?f=29&t=17889
b) Extracting simple models: http://forum.xentax.com/viewtopic.php?f=29&t=10894
"Quoting the whole thing. Would u ever stop this nonsense?"
b) Extracting simple models: http://forum.xentax.com/viewtopic.php?f=29&t=10894
"Quoting the whole thing. Would u ever stop this nonsense?"
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
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.
-
- ultra-veteran
- Posts: 586
- Joined: Sun Jun 05, 2005 12:00 pm
- Location: Ontario, Canada
- Has thanked: 36 times
- Been thanked: 243 times
Re: Offroad Legends GEO/H3D model file, convertor?
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
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
WOW! AMAZING WORK! This work perfectly! Even with UVs and correct position!mariokart64n wrote: ↑Tue Mar 16, 2021 4:16 pm
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 restartedCode: 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)
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.
-
- ultra-veteran
- Posts: 586
- Joined: Sun Jun 05, 2005 12:00 pm
- Location: Ontario, Canada
- Has thanked: 36 times
- Been thanked: 243 times
Re: Offroad Legends GEO/H3D model file, convertor?
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
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
Oops, I forgot to get the XML of carry.mariokart64n wrote: ↑Tue Mar 16, 2021 4:56 pm I did not notice any carry.xml file so I can't help you
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>
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
Hmm, it's not working with Offroad Legends 2 models. Here's the new Carry from that game,ReVolt wrote: ↑Tue Mar 16, 2021 4:48 pmWOW! AMAZING WORK! This work perfectly! Even with UVs and correct position!mariokart64n wrote: ↑Tue Mar 16, 2021 4:16 pm
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 restartedCode: 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)
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.
GEO: https://www.mediafire.com/file/2vbpspby ... s.geo/file
H3D: https://www.mediafire.com/file/hca3t835 ... s.h3d/file
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
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.)
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.)
-
- ultra-veteran
- Posts: 586
- Joined: Sun Jun 05, 2005 12:00 pm
- Location: Ontario, Canada
- Has thanked: 36 times
- Been thanked: 243 times
Re: Offroad Legends GEO/H3D model file, convertor?
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
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
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!".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)
-
- ultra-veteran
- Posts: 467
- Joined: Thu Dec 07, 2006 11:25 pm
- Has thanked: 9 times
- Been thanked: 95 times
Re: Offroad Legends GEO/H3D model file, convertor?
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.
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.
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
Which program?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.
- shakotay2
- MEGAVETERAN
- Posts: 4291
- Joined: Fri Apr 20, 2012 9:24 am
- Location: Nexus, searching for Jim Kirk
- Has thanked: 1151 times
- Been thanked: 2244 times
Re: Offroad Legends GEO/H3D model file, convertor?
If it's only this one try
. (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.
Tuts: a) Bigchillghost, viewtopic.php?f=29&t=17889
b) Extracting simple models: http://forum.xentax.com/viewtopic.php?f=29&t=10894
"Quoting the whole thing. Would u ever stop this nonsense?"
b) Extracting simple models: http://forum.xentax.com/viewtopic.php?f=29&t=10894
"Quoting the whole thing. Would u ever stop this nonsense?"
- ReVolt
- veteran
- Posts: 158
- Joined: Tue Jun 16, 2020 2:21 am
- Location: My hard-drive
- Has thanked: 19 times
- Been thanked: 5 times
- Contact:
Re: Offroad Legends GEO/H3D model file, convertor?
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.)