Page 1 of 1

Blender 3D, Protocol Buffers, Python 2.x/3.x & PyCollada

Posted: Mon Nov 19, 2012 8:37 pm
by fips
I've recently had this 'great' idea that I would pump nicely structured 3D data directly from Blender 3D into my Application using Google Protocol Buffers. Although it sounds incredibly simple and elegant it has its own big fat BUT... Since the version 2.5, Blender 3D has been switched to Python 3.x, BUT Google Protocol Buffers are still based on Python 2.x, so there's basically no chance to import the Protobufs module into Blender 3D, which makes me really mad (I hate this Python 2.x/3.x compatibility issues that's been here for many years now!). So after a few attempts and a bit of Googling, I've realised that I'm forced to change my strategy, at least until Python 3.x gets widely adopted.

So I've resolved this issues by inserting just another stage into my content pipeline. This stage is based on PyCollada, which is a very easy-to-use Collada parser for Python. So now I do the following content transformation: Blender 3D => Collada DAE => PyCollada/Protobufs (Python) => Geometry BLOB => Protobufs (C++) => Application. It's been working pretty well so far.

Bellow, you might find two code pieces (.proto + .py) to get the idea how it looks (it's still WIP, though):

VIEW THE CODE BELOW IN FULL-SCREEN (geometry.proto)

Code: Select all

/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1047
*/

package fs.engine.io;

enum Vertex_element_type
{
    VET_Float_4 = 0;
    VET_Float_3 = 1;
    VET_Float_2 = 2;
    VET_Float_1 = 3;
    VET_Uint8_4 = 4;
    VET_Uint8_3 = 5;
    VET_Uint8_2 = 6;
    VET_Uint8_1 = 7;
};

enum Vertex_element_semantics
{
    VES_Position = 0;
    VES_Normal = 1;
    VES_Color = 2;
    VES_Texcoord = 3;
};

message Vertex_element
{
    required Vertex_element_type type = 1;
    required Vertex_element_semantics semantics = 2;
}

message Vertex_format
{
    repeated Vertex_element elements = 1;
}
 
message Geometry
{
    required Vertex_format format = 1; 
    required bytes data = 2;
} 
VIEW THE CODE BELOW IN FULL-SCREEN (collada_exporter.py)

Code: Select all

#!/usr/bin/env python

# /*
# (c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
# THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
# ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1047
# */

import sys
sys.path.append("../imports")
sys.path.append("../messages")

import geometry_pb2
import collada
import struct

def expand_positions(prim):
    num_tris = len(prim.vertex_index)
    positions = []
    if len(prim.vertex):
        for i in range(0, num_tris):
            for v in range (0, 3):
                pos = prim.vertex[prim.vertex_index[i]][v]
                positions.append(struct.pack("fff", *pos))
    return positions

def expand_normals(prim):
    num_tris = len(prim.normal_index)
    normals = []
    if len(prim.normal):
        for i in range(0, num_tris):
            for v in range (0, 3):
                norm = prim.normal[prim.normal_index[i]][v]
                normals.append(struct.pack("fff", *norm))
    return normals

def expand_texcoords(prim, index):
    if index >= len(prim.texcoord_indexset):
        return []
    num_tris = len(prim.texcoord_indexset[index])
    texcoords = []
    if len(prim.texcoordset[index]):
        for i in range(0, num_tris):
            for v in range (0, 3):
                texcoord = prim.texcoordset[index][prim.texcoord_indexset[index][i]][v]
                texcoords.append(struct.pack("ff", *texcoord))
    return texcoords

def create_vertex_data(positions, normals, texcoords0, texcoords1):
    vertex_data = ""
    for i in range(0, len(positions)):
        vertex_data += positions[i] if positions else ""
        vertex_data += normals[i] if normals else ""
        vertex_data += texcoords0[i] if texcoords0 else ""
        vertex_data += texcoords1[i] if texcoords1 else ""
    return vertex_data

def export_geometry(vertex_data, has_positions, has_normals, has_texcoords0, has_texcoords1):
    geom = geometry_pb2.Geometry()
    if has_positions:
        e = geom.format.elements.add()
        e.type = geometry_pb2.VET_Float_3
        e.semantics = geometry_pb2.VES_Position
    if has_normals:
        e = geom.format.elements.add()
        e.type = geometry_pb2.VET_Float_3
        e.semantics = geometry_pb2.VES_Normal
    if has_texcoords0:
        e = geom.format.elements.add()
        e.type = geometry_pb2.VET_Float_2
        e.semantics = geometry_pb2.VES_Texcoord
    geom.data = vertex_data
    with open("geometry.bin", "wb") as f:
        f.write(geom.SerializeToString())

def inspect_geometry(obj):
    print "GEOMETRY: '%s'" % obj.original.name
    for prim in obj.primitives():
        print "- PRIMITIVE: type: '%s', triangles: %d, vertices: %d, ..." % (type(prim).__name__, len(prim), len(prim.vertex))
        if type(prim) is not collada.triangleset.BoundTriangleSet:
            continue
        num_tris = len(prim.vertex_index)
        positions = expand_positions(prim)
        normals = expand_normals(prim)
        texcoords0 = expand_texcoords(prim, 0)
        texcoords1 = expand_texcoords(prim, 1)
        print "- SOUP: triangles: %d (vertices: %d, ...)" % (num_tris, len(positions))
        vertex_data = create_vertex_data(positions, normals, texcoords0, texcoords1)
        export_geometry(vertex_data, bool(positions), bool(normals), bool(texcoords0), bool(texcoords1))

def inspect_collada(col):
    if col.scene:
        for geom in col.scene.objects("geometry"):
            inspect_geometry(geom)

if __name__ == "__main__":
    filename = sys.argv[1]
    col = collada.Collada(filename, ignore=[collada.DaeUnsupportedError, collada.DaeBrokenRefError])
    inspect_collada(col)