This was my final solution. The actual file format is pretty simple. It contains a 4 byte header ("UPE1"), 4 byte integer containing the vertex count, 4 byte integer containing face count, 4 byte integer containing texture count.
It then contains a 4 byte integer which represents the size of the string immediately after. (this method is consistent through out the file). This also ends up being the objects name.
After that, begins a block describing texture names, the diffuse image location, and the bump map location (using the same formula of 4 byte number indicating the length of the string following it). If there is no data then the number is "0". In some cases there is not a diffuse/bump image pair and that is indicated by hex 0x454C4946 (which is ascii for "FILE"). If the "FILE" marker is found the next 4 bytes are the length of the string referencing the file location.
After all that is done, all the vertex info is written as a block of 48 bytes, with 4 bytes each of: Vertex X,Y,Z , Normals X,Y,Z, U, V, Face vertex ref. 1, Face vertex ref. 2, face vertex ref. 3, texture number of the face.
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8-80 compliant>
import bpy
import bpy.props
import bpy.types
import os
import struct
import io
import bmesh
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
from bpy.types import Operator
# ************ ADD ON INFO ************
bl_info = {
"name": "Ultimate Pigg Explorer UPE format",
"author": "Phillip Proctor / Growlybear Productions",
"version": (2, 3, 1),
"blender": (2, 77, 0),
"location": "File > Import-Export",
"description": "Import UPE mesh, UV's, materials and textures, and images.",
"warning": "",
"wiki_url": "http://growlybearproductions.com",
"support": 'OFFICIAL',
"category": "Import-Export"}
# ************* IMPORT THE UPE FILE *************
def Import_UPE(self,context, CrypticGeoFile):
# ACTUAL IMPORT GOES HERE
self.report({'INFO'}, 'Function not implemented')
return {'FINISHED'}
# ************ SHOW THE OPEN FILE DIALOG *************
# ****** MAIN PROGRAM CLASS *****
class UPE_File_Import(Operator, ImportHelper):
os.system("cls") # clear the system console for now. Remove for production.
# IMPORT UPE FILE
bl_idname = "import_test.some_data"
bl_label = "Import UPE"
# IMPORTHELPER MIXIN CLASS USES THIS
filename_ext = ".upe"
# LIST OF OPERATOR PROPERTIES, THE ATTRIBUTES WILL BE ASSIGNED
# TO THE CLASS INSTANCE FROM THE OPERATOR SETTINGS BEFORE CALLING.
use_setting = BoolProperty(name="Translate",
description="Translate to blender coordinates (Swap z and y axix)",
default=True,)
use_crazybump = BoolProperty(name="CrazyBump",
description="Create nodes for crazybump maps",
default=True,)
global_scale = FloatProperty(name="Scale",
min=0.001, max=1000.0,
default=1.0,)
filter_glob = StringProperty(default="*.upe",options={'HIDDEN'})
filepath = bpy.props.StringProperty(subtype="FILE_PATH")
# **** MAIN PROGRAM EXECUTION ****
def execute(self, context):
# SET UP THE LIST ARRAYS TO HOLD OUT DATA
MyVertex = []
MyFaces = []
MyTextureNames = []
MyTextures = []
MyNormals = []
MyUvs = []
# OPEN THE FILE
fdata = open(self.filepath,"rb")
# READ THE FILE HEADER
Header = fdata.read(4).decode()
# SEE IF HEADER IS VALID
if Header != "UPE1":
# HEADER INVALID, THROW A MESSAGE AND BAIL
print('Invalid UPE header - ' + Header)
return {'CANCELLED'}
# GET THE NUMBER OF VERTICIES
VertexCount = struct.unpack('i',fdata.read(4))[0]
# GET THE NUMBER OF FACES
FaceCount = struct.unpack('i',fdata.read(4))[0]
# GET THE NUMBER OF MATERIALS
MtlCount = struct.unpack('i',fdata.read(4))[0]
# GET THE LENGTH OF THE STRING CONTAINING MODEL NAME
StrLen = struct.unpack('i',fdata.read(4))[0]
# GET THE MODEL NAME STRING
ModelName = fdata.read(StrLen).decode()
# CREATE A NEW OBJECT TO WORK WITH
me = bpy.data.meshes.new(ModelName)
obj = bpy.data.objects.new(ModelName,me)
obj.location = bpy.context.scene.cursor_location
scn = bpy.context.scene
scn.objects.link(obj)
scn.objects.active = obj
obj.select = True
# *** PROCESS AND CREATE THE TEXTURES ***
# CYCLE THROUGH MATERIALS
for textures in range(0, MtlCount):
# GET THE LENGTH OF THE NAME OF THIS MATERIAL
StrLen = struct.unpack('i',fdata.read(4))[0]
# READ THE MATERIAL NAME FROM THE FILE
mtlname = fdata.read(StrLen).decode()
# ADD TO THE LIST OF MATERIAL NAMES. <-- Do we really need this?
MyTextureNames.append(mtlname)
# CREATE NEW MATERIAL WITH NAME
new_mat = bpy.data.materials.new(mtlname)
# TELL THE MATERIAL TO USE NODES
new_mat.use_nodes = True
# ADD THE MATERIAL TO THE MATERIAL SLOTS
bpy.ops.object.material_slot_add()
# ASSIGN IT AS DEFAULT
nodes = new_mat.node_tree.nodes
# Clear all nodes so we can start from scratch
for node in nodes:
nodes.remove(node)
# Create new output node:
node_output = nodes.new(type='ShaderNodeOutputMaterial')
node_output.location = 0,0
# CREATE DIFFUSE NODE
node_diffuse = nodes.new(type='ShaderNodeBsdfDiffuse')
node_diffuse.name = "Diffuse"
node_diffuse.label = "Diffuse"
node_diffuse.location = -400,-0
# CREATE BUMP NODE
node_bump = nodes.new(type='ShaderNodeBump')
node_bump.name = "Bump"
node_bump.label = "Bump"
node_bump.location = -800,-200
# CREATE DIFFUSE IMAGE NODE
node_diffuse_image = nodes.new(type='ShaderNodeTexImage')
node_diffuse_image.name = "Diffuse Image"
node_diffuse_image.label = "Diffuse Image"
node_diffuse_image.location = -800, 200
# CREATE BUMP IMAGE NODE
node_bump_image = nodes.new(type='ShaderNodeTexImage')
node_bump_image.name = "Bump Image"
node_bump_image.label = "Bump Image"
node_bump_image.location = -1200,-500
node_bump_image.color_space = 'NONE'
# LINK THE NODES
links = new_mat.node_tree.links
# LINK DIFFUSE OUTPUT TO INPUT OF OUTPUT NODE
link = links.new(node_diffuse.outputs[0], node_output.inputs[0])
# CONNECT DIFFUSE IMAGE OUT TO DIFFUSE SHADER IN
link = links.new(node_diffuse_image.outputs[0], node_diffuse.inputs[0])
# CONNECT BUMP OUT TO DIFFUSE NORMAL IN
link = links.new(node_bump.outputs[0], node_diffuse.inputs[2])
# CONNECT THE BUMP IMAGE TO THE BUMP NODE
link = links.new(node_bump_image.outputs[0], node_bump.inputs[2])
# After texture names there should be a realative reference to the file to load. Grab that now.
# Get the next 4 bytes. If they are zero then this material had no images.
ChkByte = struct.unpack('i',fdata.read(4))[0]
if ChkByte !=0:
# check to see if its a FILE tag
if ChkByte != 0x454C4946:
# ChkByte is the size of the next string
RealativePath = fdata.read(ChkByte).decode()
# Set the difuse image here.
# self.filepath + RealativePath = image
DiffuseImage = os.path.dirname(self.filepath) + RealativePath
node_diffuse_image.image = bpy.data.images.load(DiffuseImage)
# Images should come in pairs so lets check for a bump map:
ChkByte = struct.unpack('i',fdata.read(4))[0]
if ChkByte != 0:
# We have a bump map so lets grab it.
RealativePath = fdata.read(ChkByte).decode()
# Set the bump map image here
DiffuseImage = os.path.dirname(self.filepath) + RealativePath
node_bump_image.image = bpy.data.images.load(DiffuseImage)
else:
# This was a TGA material so load the diffuse only
ChkByte = struct.unpack('i',fdata.read(4))[0]
if ChkByte != 0:
# We have a bump map so lets grab it.
RealativePath = fdata.read(ChkByte).decode()
# Set the bump map image here
DiffuseImage = os.path.dirname(self.filepath) + RealativePath
node_diffuse_image.image = bpy.data.images.load(DiffuseImage)
# we should be back on track with our file now... but for some reason we are not :(
# Create the new node and assign the material to the object
context.object.active_material = new_mat
# PROCESS VERTEX DATA
for Verticies in range(0, VertexCount):
# READ VERTEX 1
x1 = struct.unpack('f',fdata.read(4))[0]
y1 = struct.unpack('f',fdata.read(4))[0]
z1 = struct.unpack('f',fdata.read(4))[0]
nx1 = struct.unpack('f',fdata.read(4))[0]
ny1 = struct.unpack('f',fdata.read(4))[0]
nz1 = struct.unpack('f',fdata.read(4))[0]
u1 = struct.unpack('f',fdata.read(4))[0]
v1 = struct.unpack('f',fdata.read(4))[0]
# SAVE TO ARRAYS APPLYING SCALE
MyVertex.append((x1 * self.global_scale,y1 * self.global_scale,z1 * self.global_scale))
MyUvs.append((u1,v1))
# PROCESS FACES
for Faces in range(0,FaceCount):
v1 = struct.unpack('i',fdata.read(4))[0]
v2 = struct.unpack('i',fdata.read(4))[0]
v3 = struct.unpack('i',fdata.read(4))[0]
tx = struct.unpack('i',fdata.read(4))[0]
MyFaces.append((v1,v2,v3))
MyTextures.append(tx)
# ADD VERTS AND FACES WITH THE FOLLOWING LINE
me.from_pydata(MyVertex, [], MyFaces)
# ASSIGN THE TEXTURES TO THE FACES
i = 0
for poly in bpy.context.object.data.polygons:
poly.material_index = MyTextures[i]
i += 1
# ADD THE UV's
ctx = bpy.context.object.data
ctx.uv_textures.new("default")
ctx.uv_layers[-1].data.foreach_set("uv", [uv for pair in [MyUvs[l.vertex_index] for l in ctx.loops] for uv in pair])
# FLIP THE MODEL ON THE X AXIS SO Z IS UP IF SELECETED
if self.use_setting == True:
bpy.ops.transform.rotate(value=1.5708, axis=(1,0,0))
me.update()
fdata.close
return {'FINISHED'}
# ONLY NEEDED IF YOU WANT TO ADD INTO A DYNAMIC MENU
def menu_func_import(self, context):
self.layout.operator(UPE_File_Import.bl_idname, text="Import UPE")
# REGISTER THE ADDON
def register():
bpy.utils.register_class(UPE_File_Import)
bpy.types.INFO_MT_file_import.append(menu_func_import)
# UNREGISTER THE ADDON
def unregister():
bpy.utils.unregister_class(UPE_File_Import)
bpy.types.INFO_MT_file_import.remove(menu_func_import)
# EXECUTE IF RUN AS SCRIPT
if __name__ == "__main__":
register()
Note that this is not complete, I am still working a few bugs out, and have a few features to add (like the crazy bump thing) however it does load some files correctly so the heavy lifting of creating the object, textures, uv's etc all seem to be OK. For the moment I don't do anything with the normals. It seems blender does a better job at figuring them out than the data that is provided.