quakeforge/tools/io_mesh_qfmdl/export_mdl.py
Bill Currie 99c58cca73 Make exporting md16 files possible.
Not sure if it actually works as the clients don't render the result
properly (can't see anything where the model should be), but the output
model does import back into blender properly.
2012-04-23 08:54:19 +09:00

289 lines
10 KiB
Python

# vim:ts=4:et
# ##### 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
import bpy
from bpy_extras.object_utils import object_data_add
from mathutils import Vector,Matrix
from .qfplist import pldata, PListError
from .quakepal import palette
from .mdl import MDL
def check_faces(mesh):
#Check that all faces are tris because mdl does not support anything else.
#Because the diagonal on which a quad is split can make a big difference,
#quad to tri conversion will not be done automatically.
faces_ok = True
save_select = []
for f in mesh.polygons:
save_select.append(f.select)
f.select = False
if len(f.vertices) > 3:
f.select = True
faces_ok = False
if not faces_ok:
mesh.update()
return False
#reset selection to what it was before the check.
for f, s in map(lambda x, y: (x, y), mesh.polygons, save_select):
f.select = s
mesh.update()
return True
def convert_image(image):
size = image.size
skin = MDL.Skin()
skin.type = 0
skin.pixels = bytearray(size[0] * size[1]) # preallocate
cache = {}
pixels = image.pixels[:]
for y in range(size[1]):
for x in range(size[0]):
outind = y * size[0] + x
# quake textures are top to bottom, but blender images
# are bottom to top
inind = ((size[1] - 1 - y) * size[0] + x) * 4
rgb = pixels[inind : inind + 3] # ignore alpha
rgb = tuple(map(lambda x: int(x * 255 + 0.5), rgb))
if rgb not in cache:
best = (3*256*256, -1)
for i, p in enumerate(palette):
if i > 255: # should never happen
break
r = 0
for x in map (lambda a, b: (a - b) ** 2, rgb, p):
r += x
if r < best[0]:
best = (r, i)
cache[rgb] = best[1]
skin.pixels[outind] = cache[rgb]
return skin
def null_skin(size):
skin = MDL.Skin()
skin.type = 0
skin.pixels = bytearray(size[0] * size[1]) # black skin
def make_skin(mdl, mesh):
if (not mesh.uv_textures or not mesh.uv_textures[0].data
or not mesh.uv_textures[0].data[0].image):
mdl.skinwidth, mdl.skinheight = (4, 4)
skin = null_skin((mdl.skinwidth, mdl.skinheight))
else:
image = mesh.uv_textures[0].data[0].image
mdl.skinwidth, mdl.skinheight = image.size
skin = convert_image(image)
mdl.skins.append(skin)
def build_tris(mesh):
# mdl files have a 1:1 relationship between stverts and 3d verts.
# a bit sucky, but it does allow faces to take less memory
#
# modelgen's algorithm for generating UVs is very efficient in that no
# vertices are duplicated (thanks to the onseam flag), but it can result
# in fairly nasty UV layouts, and worse: the artist has no control over
# the layout. However, there seems to be nothing in the mdl format
# preventing the use of duplicate 3d vertices to allow complete freedom
# of the UV layout.
uvfaces = mesh.uv_loop_layers[0].data
stverts = []
tris = []
vertmap = [] # map mdl vert num to blender vert num (for 3d verts)
uvdict = {}
for face in mesh.polygons:
fv = list(face.vertices)
uv = uvfaces[face.loop_start:face.loop_start + face.loop_total]
uv = list(map(lambda a: a.uv, uv))
# blender's and quake's vertex order seem to be opposed
fv.reverse()
uv.reverse()
tv = []
for v, u in map(lambda a,b: (a,b), fv, uv):
k = tuple(u)
if k not in uvdict:
uvdict[k] = len(stverts)
vertmap.append(v)
stverts.append(u)
tv.append(uvdict[k])
tris.append(MDL.Tri(tv))
return tris, stverts, vertmap
def convert_stverts(mdl, stverts):
for i, st in enumerate (stverts):
s, t = st
# quake textures are top to bottom, but blender images
# are bottom to top
s = int (s * (mdl.skinwidth - 1) + 0.5)
t = int ((1 - t) * (mdl.skinheight - 1) + 0.5)
# ensure st is within the skin
s = ((s % mdl.skinwidth) + mdl.skinwidth) % mdl.skinwidth
t = ((t % mdl.skinheight) + mdl.skinheight) % mdl.skinheight
stverts[i] = MDL.STVert ((s, t))
def make_frame(mesh, vertmap):
frame = MDL.Frame()
for v in vertmap:
vert = MDL.Vert(tuple(mesh.vertices[v].co))
frame.add_vert(vert)
return frame
def scale_verts(mdl):
tf = MDL.Frame()
for f in mdl.frames:
tf.add_frame(f, 0.0) # let the frame class do the dirty work for us
size = Vector(tf.maxs) - Vector(tf.mins)
rsqr = tuple(map(lambda a, b: max(abs(a), abs(b)) ** 2, tf.mins, tf.maxs))
mdl.boundingradius = (rsqr[0] + rsqr[1] + rsqr[2]) ** 0.5
mdl.scale_origin = tf.mins
mdl.scale = tuple(map(lambda x: x / 255.0, size))
for f in mdl.frames:
f.scale(mdl)
def calc_average_area(mdl):
frame = mdl.frames[0]
if frame.type:
frame = frame.frames[0]
totalarea = 0.0
for tri in mdl.tris:
verts = tuple(map(lambda i: frame.verts[i], tri.verts))
a = Vector(verts[0].r) - Vector(verts[1].r)
b = Vector(verts[2].r) - Vector(verts[1].r)
c = a.cross(b)
totalarea += (c * c) ** 0.5 / 2.0
return totalarea / len(mdl.tris)
def get_properties(operator, mdl, obj):
mdl.eyeposition = tuple (obj.qfmdl.eyeposition)
mdl.synctype = MDL.SYNCTYPE[obj.qfmdl.synctype]
mdl.flags = ((obj.qfmdl.rotate and MDL.EF_ROTATE or 0)
| MDL.EFFECTS[obj.qfmdl.effects])
if obj.qfmdl.md16:
mdl.ident = "MD16"
script = obj.qfmdl.script
mdl.script = None
if script:
try:
script = bpy.data.texts[script].as_string()
except KeyError:
operator.report({'ERROR'},
"Script '%s' not found." % script)
return False
pl = pldata(script)
try:
mdl.script = pl.parse()
except PListError as err:
operator.report({'ERROR'}, "Script error: %s." % err)
return False
return True
def process_skin(mdl, skin, ingroup=False):
if 'skins' in skin:
if ingroup:
raise ValueError("nested skin group")
intervals=['0.0']
if 'intervals' in skin:
intervals += list(skin['intervals'])
intervals = list(map(lambda x: float(x), intervals))
while len(intervals) < len(skin['skins']):
intervals.append(intervals[-1] + 0.1)
sk = MDL.Skin()
sk.type = 1
sk.times = intervals[1:len(skin['skins']) + 1]
sk.skins = []
for s in skin['skins']:
sk.skins.append(process_skin (mdl, s, True))
return sk
else:
#FIXME error handling
name = skin['name']
image = bpy.data.images[name]
if hasattr(mdl, 'skinwidth'):
if (mdl.skinwidth != image.size[0]
or mdl.skinheight != image.size[1]):
raise ValueError("%s: different skin size (%d %d) (%d %d)"
% (name, mdl.skinwidth, mdl.skinheight, int(image.size[0]), int(image.size[1])))
else:
mdl.skinwidth, mdl.skinheight = image.size
sk = convert_image(image)
return sk
def process_frame(mdl, scene, frame, vertmap, ingroup = False,
frameno = None, name = 'frame'):
sc = bpy.context.scene
if frameno == None:
frameno = scene.frame_current + scene.frame_subframe
if 'frameno' in frame:
frameno = float(frame['frameno'])
if 'name' in frame:
name = frame['name']
if 'frames' in frame:
if ingroup:
raise ValueError("nested frames group")
intervals=['0.0']
if 'intervals' in frame:
intervals += list(frame['intervals'])
intervals = list(map(lambda x: float(x), intervals))
while len(intervals) < len(frame['frames']) + 1:
intervals.append(intervals[-1] + 0.1)
fr = MDL.Frame()
for i, f in enumerate(frame['frames']):
fr.add_frame(process_frame (mdl, scene, f, vertmap, True,
frameno + i, name + str(i + 1)),
intervals[i + 1])
if 'intervals' in frame:
return fr
mdl.frames += fr.frames[:-1]
return fr.frames[-1]
scene.frame_set (int(frameno), frameno - int(frameno))
mesh = mdl.obj.to_mesh (scene, True, 'PREVIEW') #wysiwyg?
fr = make_frame(mesh, vertmap)
fr.name = name
return fr
def export_mdl(operator, context, filepath):
obj = context.active_object
mesh = obj.to_mesh (context.scene, True, 'PREVIEW') #wysiwyg?
if not check_faces (mesh):
operator.report({'ERROR'},
"Mesh has faces with more than 3 vertices.")
return {'CANCELLED'}
mdl = MDL(obj.name)
mdl.obj = obj
if not get_properties(operator, mdl, obj):
return {'CANCELLED'}
mdl.tris, mdl.stverts, vertmap = build_tris(mesh)
if mdl.script:
if 'skins' in mdl.script:
for skin in mdl.script['skins']:
mdl.skins.append(process_skin(mdl, skin))
if 'frames' in mdl.script:
for frame in mdl.script['frames']:
mdl.frames.append(process_frame(mdl, context.scene, frame,
vertmap))
if not mdl.skins:
make_skin(mdl, mesh)
if not mdl.frames:
mdl.frames.append(make_frame(mesh, vertmap))
convert_stverts (mdl, mdl.stverts)
mdl.size = calc_average_area(mdl)
scale_verts(mdl)
mdl.write(filepath)
return {'FINISHED'}