Blender/SMD import: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(replaced VTA vertex matching algorithm; now ~15,000 times faster with complex models; removed "new scene" option due to Blender limitations)
(Redirected page to Blender Source Tools)
 
(2 intermediate revisions by one other user not shown)
Line 1: Line 1:
{{blender}} This is a script which imports SMD/VTA files, and entire QC scripts, into [[Blender]] 2.53 beta. Save it as a <code>.py</code> file in <code>.blender\scripts\io\</code>; see [[Blender]] for where to find that folder.
#redirect [[Blender Source Tools]]
 
'''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!
 
<source lang=python>
# ##### 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.5" # 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.endswith("." + ext) and not path.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()
</source>
 
[[Category:Blender]]

Latest revision as of 06:47, 11 November 2013