From 7ddd2efbe6998dd6a26e63cc06f543f6351e97c7 Mon Sep 17 00:00:00 2001 From: mfranz Date: Tue, 20 Feb 2007 11:35:59 +0000 Subject: [PATCH] animation plugin for Blender 2.43 --- utils/Modeller/fgfs_animation.py | 602 +++++++++++++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 utils/Modeller/fgfs_animation.py diff --git a/utils/Modeller/fgfs_animation.py b/utils/Modeller/fgfs_animation.py new file mode 100644 index 000000000..2fd388b39 --- /dev/null +++ b/utils/Modeller/fgfs_animation.py @@ -0,0 +1,602 @@ +#!BPY + +# """ +# Name: 'FlightGear Animation (.xml)' +# Blender: 243 +# Group: 'Export' +# Submenu: 'Generate textured lights' LIGHTS +# Submenu: 'Rotation' ROTATE +# Submenu: 'Range (LOD)' RANGE +# Submenu: 'Translation from origin' TRANS0 +# Submenu: 'Translation from cursor' TRANSC +# Tooltip: 'FlightGear Animation' +# """ + +# BLENDER PLUGIN +# Put this file into ~/.blender/scripts. You'll then find +# it in Blender under "File->Epxort->FlightGear Animation (*.xml)" +# +# For the script to work properly, make sure a directory +# ~/.blender/scripts/bpydata/config/ exists. To change the +# script parameters, edit file fgfs_animation.cfg in that +# dir, or in blender, switch to "Scripts Window" view, select +# "Scripts->System->Scripts Config Editor" and there select +# "Export->FlightGear Animation (*.xml". Don't forget to +# "apply" the changes. + + +__author__ = "Melchior FRANZ < mfranz # aon : at >" +__url__ = "http://members.aon.at/mfranz/flightgear/" +__version__ = "$Revision$ -- $Date$ -- (Public Domain)" +__bpydoc__ = """\ +== Generate textured lights == + +Adds linked "light objects" (square consisting of two triangles at +origin) for each selected vertex, and creates FlightGear animation +code that scales all faces according to view distance, moves them to +the vertex location, turns the face towards the viewer and blends +it with other (semi)transparent objects. All lights are turned off +at daylight. + + +== Translation from origin == + +Generates "translate" animation for each selected vertex and for the +cursor, if it is not at origin. This mode is thought for moving +objects to their proper location after scaling them at origin, a +technique that is used for lights. + + +== Translation from cursor == + +Same as above, except that movements from the cursor location are +calculated. + + +== Rotation == + +Generates one "rotate" animation from two selected vertices +(rotation axis), and the cursor (rotation center). + + +== Range == + +Generates "range" animation skeleton with list of all selected objects. +""" + +# $Id$ +# +# -------------------------------------------------------------------------- +# fgfs_animation +# -------------------------------------------------------------------------- +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2005: Melchior FRANZ mfranz#aon:at +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# -------------------------------------------------------------------------- + + +import sys +import Blender +from math import sqrt +from Blender import Mathutils +from Blender.Mathutils import Vector, VecMultMat + + +REG_KEY = 'fgfs_animation' +MODE = __script__['arg'] +ORIGIN = [0, 0, 0] + +FILENAME = Blender.Get("filename") +(BASENAME, EXTNAME) = Blender.sys.splitext(FILENAME) +DIRNAME = Blender.sys.dirname(BASENAME) +FILENAME = Blender.sys.basename(BASENAME) + "-export.xml" + +TOOLTIPS = { + 'PATHNAME': "where exported data should be saved (current dir if empty)", + 'INDENT': "number of spaces per indentation level, or 0 for tabs", +} + +reg = Blender.Registry.GetKey(REG_KEY, True) +if not reg: + print "WRITING REGISTRY" + Blender.Registry.SetKey(REG_KEY, { + 'INDENT': 0, + 'PATHNAME': "", + 'tooltips': TOOLTIPS, + }, True) + + + + +#================================================================================================== + + +class Error(Exception): + pass + + +class BlenderSetup: + def __init__(self): + #Blender.Window.WaitCursor(1) + self.is_editmode = Blender.Window.EditMode() + if self.is_editmode: + Blender.Window.EditMode(0) + + def __del__(self): + #Blender.Window.WaitCursor(0) + if self.is_editmode: + Blender.Window.EditMode(1) + + +class XMLExporter: + def __init__(self, filename): + cursor = Blender.Window.GetCursorPos() + self.file = open(filename, "w") + self.write('\n') + self.write("\n" % Blender.Get("filename")) + self.write("\n\n" % (cursor[0], cursor[1], cursor[2])) + self.write("\n") + self.write("\t%s.ac\n\n" % BASENAME) + + def __del__(self): + try: + self.write("\n") + self.file.close() + except AttributeError: # opening in __init__ failed + pass + + def write(self, s): + global INDENT + if INDENT <= 0: + self.file.write(s) + else: + self.file.write(s.expandtabs(INDENT)) + + def comment(self, s, e = "", a = ""): + self.write(a + "\n" + e) + + def translate(self, name, va, vb): + x = vb[0] - va[0] + y = vb[1] - va[1] + z = vb[2] - va[2] + length = sqrt(x * x + y * y + z * z) + s = """\ + + translate + %s + %s + + %s + %s + %s + + \n\n""" + self.write(s % (name, Round(length), Round(x), Round(y), Round(z))) + + def rotate(self, name, center, va, vb): + x = Round(vb[0] - va[0]) + y = Round(vb[1] - va[1]) + z = Round(vb[2] - va[2]) + self.write("\t\n" % (va[0], va[1], va[2])) + self.write("\t\n" % (vb[0], vb[1], vb[2])) + s = """\ + + rotate + %s + null + + + +
+ %s + %s + %s +
+ + %s + %s + %s + +
\n\n""" + self.write(s % (name, Round(center[0]), Round(center[1]), Round(center[2]), x, y, z)) + + def billboard(self, name, spherical = "true"): + s = """\ + + billboard + %s + %s + \n\n""" + self.write(s % (name, spherical)) + + def group(self, name, objects): + self.write("\t\n"); + self.write("\t\t%s\n" % name); + for o in objects: + self.write("\t\t%s\n" % o.name) + self.write("\t\n\n"); + + def alphatest(self, name, factor = 0.001): + s = """\ + + alpha-test + %s + %s + \n\n""" + self.write(s % (name, factor)) + + def sunselect(self, name, angle = 1.57): + s = """\ + + select + %s + %s + + + /sim/time/sun-angle-rad + %s + + + \n\n""" + self.write(s % (name + "Night", name, angle)) + + def distscale(self, name, base): + s = """\ + + dist-scale + %s + + + 0 + + + + 500 + + + + 16000 + + + + \n\n""" + self.write(s % (name, base, base, base)) + + def range(self, objects): + self.write("\t\n") + self.write("\t\trange\n") + for o in objects: + self.write("\t\t%s\n" % o.name) + self.write("""\ + 0 + + /sim/rendering/static-lod/bare\n""") + self.write("\t\n\n") + + def lightrange(self, dist = 25000): + s = """\ + + range + 0 + %s + \n\n""" + self.write(s % dist) + + +#================================================================================================== + + +def Round(f, digits = 6): + r = round(f, digits) + if r == int(r): + return str(int(r)) + else: + return str(r) + + +def serial(i, max = 0): + if max == 1: + return "" + if max < 10: + return "%d" % i + if max < 100: + return "%02d" % i + if max < 1000: + return "%03d" % i + if max < 10000: + return "%04d" % i + return "%d" % i + + +def needsObjects(objects, n): + def error(e): + raise Error("wrong number of selected mesh objects: please select " + e) + if n < 0 and len(objects) < -n: + if n == -1: + error("at least one object") + else: + error("at least %d objects" % -n) + elif n > 0 and len(objects) != n: + if n == 1: + error("exactly one object") + else: + error("exactly %d objects" % n) + + +def checkName(name): + """ check if name is already in use """ + try: + Blender.Object.Get(name) + raise Error("can't generate object '" + name + "'; name already in use") + except AttributeError: + pass + except ValueError: + pass + + +def selectedVertices(object): + verts = [] + mat = object.getMatrix('worldspace') + for v in object.getData().verts: + if not v.sel: + continue + vec = Vector([v[0], v[1], v[2]]) + vec.resize4D() + vec *= mat + v[0], v[1], v[2] = vec[0], vec[1], vec[2] + verts.append(v) + return verts + + +def createLightMesh(name, size = 1): + mesh = Blender.NMesh.New(name + "mesh") + mesh.mode = Blender.NMesh.Modes.NOVNORMALSFLIP + mesh.verts.append(Blender.NMesh.Vert(-size, 0, -size)) + mesh.verts.append(Blender.NMesh.Vert(size, 0, -size)) + mesh.verts.append(Blender.NMesh.Vert(size, 0, size)) + mesh.verts.append(Blender.NMesh.Vert(-size, 0, size)) + + face1 = Blender.NMesh.Face() + face1.v.append(mesh.verts[0]) + face1.v.append(mesh.verts[1]) + face1.v.append(mesh.verts[2]) + mesh.faces.append(face1) + + face2 = Blender.NMesh.Face() + face2.v.append(mesh.verts[0]) + face2.v.append(mesh.verts[2]) + face2.v.append(mesh.verts[3]) + mesh.faces.append(face2) + + mat = Blender.Material.New(name + "mat") + mat.setRGBCol(1, 1, 1) + mat.setMirCol(1, 1, 1) + mat.setAlpha(1) + mat.setEmit(1) + mat.setSpecCol(0, 0, 0) + mat.setSpec(0) + mat.setAmb(0) + mesh.setMaterials([mat]) + return mesh + + +def createLight(mesh, name): + object = Blender.Object.New("Mesh", name) + object.link(mesh) + Blender.Scene.getCurrent().link(object) + return object + + +# modes =========================================================================================== + + +class mode: + def __init__(self, xml = None): + self.cursor = Blender.Window.GetCursorPos() + self.objects = [o for o in Blender.Object.GetSelected() if o.getType() == 'Mesh'] + if xml != None: + BlenderSetup() + self.test() + self.execute(xml) + + def test(self): + pass + + +class translationFromOrigin(mode): + def execute(self, xml): + if self.cursor != ORIGIN: + xml.translate("BlenderCursor", ORIGIN, self.cursor) + + needsObjects(self.objects, 1) + object = self.objects[0] + verts = selectedVertices(object) + if len(verts): + xml.comment('[%s] translate from origin: "%s"' % (BASENAME, object.name), '\n') + for i, v in enumerate(verts): + xml.translate("X%d" % i, ORIGIN, v) + + +class translationFromCursor(mode): + def test(self): + needsObjects(self.objects, 1) + self.object = self.objects[0] + self.verts = selectedVertices(self.object) + if not len(self.verts): + raise Error("no vertex selected") + + def execute(self, xml): + xml.comment('[%s] translate from cursor: "%s"' % (BASENAME, self.object.name), '\n') + for i, v in enumerate(self.verts): + xml.translate("X%d" % i, self.cursor, v) + + +class rotation(mode): + def test(self): + needsObjects(self.objects, 1) + self.object = self.objects[0] + self.verts = selectedVertices(self.object) + if len(self.verts) != 2: + raise Error("you have to select two vertices that define the rotation axis!") + + def execute(self, xml): + xml.comment('[%s] rotate "%s"' % (BASENAME, self.object.name), '\n') + if self.cursor == ORIGIN: + Blender.Draw.PupMenu("The cursor is still at origin!%t|"\ + "But nevertheless, I pretend it is the rotation center.") + xml.rotate(self.object.name, self.cursor, self.verts[0], self.verts[1]) + + +class levelOfDetail(mode): + def test(self): + needsObjects(self.objects, -1) + + def execute(self, xml): + xml.comment('[%s] level of detail' % BASENAME, '\n') + xml.range(self.objects) + + +class interpolation(mode): + def test(self): + needsObjects(self.objects, -2) + + def execute(self, xml): + print + for i, o in enumerate(self.objects): + print "%d: %s" % (i, o.name) + + raise Error("this mode doesn't do anything useful yet") + + +class texturedLights(mode): + def test(self): + needsObjects(self.objects, 1) + self.object = self.objects[0] + self.verts = selectedVertices(self.object) + if not len(self.verts): + raise Error("there are no vertices to put lights at") + self.lightname = self.object.name + "X" + + checkName(self.lightname) + for i, v in enumerate(self.verts): + checkName(self.lightname + serial(i, len(self.verts))) + + def execute(self, xml): + lightname = self.lightname + verts = self.verts + object = self.object + + lightmesh = createLightMesh(lightname) + + lights = [] + for i, v in enumerate(verts): + lights.append(createLight(lightmesh, lightname + serial(i, len(verts)))) + + parent = object.getParent() + if parent != None: + parent.makeParent(lights) + + for l in lights: + l.Layer = object.Layer + + xml.lightrange() + xml.write("\t<%sparams>\n" % lightname) + xml.write("\t\t%s\n" % "0.4") + xml.write("\t\t%s\n" % "0.8") + xml.write("\t\t%s\n" % "10") + xml.write("\t\n\n" % lightname) + + if len(lights) == 1: + xml.sunselect(lightname) + xml.alphatest(lightname) + else: + xml.group(lightname + "Group", lights) + xml.sunselect(lightname + "Group") + xml.alphatest(lightname + "Group") + + for i, l in enumerate(lights): + xml.translate(l.name, ORIGIN, verts[i]) + + for l in lights: + xml.billboard(l.name) + + for l in lights: + xml.distscale(l.name, lightname) + + Blender.Redraw(-1) + + +execute = { + 'LIGHTS' : texturedLights, + 'TRANS0' : translationFromOrigin, + 'TRANSC' : translationFromCursor, + 'ROTATE' : rotation, + 'RANGE' : levelOfDetail, + 'INTERPOL' : interpolation +} + + + +# main() ========================================================================================== + + +def dofile(filename): + try: + xml = XMLExporter(filename) + execute[MODE](xml) + + except Error, e: + xml.comment("ERROR: " + e.args[0], '\n') + raise Error(e.args[0]) + + except IOError, (errno, strerror): + raise Error(strerror) + + +def main(): + try: + global FILENAME, INDENT + reg = Blender.Registry.GetKey(REG_KEY, True) + if reg: + PATHNAME = reg['PATHNAME'] or "" + INDENT = reg['INDENT'] or 0 + else: + PATHNAME = "" + INDENT = 0 + + if PATHNAME: + FILENAME = Blender.sys.join(PATHNAME, FILENAME) + + print 'writing to "' + FILENAME + '"' + dofile(FILENAME) + + except Error, e: + print "ERROR: " + e.args[0] + Blender.Draw.PupMenu("ERROR%t|" + e.args[0]) + + + +if MODE: + main() + +