Blender/SMD import: Difference between revisions
< Blender
Jump to navigation
Jump to search
TomEdwards (talk | contribs) (replaced VTA vertex matching algorithm; now ~15,000 times faster with complex models; removed "new scene" option due to Blender limitations) |
TomEdwards (talk | contribs) (handle uppercase file extensions) |
||
| Line 24: | Line 24: | ||
__author__= "Tom Edwards" | __author__= "Tom Edwards" | ||
__url__= ['http://developer.valvesoftware.com/wiki/Blender/SMD_import'] | __url__= ['http://developer.valvesoftware.com/wiki/Blender/SMD_import'] | ||
__version__= "0.3. | __version__= "0.3.6" # for Blender r30581 (2.53 beta) | ||
import math, os, time, bpy, random, mathutils | import math, os, time, bpy, random, mathutils | ||
| Line 127: | Line 127: | ||
def appendExt(path,ext): | def appendExt(path,ext): | ||
if not path.endswith("." + ext) and not path.endswith(".dmx"): | if not path.lower().endswith("." + ext) and not path.lower().endswith(".dmx"): | ||
path += "." + ext | path += "." + ext | ||
return path | return path | ||
Revision as of 23:32, 7 August 2010
This is a script which imports SMD/VTA files, and entire QC scripts, into Blender 2.53 beta. Save it as a .py file in .blender\scripts\io\; see Blender for where to find that folder.
This script does not handle bones correctly. Bone rolls are wrong and animation doesn't work at all. If you can fix it, please do so!
# ##### 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 #####
__author__= "Tom Edwards"
__url__= ['http://developer.valvesoftware.com/wiki/Blender/SMD_import']
__version__= "0.3.6" # for Blender r30581 (2.53 beta)
import math, os, time, bpy, random, mathutils
from bpy import ops,data,context
vector = mathutils.Vector
euler = mathutils.Euler
matrix = mathutils.Matrix
rMat = mathutils.RotationMatrix
tMat = mathutils.TranslationMatrix
# SMD types:
# 'REF' - $body, $model
# 'REF_ADD' - $bodygroup, $lod replacemodel
# 'PHYS' - $collisionmesh, $collisionjoints
# 'ANIM' - $sequence, $animation
# 'ANIM_SOLO' - for importing animations to scenes without an existing armature
# 'FLEX' - $model VTA
# I hate Python's var redefinition habits
class smd_info:
a = None # Armature object
m = None # Mesh datablock
file = None
jobName = None
jobType = None
startTime = 0
uiTime = 0
started_in_editmode = None
# Checks for dupe bone names due to truncation
dupeCount = {}
# boneIDs contains the ID-to-name mapping of *this* SMD's bones.
# - Key: ID (as string due to potential storage in registry)
# - Value: bone name (storing object itself is not safe)
# Use boneOfID(id) to easily look up a value from here
boneIDs = {}
# For recording rotation matrices. Children access their parent's matrix.
# USE BONE NAME STRING - MULTIPLE BONE TYPES NEED ACCESS (bone, editbone, posebone)
rotMats = {}
# For connecting bones to their first child only
hasBeenLinked = {}
class qc_info:
startTime = 0
imported_smds = []
vars = {}
in_block_comment = False
root_filename = ""
root_filedir = ""
dir_stack = []
def cd(self):
return self.root_filedir + "".join(self.dir_stack)
def getFilename(filepath):
return filepath.split('\\')[-1].split('/')[-1].rsplit(".")[0]
def getFiledir(filepath):
return filepath.rstrip(filepath.split('\\')[-1].split('/')[-1])
# joins up "quoted values" that would otherwise be delimited, removes comments
def parseQuoteBlockedLine(line):
words = line.split()
quote_start = 0
for i in range(len(words)):
#multi-line comment
if not qc.in_block_comment:
pos = words[i].find("/*")
if pos == 0 or (pos > 0 and words[i][pos-1] != "/"): # block comment begun (catch "//*****" with second test)
words[i] = words[i][0:pos]
qc.in_block_comment = True
else:
pos = words[i].find("*/")
if pos >= 0: # comment ended
qc.in_block_comment = False
words[i] = words[i][pos+2:-1]
else:
words[i] = "" # word commented out
continue
# single-line comment
for keyword in [ "//", "#", ";" ]:
pos = words[i].find(keyword)
if pos >= 0:
words[i] = words[i][0:pos]
return words[0:i]
# quote marks
if words[i].startswith("\""):
quote_start = i
if words[i].endswith("\"") or words[i].endswith("{"):
if words[i].endswith("{"):
words.insert(i+1,"{")
words[quote_start] = words[quote_start].strip("\"{")
quote_start = 0
if quote_start and quote_start != i:
words[quote_start] += words[i]
return words
def appendExt(path,ext):
if not path.lower().endswith("." + ext) and not path.lower().endswith(".dmx"):
path += "." + ext
return path
def boneOfID( id ):
try:
if context.mode == 'EDIT_ARMATURE':
return smd.a.data.edit_bones[ smd.boneIDs[int(id)] ]
else:
return smd.a.data.bones[ smd.boneIDs[int(id)] ]
except:
return None
# Identifies what type of SMD this is. Cannot tell between reference/lod/collision meshes!
def scanSMD():
for line in smd.file:
if line == "triangles\n":
smd.jobType = 'REF'
print("- This is a mesh")
break
if line == "vertexanimation\n":
print("- This is a flex animation library")
smd.jobType = 'FLEX'
break
# Finished the file
if smd.jobType == None:
print("- This is a skeltal animation") # No triangles, no flex - must be animation
for object in context.scene.objects:
if object.type == 'ARMATURE':
smd.jobType = 'ANIM'
if smd.jobType == None: # support importing animations on their own
smd.jobType = 'ANIM_SOLO'
smd.file.seek(0,0) # rewind to start of file
# Runs instead of addBones if an armature already exists, testing the current SMD's nodes block against it.
def validateBones():
ignoreErrors = False
# Copy stored data to smd.boneIDs, assigning new IDs if applicable
for existingBone in smd.a.data.bones:
try:
smd.boneIDs[ existingBone['smd_id'] ] = existingBone
except KeyError:
smd.boneIDs.append(existingBone)
existingBone['smd_id'] = len(smd.boneIDs)
for line in smd.file:
if line == "end\n":
break
values = line.split()
errors = False
# if values[2] != "-1" and boneOfID(values[2]).name != boneOfID(values[0]).parent.name:
# errors = True
# except:
# errors = True
if errors and not ignoreErrors:
smd.uiTime = time.time()
print("- WARNING: skeleton failed validation against %s! Awaiting user input..." % smd.a.name)
#retVal = Blender.Draw.PupMenu( smd.jobName + " failed skeleton validation%t|Ignore once|Ignore all errors in SMD|Create new scene|Abort")
#if retVal == 1:
# print(" ...ignoring this error.")
#if retVal == -1 or 2:
# print(" ...ignoring all errors in this SMD.")
# ignoreErrors = True
#if retVal == 3: # New scene
# print(" New scene not implemented")
#if retVal == 4:
# print(" ...aborting!")
# sys.exit()
# return # TODO: work out how to cleanly abort the whole script
smd.uiTime = time.time() - smd.uiTime
# datablock has been read
print("- SMD bones validated against \"%s\" armature" % smd.a.name)
# nodes block
def addBones():
#Blender.Window.DrawProgressBar( 0, "Skeleton..." )
# Search the current scene for an existing armature - can only be one skeleton in a Source model
for a in context.scene.objects:
# Currently assuming that the first armature is the one to go for...there shouldn't be any others in the scene
if a.type == 'ARMATURE':
smd.a = a
if smd.jobType == 'REF':
smd.jobType = 'REF_ADD'
validateBones()
return
# Got this far? Then this is a fresh import which needs a new armature.
a = smd.a = data.objects.new('Skeleton',data.armatures.new(smd.jobName))
a.x_ray = True
a.data.draw_axes = True
a.data.deform_envelope = False # Envelope deformations are not exported, so hide them
a.data.drawtype = 'STICK'
context.scene.objects.link(a)
context.scene.objects.active = a
# ***********************************
# Read bones from SMD
countBones = 0
ops.object.mode_set(mode='EDIT')
for line in smd.file:
if line == "end\n":
print("- Imported %i new bones" % countBones)
break
countBones += 1
values = line.split()
values[1] = values[1].strip("\"") # remove quotemarks
original_bone_name = values[1]
# Remove "ValveBiped." prefix, a leading cause of bones name length going over Blender's limit
ValveBipedCheck = values[1].split(".",1)
if len(ValveBipedCheck) > 1:
values[1] = ValveBipedCheck[1]
# CONFIRM: Truncation may or may not break compatibility with precompiled animation .mdls
# (IDs are used but names still recorded)
if len(values[1]) > 32: # Blender limitation
print("-- Warning: Bone name '%s' was truncated to 32 characters." % values[1])
# Truncation is done when we create the EditBone object...
newBone = a.data.edit_bones.new(values[1]) # ...here
newBone['smd_name'] = original_bone_name
newBone.tail = 0,1,0
# Now check if this newly-truncated name is a dupe of another
# FIXME: this will stop working once a name has been duped 9 times!
try:
smd.dupeCount[newBone.name]
except:
smd.dupeCount[newBone.name] = 0 # Initialisation as an interger for += ops. I hate doing this and wish I could specifiy type on declaration.
for existingName in smd.a.data.edit_bones.keys():
if newBone.name == existingName and newBone != smd.a.data.edit_bones[existingName]:
smd.dupeCount[existingName] += 1
newBone.name = newBone.name[:-1] + str( smd.dupeCount[existingName] )
try:
smd.dupeCount[newBone.name] += 1
except:
smd.dupeCount[newBone.name] = 1 # Initialise the new name with 1 so that numbers increase sequentially
if values[2] != "-1":
newBone.parent = boneOfID(values[2])
# Need to keep track of which armature bone = which SMD ID
smd.boneIDs[ int(values[0]) ] = newBone.name # Quick lookup
newBone['smd_id'] = values[0] # Persistent, and stored on each bone so handles deletion
# All bones parsed!
ops.object.mode_set(mode='OBJECT')
# skeleton block
def addFrames():
# We only care about the pose data in some SMD types
if smd.jobType not in [ 'REF', 'ANIM', 'ANIM_SOLO' ]:
return
a = smd.a
bones = a.data.bones
scn = context.scene
startFrame = context.scene.frame_current
scn.frame_current = 0
context.scene.objects.active = smd.a
ops.object.mode_set(mode='EDIT')
if smd.jobType is 'ANIM' or 'ANIM_SOLO':
ac = bpy.data.actions.new(smd.jobName)
if not a.animation_data:
a.animation_data_create()
a.animation_data.action = ac
# Enter the pose-reading loop
for line in smd.file:
if line == "end\n":
break
values = line.split()
if values[0] == "time":
scn.frame_current += 1
if scn.frame_current == 2 and smd.jobType == 'ANIM_SOLO':
# apply smd_rot properties
ops.object.mode_set(mode='OBJECT')
ops.object.mode_set(mode='EDIT')
continue # skip to next line
# The current bone
bn = boneOfID(values[0])
if not bn:
#print("Invalid bone ID %s; skipping..." % values[0])
continue
# Where the bone should be, local to its parent
destOrg = vector([float(values[1]), float(values[2]), float(values[3])])
# A bone's rotation matrix is used only by its children, a symptom of the transition from Source's 1D bones to Blender's 2D bones.
# Also, the floats are inversed to transition them from Source (DirectX; left-handed) to Blender (OpenGL; right-handed)
smd.rotMats[bn.name] = rMat(-float(values[4]), 3,'X') * rMat(-float(values[5]), 3,'Y') * rMat(-float(values[6]), 3,'Z')
# *************************************************
# Set rest positions. This happens only for the first frame, but not for an animation SMD.
# rot 0 0 0 means alignment with axes
if smd.jobType is 'REF' or (smd.jobType is 'ANIM_SOLO' and scn.frame_current == 1):
if bn.parent:
smd.rotMats[bn.name] *= smd.rotMats[bn.parent.name] # make rotations cumulative
bn.transform(smd.rotMats[bn.parent.name]) # ROTATION
bn.translate(bn.parent.head + (destOrg * smd.rotMats[bn.parent.name]) ) # LOCATION
bn.tail = bn.head + (vector([0,1,0])*smd.rotMats[bn.name]) # Another 1D to 2D artifact. Bones must point down the Y axis so that their co-ordinates remain stable
else:
bn.translate(destOrg) # LOCATION WITH NO PARENT
bn.tail = bn.head + (vector([0,1,0])*smd.rotMats[bn.name])
#bn.transform(smd.rotMats[bn.name])
# Store rotation either way
bn['smd_rot'] = euler([float(values[4]),float(values[5]),float(values[6])])
# Take a stab at parent-child connections. Not fully effective since only one child can be linked, so I
# assume that the first child is the one to go for. It /usually/ is.
# if bn.parent and not smd.hasBeenLinked.get(bn.parent):
# bn.parent.tail = bn.head
# bn.connected = True
# smd.hasBeenLinked[bn.parent] = True
# *****************************************
# Set pose positions. This happens for every frame, but not for a reference pose.
elif smd.jobType in [ 'ANIM', 'ANIM_SOLO' ]:
pbn = smd.a.pose.bones[ boneOfID(values[0]).name ]
# Blender stores posebone positions as offsets of their *rest* location. Source stores them simply as offsets of their parent.
# Thus, we must refer to the rest bone when positioning.
bn = smd.a.data.bones[pbn.name]
pbn.rotation_mode = 'XYZ'
smd_rot = vector(bn['smd_rot'])
ani_rot = vector([float(values[4]),float(values[5]),float(values[6])])
pbn.rotation_euler = (ani_rot - smd_rot)
if bn.parent:
smd.rotMats[bn.name] *= smd.rotMats[bn.parent.name] # make rotations cumulative
pbn.location = destOrg * smd.rotMats[bn.parent.name] - (bn.head_local - bn.parent.head_local)
else:
pbn.location = destOrg - bn.head_local
# TODO: compare to previous frame and only insert if different
pbn.keyframe_insert('location') # ('location', 0)
pbn.keyframe_insert('rotation_euler')
# All frames read
if smd.jobType is 'ANIM' or 'ANIM_SOLO':
scn.frame_end = scn.frame_current
# TODO: clean curves automagically (ops.graph.clean)
ops.object.mode_set(mode='OBJECT')
print("- Imported %i frames of animation" % scn.frame_current)
scn.frame_current = startFrame
# triangles block
def addPolys():
if smd.jobType not in [ 'REF', 'REF_ADD', 'PHYS' ]:
return
# Create a new mesh object, disable double-sided rendering, link it to the current scene
smd.m = data.objects.new(smd.jobName,data.meshes.new(smd.jobName+'_mesh'))
smd.m.data.double_sided = False
context.scene.objects.link(smd.m)
#TODO: put meshes in separate layers
# Create weightmap groups
for bone in smd.a.data.bones.values():
smd.m.add_vertex_group(name=bone.name)
# Apply armature modifier
modifier = smd.m.modifiers.new(type="ARMATURE",name="Armature")
modifier.use_bone_envelopes = False # Envelopes not exported, so disable them
modifier.object = smd.a
# All SMD models are textured
smd.m.data.add_uv_texture()
mat = None
# Initialisation
md = smd.m.data
lastWindowUpdate = time.time()
# Vertex values
cos = []
uvs = []
norms = []
weights = []
# Face values
mats = []
# *************************************************************************************************
# There are two loops in this function: one for polygons which continues until the "end" keyword
# and one for the vertices on each polygon that loops three times. We're entering the poly one now.
countPolys = 0
for line in smd.file:
line = line.rstrip("\n")
if line == "end" or "":
break
# Parsing the poly's material
line = line[:21] # Max 21 chars in a Blender material name :-(
try:
mat = context.main.materials[line] # Do we have this material already?
try:
md.materials[mat.name] # Look for it on this mesh
for i in range(len(md.materials)):
if md.materials[i].name == line: # No index() func on PropertyRNA :-(
mat_ind = i
except KeyError: # material exists, but not on this mesh
md.add_material(mat)
mat_ind = len(md.materials) - 1
except KeyError: # material does not exist
print("- New material: %s" % line)
mat = context.main.materials.new(name=line)
md.add_material(mat)
# Give it a random colour
randCol = []
for i in range(3):
randCol.append(random.uniform(.4,1))
mat.diffuse_color = randCol
if smd.jobType != 'PHYS':
mat.face_texture = True # For rendering in Blender
else:
smd.m.max_draw_type = 'SOLID'
mat_ind = len(md.materials) - 1
# Store index for later application to faces
mats.append(mat_ind)
# ***************************************************************
# Enter the vertex loop. This will run three times for each poly.
vertexCount = 0
for line in smd.file:
values = line.split()
vertexCount+= 1
# TODO: transform coords to flip model onto Blender XZY, possibly scale it too
# Read co-ordinates and normals
for i in range(1,4): # Should be 1,3??? Why do I need 1,4?
cos.append( float(values[i]) )
norms.append( float(values[i+3]) )
# Can't do these in the above for loop since there's only two
uvs.append( float(values[7]) )
uvs.append( float(values[8]) )
# Read weightmap data, this is a bit more involved
weights.append( [] ) # Blank array, needed in case there's only one weightlink
if len(values) > 10 and values[9] != "0": # got weight links?
for i in range(10, 10 + (int(values[9]) * 2), 2): # The range between the first and last weightlinks (each of which is *two* values)
weights[-1].append( [ smd.m.vertex_groups[boneOfID(values[i]).name], float(values[i+1]) ] ) # [Pointer to the vert group, Weight]
else: # Fall back on the deprecated value at the start of the line
weights[-1].append( [smd.m.vertex_groups[boneOfID(values[0]).name], 1.0] )
# Three verts? It's time for a new poly
if vertexCount == 3:
# Dunno what the 4th UV is for, but Blender needs it
uvs.append( 0.0 )
uvs.append( 1.0 )
break
# Back in polyland now, with three verts processed.
countPolys+= 1
# All polys processed. Add new elements to the mesh:
md.add_geometry(countPolys*3,0,countPolys)
# Fast add!
md.verts.foreach_set("co",cos)
md.verts.foreach_set("normal",norms)
md.faces.foreach_set("material_index", mats)
md.uv_textures[0].data.foreach_set("uv",uvs)
# Apply vertex groups
i = 0
for v in md.verts:
for link in weights[i]:
smd.m.add_vertex_to_group( i, link[0], link[1], 'ADD' )
i += 1
# Build faces
# TODO: figure out if it's possible to foreach_set() this data. Note the reversal of indices required.
i = 0
for f in md.faces:
i += 3
f.verts = [i-3,i-2,i-1]
# Remove doubles...is there an easier way?
context.scene.objects.active = smd.m
ops.object.mode_set(mode='EDIT')
ops.mesh.remove_doubles()
if smd.jobType != 'PHYS':
ops.mesh.faces_shade_smooth()
ops.object.mode_set(mode='OBJECT')
print("- Imported %i polys" % countPolys)
# # delete empty vertex groups - they don't deform, so aren't relevant
# for vertGroup in smd.m.vertex_groups:
# if not smd.m.getVertsFromGroup(vertGroup):
# smd.m.removeVertGroup(vertGroup)
# # DON'T delete the bone as well! It may be required for future imports
# # TODO: place bones without verts in a different armature layer?
# triangles complete!
# vertexanimation block
def addShapes():
if smd.jobType is not 'FLEX':
return
if not smd.m: # select a mesh...messy!
for obj in context.scene.objects:
if obj.type == 'MESH':
smd.m = obj
co_map = {}
making_base_shape = True
bad_vta_verts = num_shapes = 0
for line in smd.file:
line = line.rstrip("\n")
if line == "end" or "":
break
values = line.split()
if values[0] == "time":
if making_base_shape and num_shapes > 0:
making_base_shape = False
if making_base_shape:
smd.m.add_shape_key("Basis")
else:
smd.m.add_shape_key()
num_shapes += 1
continue # to the first vertex of the new shape
cur_id = int(values[0])
cur_cos = vector([ float(values[1]), float(values[2]), float(values[3]) ])
if making_base_shape: # create VTA vert ID -> mesh vert ID dictionary
# Blender faces share verts; SMD faces don't. To simulate a SMD-like list of verticies, we need to
# perform a bit of mathematical kung-fu:
mesh_vert_id = smd.m.data.faces[math.floor(cur_id/3)].verts[cur_id % 3]
if not cur_cos == smd.m.data.verts[mesh_vert_id].co:
print("Flex animation mismatch! (vert",str(cur_id),")")
else:
co_map[cur_id] = mesh_vert_id # create the new dict entry
else:
smd.m.data.shape_keys.keys[-1].data[ co_map[cur_id] ].co = cur_cos # write to the shapekey
if bad_vta_verts > 0:
print("- WARNING:",bad_vta_verts,"VTA vertices were not matched to a mesh vertex!")
print("- Imported",num_shapes-1,"flex shapes") # -1 because the first shape is the reference position
def printTimeMessage(start_time,name,type="SMD"):
elapsedtime = int(time.time() - start_time)
if elapsedtime == 1:
elapsedtime = "1 second"
elif elapsedtime > 1:
elapsedtime = str(elapsedtime) + " seconds"
else:
elapsedtime = "under 1 second"
print(type,name,"imported successfully in",elapsedtime,"\n")
# Parses a QC file
def readQC( context, filepath, newscene, doAnim ):
filename = getFilename(filepath)
filedir = getFiledir(filepath)
is_root_qc = False
global qc
try:
qc.in_block_comment = False
except NameError: # we are the outermost QC
print("\nQC IMPORTER: now working on",filename)
is_root_qc = True
qc = qc_info()
qc.startTime = time.time()
qc.root_filename = filename
qc.root_filedir = filedir
if newscene:
bpy.context.screen.scene = bpy.data.scenes.new(filename) # BLENDER BUG: this currently doesn't update bpy.context.scene
else:
bpy.context.scene.name = filename
file = open(filepath, 'r')
in_bodygroup = False
for line in file:
line = parseQuoteBlockedLine(line)
if len(line) == 0:
continue
#print(line)
# handle individual words (insert QC variable values, change slashes)
for i in range(len(line)):
if line[i].strip("$") in qc.vars:
line[i] = qc.vars[line[i].strip("$")]
line[i] = line[i].replace("/","\\") # studiomdl is Windows-only
# register new QC variable
if "$definevariable" in line:
qc.vars[line[1]] = line[2]
continue
# dir changes
if "$pushd" in line:
if line[1][-1] != "\\":
line[1] += "\\"
qc.dir_stack.append(line[1])
continue
if "$popd" in line:
try:
qc.dir_stack.pop()
except IndexError:
pass # invalid QC, but whatever
continue
def loadSMD(word_index,ext,type):
path = qc.cd() + appendExt(line[word_index],ext)
if not path in qc.imported_smds or type == 'FLEX':
qc.imported_smds.append(path)
readSMD(context,path,False,type)
# meshes
if "$body" in line or "$model" in line or "replacemodel" in line:
loadSMD(2,"smd",'REF')
continue
if "$bodygroup" in line:
in_bodygroup = True
continue
if in_bodygroup:
if "studio" in line:
loadSMD(1,"smd",'REF')
continue
if "}" in line:
in_bodygroup = False
continue
# skeletal animations
if doAnim and ("$sequence" in line or "$animation" in line):
if not "{" in line: # an advanced $sequence using an existing $animation
path = qc.cd() + appendExt(line[2],"smd")
if not path in qc.imported_smds:
qc.imported_smds.append(path)
readSMD(context,path,False,'ANIM')
continue
# flex animation
if "flexfile" in line:
loadSMD(1,"vta",'FLEX')
continue
# physics mesh
if "$collisionmodel" in line or "$collisionjoints" in line:
loadSMD(1,"smd",'PHYS')
continue
# QC inclusion
if "$include" in line:
try:
readQC(context,filedir + appendExt(line[1], "qci"),False, doAnim) # special case: ALWAYS relative to current QC dir
except IOError:
if not line[1].endswith("qci"):
readQC(context,filedir + appendExt(line[1], "qc"),False, doAnim)
file.close()
if is_root_qc:
printTimeMessage(qc.startTime,filename,"QC")
del qc
# Parses an SMD file
def readSMD( context, filepath, newscene, smd_type ):
# First, overcome Python's awful var redefinition behaviour. The smd object must be
# explicitly deleted at the end of the script.
if filepath.endswith("dmx"):
print("Skipping DMX file import: format unsupported (%s)" % getFilename(filepath))
return
global smd
smd = smd_info()
smd.jobName = getFilename(filepath)
smd.jobType = smd_type
smd.startTime = time.time()
smd.uiTime = 0
try:
smd.file = file = open(filepath, 'r')
except IOError: # TODO: work out why errors are swallowed if I don't do this!
if smd_type: # called from QC import
print("ERROR: could not open SMD file \"%s\" - skipping!" % smd.jobName)
return
else:
raise(IOError)
if newscene:
bpy.context.screen.scene = bpy.data.scenes.new(smd.jobName) # BLENDER BUG: this currently doesn't update bpy.context.scene
elif not smd_type: # only when importing standalone
bpy.context.scene.name = smd.jobName
print("\nSMD IMPORTER: now working on",smd.jobName)
if file.readline() != "version 1\n":
print ("- Warning: unrecognised/invalid SMD file. Import will proceed, but may fail!")
#if context.mode == 'EDIT':
# smd.started_in_editmode = True
# ops.object.mode_set(mode='OBJECT')
if smd.jobType == None:
scanSMD() # What are we dealing with?
for line in file:
if line == "nodes\n": addBones()
if line == "skeleton\n": addFrames()
if line == "triangles\n": addPolys()
if line == "vertexanimation\n": addShapes()
file.close()
printTimeMessage(smd.startTime,smd.jobName)
del smd
from bpy.props import *
class SmdImporter(bpy.types.Operator):
'''Load a Source engine SMD, VTA or QC file'''
bl_idname = "import_scene.smd"
bl_label = "Import SMD/VTA/QC"
filepath = StringProperty(name="File path", description="File filepath used for importing the SMD/VTA/QC file", maxlen=1024, default="")
filename = StringProperty(name="Filename", description="Name of SMD/VTA/QC file", maxlen=1024, default="")
#freshScene = BoolProperty(name="Import to new scene", description="Create a new scene for this import", default=False) # nonfunctional due to Blender limitation
doAnim = BoolProperty(name="Import animations (broken)", description="Use for comedic effect only", default=False)
def execute(self, context):
if self.properties.filepath.endswith('.qc') | self.properties.filepath.endswith('.qci'):
readQC(context, self.properties.filepath, False, self.properties.doAnim )
elif self.properties.filepath.endswith('.smd'):
readSMD(context, self.properties.filepath, False, None)
elif self.properties.filepath.endswith ('.vta'):
readSMD(context, self.properties.filepath, False, 'FLEX')
elif self.properties.filepath.endswith('.dmx'):
print("DMX import not supported")
else:
print("File format not recognised")
return {'FINISHED'}
def invoke(self, context, event):
wm = context.manager
wm.add_fileselect(self)
return {'RUNNING_MODAL'}
def menu_func(self, context):
self.layout.operator(SmdImporter.bl_idname, text="Studiomdl Data (.smd, .vta, .qc)")
def register():
bpy.types.register(SmdImporter)
bpy.types.INFO_MT_file_import.append(menu_func)
def unregister():
bpy.types.unregister(SmdImporter)
bpy.types.INFO_MT_file_import.remove(menu_func)
if __name__ == "__main__":
register()