Halloween Costume ideas 2015

PROCEDURAL / Domino Array Animation Using Drivers Offset

Doreamon's Everywhere Doors Puzzle.

Few posts ago, I wrote a very long article about Array Drivers and Python. There are plenty of information in there and you probably will not get everything at once. Trust me, one day you will need some of those information when it comes to Drivers.

Anyway, I think it is best if we put the knowledge into practice.

WHAT WE KNOW ABOUT BLENDER DRIVERS

Blender Drivers are very useful for animation. We can setup all kinds of Drivers relationship to control almost anything that can be animated. Remember what I stated earlier: "Any keyable property can be driven".

Drivers can reduce multiple animation control into a single control.

Why do we use Drivers instead of directly adding Animation Curve or NLA Action Block? Because often we need to have some kind of additional layer of control (ideally automated) on top of MANUAL animation or animation that is ACTION based.

With Drivers in Blender, we also have potential ability to procedurally orchestrate our animation using Dynamic Paint.

Drivers is often closely related to Character Rigging / Animation Setup and that is probably the next area I would like to explore in near future.

DOMINO EFFECT = ONE-TO-MANY Driver Relationship with OFFSET
In Computer Graphics, Domino Effect Animation is probably one exercise that one will encounter. You can tackle this animation problem with all kind of ways:
1. Physic Simulation.
2. Expression Driven Animation (procedural).
3. Action Driven / NLA
4. Manual Animation (hopefully this is only the last resort).

At the moment, I happened to be enrolled in this "Houdini Intro to VFX" 8 weeks workshop with Spencer Lueders and so far I gained a bit of understanding or "proceduralism" here and there.
http://workshops.cgsociety.org/instructor.php?userid=37720

There is one class where Spencer does the Domino Effect using Houdini Expression. The advantage being Houdini is that everything can almost be 100% procedural, from the setup of Domino until final Domino animation using powerful Houdini expressions. Here, I am adapting Spencer's trick inside Blender using DRIVERS.

Just like when we do NLA animation using Python, Driving ONE-TO-MANY objects using a single control is always more interesting if we do slight OFFSET for each drive object.

NOTE:
Domino Effect is not necessary need to be DOMINO. It is basically an effect where we have one object doing something that trigger another object doing similar thing, then continue to the next object, and so on.

1. PREPARE YOUR DOMINO (Semi Procedural way)

We have discussed a way to nicely place objects along curve.
http://blendersushi.blogspot.com.au/2013/03/trick-position-along-curve-and-rebuild.html

IMPORTANT: Always watch the NAMING of array of objects.

We want to be able to quickly select objects IN ORDER. This is really important to ensure that we avoid SORTING PROBLEM and create proper matching Drivers relationship. We don't want to create wrong relationship pair, don't we?

#### QUICK SELECT BY NAME, NOW MORE PYTHONIC
import bpy

# Get all objects named "NAME*" and put it in a list
mesh = bpy.data.objects
sel = [item for item in mesh if "NAME" in item.name]

for item in sel:
    item.select = True

Below is one trick if you want to quickly rename objects with naming convention: {objectname}.{digit} for example: Sushi.000, Sushi.001, Sushi.002, Sushi.003, Sushi.004 and so on.

### QUICK RENAME HACK, NOW MORE PYTHONIC
import bpy

# Get all objects named "NAME*" and put it in a list
mesh = bpy.data.objects
sel = [item for item in mesh if "NAME" in item.name]


for item in sel:
    item.name = "newname.000"

In addition, below is a useful Python script to check the sorting of object in list.

#### RETURN INDEX NUMBER OF OBJECT BASED ON SPECIFIC NAME
bpy.data.objects.find("Cube")


### EXAMPLE LOOP TO CHECK INDEX ORDER MAYBE USEFUL FOR SORTING

import bpy

mesh = bpy.data.objects

myList = [item for item in mesh if "Cube" in item.name]

for x in myList:
    print(x.name, mesh.find(x.name))

2. DOMINO PROCEDURAL ANIMATION SETUP

Before I gave you one long example script that does the setup the Domino Procedural Animation automatically, I want to make sure you understand the concept first.

THE BASIC:
  • We want to DRIVE the rotation axis of Domino "Rotation Euler" using DRIVER value.
  • The rotation in Blender Drivers is in Radian (we need math.pi)
  • We need to also specify the MIN and MAX value. Why? Because when we setup the DRIVERS, we do not want the domino to continuously rotating given the input value.  
DRIVEN = Rotation Euler in X/Y/Z axis. Depending on your Domino object.
DRIVER = Current Frame or other value, maybe we can use an Empty Location value.

The Driver value can be the Current Frame, but can also be ANY VALUE we pipe in as INPUT.

DRIVERS MIN & MAX LIMIT, The Python Way

In Houdini, there is a handy expression that can map values based on MIN and MAX.
fit(INPUT VALUE, old min, old max, new min, new max)
fit01(INPUT VALUE BETWEEN ZERO AND ONE, min, max)

In Blender, we can actually create our own Min and Max Expression and assign it as PyDrivers.

However, we can also use Driver Animation F-Curve which we can draw manually to specify the MIN and MAX. To draw manually each time is a hassle, so we use Python script like below.

Example Driver Setup Using Python - WITHOUT MIN & MAX LIMIT:
# Select at least an object and run script below:

import bpy
import math

ob = bpy.context.object

# add driver to Rotation Y axis
myDriver = ob.driver_add('rotation_euler', 1)


# assign driver type
myDriver.driver.type = 'SCRIPTED'
myDriver.driver.show_debug_info = True
myDriver.driver.expression = 'frame * 0.1'


Example Driver Setup Using Python - WITH MIN & MAX LIMIT:
# Select at least an object and run script below:

import bpy
import math

ob = bpy.context.object

# add driver to Rotation Y axis
myDriver = ob.driver_add('rotation_euler', 1)

# remove the default fcurve modifier 'GENERATOR'
fcurvetype = ob.animation_data.drivers[0].modifiers[0]
ob.animation_data.drivers[0].modifiers.remove(fcurvetype)

# add keyframes manually --- this is MIN and MAX
ob.animation_data.drivers[0].keyframe_points.insert(0, 0)
ob.animation_data.drivers[0].keyframe_points.insert(1, 2 * math.pi)

# assign driver type
myDriver.driver.type = 'SCRIPTED'
myDriver.driver.show_debug_info = True
myDriver.driver.expression = 'frame * 0.1'

You see that with MIN & MAX Limit, the object will rotate from 0 degree to 360 degree and stop. While the one without specified MIN & MAX will continue to rotate.



BLENDER SUSHI SCRIPT: Domino Effect based on TIME (Current Frame)

import bpy
import math

# http://blenderartists.org/forum/showthread.php?278678-copy-paste-a-f-curve-driver

# Get all objects named "Cube.*" and put it in a list
mesh = bpy.data.objects
sel = [item for item in mesh if "Cube." in item.name]


# Starting OFFSET value
offset = 10

for ob in sel:
    
    myDriver = ob.driver_add('rotation_euler', 1)

    # remove the default fcurve modifier 'GENERATOR'
    fcurvetype = ob.animation_data.drivers[0].modifiers[0]
    ob.animation_data.drivers[0].modifiers.remove(fcurvetype)
    
    # add keyframes manually
    ob.animation_data.drivers[0].keyframe_points.insert(0 + offset, 0)
    ob.animation_data.drivers[0].keyframe_points.insert(1 + offset, -0.36 * math.pi)
    
    offset += 1
    
    # assign driver type
    myDriver.driver.type = 'SCRIPTED'
    myDriver.driver.show_debug_info = True
    myDriver.driver.expression = 'frame * 0.1'




 

Once you created the setup, you can easily modify and tweak the setup using the code with a slight modification, for example like below, I commented the code above that we really don't need. We will not create new variable or modifier, just modify existing elements.

import bpy
import math

# Get all objects named "Cube.*" and put it in a list
mesh = bpy.data.objects
sel = [item for item in mesh if "Cube." in item.name]


# Starting OFFSET value
offset = 1

for ob in sel:
    
    #myDriver = ob.driver_add('rotation_euler', 1)

    # remove the default fcurve modifier 'GENERATOR'
    #fcurvetype = ob.animation_data.drivers[0].modifiers[0]
    #ob.animation_data.drivers[0].modifiers.remove(fcurvetype)
    
    # add keyframes manually
    #ob.animation_data.drivers[0].keyframe_points.insert(0 + offset, 0)
    #ob.animation_data.drivers[0].keyframe_points.insert(1 + offset, -0.36 * math.pi)
    
    ob.animation_data.drivers[0].keyframe_points[0].co = 0+offset, 0
    ob.animation_data.drivers[0].keyframe_points[1].co = 5 + offset, -1 * math.pi
    
    ob.animation_data.drivers[0].keyframe_points[0].interpolation = "LINEAR"    
    ob.animation_data.drivers[0].keyframe_points[1].interpolation = "LINEAR"
    
    offset += 1
    
    # assign driver type
    ob.animation_data.drivers[0].driver.type = 'SCRIPTED'
    ob.animation_data.drivers[0].driver.show_debug_info = True
    ob.animation_data.drivers[0].driver.expression = 'frame * 0.2'

 Of course, what would be nice is actually to create FUNCTION and maybe USER INTERFACE (UI) that does all above for easy modification.

F-CURVES ANIMATION & DRIVERS ANIMATION

What also interesting is the not so distance sibling relationship between the normal F-Curve keyframe animation and the F-Curve of Drivers.

We could actually GET the list of keyframes from normal F-Curve and then SET it to Drivers quite easily using Python. This means that we can animate an object like normal where TIME is the only INPUT, and then use the Animation Curve as Drivers F-Curve.

Getting the coordinates of normal keyframe animation points will be like below:

>>> bpy.context.object.animation_data.action.fcurves[0].keyframe_points[0].co
Vector((24.0, 6.7004075050354))

>>> bpy.context.object.animation_data.action.fcurves[0].keyframe_points[1].co
Vector((49.0, 9.362730979919434))

>>> bpy.context.object.animation_data.action.fcurves[0].keyframe_points[2].co
Vector((73.0, 5.008456707000732))


This is certainly something to explore in our own time.


BLENDER SUSHI SCRIPT: Domino Effect based on Empty Location 

import bpy
import math

# Get all objects named "Cube.*" and put it in a list
mesh = bpy.data.objects
sel = [item for item in mesh if "Cube." in item.name]

offset = 0

for ob in sel:
    
    myDriver = ob.driver_add('rotation_euler', 1)

    # remove the default fcurve modifier 'GENERATOR'
    fcurvetype = ob.animation_data.drivers[0].modifiers[0]
    ob.animation_data.drivers[0].modifiers.remove(fcurvetype)
    
    # add keyframes manually
    ob.animation_data.drivers[0].keyframe_points.insert(0 + offset, 0)
    ob.animation_data.drivers[0].keyframe_points.insert(1 + offset, -0.36 * math.pi)
    
    offset += 1
    
    # assign driver type
    myDriver.driver.type = 'AVERAGE'
    myDriver.driver.show_debug_info = True

    # DRIVER VARIABLE
    
    # create new variable
    newVar = myDriver.driver.variables.new()
    
    # set new variable attributes
    newVar.name = "var"
    newVar.type = 'TRANSFORMS'
    newVar.targets[0].id = bpy.data.objects['Empty']
    newVar.targets[0].transform_type = 'LOC_X'
    newVar.targets[0].transform_space = 'WORLD_SPACE'
    


A MORE ADVANCE EXAMPLE:
Array of Doraemon's Everywhere Doors!


Here we have another example case of "Domino Effect" that is using Linked Grouped Objects which happened to be duplicated along Curve Path. This is trickier than you may think.

When bringing LINKED GROUP object to be duplicated and driven we need plan thing properly...

0. Give proper clear naming system.

1. Linked object actually comes as EMPTY + Dupli "Group"

2. When we do Dupli Frame using Curve Path, it will only duplicate the EMPTY, we need to tell Blender to do Dupli "Group"

3. Do "Make Duplicate Real" to make the actual Dupli "Group" real.. However, the Data is still shared
# this handy ops command will make DUPLI real but have to do it in order! sometimes I stay away from bpy.ops when scripting, but in this case, this is the easiest.
bpy.ops.object.duplicates_make_real(use_base_parent=False, use_hierarchy=False)

4. Tap L to Make Local - "Object, Data, Material". This to ensure Blender did not crash when we do the next step.

5. Finally do "Make Single User" to all the Dupli "Group".

6. If all good and naming system works for every animated object, we can simply run the Driver script. Careful with naming so that ANIMATION ARRAY DRIVEN IS OFFSET nicely.

WHEN ARRAY SORTING IS AN ISSUE... DYNAMIC PAINT COMES TO THE RESCUE!

Sometimes it can be really difficult to keep all the array objects sorted. In such case, we can setup Drivers that does not rely on the order but still controllable. We need DYNAMIC PAINT Driver Setup. It is quite simple as I have explained it below. We simply just have some kind of trigger mechanism based on Distance.

A more complex setup probably involving a better trigger mechanism and NLA Action Editor. Because we don't always want to map "Animation Sequence" directly to the "normalized Distance". This is still a concept, but really, I think maybe in Blender 3.0, when BGE becoming more like Interactive mode, all this will be easier, I reckoned.

A. PREPARE OUR ARRAY OBJECT
1. Pre-created Objects, each with variation + similar attribute to drive
2. Create or duplicate object on EVERY POINT OF ARRAY as needed


B. SET EMPTY STATE A AND B - Parent to Cloud
Run the automatic "Parent to Vertex" by Liero to create State A and State B of array.

It will be nice to convert each as Empty, instead of duplication of object.

# CREATE EMPTY ON EVERY OBJECT
import bpy

sel = bpy.context.selected_objects

for ob in sel:
    
    bpy.context.scene.objects.active = ob
    
    # get ob digit    
    digit = ob.name.split('.')[-1]
    
    newname = "emptyB"
    
    # get location
    loc = ob.location
    wmtx = ob.matrix_world
    
    # create locator
    bpy.ops.object.empty_add(type='PLAIN_AXES', view_align=False, location=loc)
    
    # while still active, rename locator
    selectedObject = bpy.context.selected_objects
    selectedObject[0].scale = 0.25,0.25,0.25
    selectedObject[0].name = "{newname}.{digit}".format(newname=newname, digit=digit)
    

C. SET DRIVER
Create Driver setup for each object that keep track of DISTANCE between State A and State B

import bpy

# Assign driver to every selected object
# of particular channel

sel = bpy.context.selected_objects


for ob in sel:
    digit = ob.name.split('.')[-1]
    
    bpy.context.scene.objects.active = ob
    
    # single driver for scale X
    myShapekey = ob.data.shape_keys.key_blocks['Key 1']
    myDriver = myShapekey.driver_add('value')
    
    # by default should be 'SCRIPTED', but in case you want to change 
    # the driver type, you have other option
    # ('AVERAGE', 'SUM', 'SCRIPTED', 'MIN', 'MAX')
    myDriver.driver.type = 'AVERAGE'
    
    
    # enable Debug Info
    myDriver.driver.show_debug_info = True
    
    
    # DRIVER VARIABLE
    
    # create new variable
    newVar = myDriver.driver.variables.new()
    
    # variable name
    newVar.name = "distance"
    
    # variable type
    
    # PLEASE PICK ONE
    #newVar.type = 'TRANSFORMS'
    #newVar.type = 'ROTATION_DIFF'
    #newVar.type = 'LOC_DIFF'
    #newVar.type = 'SINGLE_PROP'
    
    newVar.type = 'LOC_DIFF'
    newVar.targets[0].id = bpy.data.objects['EmptyA.{digit}'.format(digit=digit)]
    newVar.targets[0].data_path = 'location.z'
    newVar.targets[0].transform_type = 'LOC_Z'
    newVar.targets[0].transform_space = 'WORLD_SPACE'
    newVar.targets[1].id = bpy.data.objects['EmptyB.{digit}'.format(digit=digit)]
    newVar.targets[1].data_path = 'location.z'
    newVar.targets[1].transform_type = 'LOC_Z'
    newVar.targets[1].transform_space = 'WORLD_SPACE'


D. DO DYNAMIC PAINT
Set Canvas and Brush.

The Canvas will be one of the Empty that is parented to Point Cloud, it will be driven by Displace Modifier, which is affected by Texture and finally Dynamic Paint.

OTHER SCRIPTS SNIPPET THAT MIGHT BE USEFUL FOR YOU

SCRIPT: Create Empty On Every Vertex of Mesh

import bpy

# for every point, create Empty and do vertex parent

# FIRST ATTEMPT
#ob = bpy.context.object
#verts = ob.data.vertices

#for num, vert in enumerate(verts):
#    print(vert.co)

# Set active scene and object
activeScene = bpy.context.scene
currentObjs = bpy.context.selected_objects

for item in currentObjs:

    print(item.name)
    if item.type == 'MESH':
        # Make meshdata
        # this is important!
        # we create a fake object for Blender (Apply Mesh Location)
        meshdata = item.to_mesh( scene=activeScene, apply_modifiers=True, settings='PREVIEW' )

        vert_list = [vertex.co for vertex in meshdata.vertices]
        for vert in vert_list:
            vert = item.matrix_world * vert # this is important!

            # Create Empty At Vertex Location
            bpy.ops.object.empty_add(type='PLAIN_AXES', location=vert)
            
            # Rename every Empty right after being created
            selectedObject = bpy.context.selected_objects
            selectedObject[0].name = "emptyB.000"
            

        # Remove meshdata data
        bpy.data.meshes.remove(meshdata)

# there will be some array of EmptyOrig (PARENTED TO VERTEX) and EmptyDisplaced

# do vertex parenting

# each will have attribute driver that measure the Distance between

# let the value drive other attribute as needed


SCRIPT: Create Cube On Every Vertex of Selected Meshes

import bpy

# Set active scene and object
activeScene = bpy.context.scene
currentObjs = bpy.context.selected_objects

for item in currentObjs:

    print(item.name)
    if item.type == 'MESH':
        # Make meshdata
        # this is important!
        # we create a fake object for Blender (Apply Mesh Location)
        meshdata = item.to_mesh( scene=activeScene, apply_modifiers=True, settings='PREVIEW' )

        vert_list = [vertex.co for vertex in meshdata.vertices]
        for vert in vert_list:
            vert = item.matrix_world * vert # this is important!
            print(vert)

            # Create Boxes At Vertex Location
            bpy.ops.mesh.primitive_cube_add(location=vert)
            selectedObject = bpy.context.selected_objects
            selectedObject[0].name = 'jimmy.000'            
            selectedObject[0].scale = 0.1,0.1,0.1


        # Remove meshdata data
        bpy.data.meshes.remove(meshdata)


SCRIPT: Set Drivers for Material

# http://www.blenderartist.org/forum/showthread.php?287330-Adding-driver-to-pose-bone-constraint-through-Python

import bpy


# select bunch of objects to drive
# each object has naming convention with number
# need to be sorted
# DRIVER and connection will be added based on number

# get the number string
# blah.split('.')[-1]

# we do not care the name 
# (assume the name of object is the same): cube.001, cube.002


def driveManyToMany(num='000'):
    
    # DRIVING THE SCALE XYZ OF ONE OBJECT USING OTHER 
    
    # ADDING DRIVER VIA SCRIPTING
    
    # add driver for location X of currently active selected object
    # locXDriver = bpy.context.object.driver_add('location', 0)
    
    myDriver = bpy.context.object.active_material.driver_add('diffuse_intensity')
    
    # by default should be 'SCRIPTED', but in case you want to change 
    # the driver type
    
    # ('AVERAGE', 'SUM', 'SCRIPTED', 'MIN', 'MAX')
    
    myDriver.driver.type = 'AVERAGE'
    ####myDriver.driver.type = 'SCRIPTED'
    
    # enable Debug Info
    myDriver.driver.show_debug_info = True
    
    # scripted expression
    ####myDriver.driver.expression = 'var'
    
    #bpy.context.object.animation_data.drivers[0].driver.expression = '#jimmy'
    
    # DRIVER VARIABLE
    
    # create new variable NUMBER 1
    newVar = myDriver.driver.variables.new()
    
    # variable name
    newVar.name = "varA"
    
    # variable type
    
    # PLEASE PICK ONE
    #newVar.type = 'SINGLE_PROP'
    #newVar.type = 'TRANSFORMS'
    #newVar.type = 'ROTATION_DIFF'
    #newVar.type = 'LOC_DIFF'
    
    newVar.type = 'LOC_DIFF'
    newVar.targets[0].id = bpy.data.objects['emptyA.{num}'.format(num=num)]
    newVar.targets[0].data_path = 'location.z'
    newVar.targets[0].transform_type = 'LOC_Z'
    newVar.targets[0].transform_space = 'WORLD_SPACE'
    newVar.targets[1].id = bpy.data.objects['emptyB.{num}'.format(num=num)]
    newVar.targets[1].data_path = 'location.z'
    newVar.targets[1].transform_type = 'LOC_Z'
    newVar.targets[1].transform_space = 'WORLD_SPACE'
    

objs = bpy.context.selected_objects

for obj in objs:
    
    bpy.context.scene.objects.active = obj

    # get that string number thing of currently active object
    myNum = obj.name.split('.')[-1]

    driveManyToMany(num=myNum)

CONCLUSION

What started as simple Driver setup now become slightly complex. I have done hundred of trials and errors for every procedural script. With great power really comes great responsibility.

What to do next is probably to create a nice UI or some kind of easy ways to modify the setup.

I found that the most stressful part when coding Blender Python is to convert bpy.ops into proper bpy.data flow. Sometimes documentation is next to nothing, however in recent version if you look through the methods or functions attached to certain functions, it will give you nice description. What would be nice is some examples.

Anyways, after sometimes, digging functions and data becoming a little bit more fun.

SOME MORE INTERESTING DRIVER RELATED PYTHON PROCEDURAL ANIMATION

http://funkboxing.com/wordpress/?p=236

ALL POTENTIALLY RELATED LINKS


Post a Comment

MKRdezign

Contact Form

Name

Email *

Message *

Powered by Blogger.
Javascript DisablePlease Enable Javascript To See All Widget