4 # Name: 'FlightGear Animation (.xml)'
7 # Submenu: 'Generate textured lights' LIGHTS
8 # Submenu: 'Rotation' ROTATE
9 # Submenu: 'Range (LOD)' RANGE
10 # Submenu: 'Translation from origin' TRANS0
11 # Submenu: 'Translation from cursor' TRANSC
12 # Tooltip: 'FlightGear Animation'
17 # Put this file into ~/.blender/scripts/. You'll then find
18 # it in Blender under "File->Export->FlightGear Animation (*.xml)"
20 # For the script to work properly, make sure a directory
21 # ~/.blender/scripts/bpydata/config/ exists. To change the
22 # script parameters, edit file fgfs_animation.cfg in that
23 # dir, or in blender, switch to "Scripts Window" view, select
24 # "Scripts->System->Scripts Config Editor" and there select
25 # "Export->FlightGear Animation (*.xml)". Don't forget to
26 # "apply" the changes.
28 # This file is Public Domain.
31 __author__ = "Melchior FRANZ < mfranz # aon : at >"
32 __url__ = "http://members.aon.at/mfranz/flightgear/"
33 __version__ = "$Revision$ -- $Date$"
35 == Generate textured lights ==
37 Adds linked "light objects" (square consisting of two triangles at
38 origin) for each selected vertex, and creates FlightGear animation
39 code that scales all faces according to view distance, moves them to
40 the vertex location, turns the face towards the viewer and blends
41 it with other (semi)transparent objects. All lights are turned off
45 == Translation from origin ==
47 Generates "translate" animation for each selected vertex and for the
48 cursor, if it is not at origin. This mode is thought for moving
49 objects to their proper location after scaling them at origin, a
50 technique that is used for lights.
53 == Translation from cursor ==
55 Same as above, except that movements from the cursor location are
61 Generates one "rotate" animation from two selected vertices
62 (rotation axis), and the cursor (rotation center).
67 Generates "range" animation skeleton with list of all selected objects.
71 #==================================================================================================
77 from Blender import Mathutils
78 from Blender.Mathutils import Vector, VecMultMat
81 REG_KEY = 'fgfs_animation'
82 MODE = __script__['arg']
85 FILENAME = Blender.Get("filename")
86 (BASENAME, EXTNAME) = Blender.sys.splitext(FILENAME)
87 DIRNAME = Blender.sys.dirname(BASENAME)
88 FILENAME = Blender.sys.basename(BASENAME) + "-export.xml"
91 'PATHNAME': "where exported data should be saved (current dir if empty)",
92 'INDENT': "number of spaces per indentation level, or 0 for tabs",
95 reg = Blender.Registry.GetKey(REG_KEY, True)
97 print "WRITING REGISTRY"
98 Blender.Registry.SetKey(REG_KEY, {
101 'tooltips': TOOLTIPS,
107 #==================================================================================================
110 class Error(Exception):
116 #Blender.Window.WaitCursor(1)
117 self.is_editmode = Blender.Window.EditMode()
119 Blender.Window.EditMode(0)
122 #Blender.Window.WaitCursor(0)
124 Blender.Window.EditMode(1)
128 def __init__(self, filename):
129 cursor = Blender.Window.GetCursorPos()
130 self.file = open(filename, "w")
131 self.write('<?xml version="1.0"?>\n')
132 self.write("<!-- project: %s -->\n" % Blender.Get("filename"))
133 self.write("<!-- cursor: %0.12f %0.12f %0.12f -->\n\n" % (cursor[0], cursor[1], cursor[2]))
134 self.write("<PropertyList>\n")
135 self.write("\t<path>%s.ac</path>\n\n" % BASENAME)
139 self.write("</PropertyList>\n")
141 except AttributeError: # opening in __init__ failed
149 self.file.write(s.expandtabs(INDENT))
151 def comment(self, s, e = "", a = ""):
152 self.write(a + "<!-- " + s + " -->\n" + e)
154 def translate(self, name, va, vb):
158 length = sqrt(x * x + y * y + z * z)
161 <type>translate</type>
162 <object-name>%s</object-name>
163 <offset-m>%s</offset-m>
170 self.write(s % (name, Round(length), Round(x), Round(y), Round(z)))
172 def rotate(self, name, center, va, vb):
173 x = Round(vb[0] - va[0])
174 y = Round(vb[1] - va[1])
175 z = Round(vb[2] - va[2])
176 self.write("\t<!-- Vertex 0: %f %f %f -->\n" % (va[0], va[1], va[2]))
177 self.write("\t<!-- Vertex 1: %f %f %f -->\n" % (vb[0], vb[1], vb[2]))
181 <object-name>%s</object-name>
182 <property>null</property>
183 <!--min-deg>0</min-deg-->
184 <!--max-deg>360</max-deg-->
185 <!--factor>1</factor-->
197 self.write(s % (name, Round(center[0]), Round(center[1]), Round(center[2]), x, y, z))
199 def billboard(self, name, spherical = "true"):
202 <type>billboard</type>
203 <object-name>%s</object-name>
204 <spherical type="bool">%s</spherical>
206 self.write(s % (name, spherical))
208 def group(self, name, objects):
209 self.write("\t<animation>\n");
210 self.write("\t\t<name>%s</name>\n" % name);
212 self.write("\t\t<object-name>%s</object-name>\n" % o.name)
213 self.write("\t</animation>\n\n");
215 def alphatest(self, name, factor = 0.001):
218 <type>alpha-test</type>
219 <object-name>%s</object-name>
220 <alpha-factor>%s</alpha-factor>
222 self.write(s % (name, factor))
224 def sunselect(self, name, angle = 1.57):
229 <object-name>%s</object-name>
232 <property>/sim/time/sun-angle-rad</property>
237 self.write(s % (name + "Night", name, angle))
239 def distscale(self, name, base):
242 <type>dist-scale</type>
243 <object-name>%s</object-name>
247 <dep alias="../../../../%sparams/light-near"/>
251 <dep alias="../../../../%sparams/light-med"/>
255 <dep alias="../../../../%sparams/light-far"/>
259 self.write(s % (name, base, base, base))
261 def range(self, objects):
262 self.write("\t<animation>\n")
263 self.write("\t\t<type>range</type>\n")
265 self.write("\t\t<object-name>%s</object-name>\n" % o.name)
269 <max-property>/sim/rendering/static-lod/detailed</max-property>
271 <min-property>/sim/rendering/static-lod/detailed</min-property>
272 <max-property>/sim/rendering/static-lod/rough</max-property>
274 <min-property>/sim/rendering/static-lod/rough</min-property>
276 <max-property>/sim/rendering/static-lod/bare</max-property>\n""")
277 self.write("\t</animation>\n\n")
279 def lightrange(self, dist = 25000):
289 #==================================================================================================
292 def Round(f, digits = 6):
300 def serial(i, max = 0):
314 def needsObjects(objects, n):
316 raise Error("wrong number of selected mesh objects: please select " + e)
317 if n < 0 and len(objects) < -n:
319 error("at least one object")
321 error("at least %d objects" % -n)
322 elif n > 0 and len(objects) != n:
324 error("exactly one object")
326 error("exactly %d objects" % n)
330 """ check if name is already in use """
332 Blender.Object.Get(name)
333 raise Error("can't generate object '" + name + "'; name already in use")
334 except AttributeError:
340 def selectedVertices(object):
342 mat = object.getMatrix('worldspace')
343 for v in object.getData().verts:
346 vec = Vector([v[0], v[1], v[2]])
349 v[0], v[1], v[2] = vec[0], vec[1], vec[2]
354 def createLightMesh(name, size = 1):
355 mesh = Blender.NMesh.New(name + "mesh")
356 mesh.mode = Blender.NMesh.Modes.NOVNORMALSFLIP
357 mesh.verts.append(Blender.NMesh.Vert(-size, 0, -size))
358 mesh.verts.append(Blender.NMesh.Vert(size, 0, -size))
359 mesh.verts.append(Blender.NMesh.Vert(size, 0, size))
360 mesh.verts.append(Blender.NMesh.Vert(-size, 0, size))
362 face1 = Blender.NMesh.Face()
363 face1.v.append(mesh.verts[0])
364 face1.v.append(mesh.verts[1])
365 face1.v.append(mesh.verts[2])
366 mesh.faces.append(face1)
368 face2 = Blender.NMesh.Face()
369 face2.v.append(mesh.verts[0])
370 face2.v.append(mesh.verts[2])
371 face2.v.append(mesh.verts[3])
372 mesh.faces.append(face2)
374 mat = Blender.Material.New(name + "mat")
375 mat.setRGBCol(1, 1, 1)
376 mat.setMirCol(1, 1, 1)
379 mat.setSpecCol(0, 0, 0)
382 mesh.setMaterials([mat])
386 def createLight(mesh, name):
387 object = Blender.Object.New("Mesh", name)
389 Blender.Scene.getCurrent().link(object)
393 # modes ===========================================================================================
397 def __init__(self, xml = None):
398 self.cursor = Blender.Window.GetCursorPos()
399 self.objects = [o for o in Blender.Object.GetSelected() if o.getType() == 'Mesh']
409 class translationFromOrigin(mode):
410 def execute(self, xml):
411 if self.cursor != ORIGIN:
412 xml.translate("BlenderCursor", ORIGIN, self.cursor)
414 needsObjects(self.objects, 1)
415 object = self.objects[0]
416 verts = selectedVertices(object)
418 xml.comment('[%s] translate from origin: "%s"' % (BASENAME, object.name), '\n')
419 for i, v in enumerate(verts):
420 xml.translate("X%d" % i, ORIGIN, v)
423 class translationFromCursor(mode):
425 needsObjects(self.objects, 1)
426 self.object = self.objects[0]
427 self.verts = selectedVertices(self.object)
428 if not len(self.verts):
429 raise Error("no vertex selected")
431 def execute(self, xml):
432 xml.comment('[%s] translate from cursor: "%s"' % (BASENAME, self.object.name), '\n')
433 for i, v in enumerate(self.verts):
434 xml.translate("X%d" % i, self.cursor, v)
437 class rotation(mode):
439 needsObjects(self.objects, 1)
440 self.object = self.objects[0]
441 self.verts = selectedVertices(self.object)
442 if len(self.verts) != 2:
443 raise Error("you have to select two vertices that define the rotation axis!")
445 def execute(self, xml):
446 xml.comment('[%s] rotate "%s"' % (BASENAME, self.object.name), '\n')
447 if self.cursor == ORIGIN:
448 Blender.Draw.PupMenu("The cursor is still at origin!%t|"\
449 "But nevertheless, I pretend it is the rotation center.")
450 xml.rotate(self.object.name, self.cursor, self.verts[0], self.verts[1])
453 class levelOfDetail(mode):
455 needsObjects(self.objects, -1)
457 def execute(self, xml):
458 xml.comment('[%s] level of detail' % BASENAME, '\n')
459 xml.range(self.objects)
462 class interpolation(mode):
464 needsObjects(self.objects, -2)
466 def execute(self, xml):
468 for i, o in enumerate(self.objects):
469 print "%d: %s" % (i, o.name)
471 raise Error("this mode doesn't do anything useful yet")
474 class texturedLights(mode):
476 needsObjects(self.objects, 1)
477 self.object = self.objects[0]
478 self.verts = selectedVertices(self.object)
479 if not len(self.verts):
480 raise Error("there are no vertices to put lights at")
481 self.lightname = self.object.name + "X"
483 checkName(self.lightname)
484 for i, v in enumerate(self.verts):
485 checkName(self.lightname + serial(i, len(self.verts)))
487 def execute(self, xml):
488 lightname = self.lightname
492 lightmesh = createLightMesh(lightname)
495 for i, v in enumerate(verts):
496 lights.append(createLight(lightmesh, lightname + serial(i, len(verts))))
498 parent = object.getParent()
500 parent.makeParent(lights)
503 l.Layer = object.Layer
506 xml.write("\t<%sparams>\n" % lightname)
507 xml.write("\t\t<light-near>%s</light-near>\n" % "0.4")
508 xml.write("\t\t<light-med>%s</light-med>\n" % "0.8")
509 xml.write("\t\t<light-far>%s</light-far>\n" % "10")
510 xml.write("\t</%sparams>\n\n" % lightname)
513 xml.sunselect(lightname)
514 xml.alphatest(lightname)
516 xml.group(lightname + "Group", lights)
517 xml.sunselect(lightname + "Group")
518 xml.alphatest(lightname + "Group")
520 for i, l in enumerate(lights):
521 xml.translate(l.name, ORIGIN, verts[i])
524 xml.billboard(l.name)
527 xml.distscale(l.name, lightname)
533 'LIGHTS' : texturedLights,
534 'TRANS0' : translationFromOrigin,
535 'TRANSC' : translationFromCursor,
537 'RANGE' : levelOfDetail,
538 'INTERPOL' : interpolation
543 # main() ==========================================================================================
546 def dofile(filename):
548 xml = XMLExporter(filename)
552 xml.comment("ERROR: " + e.args[0], '\n')
553 raise Error(e.args[0])
555 except IOError, (errno, strerror):
556 raise Error(strerror)
561 global FILENAME, INDENT
562 reg = Blender.Registry.GetKey(REG_KEY, True)
564 PATHNAME = reg['PATHNAME'] or ""
565 INDENT = reg['INDENT'] or 0
571 FILENAME = Blender.sys.join(PATHNAME, FILENAME)
573 print 'writing to "' + FILENAME + '"'
577 print "ERROR: " + e.args[0]
578 Blender.Draw.PupMenu("ERROR%t|" + e.args[0])