2
$\begingroup$

I am writing an importer for a custom file format. So far I have created my object:

 me = bpy.data.meshes.new(ModelName)
 ob = bpy.data.objects.new(ModelName,me)
 scn = bpy.context.scene
 scn.objects.link(ob)
 scn.objects.active = ob
 ob.select = True

And then I create the vertex, face, and materials:

    myvertex = []
    myfaces = []
    MyTextureNames = []

    for textures in range(0, MtlCount):
        StrLen = struct.unpack('i',fdata.read(4))[0]
        mtlname = fdata.read(StrLen).decode()
        MyTextureNames.append(mtlname)

        new_mat=bpy.data.materials.new(mtlname)
        new_mat.use_nodes = True

        bpy.ops.object.material_slot_add()
        context.object.active_material = new_mat

    for points in range(0, VertexCount):
        # read three coordinates
        p1 = struct.unpack('f',fdata.read(4))[0]
        p3 = struct.unpack('f',fdata.read(4))[0]
        p2 = struct.unpack('f',fdata.read(4))[0]

        #print ("x: " + str(p1) + " y: " + str(p2) + " z: " + str(p3))

        myvertex.append((p1,p2,p3))

    for points in range(0,FaceCount):
        # read three coordinates
        p1 = struct.unpack('i',fdata.read(4))[0]
        p3 = struct.unpack('i',fdata.read(4))[0]
        p2 = struct.unpack('i',fdata.read(4))[0]

        #print ("x: " + str(p1) + " y: " + str(p2) + " z: " + str(p3))

        myfaces.append((p1,p2,p3))


    # Add verts and faces with the following line
    me.from_pydata(myvertex, [], myfaces)

    me.update()

When I run this script I get my object, with vertices and faces intact, and when I look in the materials all my materials are listed and already set to use nodes and Diffuse BSDF. Great.

My question is: How do I now assign the material to the correct faces? (I do have an array containing the information mapping the face to the material, I just do not know how to tell blender "For this face use this material".

I also am unclear as to how I set the UV coordinates which I also have stashed in an array for access.

And not to over complicate this "question" but other things I am trying to figure out how to do are reading the image information which is packed in my file, getting it into memory and then telling blender to use that memory location for the image information to use for the diffuse.

$\endgroup$

2 Answers 2

2
$\begingroup$

For materials, this is simple: each polygon (face) as a "material_index" property which corresponds to the object's "material_slots" property.

For the UVs, that can be more complex. Here is how Blender stores all that:

UV layers are in "obj.data.uv_layers".

But to create uv layers, you need to go to "uv_textures" and add one via "obj.data.uv_textures.new( 'name' )".

You can now access to "obj.data.uv_layers['name']" and to "obj.data.uv_layers['name'].data". This last data is an array that contains information for each vertex in the mean of uv layer vertex (not 3D vertex, because a 3D vertex can be represented many times in the UV map).

What is unifying all these is the concept of 'loop'. 'loop' is a per face information.

You have a 'main loop' which is into "obj.data.loops" and contains the 3D per face information for each vertex. Say we take "loop_info = obj.data.loops[0]", it contains "loop_info.vertex_index" (the index to find the 3D vertex in "obj.data.vertices"), and other information like vertex "normal".

Now an uv layers is also a per face information (a loop). "obj.data.uv_layers['name'].data" is a loop (organized with the same index order as the main loop).

If we take "uv_info = obj.data.uv_layers['name'].data[0]", "uv_info.uv" is the 2D coordinate in this UV map.

Last thing you need to know (I think), the correspondence between a polygon and the loops: this is stored in the polygon itself. If you get "polygon = obj.data.polygons[0]", then you can get the loop projection of its vertices in "polygon.loop_indices".

For instance, "obj.data.uv_layers['name'].data[polygon.loop_indices[0]]" is valid.

I think the other aspects are relative to your file format.

Concerning images. You can start with "img = bpy.data.images.new(name, width, height)". Color information is in "img.pixels" organized from bottom left to upper right (UV like). "img.pixels" is a 4*width*height float array containing R,G,B,A values (black is 0, white is 1).

Following the comments... I cannot imagine what your file format is.

So I can only, give some advices and a little help...

Warning this code is untested (as I have no data), so surely, there will be some bugs here. But I hope that will help for the principles of how it works:

Materials part: just for the comment at the end

modelName = "xxxx"
mesh = bpy.data.meshes.new( modelName )
obj = bpy.data.objects.new( modelName, mesh )

for textures in range(0, MtlCount):
    StrLen = struct.unpack('i',fdata.read(4))[0]

    mtlname = fdata.read(StrLen).decode()

    MyTextureNames.append( mtlname )

    new_mat = bpy.data.materials.new( mtlname )
    new_mat.use_nodes = True

    #I do prefer using .data instead of .ops because ops often need a context and/or a mode
    obj.data.materials.append( new_mat )

Creating the mesh, with some guess about unpack (as I've never used it). But that should work and be faster than a for loop.

def ToTuples( aList, tupleSize ):
    return [tuple(aList[x:x+tupleSize]) for x in range(0, len(aList), tupleSize)]

#Shorter... probably much faster than loops
myVertices = ToTuples( struct.unpack( 'f', fdata.read( 4 * 3 * VertexCount ) ), 3 )
myFaces = ToTuples( struct.unpack( 'i', fdata.read( 4 * 3 * FaceCount ) ), 3 )

mesh.from_pydata( myVertices, [], myfaces )
mesh.update()

Face material attribution: 2 examples

#If face/material is describe as face index to material index in your file
for fIndex, mIndex in ToTuples( struct.unpack( 'i', fdata.read( 4 * 2 * FaceCount ) ), 2 ):
    mesh.polygons[fIndex].materialIndex = mIndex

#If face/material is describe just as a sequence of material index in the same order as the faces
for fIndex, mIndex in enumerate( struct.unpack( 'i', fdata.read( 4 * FaceCount ) ) ):
    mesh.polygons[fIndex].materialIndex = mIndex

UV map:

#Assuming you have one UV map and all polygons are tris
#=> you have 3 * FaceCount uv definitions in Blender, so this is the size of the 'loop(s)'
#But I do not know how your file is...
#So, I guess it is defined as the following, assuming the vertex order of the face is the same as above:
# faceIndex, vertex1co, vertex2co, vertex3co
# All these is 7 * 4 bytes * FaceCount

uvTex = mesh.uv_textures.new()
uvMap = mesh.uv_layers[uvTex.name]

for fi, v1x, v1y, v2x, v2y, v3x, v3y in ToTuples( struct.unpack( 'i f f f f f f', fdata.read( 7 * 4 * FaceCount ) ), 7 ):
    #Find the polygon
    polygon = mesh.polygons[fi]
    #In the uv map, get the corresponding vertex using the polygon loop index (for first, second and third) and set its uv coordinates
    uvMap.data[polygon.loop_indices[0]].uv = (v1x, v1y)
    uvMap.data[polygon.loop_indices[1]].uv = (v2x, v2y)
    uvMap.data[polygon.loop_indices[2]].uv = (v3x, v3y)

I don't think this is the place to do more... and doing more needs to know the specifications of your file format...

Hope it helps!

$\endgroup$
2
  • $\begingroup$ Hmmm. I have spent all day trying to figure out what all that means with no luck. Using the code as a base, could you possibly provide an example of: Adding the texture to the face Adding the UV coordinates to the face Adding normals to the face Thanks! $\endgroup$ Commented Mar 29, 2017 at 21:54
  • $\begingroup$ @PhillipProctor, I will try to write some templates in order to clarify. But I do not know how your file data is structured, so I'll need to imagine it. I think in this case, main thing is to know how to map your data to Blender's data. I'll need some time to do it... $\endgroup$
    – lemon
    Commented Mar 30, 2017 at 5:37
0
$\begingroup$

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.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .