warzone2100/tools/blender/pie_import.py

549 lines
20 KiB
Python

#!BPY
"""
Name: 'Warzone model (.pie)...'
Blender: 244
Group: 'Import'
Tooltip: 'Load a Warzone model file'
"""
__author__ = "Rodolphe Suescun, Gerard Krol, Kevin Gillette"
__url__ = ["blender"]
__version__ = "1.2"
__bpydoc__ = """\
This script imports PIE files to Blender.
Usage:
Run this script from "File->Import" menu and then load the desired PIE file.
"""
#
# --------------------------------------------------------------------------
# PIE Import v0.1 by Rodolphe Suescun (AKA RodZilla)
# v0.2 by Gerard Krol (gerard_)
# v0.3 by Kevin Gillette (kage)
# v1.0 --
# --------------------------------------------------------------------------
# ***** BEGIN GPL LICENSE BLOCK *****
#
# 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,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
# --------------------------------------------------------------------------
from Blender import *
from pie_common import *
import os, pie
#==================================================================================#
# Draws a thumbnail from a Blender image at x,y with sides that are edgelen long #
#==================================================================================#
gen = None
ui_ref = None
def fresh_generator(filename):
return pie.data_mutator(pie.parse(filename))
def seek_to_directive(err_on_nonmatch, fatal_if_not_found, *directives):
for i in gen:
type = i[pie.TOKEN_TYPE]
if type == "directive" and i[pie.DIRECTIVE_NAME] in directives:
return i
elif type == "error":
ui.debug(i[pie.ERROR].args[0], "error", i[pie.LINENO])
elif err_on_nonmatch:
ui.debug("expected %s directive." % directives[0], "warning", i[pie.LINENO])
if fatal_if_not_found:
ui.debug(directives[0] + " directive not found. cannot continue.",
"fatal-error")
def generate_opt_gui(rect, optlist, tooltips, limits):
""" expects rect to be [xmin, ymin, xmax, ymax] """
BGL.glRecti(*rect)
buttonwidth = 120
buttonheight = 20
margin = 5
defbuttonwidth = 20 # default button
defbuttonpos = rect[2] - defbuttonwidth - margin
buttonpos = defbuttonpos - margin - buttonwidth
labelwidth = buttonpos - rect[0] - margin
numopts = len(optlist)
for i, opt in enumerate(optlist):
name = opt['name']
val = scalar_value(opt['val'])
posY = rect[3] - (i + 1) * (buttonheight + margin)
if posY < rect[1] + margin: break
Draw.PushButton("D", i + numopts, defbuttonpos, posY, defbuttonwidth, buttonheight,
"Set this option to its default")
tooltip = tooltips.get(name, "")
if isinstance(opt['opts'], basestring):
title = opt.get('title', "")
if opt['opts'] is 'number':
opt['val'] = Draw.Number(title, i, buttonpos, posY, buttonwidth,
buttonheight, val, limits[name][0],
limits[name][1], tooltip)
elif opt['opts'] is 'bool':
opt['val'] = Draw.Toggle(title, i, buttonpos, posY, buttonwidth,
buttonheight, val, tooltip)
else:
numopts = len(opt['opts'])
if numopts < 2:
ui.debug("error: invalid option supplied to generate_opt_gui")
continue
if numopts is 2:
Draw.PushButton(opt['opts'][val], i, buttonpos, posY,
buttonwidth, buttonheight, tooltip)
else:
menustr = opt.get('title', "")
if menustr: menustr += "%t|"
menustr += '|'.join("%s %%x%i" % (opt['opts'][j], j) for j in xrange(numopts))
opt['val'] = Draw.Menu(menustr, i, buttonpos, posY, buttonwidth,
buttonheight, val, tooltip)
Draw.Label(opt['label'], rect[0] + margin, posY, labelwidth, buttonheight)
def opt_process(ui):
ui.setData('defaults/script', {'auto-layer': True, 'import-scale': 1.0,
'scripts/layers': "pie_levels_to_layers.py",
'scripts/validate': "pie_validate.py"})
ui.setData('defaults/user', Registry.GetKey('warzone_pie', True) or dict())
ui.setData('limits', {'import-scale': (0.1, 1000.0)})
ui.setData('tooltips', {'auto-layer': "Selecting this would be the same as selecting all the newly created objects and running Object -> Scripts -> \"PIE levels -> layers\"",
'import-scale': "Values under 1.0 may be useful when importing from a model edited at abnormally large size in pie slicer. Values over 1.0 probably won't be useful.",
'scripts/layers': "Basename of the \"PIE levels -> layers\" script. Do not include the path. Whitespace is not trimmed",
'scripts/validate': "Basename of the \"PIE validate\" script. Do not include the path. Whitespace is not trimmed"})
i = seek_to_directive(True, False, "PIE")
if i is None:
ui.debug("not a valid PIE. PIE directive is required", "warning")
gen = fresh_generator(ui.getData('filename'))
ui.setData('pie-version', 2, True)
else:
ui.setData('pie-version', i[pie.DIRECTIVE_VALUE], True)
ui.debug("version %i pie detected" % ui.getData('pie-version'))
i = seek_to_directive(True, False, "TYPE")
if i is None:
ui.debug("not a valid PIE. TYPE directive is required", "warning")
gen = fresh_generator(ui.getData('filename'))
ui.setData('pie-type', 0x200, True)
else:
ui.setData('pie-type', i[pie.DIRECTIVE_VALUE], True)
ui.debug("type %i pie detected" % ui.getData('pie-type'))
optlist = list()
dirs = Get('uscriptsdir'), Get('scriptsdir')
script = default_value(ui, 'scripts/layers')
for d in dirs:
if d and script in os.listdir(d):
optlist.append({'name': "auto-layer",
'label': "Assign levels to different layers?",
'opts': 'bool', 'title': "Automatically Layer"})
ui.setData('scripts/layers', os.path.join(d, script), True)
break
else:
ui.debug("Could not find '%s'. automatic layering will not be available" % script)
optlist.append({'name': "import-scale", 'label': "Scale all points by a factor.",
'opts': 'number', 'title': "Scale"})
for opt in optlist:
opt.setdefault('val', default_value(ui, opt['name']))
ui.setData('optlist', optlist)
def opt_draw(ui):
optlist = ui.getData('optlist')
numopts = len(optlist)
Draw.PushButton("Cancel", numopts * 2 + 1, 68, 15, 140, 30, "Cancel the import operation")
Draw.PushButton("Proceed", numopts * 2, 217, 15, 140, 30, "Confirm texpage selection and continue")
Draw.PushButton("Save Defaults", numopts * 2 + 3, 68, 321, 140, 30, "Save options in their current state as the default")
Draw.PushButton("Default All", numopts * 2 + 2, 217, 321, 140, 30, "Revert all options to their defaults")
BGL.glClearColor(0.7, 0.7, 0.7, 1)
BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
BGL.glColor3f(0.8, 0.8, 0.8)
BGL.glRecti(5, 5, 431, 411)
BGL.glColor3f(0.7, 0.7, 0.7)
BGL.glRecti(15, 361, 421, 401)
generate_opt_gui([15, 55, 421, 311], optlist, ui.getData('tooltips'), ui.getData('limits'))
BGL.glColor3i(0, 0, 0)
text = ("General Options", "large")
BGL.glRasterPos2i(int((406 - Draw.GetStringWidth(*text)) / 2 + 15), 377)
Draw.Text(*text)
def opt_evt(ui, val):
opts = ui.getData('optlist')
numopts = len(opts)
if val >= numopts * 2:
val -= numopts * 2
if val is 0:
if not ui.getData('defaults/user'): save_defaults(ui)
for opt in opts:
ui.setData(opt['name'], scalar_value(opt['val']), True)
return True
elif val is 1:
return False
elif val is 2:
if ui.getData('defaults/user'):
def_src = Draw.PupMenu(
"Source of default value %t|Script default|User default")
else:
def_src = 1
if def_src > 0:
for opt in opts:
name = opt['name']
if def_src is 1: opt['val'] = ui.getData('defaults/script')[name]
elif def_src is 2: opt['val'] = default_value(ui, name)
else: break
Draw.Redraw()
elif val is 3:
save_defaults(ui)
return
if val < numopts:
opt = opts[val]
if not isinstance(opt, basestring):
if len(opt['opts']) is 2:
opt['val'] = abs(opt['val'] - 1) # toggle it between 0 and 1
elif val < numopts * 2:
opt = ui.getData('optlist')[val - numopts]
name = opt['name']
if name in ui.getData('defaults/user'):
def_src = Draw.PupMenu(
"Source of default value %t|Script default|User default")
else:
def_src = 1
if def_src is 1: opt['val'] = ui.getData('defaults/script')[name]
elif def_src is 2: opt['val'] = ui.getData('defaults/user')[name]
else: return
Draw.Redraw()
def thumbnailize(img, x, y, edgelen):
try:
# BGL.glClearColor(0.7, 0.7, 0.7, 1)
# BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
if img: BGL.glColor3f(0.0, 0.0, 0.0)
else: BGL.glColor3f(1.0, 1.0, 1.0)
BGL.glRecti(x, y, x + edgelen, y + edgelen)
except NameError, AttributeError:
ui.debug("unable to load BGL. will not affect anything, though")
if not img: return
width, height = img.getSize()
edgelen = float(edgelen)
if width > height: primary, secondary, reverse = width, height, False
else: primary, secondary, reverse = height, width, True
if primary < edgelen: zoom = 1.0
else: zoom = 1.0 / (primary / edgelen)
offset = int((edgelen - zoom * secondary) / 2)
if zoom == 1.0: offset = [int((edgelen - zoom * primary) / 2), offset]
else: offset = [0, offset]
if reverse: offset.reverse()
Draw.Image(img, offset[0] + x, offset[1] + y, zoom, zoom)
def new_texpage(filename):
texpage_cache = ui_ref.getData('texpage-cache')
options = ui_ref.getData('texpage-opts')
if filename in texpage_cache:
img = texpage_cache[filename]
else:
img = Image.Load(filename)
texpage_cache[filename] = img
if filename not in options:
ui.getData('texpage-menu').val = len(options)
options.append(filename)
if 'texpage-dir' not in ui:
ui.setData('texpage-dir', os.path.dirname(filename))
return img
def texpage_process(ui):
global gen
ui.setData('texpage-menu', Draw.Create(0))
ui.setData('texpage-opts', list())
ui.setData('texpage-cache', dict())
i = seek_to_directive(False, False, "NOTEXTURE", "TEXTURE")
if i is None: # else assume NOTEXTURE and run it again after everything else
ui.debug("not a valid PIE. Either a TEXTURE or NOTEXTURE directive is required", "warning")
gen = fresh_generator(ui.getData('filename'))
texfilename = ""
ui.setData('texpage-width', 256, True)
ui.setData('texpage-height', 256, True)
else:
texfilename = i[pie.TEXTURE_FILENAME]
ui.setData('texpage-width', i[pie.TEXTURE_WIDTH], True)
ui.setData('texpage-height', i[pie.TEXTURE_HEIGHT], True)
basename, ext = os.path.splitext(texfilename.lower())
namelen = len(basename) + len(ext)
texpage_opts = ui.getData('texpage-opts')
ui.debug('basename: ' + basename)
ui.debug('ext: ' + ext)
for d in (('..', 'texpages'), ('..', '..', 'texpages'), ('..', '..', '..', 'texpages')):
d = os.path.join(ui.getData('dir'), *d)
if not os.path.exists(d): continue
ui.setData('texpage-dir', d)
for fn in os.listdir(d):
fnlower = fn.lower()
if fnlower.startswith(basename):
canonical = os.path.abspath(os.path.join(d, fn))
if fnlower.endswith(ext) and len(fn) == namelen:
texpage_opts.insert(0, canonical)
else:
texpage_opts.append(canonical)
ui.debug('texpage options: ' + str(ui.getData('texpage-opts')))
def texpage_draw(ui):
Draw.PushButton("Other texpage", 2, 15, 135, 140, 30, "Select another texpage from your filesystem")
Draw.PushButton("Cancel", 1, 15, 95, 140, 30, "Cancel the import operation")
Draw.PushButton("Proceed", 0, 15, 55, 140, 30, "Confirm texpage selection and continue")
options = ui.getData('texpage-opts')
numopts = len(options)
menustr = "Texpages %t"
menustr += ''.join("|%s %%x%i" % (options[i], i) for i in xrange(numopts))
menustr += "|NOTEXTURE %%x%i" % numopts
ui.setData('texpage-menu', Draw.Menu(menustr, 3, 15, 15, 406, 30,
ui.getData('texpage-menu').val, "Select a texpage to bind to the model"))
menu = ui.getData('texpage-menu')
if menu.val < numopts:
selected = options[menu.val]
selected = new_texpage(selected)
w, h = selected.getSize()
Draw.Label("%ix%i at %i bpp" % (w, h, selected.getDepth()), 16, 289, 136, 20)
else:
Draw.Label("model will appear white", 16, 289, 136, 20)
selected = None
BGL.glClearColor(0.7, 0.7, 0.7, 1)
BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
BGL.glColor3f(0.8, 0.8, 0.8)
BGL.glRecti(5, 5, 431, 361)
BGL.glColor3f(0.7, 0.7, 0.7)
BGL.glRecti(15, 241, 155, 311)
BGL.glRecti(15, 321, 421, 351)
BGL.glColor3i(0, 0, 0)
text = ("Texpage Selection", "large")
BGL.glRasterPos2i(int((406 - Draw.GetStringWidth(*text)) / 2 + 15), 332)
Draw.Text(*text)
thumbnailize(selected, 165, 55, 256)
def texpage_evt(ui, val):
if 0 == val:
options = ui.getData('texpage-opts')
texpage_cache = ui.getData('texpage-cache')
menu = ui.getData('texpage-menu')
msg = "selected texpage: "
currentMat = Material.New('PIE_mat')
texture = Texture.New()
if menu.val < len(options):
msg += options[menu.val]
ui.debug("binding texture to texpage")
texture.setType('Image')
image = texpage_cache[options[menu.val]]
texture.image = image
else:
msg += "NOTEXTURE"
texture.setType('None')
image = None
ui.debug(msg)
currentMat.setTexture(0, texture, Texture.TexCo.UV, Texture.MapTo.COL)
ui.setData('material', currentMat, True)
ui.setData('image', image, True)
return True
if 1 == val:
return False
elif 2 == val:
Window.ImageSelector(new_texpage, "Select a texpage",
ui.getData('texpage-dir', ui.getData('dir')))
Draw.Redraw()
elif 3 == val:
Draw.Redraw()
def model_process(ui):
i = seek_to_directive(True, True, "LEVELS")
numlevels = i[pie.DIRECTIVE_VALUE]
level, nbActualPoints, default_coords, mesh = 0, 0, None, None
scale = ui.getData('import-scale')
if scale is not 1.0:
ui.debug("scaling by a factor of %.1f" % scale)
point_divisor = 128.0 / scale
def new_point():
mesh.verts.extend(*default_coords)
ui.debug("patching invalid point or point reference: new point is at (%.1f, %.1f, %.1f)" % \
tuple(default_coords), "error")
default_coords[1] -= 0.1
return nbActualPoints + 1
if ui.getData('pie-version') >= 5:
divisorX, divisorY = 1, 1
else:
divisorX = ui.getData('texpage-width')
divisorY = ui.getData('texpage-height')
scn = Scene.GetCurrent() # link object to current scene
pieobj = scn.objects.new("Empty", "PIE_" + os.path.splitext(
os.path.basename(ui.getData('filename')))[0].upper())
while level < numlevels:
i = seek_to_directive(True, True, "LEVEL")
level += 1
if level != i[pie.DIRECTIVE_VALUE]:
ui.debug("LEVEL should have value of %i on line %i. reordered to %i" % \
(level, i[pie.LINENO], level), "warning")
mesh = Mesh.New('clean')
mesh.materials += [ui.getData('material')]
mesh.addUVLayer('base')
mesh.addUVLayer('teamcolor_meta')
i = seek_to_directive(True, True, "POINTS")
num, nbOfficialPoints, nbActualPoints = 0, i[pie.DIRECTIVE_VALUE], 0
default_coords = [1.0, 0.5, 0.0]
abandon_level = True
try:
while num < nbOfficialPoints:
i = gen.next()
if i[pie.TOKEN_TYPE] == "error":
if isinstance(i[pie.ERROR], pie.PIESyntaxError) and \
i[pie.ERROR_ASSUMED_TYPE] == "point":
num += 1
ui.debug("point no. %i is not valid." % num, "error", i[pie.LINENO])
nbActualPoints = new_point()
else:
ui.debug(i[pie.ERROR].args, "error")
break
else:
num += 1
x, y, z = i[pie.FIRST:]
#todo: convert to Mesh code
mesh.verts.extend(-x / point_divisor, -z / point_divisor, y / point_divisor)
nbActualPoints += 1
else:
abandon_level = False
if abandon_level:
ui.debug("remaining data in this LEVEL cannot be trusted.", "error")
continue
i = seek_to_directive(True, True, "POLYGONS")
num, numtotal = 0, i[pie.DIRECTIVE_VALUE]
while num < numtotal:
i = gen.next()
force_valid_points = list()
if i[pie.TOKEN_TYPE] == "error":
error = i[pie.ERROR]
if isinstance(error, pie.PIESyntaxError) and \
i[pie.ERROR_ASSUMED_TYPE] == "polygon":
num += 1
ui.debug("polygon no. %i is not valid. omitting" % num, "error", i[pie.LINENO])
continue
else:
ui.debug(str(i[pie.ERROR].args[0]), "error", i[pie.LINENO])
if isinstance(error, pie.PIEStructuralError):
i = gen.next()
if i[pie.TOKEN_TYPE] != "polygon":
ui.debug("expected polygon data. abandoning this LEVEL", "error")
break
nbPoints, pos = i[pie.FIRST + 1], pie.FIRST + 2
points = i[pos:pos + nbPoints]
for p in (nbPoints - p for p in xrange(1, nbPoints)):
if points.count(points[p]) > 1:
i[pos + p], nbActualPoints = nbActualPoints, new_point()
force_valid_points.append(p)
else: break
if i[pie.TOKEN_TYPE] != "polygon":
ui.debug("expected polygon data. abandoning this LEVEL", "error")
break
flags = i[pie.FIRST]
nbPoints = i[pie.FIRST + 1]
pos = pie.FIRST + 2
points = i[pos:pos + nbPoints]
for p in xrange(nbPoints):
if points[p] >= nbOfficialPoints and p not in force_valid_points:
points[p], nbActualPoints = nbActualPoints, new_point()
mesh.faces.extend(points, ignoreDups=True)
if not flags & 0x200: continue
pos += nbPoints
f = mesh.faces[num]
num += 1
f.mat = 0
if flags & 0x4000:
mesh.activeUVLayer = 'teamcolor_meta'
nbFrames = i[pos]
framedelay = i[pos + 1]
width = i[pos + 2]
height = i[pos + 3]
pos += 4
ui.debug('max ' + str(nbFrames))
ui.debug('time ' + str(framedelay))
ui.debug('width ' + str(width))
ui.debug('height ' + str(height))
if nbFrames < 1:
ui.debug("maximum number of teamcolors/animation frames must be at least 1", "error")
if nbFrames is 1:
ui.debug("maximum number of teamcolors/animation frames should be greater than 1", "warning")
width /= divisorX
height /= divisorY
f.uv = create_teamcolor_meta(nbPoints, width, height, nbFrames, framedelay)
mesh.activeUVLayer = 'base'
f.image = ui.getData('image')
if ui.getData('pie-version') >= 5:
uv = [Mathutils.Vector(i[pos], i[pos + 1]) for pos in range(pos, pos + 2 * nbPoints, 2)]
else:
uv = [Mathutils.Vector(i[pos] / divisorX, 1 - i[pos + 1] / divisorY) for pos in range(pos, pos + 2 * nbPoints, 2)]
ui.debug("UVs: " + repr(uv))
f.uv = uv
if flags & 0x2000:
# double sided
f.mode |= Mesh.FaceModes['TWOSIDE']
if flags & 0x800:
# transparent
f.transp = Mesh.FaceTranspModes['ALPHA']
except StopIteration: pass
ob = scn.objects.new(mesh, 'LEVEL_%i' % level)
mesh.flipNormals()
pieobj.makeParent([ob], 0, 0)
i = seek_to_directive(False, False, "CONNECTORS")
if i is not None:
num, numtotal = 0, i[pie.DIRECTIVE_VALUE]
while num < numtotal:
i = gen.next()
if i[pie.TOKEN_TYPE] == "error":
if isinstance(i[pie.ERROR], pie.PIESyntaxError) and \
i[pie.ERROR_ASSUMED_TYPE] == "connector":
num += 1
ui.debug("connector no. %i is not valid. omitting" % num, "error", i[pie.LINENO])
continue
else:
ui.debug(i[pie.ERROR].args[0], "error", i[pie.LINENO])
break
num += 1
x, y, z = i[pie.FIRST:]
#empty = scn.objects.new('Empty', "CONNECTOR_%i" % num)
empty = scn.objects.new('Empty', "CONNECTOR_%i" % num)
empty.loc = x / point_divisor, -y / point_divisor, z / point_divisor
empty.setSize(0.15, 0.15, 0.15)
pieobj.makeParent([empty], 0, 0)
if ui.getData('auto-layer'):
ui.debug("layering all levels")
Run(ui.getData('scripts/layers'))
else:
Redraw()
return True
def load_pie(filename):
global gen, ui_ref
ui = BeltFedUI(True)
ui_ref = ui
ui.append(opt_process, opt_draw, opt_evt)
ui.append(texpage_process, texpage_draw, texpage_evt)
ui.append(model_process)
gen = fresh_generator(filename)
ui.setData('dir', os.path.dirname(filename), True)
ui.setData('filename', filename, True)
ui.Run()
Window.FileSelector(load_pie, 'Import Warzone model', '*.pie')