This answer targets a beginners level and will include lots of lengthy explanations.
I understand that this answer doesn't utilize drivers as requested, but for this example a simple python script is very easy to produce and maintain, which is why I wanted to add this method.
A slight problem is, that a change of position implies a change of viewing angle with perspective cameras. In the next image there is no difference between both objects orientation, but through the camera, the objects appear clearly at an angle.
I would use a script which check's the camera's and object's transformations on each scene update.
Let's start with a condensed example. Create a camera Camera and a mesh Cube. Then add the Basis shapekey and a Key 1 key, very similiar to the setup in the question.
Paste the following python script in the text editor and click the Run Script button. I have made an effort to document it well.
# import the bpy module, with it we can access the data of the blend file
import bpy
# bpy.context.scene stores the active scene.
# The scene stores a collection of objects, from which
# objects can be accessed directly with their name.
# Get the Camera and Cube by their names and store them
# in "cam" and "obj" variables respectively
cam = bpy.context.scene.objects['Camera']
obj = bpy.context.scene.objects['Cube']
# define a method which will get called on every scene_update
def drive_shapekey(scene):
# store the transformed obj.matrix in the obj_mat variable
obj_mat = cam.matrix_world.inverted() * obj.matrix_world
# print it to the blender console for possible inspection
print(obj_mat)
# get the location, rotation and scale from the matrix
obj_loc, obj_rot, obj_sca = obj_mat.decompose()
# access the shapekey "Key 1", note that there has to be a
# shapekey with that exact name
key1 = obj.data.shape_keys.key_blocks["Key 1"]
# access the "Key 1" shapekey value
# here, we assign the absolute value of the objects x location
# transformed by the inverted camera matrix (!)
key1.value = abs(obj_loc.x)
# We have to store the method in the scene_update_pre collection.
# First, loop through the collection and possibly remove other
# methods, then append our "drive_shapekey" method. This method
# will then get called on each scene_update_pre cycle.
pre_handlers = bpy.app.handlers.scene_update_pre
[pre_handlers.remove(h) for h in pre_handlers]
pre_handlers.append(drive_shapekey)
One of the more complex lines is
obj_mat = cam.matrix_world.inverted() * obj.matrix_world
The matrix_world
is the global transformation of an object (global meaning with parenting already applied). If a location Vector would get multiplied by an object's matrix it will appear is if moved as a child of the object. By multiplying the object's matrix with the camera's inverted matrix, we get the object's matrix in camera space. This may be a confusing concept. The following gif demonstrates multiplying a position vector with the camera matrix or the inverted camera matrix.
The result of the above script is an interactive handler, which update the shapekey value, based on the relative position between the object and the camera.
In the next step, we'll chose the value, which we want to influence the shapekey: the difference between camera and object orientation. Once we have the object's matrix in camera space, we can ignore whatever values were on the camera, since they have been transfered to the object. Only change the code of the drive_shapekey
method and leave the other lines intact. In this sample, we use the rotation value of the transformed object matrix by compared a vector with its rotated counterpart.
def drive_shapekey(scene):
obj_mat = cam.matrix_world.inverted() * obj.matrix_world
eul = obj_mat.to_euler('ZYX')
vec_a = mathutils.Vector((0.0, 0.0, 1.0))
vec_b = vec_a.copy()
vec_b.rotate(eul)
difference = vec_a.dot(vec_b)
key1 = obj.data.shape_keys.key_blocks["Key 1"]
key1.value = difference
Result of the change in the script.
Now we have to fix the rotational offset of the camera. The shapekey should extend if the camera faces the blue face. We'll compare the (transformed) vectors which we want to align. At the top of the script import the math module as well.
import math
Then turn on the axes on the objects, and choose two axes which you want to align. In my case it's the camera's negative Z (0, 0, -1) and the object's positive Y (0, 1, 0).
def drive_shapekey(scene):
obj_mat = cam.matrix_world.inverted() * obj.matrix_world
obj_eul = obj_mat.to_euler('XYZ')
cam_vec = mathutils.Vector((0.0, 0.0, -1.0))
obj_vec = mathutils.Vector((0.0, 1.0, 0.0))
obj_vec.rotate(obj_eul)
difference = cam_vec.dot(obj_vec)
key1 = obj.data.shape_keys.key_blocks["Key 1"]
key1.value = difference
The difference in initial orientation has been taken into account.
Next we are going to add the positional relation to the script. Previously, we compare a camera related vector, but now we need to compare a camera-object relation vector (on the right side).
In our case, the needed vector pointing from the camera to the object is simply the object's location (normalized). This is because the camera is at (0, 0, 0), since we earlier applied the camera's world matrix to the object and we're in camera-relative space from then on.
Now, the dot product is only 1 if the object is actually facing the camera from any position.
The completed script.
import bpy
import mathutils
cam = bpy.context.scene.objects['Camera']
obj = bpy.context.scene.objects['Cube']
def drive_shapekey(scene):
obj_mat = cam.matrix_world.inverted() * obj.matrix_world
obj_eul = obj_mat.to_euler('XYZ')
cam_vec = obj_mat.to_translation().normalized()
obj_vec = mathutils.Vector((0.0, 1.0, 0.0))
obj_vec.rotate(obj_eul)
difference = cam_vec.dot(obj_vec)
key1 = obj.data.shape_keys.key_blocks["Key 1"]
key1.value = difference
pre_handlers = bpy.app.handlers.scene_update_pre
[pre_handlers.remove(h) for h in pre_handlers if h.__name__ == "drive_shapekey"]
pre_handlers.append(drive_shapekey)
As suggest in the comments by batFINGER: Checking for the name in the second line from the end will prevent other handler to get removed.
There seems to be the need to control shapekey on multiple object (as seen in the question's edit). This isn't that difficult if the parameters are properly understood.
The lookup up name in the key_blocks 'Key 1'
is simply the name of the shapekey on the object. If your shapekey has a different name, the code should be changed accordingly.
The next codeblock will update the cameraFacingKey shapekey on multiple objects: Cube and otherCube using a single method. It relies on both of them having the same shapekey name. The import
and handler
statements shouldn't be changed.
This code creates python list with a list comprehension. In the drive_shapekey(scene)
method it loops over all objects in the objs
list.
cam = bpy.context.scene.objects['Camera']
obj_names = ['Cube', 'otherCube']
objs = [bpy.context.scene.objects[name] for name in obj_names]
def drive_shapekey(scene):
for obj in objs:
obj_mat = cam.matrix_world.inverted() * obj.matrix_world
obj_eul = obj_mat.to_euler('XYZ')
cam_vec = obj_mat.to_translation().normalized()
obj_vec = mathutils.Vector((0.0, 1.0, 0.0))
obj_vec.rotate(obj_eul)
difference = cam_vec.dot(obj_vec)
key1 = obj.data.shape_keys.key_blocks["cameraFacingKey"]
key1.value = difference