flightgear/utils/Modeller/fgfs_animation.py
2022-10-20 20:29:11 +08:00

586 lines
14 KiB
Python

#!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->Export->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.
#
# This file is Public Domain.
__author__ = "Melchior FRANZ < mfranz # aon : at >"
__url__ = "http://members.aon.at/mfranz/flightgear/"
__version__ = "$Revision$ -- $Date$"
__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.
"""
#==================================================================================================
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('<?xml version="1.0"?>\n')
self.write("<!-- project: %s -->\n" % Blender.Get("filename"))
self.write("<!-- cursor: %0.12f %0.12f %0.12f -->\n\n" % (cursor[0], cursor[1], cursor[2]))
self.write("<PropertyList>\n")
self.write("\t<path>%s.ac</path>\n\n" % BASENAME)
def __del__(self):
try:
self.write("</PropertyList>\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 + "<!-- " + s + " -->\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 = """\
<animation>
<type>translate</type>
<object-name>%s</object-name>
<offset-m>%s</offset-m>
<axis>
<x>%s</x>
<y>%s</y>
<z>%s</z>
</axis>
</animation>\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<!-- Vertex 0: %f %f %f -->\n" % (va[0], va[1], va[2]))
self.write("\t<!-- Vertex 1: %f %f %f -->\n" % (vb[0], vb[1], vb[2]))
s = """\
<animation>
<type>rotate</type>
<object-name>%s</object-name>
<property>null</property>
<!--min-deg>0</min-deg-->
<!--max-deg>360</max-deg-->
<!--factor>1</factor-->
<center>
<x-m>%s</x-m>
<y-m>%s</y-m>
<z-m>%s</z-m>
</center>
<axis>
<x>%s</x>
<y>%s</y>
<z>%s</z>
</axis>
</animation>\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 = """\
<animation>
<type>billboard</type>
<object-name>%s</object-name>
<spherical type="bool">%s</spherical>
</animation>\n\n"""
self.write(s % (name, spherical))
def group(self, name, objects):
self.write("\t<animation>\n");
self.write("\t\t<name>%s</name>\n" % name);
for o in objects:
self.write("\t\t<object-name>%s</object-name>\n" % o.name)
self.write("\t</animation>\n\n");
def alphatest(self, name, factor = 0.001):
s = """\
<animation>
<type>alpha-test</type>
<object-name>%s</object-name>
<alpha-factor>%s</alpha-factor>
</animation>\n\n"""
self.write(s % (name, factor))
def sunselect(self, name, angle = 1.57):
s = """\
<animation>
<type>select</type>
<name>%s</name>
<object-name>%s</object-name>
<condition>
<greater-than>
<property>/sim/time/sun-angle-rad</property>
<value>%s</value>
</greater-than>
</condition>
</animation>\n\n"""
self.write(s % (name + "Night", name, angle))
def distscale(self, name, base):
s = """\
<animation>
<type>dist-scale</type>
<object-name>%s</object-name>
<interpolation>
<entry>
<ind>0</ind>
<dep alias="../../../../%sparams/light-near"/>
</entry>
<entry>
<ind>500</ind>
<dep alias="../../../../%sparams/light-med"/>
</entry>
<entry>
<ind>16000</ind>
<dep alias="../../../../%sparams/light-far"/>
</entry>
</interpolation>
</animation>\n\n"""
self.write(s % (name, base, base, base))
def range(self, objects):
self.write("\t<animation>\n")
self.write("\t\t<type>range</type>\n")
for o in objects:
self.write("\t\t<object-name>%s</object-name>\n" % o.name)
self.write("""\
<min-m>0</min-m>
<!--
<max-property>/sim/rendering/static-lod/detailed</max-property>
<min-property>/sim/rendering/static-lod/detailed</min-property>
<max-property>/sim/rendering/static-lod/rough</max-property>
<min-property>/sim/rendering/static-lod/rough</min-property>
-->
<max-property>/sim/rendering/static-lod/bare</max-property>\n""")
self.write("\t</animation>\n\n")
def lightrange(self, dist = 25000):
s = """\
<animation>
<type>range</type>
<min-m>0</min-m>
<max-m>%s</max-m>
</animation>\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 is not 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 is not None:
parent.makeParent(lights)
for l in lights:
l.Layer = object.Layer
xml.lightrange()
xml.write("\t<%sparams>\n" % lightname)
xml.write("\t\t<light-near>%s</light-near>\n" % "0.4")
xml.write("\t\t<light-med>%s</light-med>\n" % "0.8")
xml.write("\t\t<light-far>%s</light-far>\n" % "10")
xml.write("\t</%sparams>\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()