]> git.mxchange.org Git - flightgear.git/blob - utils/Modeller/fgfs_animation.py
2fd388b397c5dcc03458cbea75eefdcc1a45cff0
[flightgear.git] / utils / Modeller / fgfs_animation.py
1 #!BPY
2
3 # """
4 # Name: 'FlightGear Animation (.xml)'
5 # Blender: 243
6 # Group: 'Export'
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'
13 # """
14
15 # BLENDER PLUGIN
16 # Put this file into ~/.blender/scripts. You'll then find
17 # it in Blender under "File->Epxort->FlightGear Animation (*.xml)"
18 #
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.
26
27
28 __author__ = "Melchior FRANZ < mfranz # aon : at >"
29 __url__ = "http://members.aon.at/mfranz/flightgear/"
30 __version__ = "$Revision$ -- $Date$ -- (Public Domain)"
31 __bpydoc__ = """\
32 == Generate textured lights ==
33
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
39 at daylight.
40
41
42 == Translation from origin ==
43
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.
48
49
50 == Translation from cursor ==
51
52 Same as above, except that movements from the cursor location are
53 calculated.
54
55
56 == Rotation ==
57
58 Generates one "rotate" animation from two selected vertices
59 (rotation axis), and the cursor (rotation center).
60
61
62 == Range ==
63
64 Generates "range" animation skeleton with list of all selected objects.
65 """
66
67 # $Id$
68 #
69 # --------------------------------------------------------------------------
70 # fgfs_animation
71 # --------------------------------------------------------------------------
72 # ***** BEGIN GPL LICENSE BLOCK *****
73 #
74 # Copyright (C) 2005: Melchior FRANZ  mfranz#aon:at
75 #
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.
80 #
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.
85 #
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 # --------------------------------------------------------------------------
89
90
91 import sys
92 import Blender
93 from math import sqrt
94 from Blender import Mathutils
95 from Blender.Mathutils import Vector, VecMultMat
96
97
98 REG_KEY = 'fgfs_animation'
99 MODE = __script__['arg']
100 ORIGIN = [0, 0, 0]
101
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"
106
107 TOOLTIPS = {
108         'PATHNAME': "where exported data should be saved (current dir if empty)",
109         'INDENT': "number of spaces per indentation level, or 0 for tabs",
110 }
111
112 reg = Blender.Registry.GetKey(REG_KEY, True)
113 if not reg:
114         print "WRITING REGISTRY"
115         Blender.Registry.SetKey(REG_KEY, {
116                 'INDENT': 0,
117                 'PATHNAME': "",
118                 'tooltips': TOOLTIPS,
119         }, True)
120
121
122
123
124 #==================================================================================================
125
126
127 class Error(Exception):
128         pass
129
130
131 class BlenderSetup:
132         def __init__(self):
133                 #Blender.Window.WaitCursor(1)
134                 self.is_editmode = Blender.Window.EditMode()
135                 if self.is_editmode:
136                         Blender.Window.EditMode(0)
137
138         def __del__(self):
139                 #Blender.Window.WaitCursor(0)
140                 if self.is_editmode:
141                         Blender.Window.EditMode(1)
142
143
144 class XMLExporter:
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)
153
154         def __del__(self):
155                 try:
156                         self.write("</PropertyList>\n")
157                         self.file.close()
158                 except AttributeError:          # opening in __init__ failed
159                         pass
160
161         def write(self, s):
162                 global INDENT
163                 if INDENT <= 0:
164                         self.file.write(s)
165                 else:
166                         self.file.write(s.expandtabs(INDENT))
167
168         def comment(self, s, e = "", a = ""):
169                 self.write(a + "<!-- " + s + " -->\n" + e)
170
171         def translate(self, name, va, vb):
172                 x = vb[0] - va[0]
173                 y = vb[1] - va[1]
174                 z = vb[2] - va[2]
175                 length = sqrt(x * x + y * y + z * z)
176                 s = """\
177         <animation>
178                 <type>translate</type>
179                 <object-name>%s</object-name>
180                 <offset-m>%s</offset-m>
181                 <axis>
182                         <x>%s</x>
183                         <y>%s</y>
184                         <z>%s</z>
185                 </axis>
186         </animation>\n\n"""
187                 self.write(s % (name, Round(length), Round(x), Round(y), Round(z)))
188
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]))
195                 s = """\
196         <animation>
197                 <type>rotate</type>
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-->
203                 <center>
204                         <x-m>%s</x-m>
205                         <y-m>%s</y-m>
206                         <z-m>%s</z-m>
207                 </center>
208                 <axis>
209                         <x>%s</x>
210                         <y>%s</y>
211                         <z>%s</z>
212                 </axis>
213         </animation>\n\n"""
214                 self.write(s % (name, Round(center[0]), Round(center[1]), Round(center[2]), x, y, z))
215
216         def billboard(self, name, spherical = "true"):
217                 s = """\
218         <animation>
219                 <type>billboard</type>
220                 <object-name>%s</object-name>
221                 <spherical type="bool">%s</spherical>
222         </animation>\n\n"""
223                 self.write(s % (name, spherical))
224
225         def group(self, name, objects):
226                 self.write("\t<animation>\n");
227                 self.write("\t\t<name>%s</name>\n" % name);
228                 for o in objects:
229                         self.write("\t\t<object-name>%s</object-name>\n" % o.name)
230                 self.write("\t</animation>\n\n");
231
232         def alphatest(self, name, factor = 0.001):
233                 s = """\
234         <animation>
235                 <type>alpha-test</type>
236                 <object-name>%s</object-name>
237                 <alpha-factor>%s</alpha-factor>
238         </animation>\n\n"""
239                 self.write(s % (name, factor))
240
241         def sunselect(self, name, angle = 1.57):
242                 s = """\
243         <animation>
244                 <type>select</type>
245                 <name>%s</name>
246                 <object-name>%s</object-name>
247                 <condition>
248                         <greater-than>
249                                 <property>/sim/time/sun-angle-rad</property>
250                                 <value>%s</value>
251                         </greater-than>
252                 </condition>
253         </animation>\n\n"""
254                 self.write(s % (name + "Night", name, angle))
255
256         def distscale(self, name, base):
257                 s = """\
258         <animation>
259                 <type>dist-scale</type>
260                 <object-name>%s</object-name>
261                 <interpolation>
262                         <entry>
263                                 <ind>0</ind>
264                                 <dep alias="../../../../%sparams/light-near"/>
265                         </entry>
266                         <entry>
267                                 <ind>500</ind>
268                                 <dep alias="../../../../%sparams/light-med"/>
269                         </entry>
270                         <entry>
271                                 <ind>16000</ind>
272                                 <dep alias="../../../../%sparams/light-far"/>
273                         </entry>
274                 </interpolation>
275         </animation>\n\n"""
276                 self.write(s % (name, base, base, base))
277
278         def range(self, objects):
279                 self.write("\t<animation>\n")
280                 self.write("\t\t<type>range</type>\n")
281                 for o in objects:
282                         self.write("\t\t<object-name>%s</object-name>\n" % o.name)
283                 self.write("""\
284                 <min-m>0</min-m>
285                 <!--
286                 <max-property>/sim/rendering/static-lod/detailed</max-property>
287
288                 <min-property>/sim/rendering/static-lod/detailed</min-property>
289                 <max-property>/sim/rendering/static-lod/rough</max-property>
290
291                 <min-property>/sim/rendering/static-lod/rough</min-property>
292                 -->
293                 <max-property>/sim/rendering/static-lod/bare</max-property>\n""")
294                 self.write("\t</animation>\n\n")
295
296         def lightrange(self, dist = 25000):
297                 s = """\
298         <animation>
299                 <type>range</type>
300                 <min-m>0</min-m>
301                 <max-m>%s</max-m>
302         </animation>\n\n"""
303                 self.write(s % dist)
304
305
306 #==================================================================================================
307
308
309 def Round(f, digits = 6):
310         r = round(f, digits)
311         if r == int(r):
312                 return str(int(r))
313         else:
314                 return str(r)
315
316
317 def serial(i, max = 0):
318         if max == 1:
319                 return ""
320         if max < 10:
321                 return "%d" % i
322         if max < 100:
323                 return "%02d" % i
324         if max < 1000:
325                 return "%03d" % i
326         if max < 10000:
327                 return "%04d" % i
328         return "%d" % i
329
330
331 def needsObjects(objects, n):
332         def error(e):
333                 raise Error("wrong number of selected mesh objects: please select " + e)
334         if n < 0 and len(objects) < -n:
335                 if n == -1:
336                         error("at least one object")
337                 else:
338                         error("at least %d objects" % -n)
339         elif n > 0 and len(objects) != n:
340                 if n == 1:
341                         error("exactly one object")
342                 else:
343                         error("exactly %d objects" % n)
344
345
346 def checkName(name):
347         """ check if name is already in use """
348         try:
349                 Blender.Object.Get(name)
350                 raise Error("can't generate object '" + name + "'; name already in use")
351         except AttributeError:
352                 pass
353         except ValueError:
354                 pass
355
356
357 def selectedVertices(object):
358         verts = []
359         mat = object.getMatrix('worldspace')
360         for v in object.getData().verts:
361                 if not v.sel:
362                         continue
363                 vec = Vector([v[0], v[1], v[2]])
364                 vec.resize4D()
365                 vec *= mat
366                 v[0], v[1], v[2] = vec[0], vec[1], vec[2]
367                 verts.append(v)
368         return verts
369
370
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))
378
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)
384
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)
390
391         mat = Blender.Material.New(name + "mat")
392         mat.setRGBCol(1, 1, 1)
393         mat.setMirCol(1, 1, 1)
394         mat.setAlpha(1)
395         mat.setEmit(1)
396         mat.setSpecCol(0, 0, 0)
397         mat.setSpec(0)
398         mat.setAmb(0)
399         mesh.setMaterials([mat])
400         return mesh
401
402
403 def createLight(mesh, name):
404         object = Blender.Object.New("Mesh", name)
405         object.link(mesh)
406         Blender.Scene.getCurrent().link(object)
407         return object
408
409
410 # modes ===========================================================================================
411
412
413 class mode:
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']
417                 if xml != None:
418                         BlenderSetup()
419                         self.test()
420                         self.execute(xml)
421
422         def test(self):
423                 pass
424
425
426 class translationFromOrigin(mode):
427         def execute(self, xml):
428                 if self.cursor != ORIGIN:
429                         xml.translate("BlenderCursor", ORIGIN, self.cursor)
430
431                 needsObjects(self.objects, 1)
432                 object = self.objects[0]
433                 verts = selectedVertices(object)
434                 if len(verts):
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)
438
439
440 class translationFromCursor(mode):
441         def test(self):
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")
447
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)
452
453
454 class rotation(mode):
455         def test(self):
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!")
461
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])
468
469
470 class levelOfDetail(mode):
471         def test(self):
472                 needsObjects(self.objects, -1)
473
474         def execute(self, xml):
475                 xml.comment('[%s] level of detail' % BASENAME, '\n')
476                 xml.range(self.objects)
477
478
479 class interpolation(mode):
480         def test(self):
481                 needsObjects(self.objects, -2)
482
483         def execute(self, xml):
484                 print
485                 for i, o in enumerate(self.objects):
486                         print "%d: %s" % (i, o.name)
487
488                 raise Error("this mode doesn't do anything useful yet")
489
490
491 class texturedLights(mode):
492         def test(self):
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"
499
500                 checkName(self.lightname)
501                 for i, v in enumerate(self.verts):
502                         checkName(self.lightname + serial(i, len(self.verts)))
503
504         def execute(self, xml):
505                 lightname = self.lightname
506                 verts = self.verts
507                 object = self.object
508
509                 lightmesh = createLightMesh(lightname)
510
511                 lights = []
512                 for i, v in enumerate(verts):
513                         lights.append(createLight(lightmesh, lightname + serial(i, len(verts))))
514
515                 parent = object.getParent()
516                 if parent != None:
517                         parent.makeParent(lights)
518
519                 for l in lights:
520                         l.Layer = object.Layer
521
522                 xml.lightrange()
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)
528
529                 if len(lights) == 1:
530                         xml.sunselect(lightname)
531                         xml.alphatest(lightname)
532                 else:
533                         xml.group(lightname + "Group", lights)
534                         xml.sunselect(lightname + "Group")
535                         xml.alphatest(lightname + "Group")
536
537                 for i, l in enumerate(lights):
538                         xml.translate(l.name, ORIGIN, verts[i])
539
540                 for l in lights:
541                         xml.billboard(l.name)
542
543                 for l in lights:
544                         xml.distscale(l.name, lightname)
545
546                 Blender.Redraw(-1)
547
548
549 execute = {
550         'LIGHTS'   : texturedLights,
551         'TRANS0'   : translationFromOrigin,
552         'TRANSC'   : translationFromCursor,
553         'ROTATE'   : rotation,
554         'RANGE'    : levelOfDetail,
555         'INTERPOL' : interpolation
556 }
557
558
559
560 # main() ==========================================================================================
561
562
563 def dofile(filename):
564         try:
565                 xml = XMLExporter(filename)
566                 execute[MODE](xml)
567
568         except Error, e:
569                 xml.comment("ERROR: " + e.args[0], '\n')
570                 raise Error(e.args[0])
571
572         except IOError, (errno, strerror):
573                 raise Error(strerror)
574
575
576 def main():
577         try:
578                 global FILENAME, INDENT
579                 reg = Blender.Registry.GetKey(REG_KEY, True)
580                 if reg:
581                         PATHNAME = reg['PATHNAME'] or ""
582                         INDENT = reg['INDENT'] or 0
583                 else:
584                         PATHNAME = ""
585                         INDENT = 0
586
587                 if PATHNAME:
588                         FILENAME = Blender.sys.join(PATHNAME, FILENAME)
589
590                 print 'writing to "' + FILENAME + '"'
591                 dofile(FILENAME)
592
593         except Error, e:
594                 print "ERROR: " + e.args[0]
595                 Blender.Draw.PupMenu("ERROR%t|" + e.args[0])
596
597
598
599 if MODE:
600         main()
601
602