Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Raven Software Ghoul 2 Animation retargetting script
#! /usr/bin/python
import struct
####
#### Matrix Math
####
# N-Dimensional vector with operators == and * (dot product)
class Vector:
# Constructor
# Either supply something that can be indexed and supports len() or the components
def __init__( self, data, *args ):
if len(args) == 0:
self.data = [ float(x) for x in data ]
else:
self.data = [ float(data) ]
self.data.extend( [ float(x) for x in args ] )
# To string
def __repr__( self ):
return repr( self.data )
# Indexing (read)
def __getitem__( self, index ):
return self.data[ index ]
# Indexing (write)
def __setitem__( self, index, data ):
self.data[ index ] = data
# Get length
def __len__( self ):
return len( self.data )
# Dot product
def __mul__( self, rhs ):
return sum( [ x * y for x, y in zip( self, rhs ) ] )
# Equality
def __eq__( self, rhs ):
return all( [ x == y for x, y in zip( self, rhs ) ] )
# 4x4 Matrix with Matrix Multiplication and Inverse operators
class Matrix:
# Constructor
# data can be omitted, a Matrix to copy, a list of lists or bytes containing 12 floats / a compQuat
# if data is omitted, defaults to the identity matrix
def __init__( self, data=None ):
# copying a matrix (uses list)
if isinstance( data, Matrix ):
data = data.data
# initialize from list
if isinstance( data, list ):
self.data = [ Vector( row ) for row in data ]
for row in self.data:
assert( len(row) == 4 )
if len( self.data ) == 3:
self.data.append( Vector( 0, 0, 0, 1 ) )
else:
assert( len( self.data ) == 4 )
assert( self.data[3] == Vector( 0, 0, 0, 1 ) )
# read from bytes
elif isinstance( data, bytes ):
# 12 floats (first 3 rows)
if len(data) == 4*3 * 4:
self.data = [ Vector( struct.unpack( "4f", data[ 4*4*i : 4*4*(i+1) ] ) ) for i in range(3) ]
self.data.append( Vector( 0, 0, 0, 1 ) )
# quaternion + location
elif len(data) == 14:
# 0 = w, 1 = x, 2 = y, 3 = z
quat = [ float( struct.unpack( "H", data[ i : i+2 ] )[ 0 ] ) / 16383.0 - 2.0 for i in range( 0, 8, 2 ) ]
loc = [ float( struct.unpack( "H", data[ i : i+2 ] )[ 0 ] ) / 64.0 - 512.0 for i in range( 8, 14, 2) ]
# names from jk2's code, may make no sense
T = [ 2 * x for x in quat[1:] ]
Tw = [ quat[ 0 ] * x for x in T ]
Tx = [ quat[ 1 ] * x for x in T ]
Ty = [ quat[ 2 ] * x for x in T ]
Tz = [ quat[ 3 ] * x for x in T ]
self.data = [
Vector( 1.0 - ( Ty[1] + Tz[2] ), Tx[1] - Tw[2], Tx[2] + Tw[1], loc[0] ),
Vector( Tx[1] + Tw[2], 1.0 - ( Tx[0] + Tz[2] ), Ty[2] - Tw[0], loc[1] ),
Vector( Tx[2] - Tw[1], Ty[2] + Tw[0], 1.0 - ( Tx[0] + Ty[1] ), loc[2] ),
Vector( 0, 0, 0, 1 )
]
# invalid format
else:
raise RuntimeError("Invalid byte count!")
# invalid format
elif data != None:
raise RuntimeError("Invalid parameter!")
# default: identity matrix
else:
self.data = []
for i in range(4):
row = Vector( 0, 0, 0, 0 )
row[i] = 1
self.data.append(row)
# packs as 12 floats (first 3 rows)
def pack( self ):
assert( self.data[ 3 ] == Vector( 0, 0, 0, 1 ) )
return b"".join( [ struct.pack( "4f", *x ) for x in self.data[:3] ] )
# packs as compressed quaternion + translation
def toCompQuat( self ):
# rotation matrix to quaternion
quat = [
( self.data[0][0] + self.data[1][1] + self.data[2][2] + 1.0 ) / 4.0,
( self.data[0][0] - self.data[1][1] - self.data[2][2] + 1.0 ) / 4.0,
( - self.data[0][0] + self.data[1][1] - self.data[2][2] + 1.0 ) / 4.0,
( - self.data[0][0] - self.data[1][1] + self.data[2][2] + 1.0 ) / 4.0,
]
quat = [ max( 0, x ) for x in quat ]
import math
quat = [ math.sqrt( x ) for x in quat ]
def sign( x ):
if x >= 0:
return 1
else:
return -1
if all( [ quat[0] >= x for x in quat ] ):
quat[1] *= sign( self.data[2][1] - self.data[1][2] )
quat[2] *= sign( self.data[0][2] - self.data[2][0] )
quat[3] *= sign( self.data[1][0] - self.data[0][1] )
elif all( [ quat[1] >= x for x in quat ] ):
quat[0] *= sign( self.data[2][1] - self.data[1][2] )
quat[2] *= sign( self.data[1][0] + self.data[0][1] )
quat[3] *= sign( self.data[0][2] + self.data[2][0] )
elif all( [ quat[2] >= x for x in quat ] ):
quat[0] *= sign( self.data[0][2] - self.data[2][0] )
quat[1] *= sign( self.data[1][0] + self.data[0][1] )
quat[3] *= sign( self.data[2][1] + self.data[1][2] )
elif all( [ quat[3] >= x for x in quat ] ):
quat[0] *= sign( self.data[1][0] - self.data[0][1] )
quat[1] *= sign( self.data[2][0] + self.data[0][2] )
quat[2] *= sign( self.data[2][1] + self.data[1][2] )
else:
raise RuntimeError( "Coding error" )
norm = math.sqrt( sum( [ x * x for x in quat ] ) )
quat = [ x / norm for x in quat ]
# compression as short
quat = [ ( x + 2 ) * 16383 for x in quat ]
loc = [ ( row[3] + 512) * 64 for row in self.data[:3] ]
return struct.pack( "7H", *[ round( x ) for x in quat + loc] )
# Matrix Multiplication
def __mul__( self, rhs ):
return Matrix( [ [ self.row(y) * rhs.col(x) for x in range(4) ] for y in range(3) ] )
# To String
def __repr__( self ):
return "[ " + "\n ".join( [ repr( row ) for row in self.data ] ) + " ]"
def __eq__( self, rhs ):
return all( [ x == y for x, y in zip( self.data, rhs.data ) ] )
def __getitem__( self, index ):
return self.row( index )
# Returns copy of given row
def row( self, index ):
return Vector( self.data[ index ] )
# Returns copy of given column
def col( self, index ):
return Vector( [ row[ index ] for row in self.data ] )
# Inverts the matrix
def invert( self ):
denom = - self.data[0][2]*self.data[1][1]*self.data[2][0] + self.data[0][1]*self.data[1][2]*self.data[2][0] + self.data[0][2]*self.data[1][0]*self.data[2][1] - self.data[0][0]*self.data[1][2]*self.data[2][1] - self.data[0][1]*self.data[1][0]*self.data[2][2] + self.data[0][0]*self.data[1][1]*self.data[2][2]
self.data = [
Vector( [
(self.data[1][1]*self.data[2][2]-self.data[1][2]*self.data[2][1])/denom,
(self.data[0][2]*self.data[2][1]-self.data[0][1]*self.data[2][2])/denom,
(self.data[0][1]*self.data[1][2]-self.data[0][2]*self.data[1][1])/denom,
(self.data[0][3]*self.data[1][2]*self.data[2][1]-self.data[0][2]*self.data[1][3]*self.data[2][1]-self.data[0][3]*self.data[1][1]*self.data[2][2]+self.data[0][1]*self.data[1][3]*self.data[2][2]+self.data[0][2]*self.data[1][1]*self.data[2][3]-self.data[0][1]*self.data[1][2]*self.data[2][3])/denom
] ),
Vector( [
(self.data[1][2]*self.data[2][0]-self.data[1][0]*self.data[2][2])/denom,
(self.data[0][0]*self.data[2][2]-self.data[0][2]*self.data[2][0])/denom,
(self.data[0][2]*self.data[1][0]-self.data[0][0]*self.data[1][2])/denom,
(-self.data[0][3]*self.data[1][2]*self.data[2][0]+self.data[0][2]*self.data[1][3]*self.data[2][0]+self.data[0][3]*self.data[1][0]*self.data[2][2]-self.data[0][0]*self.data[1][3]*self.data[2][2]-self.data[0][2]*self.data[1][0]*self.data[2][3]+self.data[0][0]*self.data[1][2]*self.data[2][3])/denom
] ),
Vector( [
(self.data[1][0]*self.data[2][1]-self.data[1][1]*self.data[2][0])/denom,
(self.data[0][1]*self.data[2][0]-self.data[0][0]*self.data[2][1])/denom,
(self.data[0][0]*self.data[1][1]-self.data[0][1]*self.data[1][0])/denom,
(self.data[0][3]*self.data[1][1]*self.data[2][0]-self.data[0][1]*self.data[1][3]*self.data[2][0]-self.data[0][3]*self.data[1][0]*self.data[2][1]+self.data[0][0]*self.data[1][3]*self.data[2][1]+self.data[0][1]*self.data[1][0]*self.data[2][3]-self.data[0][0]*self.data[1][1]*self.data[2][3])/denom
] ),
Vector( 0, 0, 0, 1 )
]
# Returns this matrix inverted
def inverted( self ):
res = Matrix( self )
res.invert()
return res
####
#### GLA Reading
####
GLA_IDENT = b"2LGA"
GLA_VERSION = 6
class FileFormatError( RuntimeError ):
def __init__( self, message ):
self.message = message
def __str__( self ):
return self.message
# converts a NULL-terminated binary string to an ordinary string.
def decodeCString(bs):
end = bs.find(b"\0") #find null termination
if end == -1: #if none exists, there is no end.
return bs.decode()
return bs[:end].decode() # otherwise cut it off at end
class MdxaHeader:
def __init__( self ):
self.name = ""
self.scale = 1 # does not seem to be used by Jedi Academy anyway - or is it? I need it in import!
self.numFrames = -1
self.ofsFrames = -1
self.numBones = -1
self.ofsCompBonePool = -1
self.ofsSkel = -1 # this is also MdxaSkelOffsets.baseOffset + MdxaSkelOffsets.boneOffsets[0] - probably a historic leftover
self.ofsEnd = -1
def loadFromFile( self, file ):
# check ident
ident, = struct.unpack( "4s", file.read( 4 ) )
if ident != GLA_IDENT:
raise FileFormatError( "File does not start with " + str( GLA_IDENT ) + " but " + str( ident ) + " - no GLA!" )
version, = struct.unpack( "i", file.read( 4 ) )
if version != GLA_VERSION:
raise FileFormatError( "Wrong gla file version! (" + str( version ) + " should be " + str( mrw_g2_constants.GLA_VERSION) + ")" )
self.name = decodeCString( file.read( 64 ) )
self.scale, self.numFrames, self.ofsFrames, self.numBones, self.ofsCompBonePool, self.ofsSkel, self.ofsEnd = struct.unpack( "f6i", file.read( 7*4 ) )
def saveToFile( self, file ):
file.write( struct.pack( "4si64sf6i", GLA_IDENT, GLA_VERSION, self.name.encode(), self.scale, self.numFrames, self.ofsFrames, self.numBones, self.ofsCompBonePool, self.ofsSkel, self.ofsEnd ) )
def getSize( self ):
return 2*4 + 64*1 + 7*4
def setOffsets( self, hierarchy, frames, pool ):
self.ofsSkel = hierarchy.getOfsSkel()
self.ofsFrames = self.ofsSkel + hierarchy.getSize()
self.ofsCompBonePool = self.ofsFrames + frames.getSize()
self.ofsEnd = self.ofsCompBonePool + pool.getSize()
class MdxaBone:
def __init__( self ):
pass
def loadFromFile( self, file ):
self.name = decodeCString( file.read( 64 ) )
self.flags, self.parent = struct.unpack( "Ii", file.read( 2*4 ) )
self.basePoseMat = Matrix( file.read( 12*4 ) )
self.basePoseMatInv = Matrix( file.read( 12*4 ) )
assert( all( [ abs(self.basePoseMat.inverted()[y][x] - self.basePoseMatInv[y][x]) < 0.01 for x in range(4) for y in range(3) ] ) )
self.numChildren, = struct.unpack( "I", file.read( 4 ) )
self.children = [ struct.unpack( "I", file.read( 4 ) )[ 0 ] for i in range( self.numChildren ) ]
def saveToFile( self, file ):
file.write( struct.pack( "64sIi", self.name.encode(), self.flags, self.parent ) )
file.write( self.basePoseMat.pack() )
file.write( self.basePoseMatInv.pack() )
file.write( struct.pack( "{}I".format( 1 + self.numChildren ), self.numChildren, *self.children ) )
def getSize( self ):
return 64 + 4 + 4 + 12*4 + 12*4 + 4 + self.numChildren*4
class MdxaHierarchy:
def __init__( self, header ):
self.header = header
self.bonesByIndex = []
def loadFromFile( self, file ):
file.seek( self.header.getSize() )
boneOffsets = list( struct.unpack("{}I".format( self.header.numBones ), file.read( self.header.numBones * 4 ) ) )
for offset in boneOffsets:
file.seek( self.header.getSize() + offset )
bone = MdxaBone()
bone.loadFromFile( file )
self.bonesByIndex.append( bone )
def saveToFile( self, file ):
offset = len( self ) * 4
for bone in self.bonesByIndex:
file.write( struct.pack( "I", offset ) )
offset += bone.getSize()
for bone in self.bonesByIndex:
bone.saveToFile( file )
# size relative to OfsBones, ignoring bone offsets (before that)
def getSize( self ):
return sum( [ bone.getSize() for bone in self.bonesByIndex ] )
def getOfsSkel( self ):
return self.header.getSize() + 4 * len( self.bonesByIndex )
def __len__( self ):
return len( self.bonesByIndex )
def __getitem__( self, index ):
return self.bonesByIndex[ index ]
class MdxaFrame:
def __init__( self, numBones ):
self.numBones = numBones
self.indices = []
# returns maximum compQuat index
def loadFromFile( self, file ):
self.indices = [ struct.unpack( "I", file.read( 3 ) + b"\x00" )[0] for i in range( self.numBones ) ]
return max( self.indices )
def saveToFile( self, file ):
for index in self.indices:
# only write 3 bytes per index
file.write( struct.pack( "I", index)[ :3 ] )
# bones are not MdxaBones
def loadFromBones( self, bonesByIndex, compBonePool ):
self.indices = [ compBonePool.getIndex( bone.getRelativeOffset() ) for bone in bonesByIndex ]
def getSize( self ):
return 3 * self.numBones
class MdxaFrames:
def __init__( self, numBones ):
self.numBones = numBones
self.frames = []
def loadFromFile( self, file, numFrames ):
class Mutable:
pass
data = Mutable()
data.maxIndex = -1 # I can't change upvalues
def loadFrame():
frame = MdxaFrame( self.numBones )
data.maxIndex = max(frame.loadFromFile( file ), data.maxIndex )
return frame
self.frames = [ loadFrame() for i in range( numFrames ) ]
return data.maxIndex
def saveToFile( self, file ):
for frame in self.frames:
frame.saveToFile( file )
# add 32 bit padding if required
if len( self.frames ) % 4 != 0:
file.write( b"\x00" * ( 4 - len (self.frames ) % 4 ) )
def __len__( self ):
return len( self.frames )
def __getitem__( self, index ):
return self.frames[ index ]
def append( self, frame ):
self.frames.append( frame )
def getSize( self ):
if len( self.frames ) == 0:
return 0
else:
size = len( self.frames) * self.frames[ 0 ].getSize()
if size % 4 != 0:
size += 4 - size % 4
return size
class MdxaBonePool:
def __init__( self ):
self.pool = []
self.boneIndex = {}
def getIndex( self, matrix ):
compQuat = matrix.toCompQuat()
if compQuat in self.boneIndex:
return self.boneIndex[ compQuat ]
else:
index = len( self.pool )
self.boneIndex[ compQuat ] = index
self.pool.append( compQuat )
return index
def __getitem__( self, index ):
return Matrix( self.pool[ index ] )
def __len__( self ):
return len( self.pool )
def loadFromFile( self, file, numCompQuats ):
self.pool = [ file.read( 14 ) for i in range( numCompQuats ) ]
def saveToFile( self, file ):
for compQuat in self.pool:
file.write( compQuat )
def getSize( self ):
return 14 * len ( self.pool )
def saveToFile( file, header, hierarchy, frames, pool ):
header.setOffsets( hierarchy, frames, pool )
header.numFrames = len( frames )
header.saveToFile( file )
assert( file.tell() == header.getSize() )
hierarchy.saveToFile( file )
assert( file.tell() == header.ofsFrames )
frames.saveToFile( file )
assert( file.tell() == header.ofsCompBonePool )
pool.saveToFile( file )
def loadHeaderAndHierarchyFromFile( file ):
# header
header = MdxaHeader()
header.loadFromFile( file )
# hierarchy
hierarchy = MdxaHierarchy( header )
hierarchy.loadFromFile( file )
return header, hierarchy
def loadFromFile( file ):
header, hierarchy = loadHeaderAndHierarchyFromFile( file )
# frames
file.seek( header.ofsFrames )
frames = MdxaFrames( header.numBones )
maxIndex = frames.loadFromFile( file, header.numFrames )
# compressed bone pool
file.seek( header.ofsCompBonePool )
pool = MdxaBonePool()
pool.loadFromFile( file, maxIndex + 1 )
return header, hierarchy, frames, pool
####
#### Program invokation
####
import sys
usage = "Usage: gla_convert.py <sourcefile.gla> <targetfile.gla> <bonemappings.txt> <output.gla>\n\
<sourcefile.tga> Ghoul 2 Animation file to convert\n\
<targetfile.tga> Ghoul 2 Animation file whose skeleton is to be converted to\n\
<bonemappings.txt> File describing which bone in the sourcefile is to be mapped to which one in the targetfile. Source bones mapped to nothing get dropped, target bones that nothing maps to get a default value. File format:\n\
<source bone name 1> <target bone name 1>\n\
<source bone name 2> <target bone name 2>\n\
...\n\
<output.gla> Filename of file to save. Will be overwritten if it already exists."
if any( [ x in ( "-h", "--help", "/?", "/help") for x in sys.argv[1:] ] ) or len( sys.argv ) != 5:
print(usage)
exit()
# Load original file
try:
print( "Loading source data..." )
sourceFile = open( sys.argv[ 1 ], "rb" )
sourceHeader, sourceHierarchy, sourceFrames, sourcePool = loadFromFile( sourceFile )
sourceFile.close()
print( "done." )
except FileFormatError as err:
print( "Error reading source file \"{}\": {}".format( sys.argv[ 1 ], err ) )
exit()
except IOError as err:
print( err )
exit()
# load target hierarchy
try:
print( "Loading target hierarchy..." )
targetFile = open( sys.argv[ 2 ], "rb" )
targetHeader, targetHierarchy = loadHeaderAndHierarchyFromFile( targetFile )
targetFile.close()
print( "done." )
except FileFormatError as err:
print( "Error reading target file \"{}\": {}".format( sys.argv[ 2 ], err ) )
exit()
except IOError as err:
print( err )
exit()
# load bone mappings
try:
print( "Loading bone mappings..." )
bonesFile = open( sys.argv[ 3 ], "r" )
boneMappings = []
for line in bonesFile:
parts = line.split()
if len( parts ) == 0:
pass
elif len( parts ) == 2:
boneMappings.append( [ x.lower() for x in parts ] )
else:
print( "Warning: Invalid line \"{}\" in bonenames file! Format: sourcename targetname".format(line) )
print( "done." )
except IOError as err:
print( err )
exit()
sourceBoneNameByTargetBoneName = {}
for boneMapping in boneMappings:
sourceBoneNameByTargetBoneName[ boneMapping[ 1 ] ] = boneMapping[ 0 ]
# build bone hierarchy
print( "Converting..." )
class Bone:
def __init__( self, targetOnly ):
self.targetOnly = targetOnly
self.sourceChildren = []
self.targetChildren = []
self.basePoseMat = None # for checks source vs. target
self.absoluteOffset = Matrix() # for those not in the source hierarchy
self.absoluteOffsetInverted = Matrix()
# set from source
self.sourceRelativeOffset = Matrix() # identity matrix if nothing else supplied (e.g. does not exist in source)
# read for target
self.targetRelativeOffset = None
def calculateAbsoluteOffsetFromSource( self, parentAbsoluteOffset ):
self.absoluteOffset = self.sourceRelativeOffset * parentAbsoluteOffset
self.absoluteOffset = parentAbsoluteOffset * self.sourceRelativeOffset
self.absoluteOffsetInverted = self.absoluteOffset.inverted()
for child in self.sourceChildren:
child.calculateAbsoluteOffsetFromSource( self.absoluteOffset )
def calculateTargetRelativeOffset( self, parentAbsoluteOffsetInverted ):
if self.targetOnly:
self.targetRelativeOffset = Matrix()
for child in self.targetChildren:
child.calculateTargetRelativeOffset( parentAbsoluteOffsetInverted )
else:
self.targetRelativeOffset = self.absoluteOffset * parentAbsoluteOffsetInverted
self.targetRelativeOffset = parentAbsoluteOffsetInverted * self.absoluteOffset
for child in self.targetChildren:
child.calculateTargetRelativeOffset( self.absoluteOffsetInverted )
def getRelativeOffset( self ):
return self.targetRelativeOffset
# add source bones
bonesBySourceIndex = []
bonesBySourceName = {}
for mdxaBone in sourceHierarchy:
bone = Bone( False )
bonesBySourceIndex.append( bone )
bonesBySourceName[ mdxaBone.name.lower() ] = bone
bone.basePoseMat = mdxaBone.basePoseMat
bone.basePoseMatInv = mdxaBone.basePoseMatInv
# add target bones, using the same where required by mapping
bonesByTargetIndex = []
bonesByTargetName = {}
for mdxaBone in targetHierarchy:
name = mdxaBone.name.lower()
bone = None
if name in sourceBoneNameByTargetBoneName:
sourceName = sourceBoneNameByTargetBoneName[ name ]
if sourceName not in bonesBySourceName:
print( "Error: No source bone called \"{}\" in source skeleton!".format( sourceName ) )
exit()
bone = bonesBySourceName[ sourceName ]
# check if the base pose mat changed much (more than 0.1)
if any( [ abs(mdxaBone.basePoseMat[y][x] - bone.basePoseMat[y][x]) > 0.1 for x in range(4) for y in range(3) ] ):
print( "Warning: Target bone {}'s base pose changed! Expect bad results.".format( mdxaBone.name ) )
else:
bone = Bone( True )
bone.basePoseMat = mdxaBone.basePoseMat
bone.basePoseMatInv = mdxaBone.basePoseMatInv
bonesByTargetName[ name ] = bone
bonesByTargetIndex.append( bone )
# build hierarchy
sourceRoots = []
for mdxaBone, bone in zip(sourceHierarchy, bonesBySourceIndex):
bone.sourceChildren = [ bonesBySourceIndex[ index ] for index in mdxaBone.children ]
if mdxaBone.parent == -1:
sourceRoots.append(bone)
targetRoots = []
for mdxaBone, bone in zip(targetHierarchy, bonesByTargetIndex):
bone.targetChildren = [ bonesByTargetIndex[ index ] for index in mdxaBone.children ]
if mdxaBone.parent == -1:
targetRoots.append(bone)
# convert!
targetFrames = MdxaFrames( targetHeader.numBones )
targetPool = MdxaBonePool()
identityMatrix = Matrix()
for frame in sourceFrames:
# set relative bone offsets (source parent space)
for index, bone in zip( frame.indices, bonesBySourceIndex ):
bone.sourceRelativeOffset = sourcePool[ index ]
# calculate absolute offsets from them (model/world space)
for bone in sourceRoots:
bone.calculateAbsoluteOffsetFromSource( identityMatrix )
# calculate relative offsets (target parent space)
for bone in targetRoots:
bone.calculateTargetRelativeOffset( identityMatrix )
# save relative offsets
targetFrame = MdxaFrame( targetHeader.numBones )
targetFrame.loadFromBones( bonesByTargetIndex, targetPool )
targetFrames.append( targetFrame )
print( "done." )
# write result
print( "Writing output file..." )
outputFile = open( sys.argv[ 4 ], "wb" )
saveToFile( outputFile, targetHeader, targetHierarchy, targetFrames, targetPool )
outputFile.close()
print( "done." )
model_root model_root
pelvis pelvis
Motion Motion
lfemurYZ lfemurYZ
lfemurX lfemurX
ltibia ltibia
ltalus ltalus
rfemurYZ rfemurYZ
rfemurX rfemurX
rtibia rtibia
rtalus rtalus
lower_lumbar lower_lumbar
upper_lumbar upper_lumbar
thoracic thoracic
cervical cervical
cranium cranium
ceyebrow ceyebrow
jaw jaw
lblip2 lblip2
leye leye
rblip2 rblip2
ltlip2 ltlip2
rtlip2 rtlip2
reye reye
rclavical rclavical
rhumerus rhumerus
rhumerusX rhumerusX
rradius rradius
rradiusX rradiusX
rhand rhand
r_d1_j1 r_d1_j1
r_d1_j2 r_d1_j2
r_d2_j1 r_d2_j1
r_d2_j2 r_d2_j2
r_d4_j1 r_d4_j1
r_d4_j2 r_d4_j2
rhang_tag_bone rhang_tag_bone
lclavical lclavical
lhumerus lhumerus
lhumerusX lhumerusX
lradius lradius
lradiusX lradiusX
lhand lhand
l_d4_j1 l_d4_j1
l_d4_j2 l_d4_j2
l_d2_j1 l_d2_j1
l_d2_j2 l_d2_j2
l_d1_j1 l_d1_j1
l_d1_j2 l_d1_j2
face face
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.