Can I export vertex coordinates, normals and UV mapping to a specific format?

A warning to start with: I do not know any Python at all. I’m sure I can learn some, but I’m starting from scratch with this language. I think that what I want to do is easy enough, but I’d greatly appreciate some pointers regarding the process.

What I am hoping to do is use Blender to model game assets for an old game some diehards are still addicted to. I’m new to Blender but after reading the Noob to Pro tuts, searching this forum, and playing around a bit I’m finding it easy to use and very effective.

The catch is that this old game (Railroad Tycoon 3, if anyone cares) uses a custom file format. It’s called .3dp but is not related to the common 3DProfessional format. It was based on the output of the old Autodesk .3ds format, but rewritten to overcome some of the well-known limitations of .3ds.

I completely understand the RT3 .3dp file format. Everything in it is a known quantity. I know what every byte does. Apart from some trivial headers, all it contains is a list of all vertex coordinates and normals, and numbering for the vertices, along with the relevant UV mapping. The entire file is in Little Endian hex, with the vertex numbering being done as integers and the rest of the data being done as single precision floating. Anyone who has seen a .3ds file in a hex editor will recognise the similarities.

Having read the Noob to Pro tutorials I realise it is simple to write a basic script to export all vertex coordinates. I assume it is equally easy to export vertex normals, vertex numbering, and coordinates for UV mapping. What I would like to know is how to export this data in the particular order required by this file format, and how to get the export into Little Endian hex with floats or integers to suit.

To hopefully make things clearer I have attached a screenshot of a simple file with the desired output format. Each block is colour coded and its purpose noted. Apologies for the tiny text, but my editor doesn’t allow custom text sizing in that area.

Attachments


I’ve been working with binary data in a custom format. The first thing I did was to define the data types. You’ll also need to import some, or all if you’d like, of the “struct” module.


from struct import Struct, calcsize, pack, unpack
try:
    from struct import *
except:
    calcsize= unpack= None

ByteChar = 'B'
LongWordChar = '<I'
SingleChar = '<f'
WordChar = '<H'
IntegerChar = '<i'
Byte = struct.Struct(ByteChar)
Char = struct.Struct('<c')             #Single Character
Word = struct.Struct(WordChar)             #Unsigned Integer, 16 bits
Single = struct.Struct(SingleChar)           #Floating Point, 32 bits
Integer = struct.Struct(IntegerChar)      #Signed Integer, 32 bits
LongWord = struct.Struct(LongWordChar)         #Unsigned Integer, 32 bits

You will have to define the data types needed for your project.

I then created a class I called TGenericData, which provides a mechanism to store the data, and to read and write to it.


class TGenericData():
    Offset = LongWord
    Index = LongWord
    InternalOffset1 = LongWord
    InternalOffset2 = LongWord
    InternalOffset3 = LongWord
    InternalOffset4 = LongWord
    Count = LongWord
    InternalCount = LongWord
    Data = bytearray()

    def __init__(self):
        self.Offset = 0
        self.Index = 0
        self.Count = 0
        self.InternalCount = 0
        self.InternalOffset = 0
        self.InternalOffset1 = 0
        self.InternalOffset2 = 0
        self.InternalOffset3 = 0
        self.InternalOffset4 = 0
        self.Data = bytearray()
                
    def ReadByte(self):
        self.Offset += 1
        self.Index += 1
        return self.Data[self.Offset-1]

    def AddByte(self, value):
        self.Data.extend(Byte.pack(value))
        self.Index += 1
        self.Offset += 1

    def ReadWord(self):
        self.Offset += 2
        self.Index += 1
        return struct.unpack(WordChar, self.Data[self.Offset-2:self.Offset])

    def AddWord(self, value):
        self.Data.extend(Word.pack(value))
        self.Index += 1
        self.Offset += 2

    def ReadInteger(self):
        self.Index += 1
        self.Offset += 4
        return struct.unpack(IntegerChar , self.Data[self.Offset-4:self.Offset])
                 
    def AddInteger(self, value):
        self.Data.extend(Integer.pack(value))
        self.Index += 1
        self.Offset += 4

    def ReadLongWord(self):
        self.Index += 1
        self.Offset += 4
        return struct.unpack(LongWordChar , self.Data[self.Offset-4:self.Offset])

    def AddLongWord(self, value):
        self.Data.extend(LongWord.pack(value))
        self.Index += 1
        self.Offset += 4

    def InsertLongWord(self, i, value):
        v=LongWord.pack(value)
        self.Data[i]=v[0]
        self.Data[i+1]=v[1]
        self.Data[i+2]=v[2]
        self.Data[i+3]=v[3]

    def ReadSingle(self):      
        self.Index += 1
        self.Offset += 4
        return struct.unpack(SingleChar , self.Data[self.Offset-4:self.Offset])

    def AddSingle(self, value):
        self.Data.extend(Single.pack(value))
        self.Index += 1
        self.Offset += 4

    def AddUV(self,value):
        self.Data.extend(Single.pack(value.u))
        self.Data.extend(Single.pack(value.v))
        self.Index += 2
        self.Offset += 8
        
    def AddUVIndex(self,value):
        self.Data.extend(Single.pack(value[0]))
        self.Data.extend(Single.pack(value[1]))
        self.Index += 2
        self.Offset += 8

    def ReadXZY(self):
        value = MyStuff.TPoint3D()
        value.X = self.ReadSingle()
        value.Z = self.ReadSingle()
        value.Y = self.ReadSingle()
        return value

    def AddXZYas021(self,Coordinates):
        self.Data.extend(Single.pack(Coordinates[0]))
        self.Data.extend(Single.pack(Coordinates[2]))
        self.Data.extend(Single.pack(Coordinates[1]))
        self.Index += 3
        self.Offset += 12
        
    def AddXZY(self,Coordinates):
        self.Data.extend(Single.pack(Coordinates.X))
        self.Data.extend(Single.pack(Coordinates.Z))
        self.Data.extend(Single.pack(Coordinates.Y))
        self.Index += 3
        self.Offset += 12

    def ReadColor(self):
        value = TMyColor()
        value.R = self.ReadSingle()
        value.G = self.ReadSingle()
        value.B = self.ReadSingle()
        value.A = self.ReadSingle()
        return value

    def ReadVColor(self):
        value = TMyVColor(0,0,0,0)
        value.R = self.ReadByte()
        value.G = self.ReadByte()
        value.B = self.ReadByte()
        value.A = self.ReadByte()
        return value

    def AddVColor(self,VColor):
        self.Data.extend(Byte.pack(VColor.R))
        self.Data.extend(Byte.pack(VColor.G))
        self.Data.extend(Byte.pack(VColor.B))
        self.Data.extend(Byte.pack(VColor.A))
        self.Index += 4
        self.Offset += 4

    def AddColorIndex(self,Color):
        self.Data.extend(Single.pack(Color[0]))
        self.Data.extend(Single.pack(Color[1]))
        self.Data.extend(Single.pack(Color[2]))
        self.Data.extend(Single.pack(Color[3]))
        self.Index += 4
        self.Offset += 16

    def AddColorRGBA(self,Color):
        self.Data.extend(Single.pack(Color[0]))
        self.Data.extend(Single.pack(Color[1]))
        self.Data.extend(Single.pack(Color[2]))
        self.Data.extend(Single.pack(Color[3]))
        self.Index += 4
        self.Offset += 16


So, in your code, you’d have something along the lines of “MyObject = TGenericData()” to declare your data structure. You’d then write to it by “MyObject.AddByte(100)” or whatever variable contains the byte data. The same would go for the other data types as well. Reading a value from MyObject would be along the lines of “MySingle = MyObject.ReadSingle()”, etc. In my project, some data is not known until after the fact-- and after its spot in the data stream has been defined, so I wrote the “InsertLongWord(Offset, Value)” function to handle those cases. It overwrites the data at a particular location. You might not need that. I convert text to byte values, then write the byte values. Lastly, I have several different data blocks to deal with, so that is why there are multiple variables in TGenericData() to keep track of offsets within the data structure.

You can check your work by reading existing files and plugging the data in to Blender.

Reading data from a file is along the lines of:


    file=open(filenane,'rb')
    MyObject.Data = file.read(-1)
    file.close()

Writing data to a file is along the lines of:


    MyFile = open(filename,'wb')
    MyFile.write(MyObject.Data)
    MyFile.close()

That’s the basic start, and the approach I took. Those more skilled in Python may have a different or better method. I, too, learned Python through the course of my project.

Thanks for the response. I’m an old carpenter, not a programmer, but I did manage to get myself a useful working knowledge of PHP some time back just by applying enough coffee and swearing, so I figured while I was waiting I should jump in and have a go at Python.

Following the tutorials, checking the API pages, and extrapolating a bit with sheer guesswork I’ve managed to get Python to generate a list of basic data for the default cube model.

>>> for v in cubedata.vertices: print(v.index, v.co, v.normal)
... 
0 <Vector (1.0000, 1.0000, -1.0000)> <Vector (0.5773, 0.5773, -0.5773)>
1 <Vector (1.0000, -1.0000, -1.0000)> <Vector (0.5773, -0.5773, -0.5773)>
2 <Vector (-1.0000, -1.0000, -1.0000)> <Vector (-0.5773, -0.5773, -0.5773)>
3 <Vector (-1.0000, 1.0000, -1.0000)> <Vector (-0.5773, 0.5773, -0.5773)>
4 <Vector (1.0000, 1.0000, 1.0000)> <Vector (0.5773, 0.5773, 0.5773)>
5 <Vector (1.0000, -1.0000, 1.0000)> <Vector (0.5773, -0.5773, 0.5773)>
6 <Vector (-1.0000, -1.0000, 1.0000)> <Vector (-0.5773, -0.5773, 0.5773)>
7 <Vector (-1.0000, 1.0000, 1.0000)> <Vector (-0.5773, 0.5773, 0.5773)>

>>> p=cubedata.polygons[0]
>>> p.vertices
bpy.data.meshes['Cube'].polygons[0].vertices

>>> for p in cubedata.polygons: print(p.vertices)
... 
<bpy_int[4], MeshPolygon.vertices>
<bpy_int[4], MeshPolygon.vertices>
<bpy_int[4], MeshPolygon.vertices>
<bpy_int[4], MeshPolygon.vertices>
<bpy_int[4], MeshPolygon.vertices>
<bpy_int[4], MeshPolygon.vertices>

Obviously this in just in basic human-readable syntax but I wanted to start getting some grasp of things. Incidentally the example in the wiki for calling faces seems to be deprecated, since running it just gives an error message. Also “faces” isn’t listed under dir(cubedata) even though it still seems to be listed on the API pages.

Anyway, I can get the console to spit out vertices, indexing for them, and normals for them. I can also sort of see how, but stringing things together, I could spit out that data in the order I want it. No luck so far with polygons though. I can get a basic list of them but nothing in the way of relevant detail.

I also dug around in the Blender installation folder to take a look at the existing .3ds export script (export_3ds.py) since the basics of the .3ds format are similar to what I want. It looks like a modification of that script would do the trick nicely. For my purposes the modified script could be much simpler since I require far less data (no need for materials, etc) but OTOH I require normals, which .3ds doesn’t appear to use.

The code for the existing export script is nicely written and commented, and I can get a vague overall sense of what each code block is doing, but obviously I’ll have to swot up on the details.

Regarding UVs / .faces, you may wanna read this:

Thanks. Read it and bookmarked it for later.

Meh. I’m gonna have to knuckle down and learn this stuff. I have a model almost ready to go but am being stopped by not having a suitable export script. Expect more questions soon. :confused:

For future reference here’s what I am needing for a finished output.

The first section of the file is just a pile of vertex coordinates arranged as

Vertex0_X Vertex0_Y Vertex0_Z Vertex1_X Vertex1_Y Vertex1_Z Vertex2_X Vertex2_Y Vertex2_Z etc.


I have pretty much got a handle on generating this lot, but I need it as four byte floats instead of standard decimal.

The graphics section just follows straight on from the list of all vertex declarations, with no intervening header between sections.


Graphics section is list of tris, normals and UV’s arranged as:

Vertex0_index Vertex1_index Vertex2_index

Vertex0_normalX Vertex0_normalY Vertex0_normalZ

Vertex1_normalX Vertex1_normalY Vertex1_normalZ

Vertex2_normalX Vertex2_normalY Vertex2_normalZ

Vertex0_U Vertex0_V Vertex1_U Vertex1_V Vertex2_U Vertex2_V

Four byte padding (can apparently be left at 00 00 00 00 for all tris).


All of this section is in floats as well, apart from the indexing numbers for each tri.

None of this stuff has any line breaks between declarations. It’s all just one after the other.

The only catch is that the V declarations for UV mapping have to be outputted as V = (1-V) due to the game engine requiring V=0 to be at the top left of the skin, instead of the bottom left that Blender uses.

Maybe like this:

import bpy
import bmesh
import struct
from binascii import unhexlify

class QUAD:
    BEAUTY = 0
    FIXED = 1
    ALTERNATE = 2
    SHORTEDGE = 3

class NGON:
    BEAUTY = 0
    EARCLIP = 1

filepath = "path/to/file.3dp"
with open(filepath, "wb") as file:
    
    file.write(unhexlify(
        "33 44 50 46  04 00 01 00  33 44 4D 44  01 00 00 00"
        "00 00 3E C0  ED 1E 5F C1  09 0A 4D 40  49 4E 53 54"
        "00 00 00 00  00 00 00 00".replace(" ", ""))
    )    
    verts_offset = file.tell()

    scene = bpy.context.scene
    
    verts_total = sum(len(ob.data.vertices) for ob in scene.objects if ob.type == 'MESH')
    faces_offset = verts_total * 12 + verts_offset
    
    vc = 0
    tc = 0

    for ob in scene.objects:
        if ob.type != 'MESH':
            continue
        bm = bmesh.new()
        bm.from_object(ob, scene)
        bm.transform(ob.matrix_world)
        bmesh.ops.triangulate(bm, faces=bm.faces, quad_method=QUAD.BEAUTY, ngon_method=NGON.BEAUTY)
        uv_layer = bm.loops.layers.uv.active
        
        file.seek(verts_offset)
        for v in bm.verts:
            file.write(struct.pack("<3f", *v.co))
        verts_offset = file.tell()
        
        file.seek(faces_offset)
        for f in bm.faces:
            for i in range(3):
                file.write(struct.pack("<i", f.verts[i].index + vc))
            for i in range(3):
                file.write(struct.pack("<3f", *f.loops[i].calc_normal()))
            for i in range(3):
                u, v = f.loops[i][uv_layer].uv
                file.write(struct.pack("<2f", u, 1-v))
            file.write(b"\0" * 4)

        faces_offset = file.tell()
        vc += len(bm.verts)
        tc += len(bm.faces)
        bm.free()
        
    file.seek(32)
    file.write(struct.pack("<2i", vc, tc))

I wonder however how materials are assigned…

Thanks for the reply.

No worries about materials. They’re assigned by a separate file. The game engine just takes the skin name from the other file (the .car file for each locomotive, tender, or cargo car, if anyone cares) and applies that skin to the vertices and UV mapping from the .3dp.

Also, there are no quads or ngons in the .3dp mesh. I’ll be triangulating everything before trying to export, so that will be taken care of anyway. The export script won’t require any code to deal with ngons or quads.

I’ll probably also merge all relevant objects into one before exporting. The assets will have several components that need to be dealt with by separate files anyway. For instance, pistons have their own .3dp files, as do wheels (basically, anything that needs its own animation). So what I’ll do is build the whole model and then haul out the bits that need to be separate, then export those individually from their own .blend files. What’s left can then all be merged into one object prior to exporting.

My script takes all meshes in the scene and exports them as a single 3dp file (therefore the vertex counter vc: it offsets vertex indices). It already does triangulation using the beauty method for quads and ngons, so you don’t have to do that to your model manually.

I made it use loop normals to enable mixed flat and smooth shaded surfaces (needs Auto-Smooth to be enabled, see http://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.74/Modeling and the linked manual page). You could also use vertex normals or face normals.

Note that the script will throw an error if there’s no active uv map. Would be better to catch that error and inform the user.

Here’s a quick importer:

import bpy
import struct
import os

filepath = "path/to/file.3dp"

with open(filepath, 'rb') as file:
    verts = []
    faces = []
    file.seek(32)
    vert_count, face_count = struct.unpack("<2i", file.read(8))
    
    for i in range(vert_count):
        vert = struct.unpack("<3f", file.read(12))
        verts.append(vert)
    
    for i in range(face_count):
        face = struct.unpack("<3i", file.read(12))
        file.seek(64, os.SEEK_CUR)
        faces.append(face)
        
    me = bpy.data.meshes.new("imported_3dp")
    me.from_pydata(verts, [], faces)
    me.update(True)
    me.calc_normals()
    me.validate()
    ob = bpy.data.objects.new("imported_3dp", me)
    scene = bpy.context.scene
    scene.objects.link(ob)
    scene.update()
    

It imports verts and faces only, no support for normals and UV mapping. It shows however, that multiple meshes are handled correctly by the export script, and that everything is triangulated automatically.

Fair enough. I’m OCD so will probably check it all manually anyway, since sometimes you can get a better division by doing some triangulation manually. With these models being essentially fairly simple (IMO anyway) it’s not that much of drama.

Still, I suppose having it able to automatically deal with quads and ngons would be a very handy fallback for the script. Hopefully other people will use it too, and not everyone is as OCD as I am. :smiley:

I made it use loop normals to enable mixed flat and smooth shaded surfaces (needs Auto-Smooth to be enabled, see http://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.74/Modeling and the linked manual page). You could also use vertex normals or face normals.
This game engine requires vertex normals. Face normals will bork it. It won’t be able to understand them.

It’ll just take the vertex normals and apply Gouraud shading accordingly. It won’t know or care if the original .blend has the faces smooth shaded or flat shaded. As long as the vertex normals are in the .3dp that’s all that matters.

Note that the script will throw an error if there’s no active uv map. Would be better to catch that error and inform the user.
Good point. Not essential, but I suppose it would be a nice touch to make it more idiot-proof.

Hey I was looking through your code, just because I’d like to understand how it works. It is mostly making sense to me even though I know no Python at all (I’m mostly used to PHP and javascript). Like this bit:

        file.seek(verts_offset)
        for v in bm.verts:
            file.write(struct.pack("<3f", *v.co))

Pretty obvious that just grabs verts one at a time, then writes out the coordinates in groups of three floats. “<3f” is a really neat and simple syntax for doing the float conversion. I like it.

Anyway, near the start you have:

filepath = "path/to/file.3dp"

So I assume that would mean I have to set up a blank .3dp file with the name I want the finished file to have, then use the path to that to get the export script to write to the blank file.

If that’s correct, it makes sense. I don’t think there’s any need to have the export script generate the file from scratch. Starting with a blank one is fine.

I suppose the export script could be made to generate the opening header of the .3dp too, but those are easy to do manually.

The beauty triangulation setting gives very nice results, but sure, there are cases in which you want to actively control it. You can cut the faces in question into triangles yourself, but still leave the rest untouched (e.g. planar quads). They will be handled automatically.

Regarding normals: all types of normals will work, the game engine won’t reject them or anything. The visual result can differ however. These are the expectable results:

Face normals: Regardless of flat/smooth shading in Blender, every vertex would use the same (face) normal and thus result in a flat-shaded appearance in the game. I presume non-planar quads and ngons will look flat due the same (“average”) normal being used.

Vertex normals: I think models will look smooth shaded, regardless of their shading setting in Blender.

Loop normals: Shading as set in Blender should be respected, flat-shaded surfaces should look flat and smooth look smooth even if mixed in a single mesh. There might be problem with my code not doing this right, which seems actually to be a Blender bug (bmesh’s calc_normal() doesn’t seem to return proper normals, they all appear to be flat-shaded normals). Let me know if there’s a problem.

I’ve just been trying to run some basic tests. I can set up simple models where I know what the output has to be, and compare those with the stuff exported from Blender.

First problem: can’t even get the script to run. :smiley: I assume this is my fault rather than the script’s fault.

I’ve dropped the .py in the scripts folder, but when I open it in the text editor and hit the “Run Script” button I just get an error. It’s failing on “import bpy” which is the first line.

That’s weird, is there some whitespace in front of the the import command by chance? (indentation matters in Python!)

The standalone export script does not require a blank file to be present, the set modes “wb” in the call to open() stay for “write” and “binary”. “write” means it will either be created or overwritten.

A file header is also written. I took the required bits from your screenshot. I assumed the first 32 bytes to be static (same for all 3dp files). If you need some of them to have different values based on some property of the meshes or via user input, let me know.

I turned both script into an addon, so that you can import and export via File menu easily:

bl_info = {
    "name": "Railroad Tycoon 3 (.3dp)",
    "author": "CoDEmanX",
    "version": (0, 5),
    "blender": (2, 74, 0),
    "location": "File &gt; Import | File &gt; Export",
    "description": "Export (and partially import) 3dp file format.",
    "warning": "",
    "wiki_url": "",
    "category": "Import-Export",
}
    
import bpy
import bmesh
import struct
import os
from binascii import unhexlify
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty
from bpy.types import Operator


def write(context, filepath):
    
    class QUAD:
        BEAUTY = 0
        FIXED = 1
        ALTERNATE = 2
        SHORTEDGE = 3

    class NGON:
        BEAUTY = 0
        EARCLIP = 1
    
    try:
        file = open(filepath, "wb")
        
        file.write(unhexlify(
            "33 44 50 46  04 00 01 00  33 44 4D 44  01 00 00 00"
            "00 00 3E C0  ED 1E 5F C1  09 0A 4D 40  49 4E 53 54"
            "00 00 00 00  00 00 00 00".replace(" ", ""))
        )    
        verts_offset = file.tell()

        scene = context.scene
        
        verts_total = sum(len(ob.data.vertices) for ob in scene.objects if ob.type == 'MESH')
        faces_offset = verts_total * 12 + verts_offset
        
        vc = 0
        tc = 0

        for ob in scene.objects:
            if ob.type != 'MESH':
                continue
            bm = bmesh.new()
            bm.from_object(ob, scene)
            bm.transform(ob.matrix_world)
            bmesh.ops.triangulate(bm, faces=bm.faces, quad_method=QUAD.BEAUTY, ngon_method=NGON.BEAUTY)
            uv_layer = bm.loops.layers.uv.active
            if uv_layer is None:
                raise Exception("'{}' has no active UV map.".format(ob.name))
            
            file.seek(verts_offset)
            for v in bm.verts:
                file.write(struct.pack("&lt;3f", *v.co))
            verts_offset = file.tell()
            
            file.seek(faces_offset)
            for f in bm.faces:
                for i in range(3):
                    file.write(struct.pack("&lt;i", f.verts[i].index + vc))
                for i in range(3):
                    file.write(struct.pack("&lt;3f", *f.loops[i].calc_normal()))
                for i in range(3):
                    u, v = f.loops[i][uv_layer].uv
                    file.write(struct.pack("&lt;2f", u, 1-v))
                file.write(b"\0" * 4)

            faces_offset = file.tell()
            vc += len(bm.verts)
            tc += len(bm.faces)
            bm.free()
            
        file.seek(32)
        file.write(struct.pack("&lt;2i", vc, tc))
    except (IOError, OSError) as err:
        return "There was trouble writing the file '{}':
{}".format(
            bpy.path.basename(filepath), err)
    except Exception as err:
        return "An error occurred:
{}".format(err)
    finally:
        file.close()
        

def read(context, filepath):
    try:
        file = open(filepath, 'rb')
        verts = []
        faces = []
        file.seek(32)
        vert_count, face_count = struct.unpack("&lt;2i", file.read(8))
        
        for i in range(vert_count):
            vert = struct.unpack("&lt;3f", file.read(12))
            verts.append(vert)
        
        for i in range(face_count):
            face = struct.unpack("&lt;3i", file.read(12))
            file.seek(64, os.SEEK_CUR)
            faces.append(face)
            
        scene = context.scene
        me = bpy.data.meshes.new("imported_3dp")
        me.from_pydata(verts, [], faces)
        me.validate()
        me.update(True)
        me.calc_normals()
        
        for ob in scene.objects:
            ob.select = False
        ob = bpy.data.objects.new("imported_3dp", me)
        ob.select = True
        scene.objects.link(ob)
        scene.objects.active = ob
        scene.update()
    except (IOError, OSError):
        return "There was trouble reading the file '{}':
{}".format(
            bpy.path.basename(filepath), err)
    except Exception as err:
        return "An error occurred:
{}".format(err)
    finally:
        file.close()


class Export3dp(Operator, ExportHelper):
    """Export Railroad Tycoon 3 (.3dp)"""
    bl_idname = "export_mesh.3dp" 
    bl_label = "Export 3dp"

    # ExportHelper mixin class uses this
    filename_ext = ".3dp"

    filter_glob = StringProperty(
        default="*.3dp",
        options={'HIDDEN'},
    )

    def execute(self, context):
        err = write(context, self.filepath)
        if err:
            self.report({'ERROR'}, err)
            return {'CANCELLED'}
        return {'FINISHED'}


class Import3dp(Operator, ImportHelper):
    """Import Railroad Tycoon 3 (.3dp)
       NOTE: Does not support vertex normals and UV mapping
    """
    bl_idname = "import_mesh.3dp"
    bl_label = "Import 3dp"

    # ImportHelper mixin class uses this
    filename_ext = ".3dp"

    filter_glob = StringProperty(
        default="*.3dp",
        options={'HIDDEN'},
    )

    def execute(self, context):
        err = read(context, self.filepath)
        if err is not None:
            self.report({'ERROR'}, err)
            return {'CANCELLED'}
        return {'FINISHED'}
    

def menu_func_export(self, context):
    self.layout.operator(Export3dp.bl_idname, text="Railroad Tycoon 3 (.3dp)")
    
def menu_func_import(self, context):
    self.layout.operator(Import3dp.bl_idname, text="Railroad Tycoon 3 (.3dp)")


def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_export.append(menu_func_export)
    bpy.types.INFO_MT_file_import.append(menu_func_import)


def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_export.remove(menu_func_export)
    bpy.types.INFO_MT_file_import.remove(menu_func_import)


if __name__ == "__main__":
    register()

No white space. Indentation looks fine.

The file header does have some variables in it. It breaks down like this:

First 12 (0 to 11 inclusive) bytes are just gobbledegook that tells the game it’s a .3dp, but are constant in all files so could be scripted.

Bytes 12 to 15 inclusive set the number of levels of detail. I’ll just sort that manually anyway since I’ll know the value and it’s just a basic integer (it will vary depending on the file).

Bytes 16 to 27 inclusive are the attachment point for the item in question. This varies according to what part the file is for (loco body, trucks, wheels, etc). Again, this can be set manually. It’d be a real PITA trying to script it, and it’s easy manually.

Bytes 28 to 31 inclusive are just more gobbledegook, but are constant in all files so could be scripted.

Bytes 32 to 35 inclusive are the number of vertices (as an integer).

Bytes 36 to 39 inclusive are the number of tris (as an integer).

Here endeth the header. :slight_smile:

After that it’s straight into the list of verts, etc.

Edit: I’m thinking it may be best to do the headers manually, except perhaps for bytes 28 to 39 inclusive. This would make it easiest to add the extra code for LOD’s by pasting on the end of the existing. IOW, just generate a file for each LOD, then paste them together.

Hey it’s almost midnight here. I’ll play around with this some more tomorrow night.

Thanks for your time.

Try to install the last script I posted and install as addon, it should be working.

Is every 3dp file a single level of detail? Like, are there 3 files, one high-poly, one mid-poly and one low-poly mesh for the same object? Or are multiple levels of detail in a single 3dp file? If so, how? Which parts are repeated? (Everything from byte 12 on?) Is there a byte telling the number of models per file? One could use Blender layers to organize multiple LODs, then export them…

A single attachment point that is? (3xfloat) Doesn’t seem hard to script if you would just use the world or object center (in order to move the attachment point, you would move all your meshes in Blender). But I can think of more ways to specify the attachment point. You could place an Empty at the attachment location and name it e.g. “attach” and the script would look for an empty called like that and write out its coordinate as attachment point.

Talking about attachment points… I guess it would be beneficial if multiple meshes of different object parts (body, wheels…) could co-exist within the same blend and scene, but with the possibility to export them as separate 3dp files… In that case, you would model the entire object with all its parts and keep it like that. Each object’s origin could be used as attachment point. A simple integer property could be added to the Object type, so that you can set the level of detail per object/part in Blender somewhere in the properties editor, object tab.

Number of verts and tris are already written automatically by the script.

Try to install the last script I posted and install as addon, it should be working.

Is every 3dp file a single level of detail? Like, are there 3 files, one high-poly, one mid-poly and one low-poly mesh for the same object? Or are multiple levels of detail in a single 3dp file? If so, how? Which parts are repeated? (Everything from byte 12 on?) Is there a byte telling the number of models per file? One could use Blender layers to organize multiple LODs, then export them…

I’ll leave running the script until tonight my time, when I’ll be able to devote some time to it. :slight_smile:

For your other questions: The .3dp file can be a single level of detail for some simple components, but can have up to 6 LOD’s for more complex components. All files share the same basic syntax in the header, regardless of how many LOD’s there actually are.

The multiple LOD’s are done as shown in the screenshots below. I think it’s clear, but ask away if it isn’t.

I’ve attached a copy of the file as well, in case you want to take a look at it. I can also supply a bookmarks file for HexEditorNeo if you use that editor.

A single attachment point that is? (3xfloat) Doesn’t seem hard to script if you would just use the world or object center (in order to move the attachment point, you would move all your meshes in Blender). But I can think of more ways to specify the attachment point. You could place an Empty at the attachment location and name it e.g. “attach” and the script would look for an empty called like that and write out its coordinate as attachment point.

That idea of using an empty could work well, although obviously there would be several of them and they’d each have to be linked to the relevant object. It may need manual editing later anyway, depending on how the model behaves in the game, but having some value in the .3dp to start with would be handy.

Talking about attachment points… I guess it would be beneficial if multiple meshes of different object parts (body, wheels…) could co-exist within the same blend and scene, but with the possibility to export them as separate 3dp files… In that case, you would model the entire object with all its parts and keep it like that. Each object’s origin could be used as attachment point. A simple integer property could be added to the Object type, so that you can set the level of detail per object/part in Blender somewhere in the properties editor, object tab.

That sounds handy, although personally I’d prefer to not use each object’s origin as the attachment point as I can see that leading to people getting themselves in the crap. At the moment it seems better to me to use the empties as attachment points and not screw around with object origins. I think this would generally be less strain on the brain.

Still, ultimately the whole modelling process will require some knowledge of hex editing. This is just due to how the game files handle various things. It won’t be possible to reduce the whole thing to being completely idiot-proof, so as long as the basics work there’s not really any need to go overboard trying to handle every detail. If you’re enjoying the coding challenge then fair enough, but don’t drive yourself nuts on this. My 2c.

Number of verts and tris are already written automatically by the script.

Ok, cool.

Attachments



H10282L_Body.zip (39.7 KB)

Ok, I gave it a test run on a basic cube that I had already UV mapped. I figure start with a simple test case.

It works, and generally works well, but there’s a catch. It is exporting face normals rather than vertex normals.

01 00 00 00 02 00 00 00 03 00 00 00
00 00 00 00 00 00 00 00 00 00 80 BF
00 00 00 00 00 00 00 80 00 00 80 BF
00 00 00 80 00 00 00 00 00 00 80 BF
E0 20 9E 3E 00 DF 50 3B 6B 47 3C 3E
00 DD 50 3B 6D 47 3C 3E 10 9C 81 3E
00 00 00 00

The 00 00 00 00 00 00 00 00 00 00 80 BF is clearly a face normal that is pointing straight down, which makes sense for the bottom face of the cube.

This game requires vertex normals. Face normals will not work.* This is not BGE, it’s an old custom engine that runs over DirectX 8. We can haz vertex normals, yes? :smiley:

Also, I remembered something you posted earlier:

I made it use loop normals to enable mixed flat and smooth shaded surfaces (needs Auto-Smooth to be enabled, see http://wiki.blender.org/index.php/De.../2.74/Modeling and the linked manual page).

I honestly don’t think this is a good idea. The game engine just wants vertex normals. Requiring a particular type of shading be enabled in Blender before exporting is just one more step that may trip people up sometimes. The game doesn’t use that information anyway, so offhand I can’t see any reason to include this in the script.

I think it would make more sense to completely ignore whatever shading is set in Blender, and just export the vertex normals as calculated from the mesh geometry. Regardless of what is set in Blender, the game is going to apply Gouraud shading. If a vertex is shared between adjacent faces it will automatically be smooth shaded (or as close to that as Gouraud can get).

The only way of getting flat shading in the game is to use extra verts and seams to separate faces from each other. That’s useful sometimes (like when you really want a box to be shaded as a box) but a lot of the time people probably won’t bother.

*I realise you said either face or vertex normals could be used, but every .3dp file in the game uses vertex normals. Face normals are never used. My assumption is that the guys who wrote the game would have known which normals were required.