Hey @thomasb,
Thank you for the clarification. Yeah, this setup requires you modifying the cache. So, the slow performance version with disabling the optimization is the best you can do when approaching things in such brutish manner.
FYI: I do not have much time this week, so this is all the help you will get this week from me, but I am happy to help you next week if you still need help then.
Things you can do:
Turning off the optimization will calculate the cache every time Cinema 4D asks for it. Depending on how your blinking works, you might not have to calculate the cache every frame. Just determine when a new cache is needed and when not, as demonstrated in my first posting.
When in 99% of the cases 99% of your old invalid cache is still good, nothing prevents you from either caching expensive to compute parts yourself or modifying the existing cache and return that as the new one. Changing a selection state is such an example of where 99.9% of the expensive work is still valid.
It would be quite easy to do, when selection tags could reach into caches, but they cannot. So, you cannot have a selection tag on a generator (in Python) which indexes elements of the cache. But you can have a selection tag inside the cache which is referenced by for example a material on the generator holding the cache.
With this knowledge, you can:
Write a solution following (2.) where everything happens in the object, but in most cases, you just modify an existing cache instead of creating a new one.
Do the same, but here you use a tag to modify the cache. This is a little bit dicey,
as you should not mess with caches. But in this specific form, where we only change the selection state of a selection tag inside the cache, it should be okay. All other external cache modifications are off limits and can lead to crashes when you do not know what you are doing. In a nicer variant, you would implement the tag as a TagData plugin, but I provided a simple Python programming tag version below.
The shader solution is not viable in Python; it will be too slow. You will also need quite some math knowledge and reverse engineering skills, as you would have to sort of reimplement texture mapping.
PS: In your more complex setup, this could mean that you just change the materials on things. Although consolidating things inside caches is always advantageous. The more generators your cache contains, the more expensive it will be to evaluate the cache of your object. When possible, it is always better to return a single polygon object as your cache result, or at least a tree which contains only null objects and polygon objects, and no generator objects as the Sphere object, instance-objects, cloners, etc., i.e., things which must be cached themselves.
When the cache for an object is being built, all that stuff is converted to polygons anyway. But when you return one hundred instance objects which reference a sphere generator each, cinema will have to build 102 caches in total: one for your object, one for the sphere object, and one hundred for the instance objects. When you just return one polygon object which contains all the geometry, Cinema 4D must build only one cache. In Python this fact is a little bit mitigated by the slowness of Python, and it can be advantageous to push things to C++, but your cache is too complicated IMHO. Just construct your LED once, then build the cache for it, copy the cache thirty-five times, modify the position and material of each copy, and return these thirty-five copies under a null object as your object cache.
Cheers,
Ferdinand
File: led.c4d
Result:led_ani.gif
Python Generator object:
import c4d
op: c4d.BaseObject # The Python Generator object containing this code.
def main() -> c4d.BaseObject:
"""Returns a clone of the polygon object linked in its first user data field.
"""
source: c4d.PolygonObject = op[c4d.ID_USERDATA, 1]
if not isinstance(source, c4d.PolygonObject):
return c4d.BaseObject(c4d.Onull)
clone: c4d.PolygonObject = source.GetClone(c4d.COPYFLAGS_NO_HIERARCHY | c4d.COPYFLAGS_NO_BITS)
clone.SetMg(c4d.Matrix())
return clone
Python Programming tag:
import c4d
doc: c4d.documents.BaseDocument # The document evaluating this tag.
op: c4d.BaseTag # They Python Programming tag containing this code.
def main():
"""Reaches into the cache of its host object and modifies it.
"""
# Get the host object, its cache, and find the polygon selection tag on it.
obj: c4d.BaseObject = op.GetMain()
if not isinstance(obj, c4d.BaseObject):
return
cache: c4d.BaseObject = obj.GetCache()
if not isinstance(cache, c4d.PolygonObject):
return
tag: c4d.SelectionTag = cache.GetTag(c4d.Tpolygonselection)
if not isinstance(tag, c4d.SelectionTag):
return
# Get the current document frame and the selection of the tag and flush it. In practice you could
# also make this parameter driven, but for expressions, tags, it is also fine to make things
# automatic as such.
frame: int = doc.GetTime().GetFrame(doc.GetFps())
bs: c4d.BaseSelect = tag.GetBaseSelect()
bs.DeselectAll()
# Define the indices of 100 cap polygons, and pick the polygon which matches the current time.
states: list[int] = [n for n in range(4, 599, 6)]
i: int = states[frame % 100]
# Set the new selected element.
bs.Select(i)