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