This is part 2 in a series of posts describing how to extend Blender to fit with your own content production process, specifically with regard to producing content for iPhone.
- iPhone Content Creation with Blender – Part 1
- iPhone Content Creation with Blender – Part 2
The 2.5 release of Blender is just around the corner. From the looks of the feature improvements already described on the Blender site we can expect some substantial changes to almost every aspect of the application. One significant area is the UI, in particular scripting updates to the UI.
What I’ll describe here is relevant to scripting a UI for an exporter using the 2.49.2 release of Blender. Below is a snapshot of the UtopiaGL exporter in Blender.
One of the things that bugged me about setting up a UI for the UtopiaGL exporter was that Blender exposes a pretty low-level API for this purpose. This means that you end up dealing with absolute coordinates when positioning textboxes and buttons and so on. From looking at some of the existing Blender exporter plugins, the following type of UI code is not uncommon.
def draw_gui():
... # globals removed to save space!
# Title
glClear(GL_COLOR_BUFFER_BIT)
glRasterPos2d(10, 290)
Text("UtopiaGL .model Export")
# VNormals / UVs / VColors / VWeights
Label( "Properties To Export", 430, 190, 120, 20 )
g_toggle_outputvnormals = Toggle("Vertex Normals", EVENT_NOEVENT, 430, 160, 100, 20, g_toggle_outputvnormals.val, "Output Vertex Normals" )
g_toggle_outputuvs = Toggle("Vertex UVs", EVENT_NOEVENT, 540, 160, 100, 20, g_toggle_outputuvs.val, "Output Vertex UV Coordinates" )
g_toggle_outputvcolors = Toggle("Vertex Colors", EVENT_NOEVENT, 430, 130, 100, 20, g_toggle_outputvcolors.val, "Output Vertex Colors" )
g_toggle_outputvweights = Toggle("Vertex Weights", EVENT_NOEVENT, 540, 130, 100, 20, g_toggle_outputvweights.val, "Output Vertex Weights" )
Label( "Faces", 430, 90, 80, 20 )
g_menu_facewinding = Menu("Face Winding %t|Counter-Clockwise %x1|Clockwise %x2|", EVENT_NOEVENT, 430, 60, 150, 20, g_menu_facewinding.val, "Face winding to use" )
# Content Root / Model File
Label( "Content Root Path", 10, 240, 80, 20 )
g_content_root = String("", EVENT_NOEVENT, 10, 210, 300, 20, g_content_root.val, 255, "Content root path")
Button( "Browse", EVENT_CHOOSE_CONTENT_ROOT, 310, 210, 80, 20 )
Label( "Model File", 10, 180, 80, 20 )
g_filename = String("", EVENT_NOEVENT, 10, 150, 300, 20, g_filename.val, 255, "Model file to save")
Button( "Browse", EVENT_CHOOSE_FILENAME, 310, 150, 80, 20 )
g_toggle_outputshaders = Toggle("Output Shaders/Skins", EVENT_NOEVENT, 10, 120, 130, 20, g_toggle_outputshaders.val, "Output Shader and Skin files" )
# Log / Log Level
Label( "Logging", 10, 90, 80, 20 )
g_toggle_outputtolog = Toggle("Output Log", EVENT_NOEVENT, 10, 60, 80, 20, g_toggle_outputtolog.val, "Output export progress to log file" )
Label( "Log Level", 160, 60, 80, 20 )
g_integer_loglevel = Menu("Log Level %t|Debug %x1|Info %x2|Warning %x3|Error %x4|Critical %x5", EVENT_NOEVENT, 230, 60, 80, 20, g_integer_loglevel.val, "Logging Level to use" )
# Export / Exit
Button( "Export", EVENT_SAVE_MODEL, 10, 10, 80, 20 )
Button( "Exit", EVENT_EXIT ,100, 10, 80, 20 )
The above approach works and if your UI is simple and not subject to change you should be fine to implement a UI like this. If on the other hand, you plan to iteratively extend the exporter as and when new requirements appear, you’ll probably want something a little more dynamic. For example, inserting a button in the middle of a UI implemented like this means calculating and changing the coordinates of all other UI elements in the surrounding area.
Silverlight and WPF have various types of Panel controls that makes insertion and removal of controls in a layout very easy. I didn’t have time to implement a complete panel control, so instead I implemented a Cursor object that allows me to output UI elements at the current location of the Cursor.
class Cursor:
"""A Cursor object, use to maintain context of where to insert UI elements."""
def __init__(self):
self.x = 10
self.y = 10
self.width = 80
self.height = 20
self.virticalpad = 10
self.horizontalpad = 10
def set_x(self, newx):
self.x = newx
def set_y(self, newy):
self.y = newy
def set_width(self, newwidth):
self.width = newwidth
def set_height(self, newheight):
self.height = newheight
def set_virtical_pad(self, newvirticalpad):
self.virticalpad = newvirticalpad
def set_horizontal_pad(self, newhorizontalpad):
self.horizontalpad = newhorizontalpad
def move_up(self):
self.y = self.y + (self.height + self.virticalpad)
def move_down(self):
self.y = self.y - (self.height + self.virticalpad)
def offset_up(self, up):
self.y = self.y + up
def offset_down(self, down):
self.y = self.y - down
def move_left(self):
self.x = self.x - (self.width + self.horizontalpad)
def move_right(self):
self.x = self.x + (self.width + self.horizontalpad)
def offset_left(self, left):
self.x = self.x - left
def offset_right(self, right):
self.x = self.x + right
def Button(self, title, handle):
Button(title, handle, self.x, self.y, self.width, self.height)
def Toggle(self, title, handle, value, tip):
return Toggle(title, handle, self.x, self.y, self.width, self.height, value, tip)
def Label(self, title):
Label(title, self.x, self.y, self.width, self.height)
def Menu(self, options, handle, value, tip):
return Menu(options, handle, self.x, self.y, self.width, self.height, value, tip)
def String(self, text, handle, value, l, tip):
return String(text, handle, self.x, self.y, self.width, self.height, value, l, tip)
By using the cursor we can convert the hard-coded UI code from above to look more like the following. Note, this code now makes it much easier to add and remove UI elements without the need to update the coordinates of the surrounding elements. It does increase the amount of code a little, but the trade off is probably worth it.
def draw_gui():
... # globals removed to save space!
glClear(GL_COLOR_BUFFER_BIT)
# Export / Exit
cursor = Cursor()
cursor.Button( "Export", EVENT_SAVE_MODEL)
cursor.move_right()
cursor.Button( "Exit", EVENT_EXIT)
# Log / Log Level
cursor.move_left()
cursor.move_up()
cursor.offset_up(20)
g_toggle_outputtolog = cursor.Toggle("Output Log", EVENT_NOEVENT, g_toggle_outputtolog.val, "Output export progress to log file")
cursor.offset_right(150)
cursor.Label( "Log Level" )
cursor.move_right()
g_integer_loglevel = cursor.Menu("Log Level %t|Debug %x1|Info %x2|Warning %x3|Error %x4|Critical %x5", EVENT_NOEVENT, g_integer_loglevel.val, "Logging Level to use")
cursor.set_x(10)
cursor.move_up()
cursor.Label( "Logging")
# Content Root / Model File
cursor.move_up()
cursor.set_width(130)
g_toggle_outputshaders = cursor.Toggle("Output Shaders/Skins", EVENT_NOEVENT, g_toggle_outputshaders.val, "Output Shader and Skin files" )
cursor.move_up()
cursor.set_width(300)
g_filename = cursor.String("", EVENT_NOEVENT, g_filename.val, 255, "Model file to save" )
cursor.offset_right(300)
cursor.set_width(80)
cursor.Button( "Browse", EVENT_CHOOSE_FILENAME )
cursor.move_up()
cursor.set_x(10)
cursor.Label( "Model File")
cursor.move_up()
cursor.set_width(300)
g_content_root = cursor.String("", EVENT_NOEVENT, g_content_root.val, 255, "Content root path" )
cursor.offset_right(300)
cursor.set_width(80)
cursor.Button( "Browse", EVENT_CHOOSE_CONTENT_ROOT)
cursor.move_up()
cursor.set_x(10)
cursor.Label( "Content Root Path")
# Title
cursor.move_up()
cursor.move_up()
cursor.set_width(300)
cursor.Label( "UtopiaGL .model Export")
# VNormals / UVs / VColors / VWeights
cursor.set_x(430)
cursor.set_y(10)
cursor.move_up()
cursor.offset_up(20)
g_menu_facewinding = cursor.Menu("Face Winding %t|Counter-Clockwise %x1|Clockwise %x2|", EVENT_NOEVENT, g_menu_facewinding.val, "Face winding to use" )
cursor.move_up()
cursor.Label( "Faces")
cursor.move_up()
cursor.offset_up(10)
cursor.set_width(100)
g_toggle_outputvcolors = cursor.Toggle("Vertex Colors", EVENT_NOEVENT, g_toggle_outputvcolors.val, "Output Vertex Colors")
cursor.move_right()
g_toggle_outputvweights = cursor.Toggle("Vertex Weights", EVENT_NOEVENT, g_toggle_outputvweights.val, "Output Vertex Weights")
cursor.move_left()
cursor.move_up()
g_toggle_outputvnormals = cursor.Toggle("Vertex Normals", EVENT_NOEVENT, g_toggle_outputvnormals.val, "Output Vertex Normals")
cursor.move_right()
g_toggle_outputuvs = cursor.Toggle("Vertex UVs", EVENT_NOEVENT, g_toggle_outputuvs.val, "Output Vertex UV Coordinates")
cursor.move_left()
cursor.move_up()
cursor.set_width(120)
cursor.Label( "Properties To Export")
Here’s hoping the improvements made in the 2.5 release make this stuff redundant. But for now, this is a manageable way to implement an exporter UI in Blender to make updates and modifications a little easier.
I didn’t get around to writing about the Material to Shader mismatch that I mentioned in the previous post. So, there’s definitely going to be a part 3 to this series at a minimum. Hopefully, I’ll get to it soon.




