File size: 13,745 Bytes
0390e84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
#
# Copyright (C) 2019 Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG),
# acting on behalf of its Max Planck Institute for Intelligent Systems and the
# Max Planck Institute for Biological Cybernetics. All rights reserved.
#
# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is holder of all proprietary rights on this computer program.
# You can only use this computer program if you have closed a license agreement with MPG or you get the right to use the computer program from someone who is authorized to grant you that right.
# Any use of the computer program without a valid license is prohibited and liable to prosecution.
# Contact: ps-license@tuebingen.mpg.de
#

#
# Usage:
#   + Blender Python integration: "Run Script"
#
# Known issues:
#   + When avatar shape is baked all pose shapes will also be removed.
#
# Requirements:
#   + Config files (female/male regressor + body definition) must be located in same folder as .py/.blend file
#   + Blender 2.79 Version 2018-04-10 or later for proper alphabetical shape key name import from FBX
#
# Version: 20180427
#

import argparse
import bpy
import json
from mathutils import Vector
import numpy as np
import os
import sys

############################################################
# Variables
############################################################

bakeShape = True
removePoseShapeKeys = True
exportToFBX = True

femaleJointRegressorName = "betas2Jnt_unity_f.json"
maleJointRegressorName = "betas2Jnt_unity_m.json"
bodyConfigName = "body.json"
fbxName = "SMPL-Blender.fbx"

# Dictionary for matching joint index to name
jointNames = {
    0:  'Pelvis',
    1:  'L_Hip',        4:  'L_Knee',            7:  'L_Ankle',           10: 'L_Foot',
    2:  'R_Hip',        5:  'R_Knee',            8:  'R_Ankle',           11: 'R_Foot',
    3:  'Spine1',       6:  'Spine2',            9:  'Spine3',            12: 'Neck',            15: 'Head',
    13: 'L_Collar',     16: 'L_Shoulder',       18: 'L_Elbow',            20: 'L_Wrist',         22: 'L_Hand',
    14: 'R_Collar',     17: 'R_Shoulder',       19: 'R_Elbow',            21: 'R_Wrist',         23: 'R_Hand',
}

############################################################
# Returns offset between lowest point on feet and floor
############################################################
def floorOffset(skinnedMesh):
    minZ = 999999

    # Important: Update scene to ensure that recent modifications of skin location are taken into account
    bpy.context.scene.update()

    bakedMesh = skinnedMesh.to_mesh(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW')
    bakedMesh.transform(skinnedMesh.matrix_world)

    for vertex in bakedMesh.vertices:
        if vertex.co[2] < minZ:
            minZ = vertex.co[2]
    bpy.data.meshes.remove(bakedMesh)

    return minZ

############################################################
# Create avatar and optionally export to FBX
############################################################
def createAvatar():

    # TODO: parse command-line

    print("########################################")
    print("# Creating avatar")
    print("########################################\n")

    # Setup config file paths
    scriptDir = os.path.split(__file__)[0]

    # Fix path if we are running script from within Blender to point to parent directory of .blend file
    if scriptDir.endswith('.blend'):
        scriptDir = os.path.split(scriptDir)[0]

    configDir = scriptDir
    print("Config directory:", configDir)

    femaleJointRegressorPath = os.path.join(configDir, femaleJointRegressorName)
    maleJointRegressorPath = os.path.join(configDir, maleJointRegressorName)
    bodyConfigPath = os.path.join(configDir, bodyConfigName)
    fbxPath = os.path.join(configDir, fbxName)

    ############################################################
    # Load shape parameters and gender from local file
    ############################################################

    print("--------------------------------------------------")
    print("Loading shape parameters:", bodyConfigPath)

    if not os.path.exists(bodyConfigPath):
        print("ERROR: Missing body configuration: ", bodyConfigPath)
        return False

    bodyConfigData = json.load(open(bodyConfigPath))

    gender = bodyConfigData['gender']
    betaRange = bodyConfigData['betaRange']
    betas = np.array(bodyConfigData['betas'])

    print("Gender: " + gender)
    print("Beta range: [-%dSD, +%dSD]" % (betaRange, betaRange))

    np.set_printoptions(formatter={'float': '{: 0.3f}'.format})
    print("Betas: " + np.array2string(betas, separator=','))

    ############################################################
    # Setup gender
    ############################################################

    if gender == 'female':
        avatarName = 'Female'
        armatureName = 'ArmatureFemale'
        genderPrefix = 'f_avg'
        jointRegressorPath = femaleJointRegressorPath
    else:
        avatarName = 'Male'
        armatureName = 'ArmatureMale'
        genderPrefix = 'm_avg'
        jointRegressorPath = maleJointRegressorPath

    ############################################################
    # Load joint regressor
    ############################################################

    print("--------------------------------------------------")
    print("Loading joint regressor")


    if not os.path.exists(jointRegressorPath):
        print("ERROR: Missing joint regressor: ", jointRegressorPath)
        return False

    jointRegressorData = json.load(open(jointRegressorPath))
    jointFromBetaMatrix = np.array(jointRegressorData['betasJ_regr'])
    jointTemplate = np.array(jointRegressorData['template_J'])

    ############################################################
    # Apply shape key weights
    ############################################################
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.context.scene.objects.active = bpy.data.objects[genderPrefix]

    for beta in range(0, 10):
        # Map beta range to [0, 1]
        weight = betas[beta] / betaRange

        shapenamePos = "Shape%03d_pos" % (beta)
        shapenameNeg = "Shape%03d_neg" % (beta)

        if weight >= 0.0:
            shapename = shapenamePos
            shapenameReset = shapenameNeg
        else:
            shapename = shapenameNeg
            shapenameReset = shapenamePos

        bpy.context.object.data.shape_keys.key_blocks[shapename].value = abs(weight)
        bpy.context.object.data.shape_keys.key_blocks[shapenameReset].value = 0.0

    ############################################################
    # Determine new floor offset
    ############################################################

    # If the script was run before it introduced Armature location offset to
    # put the feet on the ground.
    # Reset any offset on skinned mesh during floor offset calculation so that we have proper default positions.
    skinnedMesh = bpy.data.objects[genderPrefix]
    skinnedMesh.location = Vector((0.0, 0.0, 0.0))
    feetFloorOffsetM = floorOffset(skinnedMesh)

    ############################################################
    # Store default bone positions
    ############################################################

    # We must select Armature as current selection before switching to Edit Mode
    bpy.context.scene.objects.active = bpy.data.objects[avatarName]
    bpy.ops.object.mode_set(mode='EDIT')

    defaultBonePositions = {}
    for bone in bpy.data.armatures[armatureName].edit_bones:
        defaultBonePositions[bone.name] = bone.head

    ############################################################
    # Calculate new bone world positions
    ############################################################
    newJoints = jointFromBetaMatrix.dot(betas)
    newJoints = np.add(newJoints, jointTemplate)

    # Convert regressor [m] units to [cm] for Blender joint positions
    newJoints *= 100

    ############################################################
    # Set new bone world positions
    ############################################################
    print("--------------------------------------------------")
    print("Set new bone positions")

    numJoints = newJoints.shape[0]
    for i in range(0, numJoints):
        boneName = genderPrefix + "_" + jointNames[i]
        newPosition = Vector(newJoints[i])
        oldPosition = defaultBonePositions[boneName]
        offset = newPosition - oldPosition

        # Translate bone (head and tail) to new target position
        bpy.data.armatures[armatureName].edit_bones[boneName].translate(offset)

        # Apply previously calculated floor offset to position feet on floor
        bpy.data.armatures[armatureName].edit_bones[boneName].translate(Vector((0.0, -feetFloorOffsetM*100, 0.0)))

    ############################################################
    # Place skinned mesh on ground
    ############################################################
    bpy.ops.object.mode_set(mode='OBJECT')
    skinnedMesh.location[1] = -feetFloorOffsetM * 100

    ############################################################
    # Remove pose shape keys (optional)
    ############################################################
    if removePoseShapeKeys:
        print("--------------------------------------------------")
        print("Performance optimization: Removing pose shape keys")
        bpy.context.scene.objects.active = bpy.data.objects[genderPrefix]
        numShapeKeys = len(bpy.context.object.data.shape_keys.key_blocks.keys())

        currentShapeKeyIndex = 0
        for index in range(0, numShapeKeys):
            bpy.context.object.active_shape_key_index = currentShapeKeyIndex

            if bpy.context.object.active_shape_key is not None:
                if bpy.context.object.active_shape_key.name.startswith('Pose'):
                    bpy.ops.object.shape_key_remove(all=False)
                else:
                    currentShapeKeyIndex = currentShapeKeyIndex + 1

    ############################################################
    # Bake shape by removing the Shape shape keys (optional)
    ############################################################
    if bakeShape:
        print("--------------------------------------------------")
        print("Bake body shape (removing all shape keys)")
        bpy.context.scene.objects.active = bpy.data.objects[genderPrefix]

        # Create shape mix for current shape
        bpy.ops.object.shape_key_add(from_mix=True)
        numShapeKeys = len(bpy.context.object.data.shape_keys.key_blocks.keys())

        #FIXME: Do not remove pose shapes if they still exist

        # Delete all shape keys except newly added one
        bpy.context.object.active_shape_key_index = 0
        for count in range(0, numShapeKeys):
            bpy.ops.object.shape_key_remove(all=False)

    ############################################################
    # Export to FBX (optional)
    ############################################################
    if exportToFBX:

        print("--------------------------------------------------")
        print("Exporting to FBX:", fbxPath)

        # Deselect all selected objects
        for obj in bpy.context.selected_objects:
            obj.select = False

        # Select bones and skin
        bpy.data.objects[avatarName].select = True
        bpy.data.objects[genderPrefix].select = True

        bpy.ops.export_scene.fbx(filepath=fbxPath, use_selection=True)
    else:
        print("--------------------------------------------------")
        print("FBX export disabled")

    return True

###############################################################################
# Main
###############################################################################

# Parse command line arguments for Python script
argv = sys.argv

if "--" not in argv:
    argv = []  # as if no args are passed
else:
    argv = argv[argv.index("--") + 1:]  # get all args after "--"

if len(argv) > 0:
    # Note: \n characters are not processed in epilog string
    parser = argparse.ArgumentParser(description='Create and export FBX from SMPL template FBX and body.json description.',
                                     epilog = 'Example: blender.exe createSMPLAvatar.blend --background --python createSMPLAavatar.py -- --body="body.json" --name="SMPL-Blender.fbx"')
    parser.add_argument('--body', dest="body", type=str, help='Body JSON file name')
    parser.add_argument('--name', dest="name", type=str, help='Output file name')
    parser.add_argument('--noposeshapes', dest="noposeshapes", help='Remove all pose shape keys', action="store_true")
    parser.add_argument('--nobakeshape', dest="nobakeshape", help='Keep all shape keys for the body shape', action="store_true")
    parser.add_argument('--test', dest="test", help='Do not create output file', action="store_true")

    args = parser.parse_args(argv)

    if args.name is not None:
        fbxName = args.name

    if args.body is not None:
        bodyConfigName = args.body

    if args.noposeshapes:
        removePoseShapeKeys = True

    if args.nobakeshape:
        bakeShape = False

    if args.test:
        exportToFBX = False

success = createAvatar()
if success:
    print("--------------------------------------------------")
    print("Finished\n")