Showing posts with label Blender. Show all posts
Showing posts with label Blender. Show all posts

Tuesday, February 16, 2016

"Replicator Grid" Blender script

This Blender script creates a 2D grid of replicated objects with a descending line of parenting control, running left to right, then down to the next row. The following video demonstrates:

 
 
# ---------
# Tested in Blender 2.75a and 2.76
# Makes a 'replicator grid' -- multiple copies of a given object, parented in a tranformation-control chain.
# Provides a single empty that controls the relative size, displacement and rotation of each successive object in relation to
# its parent.
#
# Usage: 
# Change the number of rows and columns below.
# Select target object to be replicated.
# Run the script.
# If you did this with an object named 'cube', for example,
# You will end up with a 'cube_replicator' empty, below which all of the duplicated objects
# have been parented.
# And you will have a new cube_replicator_control empty, which can be moved, scaled and rotated
# to control the overall replicator grid.

# Bret Battey / BatHatMedia.com September 2015
# ---------
# 

import bpy
from bpy.props import *

# specify number of rows and columns here!
rows = 5
columns = 5

# The following function is adapted from Nick Keeline "Cloud Generator" 
# addNewObject in object_cloud_gen.py

def duplicateObject(scene, name, copyobj):

    # Create new mesh
    mesh = bpy.data.meshes.new(name)

    # Create a new object.
    ob_new = bpy.data.objects.new(name, mesh)
    tempme = copyobj.data
    ob_new.data = tempme.copy()
    ob_new.scale = copyobj.scale
    ob_new.location = copyobj.location

    # Link new object to the given scene and select it.
    scene.objects.link(ob_new)
    ob_new.select = True

    return ob_new            


# Build the parenting chain of replications tied to empties and control 
# constraints. If we built a parented chain of objects, we wouldn't be able to 
# transform each object independently. So instead we build a parented chain of
# empties, then parent each duplicated object to one of the empties.

def gridOfReps(rows, cols, source_obj, ctrl, scene):
    for row in range(rows):
        for col in range(cols):
            #Create a locator empty
            bpy.ops.object.add(type="EMPTY", location=(0,0,0))
            rep_loc = bpy.context.object
            # The first empty becomes the root of the whole beast, and the 
            # source object gets parented to it
            if((row == 0) and (col == 0)):
                rep_loc.name = source_obj.name+"_replicator"
                parent_obj = rep_loc
                prev_row_locator = rep_loc
                source_parent = source_obj.parent
                # If the source object has a parent, locate and parent 
                # the new replicator base empty to that.
                if((source_parent) and (source_parent.type == 'EMPTY')):
                    rep_loc.location = source_parent.location
                    rep_loc.parent = source_parent
                # Otherwise just position the base empty to the source obj,
                # zero the source obj location, and parent to the base
                else:  
                    rep_loc.location = source_obj.location
                    source_obj.location = (0,0,0)
                    source_obj.parent = rep_loc   
            else: 
                rep_loc.name = "rep_loc_r"+str(row)+"c_"+str(col)  # name can't 
                    #be specified via the .add method
                rep_loc.parent = parent_obj # parent to the previous locator
                # First locator in each row other than 0 gets special treatment
                if((row > 0) and (col == 0)):
                    # First location constraint locks to the prev. row locator
                    constraint = rep_loc.constraints.new("COPY_LOCATION")  
                    constraint.target = prev_row_locator
                    constraint.use_x = True
                    constraint.use_y = True
                    constraint.use_z = True       
                    constraint.owner_space = "WORLD"
                    constraint.target_space = "WORLD"
                    constraint.influence = 1.0
                    # Second location constraint implements the z offset 
                    # from the controller      
                    constraint = rep_loc.constraints.new("COPY_LOCATION")  
                    constraint.target = ctrl
                    constraint.use_x = False 
                    constraint.use_y = False
                    constraint.use_z = True       
                    constraint.owner_space = "LOCAL"
                    constraint.target_space = "WORLD"
                    constraint.influence = 1.0
                    constraint.use_offset = True   # use the controller's 
                    # location as an offset rather than an absolute location
                    # And ready this locator to be basis of next row position
                    prev_row_locator = rep_loc
                else:
                    # For other items in a row, offset location x on basis of 
                    # controller
                    constraint = rep_loc.constraints.new("COPY_LOCATION")  
                    constraint.target = ctrl
                    constraint.use_x = True  # for offsetting a row, only x is 
                        # drawn from the controller 
                    constraint.use_y = False
                    constraint.use_z = False       
                    constraint.owner_space = "LOCAL"
                    constraint.target_space = "WORLD"
                    constraint.influence = 1.0
                    constraint.use_offset = True   # use the controller's 
                        #location as an offset rather than an absolute location
                # For all locators except the root, apply these influences from 
                    #the controller:
                # Copy_Rotation Constraint 
                constraint = rep_loc.constraints.new("COPY_ROTATION")  
                constraint.target = ctrl
                constraint.owner_space = "LOCAL"
                constraint.target_space = "WORLD"
                constraint.influence = 1.0
                # Copy_Scale Constraint. This will also cascade down the chain 
                    # due to the combined effect with the parenting 
                constraint = rep_loc.constraints.new("COPY_SCALE")  
                constraint.target = ctrl
                constraint.owner_space = "LOCAL"
                constraint.target_space = "WORLD"
                constraint.influence = 1.0
                # In all cases (except root), duplicate source object and parent
                    # to the replicator empty.
                # This strategy allows later transformations of the visible 
                    #object without influencing the children
                new_obj = duplicateObject(scene, "rep", source_obj)
                new_obj.parent = rep_loc    
                # Advance parent assignment
                parent_obj = rep_loc    
    return parent_obj

# get the source object

source_obj = bpy.context.object
source_loc = source_obj.location
source_scene = bpy.context.scene

# create and name the controller empty

bpy.ops.object.add(type="EMPTY", location=(0,0,0))
ctrl = bpy.context.object  
ctrl.name = source_obj.name+"_replicator_control"  # name can't be specified 
        #via the .add method

#replicate!
last_locator = gridOfReps(rows, columns, source_obj, ctrl, source_scene)
 

Blender Script for creating animated motion-trail ribbon

Given an object for which a motion-path has been generated, will create a Beziér-spline ribbon that follows the motion of the object for a given tail length (designated in frames).




# ---------
# Tested in Blender 2.75a and 2.76
# Makes a Bezier-spline motion trail for an object.
# Set the length of the tail in frames via the tail_length variable below.
# To run, select object, generate a motion path for the object (Editor>Motion Paths) across the total desired frame range, and Run Script.
# Warning: this can take a long time to run with long motion paths and long tail sizes
# Once the spline has been generated, extrude and/or add bevel and taper objects to the Geometry settings of the spline.
# Bret Battey / BatHatMedia.com September 2015
# ---------

import bpy
from bpy.props import *
from mathutils import *
from math import *
import time

# ----- FUNCTION DEFINITIONS -----

# Time utils thanks to http://blenderscripting.blogspot.co.uk/search/label/time

def get_last_time():  
    if len(time_list) < 2:  
        return "ERROR: must have two time entries to calculate the difference"  
    return time_list[-1] - time_list[-2]      
  
def mark_time():  
    time_list.append(time.time())      
  
time_list = [] 


##------------------------------------------------------------
#### Curve creation functions
# sets bezierhandles to auto
def setBezierHandles(obj, mode = 'AUTOMATIC'):
    scene = bpy.context.scene
    if obj.type != 'CURVE':
        return
    scene.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=True)
    bpy.ops.curve.select_all(action='SELECT')
    bpy.ops.curve.handle_type_set(type=mode)
    bpy.ops.object.mode_set(mode='OBJECT', toggle=True)


# create new CurveObject 
def createCurve(verts, name):
      
    # create curve
    scene = bpy.context.scene  
    newCurve = bpy.data.curves.new(name, type = 'CURVE') # curvedatablock
    newSpline = newCurve.splines.new(type = 'BEZIER') # spline

    # The new spline already has one point. Add the remaining needed.
    newSpline.bezier_points.add(verts-1)

    # set curveOptions
    newCurve.dimensions = '3D'

    # create object with newCurve
    new_obj = bpy.data.objects.new(name, newCurve) # object
    scene.objects.link(new_obj) # place in active scene
    new_obj.select = True # set as selected
    scene.objects.active = new_obj  # set as active

    # set bezierhandles
    setBezierHandles(new_obj)

    return new_obj

#based on animation_rotobezier.py
def keyframeBezier(Obj, frame):

    Data = Obj.data
    
    for Spline in Data.splines:
        for CV in Spline.bezier_points:
            CV.keyframe_insert(data_path='co', frame = frame)
            CV.keyframe_insert(data_path='handle_left', frame = frame)
            CV.keyframe_insert(data_path='handle_right', frame = frame)
    
# -------------  SCRIPT START

tail_length = 60   # length of the spline in frames
object = bpy.context.object
path = object.motion_path
frame_start = path.frame_start
frame_end = path.frame_end
path_length = path.length

# --- create the Bezier curve
trail = createCurve(tail_length,object.name+" motion_trail")

mark_time() # start measuring elapsed time

# --- key frame the curve at every frame in the motion path

for i in range(tail_length-1,path_length): # motion path index
    print("Point "+str(i))
    for j in range(tail_length): # curve point index 
        trail.data.splines[0].bezier_points[j].co = path.points[(i-tail_length)+j].co # set the coordinates of the motion tail to the corresponding point on the motion path
    # keyframe it
    keyframeBezier(trail, frame_start+i)
    
mark_time()

print('The motion trail keyframing took {: 5g} seconds'.format(get_last_time()))




Blender Script for object-motion measurements

In Blender, given an object that has a motion path, will provide animated empties which indicate the object's velocity, acceleration, magnitude, and accumulated distance moved.


# ---------
# Tested on Blender 2.75a and 2.76
# For a given object, makes an empty, which moves to indicate the velocity (displacement between frames) for each axis of the original object
# and another empty providing magnitude of the movement
# and another empty providing accumulative displacement for each axis, 
# and yet another for acceleration. 
# To run, select object, generate a motion path for the object (Editor>Motion Paths) across the total desired frame range, and Run Script.
# Bret Battey / BatHatMedia.com Dec 2015
# ---------

import bpy
from bpy.props import *
from mathutils import *
from math import *
import time

# ----- FUNCTION DEFINITIONS -----

# Time utils thanks to http://blenderscripting.blogspot.co.uk/search/label/time

def get_last_time():  
    if len(time_list) < 2:  
        return "ERROR: must have two time entries to calculate the difference"  
    return time_list[-1] - time_list[-2]      
  
def mark_time():  
    time_list.append(time.time())      
  
time_list = [] 


# -------------  SCRIPT START

object = bpy.context.object
path = object.motion_path
frame_start = path.frame_start
frame_end = path.frame_end
path_length = path.length
# velocity empty
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
velempty = bpy.context.object
velempty.name = object.name + '_velocity'
# vector magnitude 
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
magempty = bpy.context.object
magempty.name = object.name + '_magnitude'
# accumulative distance traveled 
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
accumempty = bpy.context.object
accumempty.name = object.name + '_accum_distance'
# acceleration 
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
accelempty = bpy.context.object
accelempty.name = object.name + '_acceleration'

mark_time() # start measuring elapsed time

for i in range(0,path_length-2): # motion path index
    nextPointCo = path.points[i+1].co # next time point
    thisPointCo = path.points[i].co # this time point
    dif = nextPointCo-thisPointCo # x,y,z distances
    mag = sqrt(dif[0]*dif[0]+dif[1]*dif[1]+dif[2]*dif[2]) # vector magnitude via pythagorean theorem
    velempty.location = dif # set vel empty's location to the velocity vector
    magempty.location[2] = mag # set z position of mag empty's location to the magnitude
    frame = frame_start+i  # get absolute frame number
    velempty.keyframe_insert(data_path='location',frame=frame,index=-1) # set keyframe for velocity. index -1 = set all three axes
    magempty.keyframe_insert(data_path='location',frame=frame,index=2) # set keyframe for velocity. index -1 = set all three axes
    if i == 0:
        accumempty.keyframe_insert(data_path='location',frame=frame,index=-1) # set keyframe. index -1 = set all three axes
        accumdif = accumempty.location # start accumulated distance with a convenient 0,0,0 vector
    else:
        accumdif[0] += abs(dif[0]) # add absolute value of velocity to the accumulative distance
        accumdif[1] += abs(dif[1])
        accumdif[2] += abs(dif[2])
        accumempty.location = accumdif # move empty to this point
        accumempty.keyframe_insert(data_path='location',frame=frame,index=-1) # set keyframe. index -1 = set all three axes
        accelempty.location = dif-lastdif # acceleration = dif between this frame's velocity and the previous frame's
        accelempty.keyframe_insert(data_path='location',frame=frame-1,index=-1) # set keyframe ONE FRAME BACK. index -1 = set all three axes
    lastdif = dif

   
mark_time()

print('The keyframing took {: 5g} seconds'.format(get_last_time()))



Friday, August 10, 2012

Duplicating Objects in Blender 2.6 Python Scripting

Duplicating objects with Blender 2.6 Python scripting is not quite as straightforward as one might hope, and for me a web search failed to return a simple, clear solution. If found my best solution by searching through the Blender scripts addons folder for files containing the 'copy()' function.

One has to create a new mesh, then copy the data of the source object, then link to the scene. Here's the short function I'm using to do this.

# The following function is adapted from 
# Nick Keeline "Cloud Generator" addNewObject 
# from object_cloud_gen.py (an addon that comes with the Blender 2.6 package)
#
def duplicateObject(scene, name, copyobj):

    # Create new mesh
    mesh = bpy.data.meshes.new(name)

    # Create new object associated with the mesh
    ob_new = bpy.data.objects.new(name, mesh)

    # Copy data block from the old object into the new object
    ob_new.data = copyobj.data.copy()
    ob_new.scale = copyobj.scale
    ob_new.location = copyobj.location

    # Link new object to the given scene and select it
    scene.objects.link(ob_new)
    ob_new.select = True

    return ob_new

Thursday, August 9, 2012

Towards Essential Body Relationships


Though I am intending to map the motions of Tofail Ahmed to an abstract visualisation, I want to ensure that important perceptual aspects of the body motion translate effectively. So it is helpful to analyze the performance motions. My intent is to identify high-priority relationships or parameters that I will seek to honour in the abstract visualisation. What "honouring" can and will mean in practice remains to be seen.

Gesture Types


Borrowing terms from Martin Clayton's study of gesture in Khyāl performance (2007), the performances I recorded of Mr Ahmed contained physical gestures that serve as "markers", "illustrators" and "emblems".

Marker gestures indicate a specific time point in musical structure, such as beating a pulse, identifying a downbeat, etc.

Illustrators appear analogous to the melodic flow/motion. The vast majority of the motion falls into this category. It is one vast territory that covers a lot that is very interesting — and very difficult to talk about analytically. This, perhaps, is precisely why it is so valuable!

Emblems, or symbolic gestures, are more based on cultural convention and can be translated relatively readily into a verbal equivalent. An example would be indicating approval with a hand wave.

Emblems would be the most problematic type of gesture given my intent. An emblem is at high risk of disappearing in an abstraction, given the very precise body-arrangement and audience reading it entails. Fortunately, there are very few emblems in the performance I am working with. The closest is an invocation-type emblem. This general position — palms close together, often in front of the face — is important. It creates the impression of focus and preparation, and occurs at the beginning of the first and last phrases of the performance. It also appears at the start of 11 other  phrases of the 60 total phrases. This invocation emblem is also often acting as a marker of the start of phrases. It is doing dual duty.

Invocation: Hand position at start of phrase 1

I am wagering that the element of proximity/closedness is as crucial here as the fact that this can be read as a sign of invocation/gathering.

If the invocation emblem often marks the start of a phrase, the most common marker of a phrase start involves the fingers oriented towards each other horizonally, at mid-body level. The example below is appears at the start of phrase 3:

Rest Position: Hand position at start of phrase 3
I am calling this the rest position. 27 of the 60 phrases start with the hands arranged in this general position. Five others are in closely related positions.

Though there are true full-rest positions of hands on knees (at start) or hands in lap (at end), these seem like outliers that won't be useful as a base position, since none of the Illustrator gestures operate in those spaces.

Measures


Given the above clues, I created some measures to help explore what body relationships might be highly correlated to the structure of the music. Basically, I am assuming that if my abstract visualisations at least clearly carry some of large scale articulators of phrasing, the details within will "take care of themselves".

Proximity to Rest Position (PRP): Clayton (2007) used distance of hand from a rest position in his analysis. I am taking a similar approach, measuring the distance from each finger tip to the neutral position and summing those distances. [Aug 10: However, I might consider using a different term, like neutral position, since Clayton appeared to use rest position to apply to true resting of hands on the legs.]

I did this in Blender by creating an Empty at the rest position and an Empty to represent the distance. I animated the Z axis of the latter with a Driver:



Finger Tip Proximity (FP) and Thumb+Finger Tip Proximity (FTP): The finger tips are in close proximity in both the invocation and the neutral position. So it could be of value to simply measure the distance between the finger tips.

Plus, I took a more refined measurement of hand tip proximity is to take the distance between the finger tips and between the thumb tips, and average them.

As it turns out, close finger tip proximity is often closely related to the beginning of phrases. However, finger tip proximity also closes in fairly often during other mid-phrase events.

Shoulder to Hand Proximity (SHP): The close-to-body versus far-from-body contrast also seems important. As the arms straighten, hands move away from the body. So a simply measure of the degrees of far-from-body is the distance between the shoulder and the hand. This measure sums the left and right shoulder-to-wrist distances.

Hand Closedness (HC): This sums the left and right finger-tip to wrist distances, to provide a measure of the open versus closed state of the hand.

Video

The above video shows the first 18 phrases, with graphic representation of the above measures. This is enough to convince me that Proximity to Rest Position is an excellent candidate to focus on. (But how?)

I could spend a lot of time now analyzing the relationships of these measurements to the music. But that probably will not actually help me attain my immediate goals, so I might have to set that aside for another time.


References

Clayton, M. (2007) "Time, Gesture and Attention in Khyāl Performance". Asian Music v.38 n.2.

Sunday, August 5, 2012

Tofail Motion Capture Mapping Test "simple-03"

This test makes me confident that the hierarchical motion mapping idea is worth pursing:


This is a very simple mapping of the upper body motion of Tofail Ahmed as he sings an khyal alaap in raag Bhairavi.

The spheres follow the middle finger and thumb endpoints, with the camera in the viewer position facing Tofail.

The planks are linked in a parenting hierarchy and receive local rotations of bones in the skeleton. (The capacity to do this is easily missed in Blender: when identifying the target object of a Copy Rotations constraint, one indicate the armature, then a second drop down will appear that lets one indicate the target bone.)

Also, to smooth out issues I was having with sudden shifts in the thorax and pelvis rotations, I substituted in the head at the thorax point and made that the root. So the line is head : clavicle: humerus : radius : <hand : finger > <thumb>.

An empty provides parenting to the root, and the empty is rotated 180° on each axis across the whole performance. This provides a foundation of slow, continuous motion to match, if you will, the fundamental drone. The head block, then, applies the rotations from the head as an offset to this base angle.

I'm intrigued by the juxtaposition of the direct position mapping (the balls) with the hierarchical mapping (the planks).

Saturday, August 4, 2012

Importing Vicon IQ Motion Capture into Blender

In the Fused Media Lab at De Montfort University's Faculty of Technology, I used a Vicon multi-camera infrared tracking system to capture the upper-body, arm and hand motions of Tofail Ahmed while he sang khyāl alāps. The software was Vicon IQ. IQ is no longer supported by Vicon, and its export formats are not widely recognized any more.

Therefore, I explored a multitude of dead-ends in trying to get the motion capture data into Blender 2.6x. Here's the solution I ultimately developed. One probably wouldn't want to go through this for high volumes of motion capture sessions for different subjects, but it is a reasonable solution for transferring one session.

Export Data

First, export the skeleton joint movement and rotations data from Vicon, using the CSV (comma separated value) format, global rather than local orientation. It contains world-space rotations and translations for each joint. The joint angles are Euler angles, specified by X Y and Z rotations in degrees, applied in that order (as they appear in the spreadsheet).

Create The Blender Skeleton

The next step is to manually create a skeleton of armatures in Blender matching the calibrated skeleton used in the motion capture. One can guide the process by looking at the calibrated skeleton file — the Vicon.vsk file. The .vsk is in XML format, so can be opened in a text editor. The first section is the <KinematicModel> section. Within that, the <Parameters> section will list the name and values of parameters used in the construction of the skeleton. After that, the <Skeleton> section defines each "segment" or bone and the hierarchy of relationships between the bones. The hierarchy is modeled within the XML hierarchy itself. For example, in my skeleton, pelvis is the parent of thorax is the parent of head, lclavicle and rclavicle. So this part of the XML, simplified, is arranged as follows (… indicates stuff left out). Notice how the parameters defined above now appear in the skeleton definition, usually in defining the position of joints (and hence the length of bones):

<Skeleton>
    <Segment NAME="pelvis" POSITION="0 0 0" …>
        …
        <Segment NAME="thorax" POSITION="-50 0 Back" …>
            …
            <Segment NAME="head" POSITION="0 0 Neck" …>
            …
            </Segment>
            …
            <Segment NAME="lclavicle" POSITION="0 0 Neck" …>
                … (whole left arm descends from here)
            …
            </Segment>
            <Segment NAME="rclavicle" POSITION="0 0 Neck" …>
               … (whole right arm descends from here)
            …
            </Segment>
        …

Notice that the position of each joint is defined relative to its parent rather than in global coordinates. We will need global coordinates to create the skeleton in Blender.

Further, the Vicon coordinate system is Y pointing left, X pointing back, Z pointing up, while the Blender coordinate system is X pointing right, Y pointing back, Z pointing up. So we need to map Vicon X to Blender Y, the negative of Vicon Y to Blender X, and Vicon Z to Blender Z.

I built an Excel spreadsheet to take the Vicon local coordinates and convert them to Blender coordinates, then, based on the hierarchy, accumulate the values into global coordinates:



So, using these absolute/global values, one can create the skeleton in Blender. To be safe, I took care to ensure that the roll was set to 0 for all bones. This skeleton will be huge by the standards of Blender units. Scaling will come later.


Import Motion Data


To import the Vicon IQ motion data from the CSVs, I used Hans P.G.'s CSV F-Curve Importer Blender addon (much thanks to Hans). This requires some preparation, however…

Convert Frame Rate

My data was captured at 120 fps, so it had to be converted to 30 fps by throwing away 3 out of every 4 rows. I did this by using Excel's "Advanced Filter". First I added a column next to the 'frames' column and using mod(4) applied to the frame number (using Fill Down to copy the formula to all cells)…


… then I added second sheet and placed the search criteria for a filtering operation there. We want to select any row where FrameMod = 1. Notice in the formula bar that one has to enter ="=1" for this to work.




Using Data > Advanced Filter…, designate "Copy to another location". The list range will be the range of the original data, criteria range will be the two search criteria cells, and destination should be a starting cell somewhere below the original data. The filtered data will appear here.

The frame numbers will need to be resequenced. I copied frame numbers from the original data and pasted to the new, filtered data.

I then deleted the FrameMod column.

Convert Axis Orientation

The Y values of the translation parameters (NOT the angle parameters) need to be multiplied by -1. One way to do this is to place a -1 in an empty cell and copy the cell. Then select the column that needs to be altered and choose Edit > Paste Special… > Multiply. (The translation parameters are indicated by the <t-X>, <t-Y>, and <t-Z> column heads.)

Convert to Headerless CSV

The header row (the row giving the names for each column) needs to be removed. Now save to a CSV file.

It will be useful to copy that header row into another Excel file for easy reference during the import step, since we will need to know which column number is which. The columns will be referred to by 0-based count, so I found it useful to number them:



Setup Locator Empties for the Joints

Create an Empty for each joint. These are what will be keyframed…

Import the Joint Locations

I used the CSV F-Curve Importer v0_7_alpha1 to import the joint location data one joint at a time. Select a single joint-locator Empty and run the importer to keyframe its location.

I had to comment out the following in the v0_7_alpha1code to get it to run in Blender 2.63. That may not be necessary now:
* import unittest
* the def main()... block
* the class Test_FCurvePointAdder... block

The spreadsheet now contains Vicon global X -Y Z for each joint. I swapped this to -Y X Z during the input to map properly. I assigned a single Action Name to each XYZ set. Here is an example of reading in the Head location from 0-based column numbers 13, 14 and 15, indexed 1 0 2 to do the axis swap (notice that the F-Curve Importer pane shows up under Scene Properties):



Constrain the Bones to the Joint Locators

Working in Pose Mode, one can apply bone constraints. The root of the skeleton (Pelvis, in my case) should have a Copy Location constraint tying it to the Pelvis joint locator Empty. Then add a TrackTo constraint pointing at the next joint locator (Thorax, in my case) [Edit 22 Sep 2015 a better solution for all of the tracking of bones to locators is to use the StretchTo constraint rather than TrackTo, no volume effect, Plane = z was my preference]


Each bone after this point requires only one constraint: a TrackTo to the next joint location:


This will likely move the skeleton way off to some other location in Blender space, but it should now be animated.

N.B. the above may not treat location of free joints (pelvis, thorax, and head in this example) precisely correctly. Comparing the global and local joint export files from IQ, I was not able to come up with a consistent interpretation of how to handle these. For example, it seems to me that free joints below the root joint imply variable bone-lengths, which does not make much sense to me – and I suspect can't be implemented in Blender. However, the above worked well enough for my purposes.

Add Head Rotations

So we are able to get this far without having to import any actual rotation data. The head provides an exception. We now know its location, but not its rotation. This will need to be imported from the Global CSV. But this requires some more prep. The Vicon CSV is in angles, but Blender's internal routines work in radians (even if the interface displays degrees). So the CSV angle data needs to be converted to radians.

One way to do this is to copy the column of data to another spreadsheet. Then fill the next column with an =RADIANS formula. Select and copy the resulting numbers, and paste over the data in the original spreadsheet using Paste Special > Values.

Select the Head locator empty and run the F-Curve importer to import the X Y & Z rotations. As with the translation import above, these should be indexed 1 0 2 in order to swap the X and Y axis.

In my case, it made sense to add a block to represent the head, and add a Copy Location constraint and Copy Rotation constraint, both tied to the Head locator.

Parent, Reposition and Scale

I created an empty at the exact origin of the pelvis, then parented all locator Empties, the head block, and the skeleton to this Empty. This empty serves as the root of the whole bundle, providing one point of control for positioning, rotation and scaling. A scale of 0.01 brought my figure down to something closer to normal Blender working scale.





Optional Joint Rotations


If one needs to make joint locators also reflect joint rotation, one could add a Copy Rotation constraint to a locator, select the Armature as the target, then — in the bone indicator that will appear — indicate the bone. [Edit 22 Sep 2015 -- this is a bad idea, actually. Creates a circular definition between the skeleton constraints and the locator, yielding a 'dependency cycle' error]