2
$\begingroup$

I would like to use the position of an object ('driver object') as a parameter that interactively sets the positions of many vertices in a mesh. These positions are some nontrivial function of the position of the driver object, and so I want to use the power of python.

Here's what I did so far: I created an empty ('driven object') who's x position is set by a driver with a variable var that is set by the x position of the driver object. But the driver expression is set to update(var), where update is a python script that was registered in the driver namespace using bpy.app.driver_namespace.

The python script goes over the vertex positions of all vertices in a mesh and changes their position:

    ob = bpy.data.objects['graph_plane']
    for i in range(len(ob.data.vertices)):
        co = ob.data.vertices[i].co
        co[2]= co[1]*var/2 #that's just an example
        ob.data.vertices[i].co=co

My problem is that while the mesh is changed when the driver object is moved, the appearance of the object in the view remains the same. It only updates after I apply ob.data.update() to the object in the console after I finish moving the driver object, or if I select the object and go into edit mode. I tried to update the view within the script by inserting bpy.context.view_layer.update(), but then I get a "Dependency graph update requested during evaluation" error.

Any ideas? Thanks.

$\endgroup$

1 Answer 1

3
$\begingroup$

Use frame change handler

Drivers are designed, and best used, where the field you add a driver to is the value the driver controls. It seems the way you use a driver is add it to some field where you're not actually interested in the value returned from the custom function added to the driver namespace, but rather use the side effects. Since adding a custom function to a driver namespace requires you to run a script after opening the .blend file each time, with its downsides, you can as well use a standard technique with the same downsides, that is actually intended for such purposes. So instead of:

import bpy
from math import sin, cos, atan2

def test():
    ob = bpy.data.objects['Cube']
    for co in (v.co for v in ob.data.vertices):
        angle = atan2(co.y, co.x) + .05
        co.x = cos(angle) * 2**.5
        co.y = sin(angle) * 2**.5
        
bpy.app.driver_namespace['test'] = test

You can do:

import bpy
from math import sin, cos, atan2

def test(scene, depsgraph=None):
    ob = bpy.data.objects['Cube']
    for co in (v.co for v in ob.data.vertices):
        angle = atan2(co.y, co.x) + .05
        co.x = cos(angle) * 2**.5
        co.y = sin(angle) * 2**.5
        
listeners = bpy.app.handlers.frame_change_pre
listeners.clear()  # remove old handlers before adding updated versions
listeners.append(test)

(1 last line changes to 3 last lines; added arguments to the function so it's compatible with a listener interface)

Of course there may be better ways to approach your problem. For a cube or other simple meshes you could create a shape keys for each vertex (and for each axis perhaps) and then control the shape keys with drivers. This could all be set up with the Python script, that would only need to be run once, and then you would have a setup you can easily share without others needing to run a Python script or trust you.

And of course geometry nodes is a recent possibility that should work for something as simple as moving vertices.

If you don't want it for an animation

Here's another handler, that can be used without animation:

import bpy
from math import sin, cos, atan2
from bpy.types import SpaceView3D

last_value = None

def test():
    global last_value
    ob = bpy.data.objects['Cube']
    # if ob['prop'] == last_value:
    if ob.location.xyz == last_value:
        return
    for co in (v.co for v in ob.data.vertices):
        angle = atan2(co.y, co.x) + .05
        co.x = cos(angle) * 2**.5
        co.y = sin(angle) * 2**.5
    last_value = ob.location.xyz
    # last_value = ob['prop']

try:
    SpaceView3D.draw_handler_remove(SpaceView3D.my_handler, 'WINDOW')
except (AttributeError, ValueError):
    pass

SpaceView3D.my_handler = SpaceView3D.draw_handler_add(test, (), 'WINDOW', 'PRE_VIEW')

I added a custom property (with default name) to the object, and update the vertices only if it changes (though the position of vertices don't depend on it, they probably should... So e.g. angle = ob['prop']) - keep in mind you need to add a custom property to the object to make it work:

Of course you don't need to use a custom property, and as visible in Jhon's Paul edit, you can instead use something like location...

You could also use Message Bus but I often find it problematic (e.g. with custom properties).

$\endgroup$
5
  • $\begingroup$ Thanks for your clear and detailed answer. I was in fact aware of the frame change handler, however assuming I understand it correctly, it is not helpful in my case. It's not animating that I'm after but interaction: I want a simple way to physically change a parameter and interactively see my 3d graph change in the viewport. If only there was a handler that is triggered when a specific object is moved or an attribute is changed.. $\endgroup$
    – John Paul
    Dec 25, 2021 at 0:14
  • $\begingroup$ This does work btw, if one just lets the animation run indefinitely, but it feels contrived as a solution and it makes the cpu run continuously because the update function runs all the time... $\endgroup$
    – John Paul
    Dec 25, 2021 at 1:20
  • $\begingroup$ @JhonPaul see edit. $\endgroup$ Dec 25, 2021 at 1:51
  • $\begingroup$ Wow thanks, this works, and I would never have found it by myself! I did search for blender handlers but I don't think it ever came up. Only issue is it doesn't recognize ob['prop'], so instead I saved list(ob.location) in last value. Maybe just ob.location would have sufficed, I settled for the first thing that worked. Oh, and in the try, WINDOW should be in parenthesis. I'm a newbie, not sure if I can edit the answer myself, will try. $\endgroup$
    – John Paul
    Dec 25, 2021 at 16:44
  • 1
    $\begingroup$ @JhonPaul yea, sorry for unquoted WINDOW, as for converting the location to a list, I think using .xyz is more explicit, also a .copy() could be used to get a Vector instead of a tuple of coordinates. $\endgroup$ Dec 25, 2021 at 17:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .