|
|
| (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]]
| |