1#!BPY
2# Version 9: Modified "top", so upper edges means all on the upper side of the
3#            mesh, not any edges facing up. Took as long as all the rest of this
4#            script.
5#
6#            Friction is now sum of R, G, B components of material color.
7# Version 8: added the "top" property, and an error message
8# Version 7: "foreground" meshes and objects now possible
9#            friction can now be set for materials (using color components)
10# Version 6: merge object types, round numbers to integer
11# Version 5: objects
12# Version 4: read edge_height property
13# Version 3: process geometry into clockwise triangles
14# Version 2: materials
15# Version 1: only mesh geometry
16
17"""
18Name: 'Allegro Demo Game Level (.txt)...'
19Blender: 249
20Group: 'Export'
21Tooltip: 'Export Allegro Demo Game Level (.txt)'
22"""
23
24__bpydoc__ = """\
25All mesh objects are exported as triangles. Coordinates are scaled by a factor
26of 10, so something at Blender coordinate x=10 will be x=100 in the game. And the
27y axis is flipped, since in the game, positive y is downwards.
28
29Every mesh must have a material assigned for each of its faces. You can do this
30by just giving a material to the mesh object, or also by applying per-face
31materials.
32
33A material must have texture slot 0 and 1 filled in and set to Image textures,
34where the first texture's image is the base texture, and the second the edge
35texture.
36The sum of the R,G,B components of the material color is used as friction.
37All other material settings are ignored.
38
39Mesh object understand the following properties:
40
41"edge_height" - The edge height for all faces in the mesh.
42"foreground" - All edges are marked foreground.
43"background" - No edge is marked collidable.
44"top" - Only upper edges of the mesh are marked as edges. Selected are all those
45edges part of the outer contour of the mesh, which are lying between the leftmost
46and rightmost vertex of that contour, in clockwise order.
47
48Objects are inserted as Blender "Empties". Each object has the following
49properties:
50
51"bitmap" - Name of the bitmap to use.
52"sound" - Name of the sound to play.
53"bonus" - If 1, this object does not count towards the number of needed fruits.
54          (So if a level has 30 fruits, and 10 are marked "bonus", only 20 need
55          to be collected. It doesn't matter at all which out of the 30 though.)
56"foreground" - This is a foreground object.
57"deco" - This object is not collidable.
58
59There are some reserved names. Empties whose name starts with "player" or
60"Player" mark the player starting position. Empties whose name starts with
61"exit" or "Exit" mark the level exit position.
62
63All other objects (cameras, lamps, ...) are ignored.
64
65Currently, there is no sanity checking done, so be sure to:
66- Assign materials to meshes/faces.
67- Make only 2D meshes. Blender will happily twist your meshes around, but the
68  exported level simply will crash. Only a planar mesh is allowed.
69- Don't forget the Empties for the player and exit positions, and set at least
70  one other collectible.
71"""
72
73import Blender, meshtools, sys, time
74
75# scaling factor when exporting vertex coordinates from blender
76SCALE_X = 10
77SCALE_Y = -10
78
79COMMENTS = False # Set to True to enable additional comments in the output
80
81# can be modified on a per-mesh basis via the "edge_height" property
82DEFAULT_EDGE_HEIGHT = 2
83
84message_string = "?"
85
86def message_draw():
87	# clearing screen
88	Blender.BGL.glClearColor(0.5, 0.5, 0.5, 1)
89	Blender.BGL.glColor3f(1.,1.,1.)
90	Blender.BGL.glClear(Blender.BGL.GL_COLOR_BUFFER_BIT)
91
92	# Buttons
93	Blender.Draw.Button("Oh oh", 1, 10, 40, 100, 25)
94
95	#Text
96	Blender.BGL.glColor3f(1, 1, 1)
97	Blender.BGL.glRasterPos2d(10, 310)
98	Blender.Draw.Text(message_string)
99
100def message_event(evt, val):
101	if evt == Blender.Draw.ESCKEY and not val: Blender.Draw.Exit()
102
103def message_bevent(evt):
104
105	if evt == 1: Blender.Draw.Exit()
106
107class MessageException(Exception): pass
108
109def message(str):
110    global message_string
111    message_string = str
112    Blender.Draw.Register(message_draw, message_event, message_bevent)
113    raise MessageException
114
115def transform(matrix, vector):
116    """
117    Given a matrix and a vector, return the result of transforming the vector by
118    the matrix.
119    matrix = [[0, 0, 0, 0], [0, 0, 0, 0]]
120    vector = [0, 0, 0, 0]
121    return = [0, 0]
122    """
123    vout = []
124    for j in range(2):
125        x = 0
126        for i in range(4):
127            x += matrix[i][j] * vector[i]
128        vout += [x]
129    return vout
130
131
132def direction(tri):
133    """
134    Given a triangle, determine if it is counterclockwise or not.
135    tri = [i, i, i],
136    where i is a global vertex index.
137    """
138    v1 = (V[tri[1]][0] - V[tri[0]][0], V[tri[1]][1] - V[tri[0]][1])
139    v2 = (V[tri[2]][0] - V[tri[1]][0], V[tri[2]][1] - V[tri[1]][1])
140    v = v1[0] * v2[1] - v1[1] * v2[0]
141    return v
142
143def upper(i1, i2):
144    """
145    Given two vertex ids, determine if the edge is an upper face.
146    """
147    v = V[i2][0] - V[i1][0]
148    return v
149
150def get_prop(ob, name, val):
151    try:
152        prop = ob.getProperty(name)
153        v = prop.getData()
154    except (AttributeError, RuntimeError):
155        v = val
156    return v
157
158def make_contour(ob, base_i):
159    """
160    ob # the Blender mesh object
161
162    Returns a pair (edges, upper_contour).
163    edges = {v: n} # maps vertex to sucessor
164    upper_contour = {v: 1} # set of vertices in the upper mesh outline
165    """
166    mesh = ob.getData()
167    # count how often each edge is present in the mesh
168    boundaries = {}
169    for f in mesh.faces:
170        prev = f.v[-1]
171        for v in f.v:
172            vs = [prev.index, v.index]
173            vs.sort()
174            vs = tuple(vs)
175            if vs in boundaries:
176                boundaries[vs] += 1
177            else:
178                boundaries[vs] = 1
179            prev = v
180
181    # map each vertex to its connected vertex
182    topmost = None
183    edges = {}
184    for f in mesh.faces:
185        prev = f.v[-1]
186        for v in f.v:
187            vs = [prev.index, v.index]
188            vs.sort()
189            vs = tuple(vs)
190            if vs in boundaries and boundaries[vs] == 1:
191                if not base_i + prev.index in edges: edges[base_i + prev.index] = {}
192                if not base_i + v.index in edges: edges[base_i + v.index] = {}
193                edges[base_i + prev.index][base_i + v.index] = 1
194                edges[base_i + v.index][base_i + prev.index] = 1
195                if not topmost or V[base_i + prev.index][1] > V[topmost][1]:
196                    topmost = base_i + prev.index
197            prev = v
198
199    # Build the contour list. We start by picking the topmost vertex, then
200    # finding its contour neighbour so we are facing a clockwise direction.
201    cv = topmost # pick topmost contour vertex
202    next = list(edges[cv])[0] # next one
203    ok = False
204    # find the face this edge is part of
205    edge = (cv, next)
206    for f in mesh.faces:
207        for i in range(len(f.v)):
208            if (base_i + f.v[i - 1].index, base_i + f.v[i].index) == edge or\
209                (base_i + f.v[i].index, base_i + f.v[i - 1].index) == edge:
210                j = base_i + f.v[(i + 1) % len(f.v)].index
211                if direction([cv, next, j]) < 0:
212                    ok = True
213                    break
214        if ok: break
215    if not ok:
216        next = list(edges[cv])[1]
217
218    full_contour = [cv] # a list of vertex indices, representing the mesh outline
219    prev = cv
220    while next != cv:
221        full_contour += [next]
222        v = list(edges[next])[0]
223        if v == prev: v = list(edges[next])[1]
224        prev = next
225        next = v
226
227    leftmost = None
228    rightmost = None
229    if COMMENTS: print "\t# contour:",
230    for vi in range(len(full_contour)):
231        v = full_contour[vi]
232        if COMMENTS: print "%d" % v,
233        if leftmost == None or V[v][0] < V[full_contour[leftmost]][0]:
234            leftmost = vi
235        if rightmost == None or V[v][0] > V[full_contour[rightmost]][0]:
236            rightmost = vi
237    if COMMENTS: print
238
239    if COMMENTS: print "\t# left: %d right: %d" % (full_contour[leftmost],
240        full_contour[rightmost])
241
242    upper_contour = {}
243    for v in range(len(full_contour)):
244        if rightmost > leftmost:
245            if v >= leftmost and v < rightmost:
246                upper_contour[full_contour[v]] = 1
247        else:
248            if v >= leftmost or v < rightmost:
249                upper_contour[full_contour[v]] = 1
250    return (edges, upper_contour)
251
252def write(filename):
253    global V
254    file = open(filename, "wb")
255    stdout = sys.stdout
256    sys.stdout=file
257
258    print "# Exported from Blender %s" % Blender.Get("version")
259    print "# on %s (%s)" % (time.ctime(), time.tzname[0])
260    print "# do not edit, edit %s instead" % Blender.Get("filename")
261    print
262
263    # Retrieve a list of all mesh objects in Blender.
264    meshobs = [ob for ob in Blender.Object.Get() if ob.getType() == "Mesh"]
265
266    # Export all current Blender materials.
267    mnum = 0
268    materials = {}
269    print "# Materials"
270    print "{"
271    for m in Blender.Material.Get():
272        t = []
273        for tex in m.getTextures()[:2]:
274            if tex and tex.tex.getImage():
275                t += [tex.tex.getImage().name.split(".")[0]]
276            else:
277                t += [""]
278        print """\t{"%s", "%s", %f} # %d \"%s\"""" % (t[0], t[1],
279            m.R + m.G + m.B, mnum, m.getName())
280        materials[m.name] = mnum
281        mnum += 1
282    print "}"
283    print
284
285    # Export all vertices of all mesh objects.
286    V = {}
287    print "# Vertices"
288    print "{"
289    i = 0
290    for ob in meshobs:
291        print "\t# Mesh \"%s\"" % ob.name
292        matrix = ob.matrix
293        mesh = ob.getData()
294        for v in mesh.verts:
295            pos = transform(matrix, [v.co.x, v.co.y, v.co.z, 1])
296            V[i] = pos
297            print "\t{%d, %d} # %d" % (round(pos[0] * SCALE_X),
298                round(pos[1] * SCALE_Y), i)
299            i += 1
300    print "}"
301    print
302
303    # Export all faces of all mesh objects (split into triangles).
304    print "# Triangles"
305    print "{"
306
307    base_i = 0
308    for ob in meshobs:
309        print "\t# Mesh \"%s\"" % ob.name
310
311        mesh = ob.getData()
312
313        meshmaterials = mesh.getMaterials(1)
314
315        edge_height = get_prop(ob, "edge_height", DEFAULT_EDGE_HEIGHT)
316        foreground = get_prop(ob, "foreground", 0)
317        background = get_prop(ob, "background", 0)
318        top = get_prop(ob, "top", 0)
319
320        edges, upper_contour = make_contour(ob, base_i)
321
322        for f in mesh.faces:
323            # First triangle.
324            tri1 = [base_i + f.v[2].index, base_i + f.v[1].index, base_i +
325                f.v[0].index]
326            if direction(tri1) > 0:
327                tri1.reverse()
328            tris = [tri1]
329
330            # If the face has 4 vertices, add another triangle.
331            if len(f.v) > 3:
332                tri2 = [tri1[2], base_i + f.v[3].index, tri1[0]]
333                if direction(tri2) > 0:
334                    tri2.reverse()
335                tris += [tri2]
336
337            for tri in tris:
338                # Handle one triangle.
339                print "\t{"
340                for i in range(3):
341                    v = tri[i]
342                    if i == 2:
343                        next = tri[0]
344                    else:
345                        next = tri[i + 1]
346                    flags = ""
347                    edge_ok = False
348                    if v in edges and next in edges[v]: edge_ok = True
349                    if top and not v in upper_contour: edge_ok = False
350                    if edge_ok:
351                        flags += " \"edge\""
352                        if background == 0 and foreground == 0:
353                             flags += " \"collidable\""
354                    if foreground != 0:
355                        flags += " \"foreground\""
356                    if flags != "":
357                        flags = ", " + flags
358                    print "\t\t{%d, %d%s}," % (v, edge_height, flags)
359
360                try:
361                    print "\t\t%d" % materials[meshmaterials[f.mat].name]
362                except IndexError:
363                    message("You forgot to assign a material to mesh %s (or \
364one of its faces)" % ob.name)
365                print "\t}"
366        base_i += len(mesh.verts)
367    print "}"
368    print
369
370    # Retrieve a list of all empty objects in Blender.
371    gameobs = [ob for ob in Blender.Object.Get() if ob.getType() == "Empty"]
372
373    obtypes = {}
374    n = 0
375
376    print "# Object types"
377    print "{"
378    for ob in gameobs:
379        if ob.name.lower().startswith("player"):
380            continue
381        if ob.name.lower().startswith("exit"):
382            continue
383        bitmap = get_prop(ob, "bitmap", "")
384        sound = get_prop(ob, "sound", "")
385
386        if not (bitmap, sound) in obtypes:
387            print "\t{ \"%s\", \"%s\" } # %d" % (bitmap, sound, n)
388            obtypes[(bitmap, sound)] = n
389            n += 1
390    print "}"
391    print
392
393    collectibles_count = 0
394    print "# Objects"
395    print "{"
396    for ob in gameobs:
397        if ob.name.lower().startswith("player"):
398            continue
399        x, y, z = ob.getLocation()
400        anglex, angley, anglez = ob.getEuler()
401        deco = get_prop(ob, "deco", 0)
402        if ob.name.lower().startswith("exit"):
403            type = -1
404        else:
405            bitmap = get_prop(ob, "bitmap", "")
406            sound = get_prop(ob, "sound", "")
407            type = obtypes[(bitmap, sound)]
408            bonus = get_prop(ob, "bonus", 0)
409            if bonus == 0 and deco == 0: collectibles_count += 1
410        foreground = get_prop(ob, "foreground", 0)
411        flags = ""
412        if deco == 0: flags += "\"collidable\""
413        if foreground != 0: flags += "\"foreground\""
414        if flags != "":
415            flags = ", " + flags
416
417        print "\t{ %d, %d, %d, %d%s } # \"%s\"" % (round(x * SCALE_X), round(y * SCALE_Y),
418            round(anglez), type, flags, ob.name)
419    print "}"
420    print
421
422    print "# Level"
423    print "{"
424    for ob in gameobs:
425        if ob.name.lower().startswith("player"):
426            x, y, z = ob.getLocation()
427            print "\t%d, %d, %d" % (round(x * SCALE_X), round(y * SCALE_Y),
428                collectibles_count)
429            break
430    print "}"
431
432    sys.stdout = stdout
433
434def fs_callback(filename):
435    # Mesh access is better done outside edit mode I believe.
436    editmode = Blender.Window.EditMode()
437    if editmode: Blender.Window.EditMode(0)
438    try:
439        write(filename)
440    except MessageException:
441        pass
442    if editmode: Blender.Window.EditMode(1)
443
444Blender.Window.FileSelector(fs_callback, "Export Allegro Demo Game Level",
445    "mylevel.txt")
446