# cycles adding texture to face via python / import script

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.active = ob
ob.select = True


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

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

for textures in range(0, MtlCount):
MyTextureNames.append(mtlname)

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

context.object.active_material = new_mat

for points in range(0, VertexCount):

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

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

for points in range(0,FaceCount):

#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.

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).

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):

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()


#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!

• 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! – Phillip Proctor Mar 29 '17 at 21:54
• @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... – lemon Mar 30 '17 at 5:37

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
#
#  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")

#   SEE IF HEADER IS VALID
#   HEADER INVALID, THROW A MESSAGE AND BAIL
return {'CANCELLED'}

#   GET THE NUMBER OF VERTICIES
#   GET THE NUMBER OF FACES
#   GET THE NUMBER OF MATERIALS
#   GET THE LENGTH OF THE STRING CONTAINING MODEL NAME
#   GET THE MODEL NAME STRING

#   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.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
#   READ THE MATERIAL NAME FROM THE FILE
#   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
#   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.location = 0,0

#   CREATE DIFFUSE NODE
node_diffuse.name = "Diffuse"
node_diffuse.label = "Diffuse"
node_diffuse.location = -400,-0

#   CREATE BUMP NODE
node_bump.name = "Bump"
node_bump.label = "Bump"
node_bump.location = -800,-200

#   CREATE DIFFUSE IMAGE NODE
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.name = "Bump Image"
node_bump_image.label = "Bump Image"
node_bump_image.location = -1200,-500
node_bump_image.color_space = 'NONE'

#   LINK DIFFUSE OUTPUT TO INPUT OF OUTPUT NODE

#   CONNECT DIFFUSE IMAGE OUT TO DIFFUSE SHADER IN

#   CONNECT BUMP OUT TO DIFFUSE NORMAL IN

#   CONNECT THE BUMP IMAGE TO THE BUMP NODE

# 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.

if ChkByte !=0:

# check to see if its a FILE tag

if ChkByte != 0x454C4946:

# ChkByte is the size of the next string

# Set the difuse image here.
# self.filepath + RealativePath = image

DiffuseImage = os.path.dirname(self.filepath) + RealativePath

# Images should come in pairs so lets check for a bump map:

if ChkByte != 0:
# We have a bump map so lets grab it.
# Set the bump map image here

DiffuseImage = os.path.dirname(self.filepath) + RealativePath
else:

# This was a TGA material so load the diffuse only

if ChkByte != 0:
# We have a bump map so lets grab it.
# Set the bump map image here

DiffuseImage = os.path.dirname(self.filepath) + RealativePath

# 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):

#   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):
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

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
self.layout.operator(UPE_File_Import.bl_idname, text="Import UPE")

def register():
bpy.utils.register_class(UPE_File_Import)