iPhone Content Creation with Blender ~ Part 2

Development

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.

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.

ExporterUI

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.

Filed under: , .

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>