3
$\begingroup$

I have this blinking driver. I want to use it on multiple characters during a scene except they blink at the same time and interfere with each other.

import bpy
from mathutils import noise

def blink_longer():

    # still blinking
    if blink_longer.time == 3:
        blink_longer.time -= 1
        return .5
    # new blink?
    elif blink_longer.time == 2:
        blink_longer.time -= 1
        return 1
    elif blink_longer.time == 1:
        blink_longer.time -= 1
        return .33
    elif noise.random() > 0.97:
        blink_longer.time = 3 # max blink time
        return 0
    # not blinking
    else:
        return 0

blink_longer.time = 0 

#blink_longer.time = 0  # avoid a global to track blinking

# add blink function to the driver namespace
bpy.app.driver_namespace['blink_longer'] = blink_longer

I tried using a class where each class sets their own seed to the noise.seed_set() method. But then they don't blink because I believe this class.method combo is called each frame so it's always setting the seed and getting the first result.

Is there some way to call or generate noise independently for each object using the driver?

$\endgroup$
4
  • $\begingroup$ can you share a simple blend file blend-exchange where they all blink at the same time, just a very basic setup. $\endgroup$ Aug 19, 2023 at 12:45
  • 1
    $\begingroup$ #blink_longer.time = 0 # avoid a global to track blinking - I imagine this nonsense was generated by GPT? 😩 $\endgroup$ Aug 19, 2023 at 13:23
  • $\begingroup$ you could e.g. use parts of the id of the object as seed. or - if you really wanna make sure they blink all on different times, you could make a class with a class variable list, which provides always "new" numbers for each object who "asks" $\endgroup$
    – Chris
    Aug 19, 2023 at 13:26
  • $\begingroup$ @HarryMcKenzie I have added the file to the post $\endgroup$
    – TheJeran
    Aug 19, 2023 at 14:50

2 Answers 2

5
$\begingroup$

X Problem

# avoid a global to track blinking comment is on point. Your problem comes from the fact, that all drivers use the same function, and each function call uses the same variable. While an attribute of a function is not a global, the function itself is in the global namespace, and so your current solution doesn't actually deal with the problem mentioned in the comment.

Moreover, even though the driver users are synchronized, each time you render your animation, it will render differently, because your RNG (random number generator) uses a default, time based seed (and you will render your project at different times, so with different seeds). This is a problem if you want to e.g. re-render your project and mix with old renders to improve sample counts. Similarly if you want to re-render just one particular frame, with your current approach, the state wouldn't match the rest of the frames.

The first problem is already solved by Chris; you don't need a class for it, just a global dictionary will do. The 2nd problem could be solved by using Geometry Nodes + a simulation zone, to maintain the state consistently… This consistency behavior can be reproduced in drivers alone, but it would require you to either:

  • render the entire animation every time (so you can render the animation again for the purpose of mixing with more samples)
  • use per-frame cache like I do here: Distributed interaction visualization, play the animation once (just in viewport, 1 sample, eevee…), and then render the frame range of choice
  • after evaluating the per-frame cache, save it in some way (e.g. in a text block), so you don't need to reevaluate it again…

So here's the 1st, simplest approach:

import bpy
from random import random, seed
from collections import defaultdict

time_history = defaultdict(int)


def random_per_object_and_frame(name, frame):
    seed(f"{name}\t{frame}")
    return random()


def blink_longer(self, frame):
#    return 0
    name = self.name
    # still blinking
    if time_history[name] == 3:
        time_history[name] -= 1
        return .5
    # new blink?
    elif time_history[name] == 2:
        time_history[name] -= 1
        return 1
    elif time_history[name] == 1:
        time_history[name] -= 1
        return .33
    elif random_per_object_and_frame(name, frame) > 0.97:
        time_history[name] = 3 # max blink time
    return 0
    
    
bpy.app.driver_namespace['blink_longer'] = blink_longer

Driving $z$ positions using blink_longer(self, frame) (you need to enable "Self"!)

  • when using this in a material, self will be the material, and if all lights share the same material, they will share the same name! To deal with this, use a custom property on an object, drive that, and in the material use "Attribute" node in "object" mode, and the property name (e.g. prop by default)
  • you need to set the first frame, then rerun the script to reset the history, before rendering your animation if you want consistent results
  • I used the random.random instead of mathutils.noise in order to be able to set a string as seed and so implement the randomness per object, as "Random per Object" works in shaders
  • Notice how using the frame for a seed avoid the need to introduce a history of seeds for each object, which would look this way:
rng_history = {}
def random_per_object(name):
    if name in rng_history:
        seed(rng_history[name])
    else:
        seed(name)
    value = random()
    rng_history[name] = value
    return value

Related:

How to randomize any value every frame between specific interval?

Y Problem

You could achieve a similar effect with a much simpler logic, that wouldn't involve the mentioned inconsistency problems. To my understanding, you want an animation of blinking:

  1. 50 % power (one frame)
  2. 100% power (one frame)
  3. 33% power (one frame)
  4. 0% power (some number of frames, until the uniform random in range $[0,1)$ becomes higher than $0.97$

The above description of your problem would be the ideal way to ask your question, instead of focusing on the implementation details that you've chosen. See the Wikipedia page about the XY problem

Ad 4: on each frame you have 3% chance to go from stage 4 to stage 1 again. We can use this simple code to quickly find out it's going to be 33 frames on average:

from random import random
results = []
for i in range(10000):
    j = 0
    while True:
        j += 1
        if random() > 0.97:
            break
    results.append(j)
print(sum(results)/len(results))

So 33 frames + 3 frames of stages 1-3 gives a total of 36 frames. let's then divide the current frame number by 36, and floor it, giving us the same value for each zone of 36 frames:

$$y\ =\operatorname{floor}\left(\frac{x}{36}\right)$$

https://www.desmos.com/calculator

Or we can multiply it by 36 again to get the starting frame of the range (and it still works as a seed):

$$y\ =\operatorname{floor}\left(\frac{x}{36}\right)\cdot36$$

Now we can use this number as a seed to figure out where in this zone the blink will happen:

import bpy
from math import floor
from random import seed, randint


def blink_longer(self, frame):
    name = self.name
    start_min = floor(frame/36)*36
    seed(f"{name}\t{start_min}")
    start = start_min + randint(0, 33)
    mapping = {start: .5, start+1: 1, start+2: .33}
    return mapping.get(frame, 0)
    
    
bpy.app.driver_namespace['blink_longer'] = blink_longer

This algorithm can be reproduced in a shader, and the only driver you need is simple #frame:

$\endgroup$
3
  • $\begingroup$ Man. Do you get immense satisfaction for the quality and effort you put in and the way you help people? Cause this is incredible. Only caveat I have to say is that the driver inherits the name from the shapekey. So if the two objects have the same name for their shapekey they aren't independent. But wow... thank you so much. $\endgroup$
    – TheJeran
    Aug 19, 2023 at 16:45
  • $\begingroup$ @TheJeran it all depends where you put the driver. In my example the driver is run in the context of the object, so self means the object, and self.name is the object's name. But as mentioned, in case of a material self is the material, which might be shared, and I proposed a solution - didn't think about shape keys, though; that one is tricky and probably justifies another question. As for my motivation - when I write a simple answer, I learn nothing, and in a big answer in the very least I can exercise the Feynman Technique $\endgroup$ Aug 19, 2023 at 17:26
  • $\begingroup$ @TheJeran Haven't you thought of simply putting a different number in each driver as a seed? $\endgroup$ Aug 19, 2023 at 17:29
2
$\begingroup$
import bpy

class SeedList:
    
    seed_list = {}
        

    def getSeedFor(self, obj):  
        
        if self.seed_list.get(obj) != None:
            return self.seed_list[obj]
        
        self.seed_list[obj] = len(self.seed_list)
        
        return self.seed_list[obj]
        
print (SeedList().getSeedFor(bpy.data.objects["Cube"]))
print (SeedList().getSeedFor(bpy.data.objects["Cube.001"]))
print (SeedList().getSeedFor(bpy.data.objects["Cube.002"]))
print (SeedList().getSeedFor(bpy.data.objects["Cube"]))

So this little code snippet gives you a different integer for each object. You can use

SeedList().getSeedFor(obj)

to get a different integer for each object.

Hope that helps.

Result for the test prints:

0
1
2
0
$\endgroup$
2
  • 1
    $\begingroup$ How thoroughly have you tested it? You're using python object wrappers as keys, but such wrappers are short-lived, so I'm not sure what happens… it's hashed, so perhaps the == operator works, even if old wrappers get invalidated. I think it's safer to use object names instead (this will consider a new object given name of old object as that old object, but you may want that, and it's consistent with the behavior of "random per object" in shader) $\endgroup$ Aug 19, 2023 at 13:56
  • $\begingroup$ i didn't test that very much. Just added some objects and got different and same numbers. You might be right, that using object names are more safe. $\endgroup$
    – Chris
    Aug 19, 2023 at 13:58

You must log in to answer this question.

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