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'
16 # Put this file into ~/.blender/scripts. You'll then find
17 # it in Blender under "File->Epxort->FlightGear Animation (*.xml)"
19 # For the script to work properly, make sure a directory
20 # ~/.blender/scripts/bpydata/config/ exists. To change the
21 # script parameters, edit file fgfs_animation.cfg in that
22 # dir, or in blender, switch to "Scripts Window" view, select
23 # "Scripts->System->Scripts Config Editor" and there select
24 # "Export->FlightGear Animation (*.xml". Don't forget to
25 # "apply" the changes.
28 __author__ = "Melchior FRANZ < mfranz # aon : at >"
29 __url__ = "http://members.aon.at/mfranz/flightgear/"
30 __version__ = "$Revision$ -- $Date$ -- (Public Domain)"
32 == Generate textured lights ==
34 Adds linked "light objects" (square consisting of two triangles at
35 origin) for each selected vertex, and creates FlightGear animation
36 code that scales all faces according to view distance, moves them to
37 the vertex location, turns the face towards the viewer and blends
38 it with other (semi)transparent objects. All lights are turned off
42 == Translation from origin ==
44 Generates "translate" animation for each selected vertex and for the
45 cursor, if it is not at origin. This mode is thought for moving
46 objects to their proper location after scaling them at origin, a
47 technique that is used for lights.
50 == Translation from cursor ==
52 Same as above, except that movements from the cursor location are
58 Generates one "rotate" animation from two selected vertices
59 (rotation axis), and the cursor (rotation center).
64 Generates "range" animation skeleton with list of all selected objects.
69 # --------------------------------------------------------------------------
71 # --------------------------------------------------------------------------
72 # ***** BEGIN GPL LICENSE BLOCK *****
74 # Copyright (C) 2005: Melchior FRANZ mfranz#aon:at
76 # This program is free software; you can redistribute it and/or
77 # modify it under the terms of the GNU General Public License
78 # as published by the Free Software Foundation; either version 2
79 # of the License, or (at your option) any later version.
81 # This program is distributed in the hope that it will be useful,
82 # but WITHOUT ANY WARRANTY; without even the implied warranty of
83 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
84 # GNU General Public License for more details.
86 # You should have received a copy of the GNU General Public License
87 # along with this program; if not, write to the Free Software Foundation,
88 # --------------------------------------------------------------------------
94 from Blender import Mathutils
95 from Blender.Mathutils import Vector, VecMultMat
98 REG_KEY = 'fgfs_animation'
99 MODE = __script__['arg']
102 FILENAME = Blender.Get("filename")
103 (BASENAME, EXTNAME) = Blender.sys.splitext(FILENAME)
104 DIRNAME = Blender.sys.dirname(BASENAME)
105 FILENAME = Blender.sys.basename(BASENAME) + "-export.xml"
108 'PATHNAME': "where exported data should be saved (current dir if empty)",
109 'INDENT': "number of spaces per indentation level, or 0 for tabs",
112 reg = Blender.Registry.GetKey(REG_KEY, True)
114 print "WRITING REGISTRY"
115 Blender.Registry.SetKey(REG_KEY, {
118 'tooltips': TOOLTIPS,
124 #==================================================================================================
127 class Error(Exception):
133 #Blender.Window.WaitCursor(1)
134 self.is_editmode = Blender.Window.EditMode()
136 Blender.Window.EditMode(0)
139 #Blender.Window.WaitCursor(0)
141 Blender.Window.EditMode(1)
145 def __init__(self, filename):
146 cursor = Blender.Window.GetCursorPos()
147 self.file = open(filename, "w")
148 self.write('<?xml version="1.0"?>\n')
149 self.write("<!-- project: %s -->\n" % Blender.Get("filename"))
150 self.write("<!-- cursor: %0.12f %0.12f %0.12f -->\n\n" % (cursor[0], cursor[1], cursor[2]))
151 self.write("<PropertyList>\n")
152 self.write("\t<path>%s.ac</path>\n\n" % BASENAME)
156 self.write("</PropertyList>\n")
158 except AttributeError: # opening in __init__ failed
166 self.file.write(s.expandtabs(INDENT))
168 def comment(self, s, e = "", a = ""):
169 self.write(a + "<!-- " + s + " -->\n" + e)
171 def translate(self, name, va, vb):
175 length = sqrt(x * x + y * y + z * z)
178 <type>translate</type>
179 <object-name>%s</object-name>
180 <offset-m>%s</offset-m>
187 self.write(s % (name, Round(length), Round(x), Round(y), Round(z)))
189 def rotate(self, name, center, va, vb):
190 x = Round(vb[0] - va[0])
191 y = Round(vb[1] - va[1])
192 z = Round(vb[2] - va[2])
193 self.write("\t<!-- Vertex 0: %f %f %f -->\n" % (va[0], va[1], va[2]))
194 self.write("\t<!-- Vertex 1: %f %f %f -->\n" % (vb[0], vb[1], vb[2]))
198 <object-name>%s</object-name>
199 <property>null</property>
200 <!--min-deg>0</min-deg-->
201 <!--max-deg>360</max-deg-->
202 <!--factor>1</factor-->
214 self.write(s % (name, Round(center[0]), Round(center[1]), Round(center[2]), x, y, z))
216 def billboard(self, name, spherical = "true"):
219 <type>billboard</type>
220 <object-name>%s</object-name>
221 <spherical type="bool">%s</spherical>
223 self.write(s % (name, spherical))
225 def group(self, name, objects):
226 self.write("\t<animation>\n");
227 self.write("\t\t<name>%s</name>\n" % name);
229 self.write("\t\t<object-name>%s</object-name>\n" % o.name)
230 self.write("\t</animation>\n\n");
232 def alphatest(self, name, factor = 0.001):
235 <type>alpha-test</type>
236 <object-name>%s</object-name>
237 <alpha-factor>%s</alpha-factor>
239 self.write(s % (name, factor))
241 def sunselect(self, name, angle = 1.57):
246 <object-name>%s</object-name>
249 <property>/sim/time/sun-angle-rad</property>
254 self.write(s % (name + "Night", name, angle))
256 def distscale(self, name, base):
259 <type>dist-scale</type>
260 <object-name>%s</object-name>
264 <dep alias="../../../../%sparams/light-near"/>
268 <dep alias="../../../../%sparams/light-med"/>
272 <dep alias="../../../../%sparams/light-far"/>
276 self.write(s % (name, base, base, base))
278 def range(self, objects):
279 self.write("\t<animation>\n")
280 self.write("\t\t<type>range</type>\n")
282 self.write("\t\t<object-name>%s</object-name>\n" % o.name)
286 <max-property>/sim/rendering/static-lod/detailed</max-property>
288 <min-property>/sim/rendering/static-lod/detailed</min-property>
289 <max-property>/sim/rendering/static-lod/rough</max-property>
291 <min-property>/sim/rendering/static-lod/rough</min-property>
293 <max-property>/sim/rendering/static-lod/bare</max-property>\n""")
294 self.write("\t</animation>\n\n")
296 def lightrange(self, dist = 25000):
306 #==================================================================================================
309 def Round(f, digits = 6):
317 def serial(i, max = 0):
331 def needsObjects(objects, n):
333 raise Error("wrong number of selected mesh objects: please select " + e)
334 if n < 0 and len(objects) < -n:
336 error("at least one object")
338 error("at least %d objects" % -n)
339 elif n > 0 and len(objects) != n:
341 error("exactly one object")
343 error("exactly %d objects" % n)
347 """ check if name is already in use """
349 Blender.Object.Get(name)
350 raise Error("can't generate object '" + name + "'; name already in use")
351 except AttributeError:
357 def selectedVertices(object):
359 mat = object.getMatrix('worldspace')
360 for v in object.getData().verts:
363 vec = Vector([v[0], v[1], v[2]])
366 v[0], v[1], v[2] = vec[0], vec[1], vec[2]
371 def createLightMesh(name, size = 1):
372 mesh = Blender.NMesh.New(name + "mesh")
373 mesh.mode = Blender.NMesh.Modes.NOVNORMALSFLIP
374 mesh.verts.append(Blender.NMesh.Vert(-size, 0, -size))
375 mesh.verts.append(Blender.NMesh.Vert(size, 0, -size))
376 mesh.verts.append(Blender.NMesh.Vert(size, 0, size))
377 mesh.verts.append(Blender.NMesh.Vert(-size, 0, size))
379 face1 = Blender.NMesh.Face()
380 face1.v.append(mesh.verts[0])
381 face1.v.append(mesh.verts[1])
382 face1.v.append(mesh.verts[2])
383 mesh.faces.append(face1)
385 face2 = Blender.NMesh.Face()
386 face2.v.append(mesh.verts[0])
387 face2.v.append(mesh.verts[2])
388 face2.v.append(mesh.verts[3])
389 mesh.faces.append(face2)
391 mat = Blender.Material.New(name + "mat")
392 mat.setRGBCol(1, 1, 1)
393 mat.setMirCol(1, 1, 1)
396 mat.setSpecCol(0, 0, 0)
399 mesh.setMaterials([mat])
403 def createLight(mesh, name):
404 object = Blender.Object.New("Mesh", name)
406 Blender.Scene.getCurrent().link(object)
410 # modes ===========================================================================================
414 def __init__(self, xml = None):
415 self.cursor = Blender.Window.GetCursorPos()
416 self.objects = [o for o in Blender.Object.GetSelected() if o.getType() == 'Mesh']
426 class translationFromOrigin(mode):
427 def execute(self, xml):
428 if self.cursor != ORIGIN:
429 xml.translate("BlenderCursor", ORIGIN, self.cursor)
431 needsObjects(self.objects, 1)
432 object = self.objects[0]
433 verts = selectedVertices(object)
435 xml.comment('[%s] translate from origin: "%s"' % (BASENAME, object.name), '\n')
436 for i, v in enumerate(verts):
437 xml.translate("X%d" % i, ORIGIN, v)
440 class translationFromCursor(mode):
442 needsObjects(self.objects, 1)
443 self.object = self.objects[0]
444 self.verts = selectedVertices(self.object)
445 if not len(self.verts):
446 raise Error("no vertex selected")
448 def execute(self, xml):
449 xml.comment('[%s] translate from cursor: "%s"' % (BASENAME, self.object.name), '\n')
450 for i, v in enumerate(self.verts):
451 xml.translate("X%d" % i, self.cursor, v)
454 class rotation(mode):
456 needsObjects(self.objects, 1)
457 self.object = self.objects[0]
458 self.verts = selectedVertices(self.object)
459 if len(self.verts) != 2:
460 raise Error("you have to select two vertices that define the rotation axis!")
462 def execute(self, xml):
463 xml.comment('[%s] rotate "%s"' % (BASENAME, self.object.name), '\n')
464 if self.cursor == ORIGIN:
465 Blender.Draw.PupMenu("The cursor is still at origin!%t|"\
466 "But nevertheless, I pretend it is the rotation center.")
467 xml.rotate(self.object.name, self.cursor, self.verts[0], self.verts[1])
470 class levelOfDetail(mode):
472 needsObjects(self.objects, -1)
474 def execute(self, xml):
475 xml.comment('[%s] level of detail' % BASENAME, '\n')
476 xml.range(self.objects)
479 class interpolation(mode):
481 needsObjects(self.objects, -2)
483 def execute(self, xml):
485 for i, o in enumerate(self.objects):
486 print "%d: %s" % (i, o.name)
488 raise Error("this mode doesn't do anything useful yet")
491 class texturedLights(mode):
493 needsObjects(self.objects, 1)
494 self.object = self.objects[0]
495 self.verts = selectedVertices(self.object)
496 if not len(self.verts):
497 raise Error("there are no vertices to put lights at")
498 self.lightname = self.object.name + "X"
500 checkName(self.lightname)
501 for i, v in enumerate(self.verts):
502 checkName(self.lightname + serial(i, len(self.verts)))
504 def execute(self, xml):
505 lightname = self.lightname
509 lightmesh = createLightMesh(lightname)
512 for i, v in enumerate(verts):
513 lights.append(createLight(lightmesh, lightname + serial(i, len(verts))))
515 parent = object.getParent()
517 parent.makeParent(lights)
520 l.Layer = object.Layer
523 xml.write("\t<%sparams>\n" % lightname)
524 xml.write("\t\t<light-near>%s</light-near>\n" % "0.4")
525 xml.write("\t\t<light-med>%s</light-med>\n" % "0.8")
526 xml.write("\t\t<light-far>%s</light-far>\n" % "10")
527 xml.write("\t</%sparams>\n\n" % lightname)
530 xml.sunselect(lightname)
531 xml.alphatest(lightname)
533 xml.group(lightname + "Group", lights)
534 xml.sunselect(lightname + "Group")
535 xml.alphatest(lightname + "Group")
537 for i, l in enumerate(lights):
538 xml.translate(l.name, ORIGIN, verts[i])
541 xml.billboard(l.name)
544 xml.distscale(l.name, lightname)
550 'LIGHTS' : texturedLights,
551 'TRANS0' : translationFromOrigin,
552 'TRANSC' : translationFromCursor,
554 'RANGE' : levelOfDetail,
555 'INTERPOL' : interpolation
560 # main() ==========================================================================================
563 def dofile(filename):
565 xml = XMLExporter(filename)
569 xml.comment("ERROR: " + e.args[0], '\n')
570 raise Error(e.args[0])
572 except IOError, (errno, strerror):
573 raise Error(strerror)
578 global FILENAME, INDENT
579 reg = Blender.Registry.GetKey(REG_KEY, True)
581 PATHNAME = reg['PATHNAME'] or ""
582 INDENT = reg['INDENT'] or 0
588 FILENAME = Blender.sys.join(PATHNAME, FILENAME)
590 print 'writing to "' + FILENAME + '"'
594 print "ERROR: " + e.args[0]
595 Blender.Draw.PupMenu("ERROR%t|" + e.args[0])