Blender 2.49 Scripting: Impression using Different Mesh on Each Frame of Object

Exclusive offer: get 50% off this eBook here
Blender 2.49 Scripting

Blender 2.49 Scripting — Save 50%

Extend the power and flexibility of Blender with the help of the high-level, easy-to-learn scripting language, Python

$23.99    $12.00
by Michel J. Anders | May 2010 | Open Source Web Graphics & Video

Having covered what script links and space handlers are in the previous article, we will take the discussion further on how Python can be used in Blender. Although softbody and cloth simulators that are available in Blender do an excellent job in many situations, sometimes you want to have more control over the way a mesh is deformed or simulate some specific behavior that is not quite covered by Blender's built-in simulators. This article shows how to calculate the deformation of a mesh that is touched, but not penetrated by another mesh. This is not meant to be physically accurate. The aim is to give believable results for solid things touching an easily deformable or gooey surface such as a finger taking a lick of butter or a wheel running through a soft shoulder.

In this article by Michel Anders, author of Blender 2.49 Scripting, you will learn:

  • How to implement a scheme to associate a different mesh with an object on each frame
  • How to augment the functionality of the 3D View

(Read more interesting articles on Blender 2.49 Scripting here.)

Revisiting mesh—making an impression

The following illustration gives some impression of what is possible. The tracks are created by animating a rolling car tire on a subdivided plane:

In the following part, we will refer to the object mesh being deformed as the source and the object, or objects, doing the deforming as targets. In a sense, this is much like a constraint and we might have implemented these deformations as pycontraints. However, that wouldn't be feasible because constraints get evaluated each time the source or targets move; thereby causing the user interface to come to a grinding halt as calculating the intersections and the resulting deformation of meshes is computationally intensive. Therefore, we choose an approach where we calculate and cache the results each time the frame is changed.

Our script will have to serve several functions, it must:

  • Calculate and cache the deformations on each frame change
  • Change vertex coordinates when cached information is present

And when run standalone, the script should:

  • Save or restore the original mesh
  • Prompt the user for targets
  • Associate itself as a script link with the source object
  • Possibly remove itself as a script link

An important consideration in designing the script is how we will store or cache the original mesh and the intermediate, deformed meshes. Because we will not change the topology of the mesh (that is, the way vertices are connected to each other), but just the vertex coordinates, it will be sufficient to store just those coordinates. That leaves us with the question: where to store this information.

If we do not want to write our own persistent storage solution, we have two options:

  • Use Blender's registry
  • Associate the data with the source object as a property

Blender's registry is easy to use but we must have some method of associating the data with an object because it is possible that the user might want to associate more than one object with an impression calculation. We could use the name of the object as a key, but if the user would change that name, we would lose the reference with the stored information while the script link functionality would still be there. This would leave the user responsible for removing the stored data if the name of the object was changed.

Associating all data as a property would not suffer from any renaming and the data would be cleared when the object is deleted, but the types of data that may be stored in a property are limited to an integer, a floating point value, or a string. There are ways to convert arbitrary data to strings by using Python's standard pickle module, but, unfortunately, this scenario is thwarted by two problems:

  • Vertex coordinates in Blender are Vector instances and these do not support the pickle protocol
  • The size of string properties is limited to 127 characters and that is far too small to store even a single frame of vertex coordinates for a moderately sized mesh

Despite the drawbacks of using the registry, we will use it to devise two functions—one to store vertex coordinates for a given frame number and one to retrieve that data and apply it to the vertices of the mesh. First, we define a utility function ckey() that will return a key to use with the registry functions based on the name of the object whose mesh data we want to cache(download full code from here):

def ckey(ob):
return meshcache+ob.name

Not all registries are the same
Do not confuse Blender's registry with the Windows registry. Both serve the similar purpose of providing a persistent storage for all sorts of data, but both are distinct entities. The actual data for Blender registry items that are written to disk resides in .blender/scripts/bpydata/config/ by default and this location may be altered by setting the datadir property with Blender.Set().

Our storemesh() function will take an object and a frame number as arguments. Its first action is to extract just the vertex coordinates from the mesh data associated with the object. Next, it retrieves any data stored in Blender's registry for the object that we are dealing with and we pass the extra True parameter to indicate that if there is no data present in memory, GetKey() should check for it on disk. If there is no data stored for our object whatsoever, GetKey() will return None, in which case we initialize our cache to an empty dictionary.

Subsequently, we store our mesh coordinates in this dictionary indexed by the frame number (highlighted in the next code snippet). We convert this integer frame number to a string to be used as the actual key because Blender's SetKey() function assumes all of the keys to be strings when saving registry data to disk, and will raise an exception if it encounters an integer. The final line calls SetKey() again with an extra True argument to indicate that we want the data to be stored to disk as well.

def storemesh(ob,frame):
coords = [(v.co.x,v.co.y,v.co.z) for v in ob.getData().verts]
d=Blender.Registry.GetKey(ckey(ob),True)
if d == None: d={}
d[str(frame)]=coords
Blender.Registry.SetKey(ckey(ob),d,True)

The retrievemesh() function will take an object and a frame number as arguments. If it finds cached data for the given object and frame, it will assign the stored vertex coordinates to vertices in the mesh. We first define two new exceptions to indicate some specific error conditions retrievemesh() may encounter:

class NoSuchProperty(RuntimeError): pass;
class NoFrameCached(RuntimeError): pass;

retrievemesh() will raise the NoSuchProperty exception if the object has no associated cached mesh data and a NoFrameCached exception if the data is present but not for the indicated frame. The highlighted line in the next code deserves some attention. We fetch the associated mesh data of the object with mesh=True. This will yield a wrapped mesh, not a copy, so any vertex data we access or alter will refer to the actual data. Also, we encounter Python's built-in zip() function that will take two lists and returns a list consisting of tuples of two elements, one from each list. It effectively lets us traverse two lists in parallel. In our case, these lists are a list of vertices and a list of coordinates and we simply convert these coordinates to vectors and assign them to the co-attribute of each vertex:

def retrievemesh(ob,frame):
d=Blender.Registry.GetKey(ckey(ob),True)
if d == None:
raise NoSuchProperty("no property %s for object %s"
%(meshcache,ob.name))
try:
coords = d[str(frame)]
except KeyError:
raise NoFrameCached("frame %d not cached on object %s"
%(frame,ob.name))
for v,c in zip(ob.getData(mesh=True).verts,coords):
v.co = Blender.Mathutils.Vector(c)

To complete our set of cache functions we define a function clearcache() that will attempt to remove the registry data associated with our object. The try … except … clause will ensure that the absence of stored data is silently ignored:

def clearcache(ob):
try:
Blender.Registry.RemoveKey(ckey(ob))
except:
pass

Blender 2.49 Scripting Extend the power and flexibility of Blender with the help of the high-level, easy-to-learn scripting language, Python
Published: April 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

The user interface

Our script will not only be used as a script link associated with an object but it will also be used standalone (by pressing Alt + P in the text editor for example) to provide the user with the means to identify a target that will make the impression to clear the cache, and to associate the script link with the active object. If used in this fashion, it will present the end user with a few pop-up menus, both shown in the screenshots. The first one shows the possible actions:

The second screenshot shows the pop up offered to select an object from a list of Mesh objects that the user can choose to make an impression:

We first define a utility function that will be used by the pop-up menu that will present the user with a choice of Mesh objects to be used as a target to make an impression. getmeshobjects() will take a scene argument and will return a list of names of all Mesh objects. As depicted in the screenshot, the list of target objects includes the source object as well. Although this is legal, it is debatable whether this is very useful:

def getmeshobjects(scene):
return [ob.name for ob in scene.objects if ob.type=='Mesh']

The menu itself is implemented by the targetmenu() function defined as follows:

def targetmenu(ob):
meshobjects=getmeshobjects(Blender.Scene.GetCurrent())
menu='Select target%t|'+ "|".join(meshobjects)
ret = Blender.Draw.PupMenu(menu)
if ret>0:
try:
p = ob.getProperty(impresstarget)
p.setData(meshobjects[ret-1])
except:
ob.addProperty(impresstarget,meshobjects[ret-1])

It will fetch the list of all of the Mesh objects in the scene and present this list as a choice to the user by using Blender's Draw.PupMenu() function. If the user selects one of the menu entries (the return value will be positive and non-zero, see the highlighted line of the preceding code), it will store the name of this Mesh object as a property associated with our object. impresstarget is defined elsewhere as the name for the property. First, the code checks whether there already is such a property associated with the object by calling the getProperty() method and setting the properties data, if there is. If getProperty() raises an exception because the property does not yet exist, we then add the new property to the object and assign data to it with a single call to the addProperty() method.

The main user interface is defined in the top level of the script. It verifies that it is not running as a script link and then presents the user with a number of choices:

if not Blender.bylink:
ret = Blender.Draw.PupMenu('Impress scriptlink%t|Add/Replace' +
'scriptlink|Clear cache|Remove' +
'all|New Target')
active = Blender.Scene.GetCurrent().objects.active
if ret > 0:
clearcache(active)
if ret== 1:
active.clearScriptLinks([scriptname])
active.addScriptLink(scriptname,'FrameChanged')
targetmenu(active)
elif ret== 2:
pass
elif ret== 3:
active.removeProperty(meshcache)
active.clearScriptLinks([scriptname])
elif ret== 4:
targetmenu(active)

Any valid choice will clear the cache (highlighted) and the subsequent checks perform the necessary actions associated with each individual choice: Add/Replace scriptlink will remove the script link, if it is already present, to prevent duplicates and then add it to the active object. It then presents the target menu to select a Mesh object to use to make an impression. As we already cleared the cache, the second choice, Clear cache, will do nothing specific, so we just pass. Remove All will try to remove the cache and attempt to dissociate itself as a script link and the final New target menu will present the target selection menu to allow the user to select a new target object without removing any cached results.

If we are running as a script link we first check that we are acting on a FrameChanged event and then try to retrieve any stored vertex coordinates for the current frame (highlighted in the next code). If there is no previously stored data, we have to calculate the effects of the target object for this frame. We therefore get a list of target objects for the object under consideration by calling the utility function gettargetobjects() (for now, a list of just one object will be returned) and for each object we calculate the effect on our mesh by calling impress(). Then, we store these possibly changed vertex coordinates and update the display list so that the Blender GUI knows how to display our altered mesh:

elif Blender.event == 'FrameChanged':
try:
retrievemesh(Blender.link,Blender.Get('curframe'))
except Exception as e: # we catch anything
objects = gettargetobjects(Blender.link)
for ob in objects:
impress(Blender.link,ob)
storemesh(Blender.link,Blender.Get('curframe'))
Blender.link.makeDisplayList()

That leaves us with the actual calculation of the impression of a target object on our mesh.

Calculating an impression

When determining the effect of a target object making an impression, we will approach this as follows:

For each vertex in the mesh receiving the impression:

  1. Determine if it is located inside the target object and if so:
  2. Set the location of the vertex to the location of the closest vertex on the object making the impression

There are some important issues to address here. The location of a vertex in a mesh is stored relative to the object's transformation matrix. In other words, if we want to compare vertex coordinates in two different meshes, we have to transform each vertex by the transformation matrices of their respective objects before doing any comparison.

Also, a Blender.Mesh object has a pointInside() method that will return True if a given point is inside the mesh. This will, however, only work reliably on closed meshes so the user has to verify that the objects that will make the impression are in fact closed. (They may have interior bubbles but their surfaces must not contain edges that are not shared by exactly two faces. These so-called non-manifold edges can be selected in edge select mode by selecting Select | Non Manifold in the 3D view or pressing Ctrl + Shift + Alt + M.)

Finally, moving vertices to the closest vertex on the target object may be quite inaccurate when the target mesh is rather coarse. Performance wise, however, it is good to have relatively few points—as our algorithm is rather inefficient because by first determining whether a point is inside a mesh and then separately calculating the closest vertex duplicates a lot of calculations. However, as the performance is acceptable even for meshes consisting of hundreds of points, we stick with our approach, as it keeps our code simple and saves us having to write and test very intricate code.

The implementation starts with a function to return the distance to and the coordinates of the vertex closest to a given point pt:

def closest(me,pt):
min = None
vm = None
for v in me.verts:
d=(v.co-pt).length
if min == None or d<min:
min = d
vm = v.co
return min,vm

The impress() function itself takes a source and a target object as arguments and will modify the mesh data of the source object if the target mesh makes an impression. The first thing that it does is retrieve the transformation matrices of the objects. As indicated before, these will be needed to transform the coordinates of the vertices so that they might be compared. We also retrieve the inverse matrix of the source object. This will be needed to transform coordinates back to the space of the source object.

The highlighted line retrieves the wrapped mesh data of the source object. We need wrapped data because we might want to change some of the vertex coordinates. The next two lines retrieve copies of the mesh data. We also need copies because the transformation we will perform may not affect the actual mesh data. Instead of copying we could have left out the mesh=True argument, which would have given us a reference to an Nmesh object instead of a Mesh object. However, Nmesh objects are not wrapped and are marked as deprecated. Also, they lack the pointInside() method we need, so we opt for copying the meshes ourselves.

Next, we transform these mesh copies by their respective object transform matrices. Using the transform() method of these meshes saves us from iterating over each vertex and multiplying the vertex coordinates by the transform matrix ourselves, and this method is probably a bit faster as well as transform() is completely implemented in C:

from copy import copy
def impress(source,target):
srcmat=source.getMatrix()
srcinv=source.getInverseMatrix()
tgtmat=target.getMatrix()
orgsrc='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original=source.getData(mesh=True)
mesrc='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original=copy(source.getData(mesh=True))
metgt=copy(target.getData(mesh=True))
mesrc.transform(srcmat)
metgt.transform(tgtmat)
for v in mesrc.verts:
if metgt.pointInside(v.co):
d,pt = closest(metgt,v.co)
orgsrc.verts[v.index].co=pt*srcinv

The final part of the impress() function loops over all of the vertices in the transformed source mesh and checks if the vertex lies enclosed within the (transformed) target mesh. If they are, it determines which vertex on the target mesh is closest and sets the affected vertex in the original mesh to these coordinates.

This original mesh is not transformed, so we have to transform this closest point back to the object space of the source object by multiplying the coordinates with the inverse transformation matrix. Because transformation calculations are expensive, modifying the transformed mesh and transforming the complete mesh back at the end may take a considerate amount of time. Keeping a reference to the untransformed mesh and just transforming back individual points may, therefore, be preferable when only relatively few vertices are affected by the impression. The full script is available as ImpressScriptLink.py in scriptlinks.blend. The following illustration shows what is possible. Here we made a small animation of a ball (an icosphere) rolling along and descending into the mud (a subdivided plane).

When working with the script it is important to keep in mind that when the impression is calculated, none of the vertices of the mesh that receives the impression should be located inside the target before it moves. If that happens it is possible for a vertex to be swept along with the movement of the target, distorting the source mesh along the way. For example, to make the illustration of the wheel track in the mud, we animate a rolling wheel along a path, calculating the impressions it makes at every frame. In the first frame that we animate we should make sure that the wheel is not touching the floor plane that will be distorted because if a vertex of the floor plane is inside the wheel and close to the inner rim, it will be moved to the closest vertex on that rim. If the wheel rolls slowly, this vertex will stay close to that inner rim and will thereby be effectively glued to that moving inner rim, ripping up the floor plane in the process. The same disruptive process may occur if the target object is very small compared to the source mesh or moving very fast. In these circumstances a vertex may penetrate the target object so fast that the closest vertex will not be on the leading surface making the impression but somewhere else in the target which will result in vertices being pulled outward instead of pushed inward. In the illustration of the rolling tractor tire, we carefully positioned the tire at frame one to sit just to the right of the subdivided plane before we key framed the rolling motion towards the left. The picture shown is taken at frame 171 without any smoothing or materials applied to the plane.

Summary

In this article, we learned:

  • How to implement a scheme to associate a different mesh with an object on each frame
  • How to augment the functionality of the 3DView

If you have read this article you may be interested to view :


Blender 2.49 Scripting Extend the power and flexibility of Blender with the help of the high-level, easy-to-learn scripting language, Python
Published: April 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Michel J. Anders

Although trained as a chemist and physicist, Michel J Anders has mainly been employed as an IT manager working for Internet providers and IT consultancy firms and is currently working for a software development company focusing on correspondence and document production applications. He has been actively involved in providing advice to Blender scripters on public forums.

Books From Packt


Blender 3D 2.49 Incredible Machines
Blender 3D 2.49 Incredible Machines

SketchUp 7.1 for Architectural Visualization: Beginner's Guide
SketchUp 7.1 for Architectural Visualization: Beginner's Guide

3D Game Development with Microsoft Silverlight 3: Beginner's Guide
3D Game Development with Microsoft Silverlight 3: Beginner's Guide

Drupal Multimedia
Drupal Multimedia

Blender 3D Architecture, Buildings, and Scenery
Blender 3D Architecture, Buildings, and Scenery

Getting started with Audacity 1.3
Getting started with Audacity 1.3

Learning VirtualDub: The Complete Guide to Capturing, Processing and Encoding Digital Video
Learning VirtualDub: The Complete Guide to Capturing, Processing and Encoding Digital Video

Joomla! 1.5 Multimedia
Joomla! 1.5 Multimedia


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
V
n
L
W
r
b
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software