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:
- 50 % power (one frame)
- 100% power (one frame)
- 33% power (one frame)
- 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:


#blink_longer.time = 0 # avoid a global to track blinking- I imagine this nonsense was generated by GPT? – Markus von Broady Aug 19 '23 at 13:23