DescID and DescLevel for Shader Layers and properties
-
So, I am working on a couple things dealing with layer shaders from a Shader Field.
1.) Adding ports to an Object Xpresso Operator with the Shader as the linked object.
2.) Adding keyframes to an animation track for the Gradient in a Gradient Layer of the Shader.I have had no issue creating XPresso nodes and ports in general. As an example, here is a snippet of how I add ports for object operators:
ObjectNode = NodeMaster.CreateNode(Root,c4d.ID_OPERATOR_OBJECT, insert=None, x=700, y=850) ObjectNode.SetParameter(c4d.DescID(c4d.GV_OBJECT_OBJECT_ID), Null, c4d.DESCFLAGS_SET_NONE) ObjectNodeIn = ObjectNode.AddPort(c4d.GV_PORT_INPUT, c4d.DescID(c4d.DescLevel(c4d.ID_BASEOBJECT_GLOBAL_POSITION)), flag=c4d.GV_PORT_FLAG_IS_VISIBLE, message=False)
I can add an object operator, and link the shader to the object operator, but I am having trouble creating the ports using the DescID and DescLevels, simply because I’m not sure what IDs to use in the DescLevel to access the parameters (as shown in the image below).
Similarly, I have no problems creating tracks and keys for animated parameters, so long as I know the DescID and DescLevels for those parameters. I have combed through the forum and SDKs, and can’t seem to find how to access the shader layer parameters through this method. I also have no problem creating Shader Layers and accessing their parameters after instantiation, but not how to reference them through the DescLevel method.
Here is an example of how I set key and tracks normally:
pid_pmix = c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0), c4d.DescLevel(4)) mixtrack = c4d.CTrack(Xpresso, pid_pmix) Xpresso.InsertTrackSorted(mixtrack) defkey, dub = doc.GetDefaultKey() mixcurve = mixtrack.GetCurve() mixval1 = 0 mixkey1 = defkey.GetClone() mixkey1.SetValue(mixcurve, mixval1) mixkey1.SetTime(mixcurve, c4d.BaseTime(46/20)) mixcurve.InsertKey(mixkey1, True
As mentioned above, the only problem I’m having is finding and accessing the DescLevel IDs for the shader layers and their properties for these two scenarios. I am currently working on C4D 2025.2.0. Any advice or direction would be greatly appreciated. Thank you.
-
Hey @alcol68,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Getting Started
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
- Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
- Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
- Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: How to Ask Questions.
About your First Question
Your question is a little bit ambiguous in what
DescID
you are looking for a layer shader, you probably mean the properties of one of its children and not the layer shader itself. I would recommend to have a look at BaseShader: Access and Structure (C++) to understand how shaders are nested. You can then check out this posting for how to manipulate a layer shader in Python where I also documented its more fringe parameters.Cheers,
Ferdinand -
I have previously looked at the “How to Create and Populate a Layer Shader” post. The shader that I am referencing above, I can easily create using those same methods. Below is the code for creating the Shader I am working with:
Field1 = doc.SearchObject("Shader Field") Shader = c4d.LayerShader() ShaderLayer1 = Shader.AddLayer(c4d.TypeShader) GradientShader1 = c4d.BaseList2D(c4d.Xgradient) GradientShader1[c4d.ID_BASELIST_NAME] = "Gradient Circular" GradientShader1[c4d.SLA_GRADIENT_TYPE] = c4d.SLA_GRADIENT_TYPE_2D_CIRC GradientShader1[c4d.SLA_GRADIENT_CYCLE] = False GradientShader1[c4d.SLA_GRADIENT_ANGLE] = 3.14159 Gradient1 = GradientShader1[c4d.SLA_GRADIENT_GRADIENT] Gradient1.FlushKnots() Gradient1.InsertKnot(c4d.Vector(0,0,0), 1, 0, 0.5, 0) Gradient1.InsertKnot(c4d.Vector(0,0,0), 1, 1, 0.5, 1) GradientShader1[c4d.SLA_GRADIENT_GRADIENT] = Gradient1 GradientShader1.InsertUnder(Shader) ShaderLayer1.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, GradientShader1) ShaderLayer2 = Shader.AddLayer(c4d.TypeShader) GradientShader2 = c4d.BaseList2D(c4d.Xgradient) GradientShader2[c4d.ID_BASELIST_NAME] = "Gradient U" GradientShader2[c4d.SLA_GRADIENT_CYCLE] = False GradientShader2[c4d.SLA_GRADIENT_ANGLE] = 3.14159 Gradient2 = GradientShader1[c4d.SLA_GRADIENT_GRADIENT] Gradient2.FlushKnots() Gradient2.InsertKnot(c4d.Vector(0,0,0), 1, 0.125, 0.5, 0) Gradient2.InsertKnot(c4d.Vector(1,1,1), 1, 1, 0.5, 1) GradientShader2[c4d.SLA_GRADIENT_GRADIENT] = Gradient2 GradientShader2.InsertUnder(Shader) ShaderLayer2.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, GradientShader2) ShaderLayer3 = Shader.AddLayer(c4d.TypeShader) NoiseShader = c4d.BaseList2D(c4d.Xnoise) NoiseShader[c4d.SLA_NOISE_COLOR1] = c4d.Vector(1,1,1) NoiseShader[c4d.SLA_NOISE_COLOR2] = c4d.Vector(0,0,0) NoiseShader[c4d.SLA_NOISE_SEED] = 666 NoiseShader[c4d.SLA_NOISE_GLOBAL_SCALE] = 2 NoiseShader[c4d.SLA_NOISE_ANI_SPEED] = 1 NoiseShader[c4d.SLA_NOISE_CONTRAST] = 1 NoiseShader.InsertUnder(Shader) ShaderLayer3.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, NoiseShader) ShaderLayer3.SetParameter(c4d.LAYER_S_PARAM_SHADER_MODE, 11) ShaderLayer4 = Shader.AddLayer(c4d.TypeTransform) ShaderLayer4.SetParameter(c4d.LAYER_S_PARAM_TRANS_ANGLE, 3.14159) Field1.InsertShader(Shader) Field1[c4d.ID_MG_SHADER_SHADER] = Shader
Using the port-creation example I gave before, for an object operator, I use the DescLevel to get the ID of the property I want to create a port for. In the above example, I use the integer “c4d.ID_BASEOBJECT_GLOBAL_POSITION.” Then if I want to use the specific Z parameter, I use the second level integer of “c4d.VECTOR_Z.”
I assume there is a similar level system to grab the angle parameter of the Transform layer in my Shader. The Shader itself is linked to the object operator, so accessing its layers should be the first level.
ObjectNode.AddPort(c4d.GV_PORT_INPUT, c4d.DescID(c4d.DescLevel(c4d.SLA_LAYER_BLEND), c4d.DescLevel(c4d.LAYER_S_PARAM_TRANS_ANGLE)), flag=c4d.GV_PORT_FLAG_IS_VISIBLE, message=False)
If I use just the single level integer of “c4d. LAYER_S_PARAM_TRANS_ANGLE” the port is not created. So, I assume there is another level above this one that needs to be called as well, namely the actual Transform layer itself. But I don’t know how to call that layer as an integer in the DescLevel method, because I cannot pass an object (“ShaderLayer4” from my above creation code) in the integer parameter of DescLevel. I used the placeholder of “c4d.SLA_LAYER_BLEND” as that first level, but it creates an empty port, not the angle parameter port. Using “c4d.TypeTransform” or “c4d. LAYER_S_PARAM_ALL_ACTIVE” do not work either.
This is what I am trying to figure out: What is the integer that I need to use in order to access the Transform Layer itself to use in the DescLevel method, to use in the AddPort method? Unless there is a different way to add a port, I cannot find any information on this particular issue.
-
I guess another way to do this is to create a Python node, rather than an object node, and access the Shader Layer parameters that way.
-
Hey @alcol68,
We cannot always answer right away.
A layer shader stores its layer in a data type unexposed to the Python and public C++ API (
BlendDataType
) in its parameterSLA_LAYER_BLEND
. Which is why this abstraction withLayerShaderLayer
exists.Your code went into the right direction, as you probably have to use id decomposition (
(param, sub_channel)
) when you want to access such value without the layer abstraction. The issue with your code is that you do not take into account that this must be a dynamic description, i.e., you have to encode which layer you mean.I currently do not have much time due to approaching releases and would probably only find time next week to have a closer look. Here is how the data type reroutes parameter set events internally. As you can see, it encodes both the layer and the item into a desc level. How this works exactly and if this translates to Python, I would have to find out myself.
static const Int32 BLEND_DATA_OFFSET = 10000; static const Int32 BLEND_DATA_STEP_SIZE = 20; Bool Foo::GetParameter(...) { iBlendDataType *s = (iBlendDataType*)data; if (!s) return false; Int32 lID = id[0].id; lID -= BLEND_DATA_OFFSET; DescID idNew = id << 1; // push out id[0] Int32 lItem = lID % BLEND_DATA_STEP_SIZE; Int32 lLayer = lID / BLEND_DATA_STEP_SIZE; //BlendLayer is what you know as LayerShaderLayer BlendLayer* pLayer = s->m_BlendLayers.SearchLayer(lLayer); if (!pLayer) return false; pLayer->GetParameter(lItem, idNew, t_data, flags); return CustomDataTypeClass::GetParameter(data, id, t_data, flags); }
Cheers,
Ferdinand -
I appreciate your help, Ferdinand. I'm in no rush for this. I am now going through the C++ header files to try to get a better understanding of this and the code you posted.
I saw an older post where you noted that A LayerShaderLayer is not a scene element, but stored in the data container of a LayerShader. If that is true, is there a way to return a list of all the data stored within that LayerShader data container, like you can with the "GetUserDataContainer" function?
-
To slightly answer my own, most recent question and to complete my second initial question about adding keyframes to an animation track for the Gradient in a Gradient Layer of the Shader:
As shown in the picture below, if you right-click on the “Layer” parameter of the Shader, navigate over User Interface, and click ‘Show Subchannels,” you can then see all the parameters of the Shader Layers. You can then drag the Transform Angle parameter into the console and get its description ID. Doing this, I was able to get that the dynamic DescID of the Transform Angle parameter (like you showed before): “Layer[c4d.SLA_LAYER_BLEND, 10120].”
Putting this into the correct DescID format, I can create an animation track for any of the Layer parameters. I can create an animation track for this specific Transform Angle parameter with the above DescID.c4d.DescID(c4d.DescLevel(c4d.SLA_LAYER_BLEND), c4d.DescLevel(10120, c4d.DTYPE_REAL))
HOWEVER, when I use this same DescID to add a port into the Layer/Object operator that I am working with, an empty port is created, not the Transform Angle port I want. But, if I go to manually add the correct port I want on this same operator, it is renamed to “Color Profile” and greyed out, as if it is currently being used (shown in the picture below). So, there is something missing in how I’m adding this port, either another level or a declared type with the first “c4d.SLA_LAYER_BLEND” level. I’m not sure what exactly is incorrect or missing with the adding of this port.
We are getting closer to the answer, though. -
Hey @alcol68,
sorry, I have not forgotten you, but I was busy with the fall release. I will have a look tomorrow. What you are doing
c4d.DescLevel(10120, ...)
(i.e.,BLEND_DATA_OFFSET
+ X) is the right direction.Xpresso is an unholy hellhole when it comes to its operator IDs and things can fail when you are not ultra precise in your
DescID
definitions (yourDescLevel
is there for example missing the creator, and Xpresso can be picky about that - everywhere else the creator usually does not matter).Cheers,
Ferdinand -
hahaha I apreciate your help. Yea this has been wild trying to figure this out, but the creator id note gives me even more direction. Thank you.
-
Hey @alcol68,
so, I spent some time with this today, and both the Xpresso and Layer Shader API drive me nuts every time I have to deal with them. The TLDR is here that both Xpresso and the Layer Shader are irregular in their implementations, especially in regards to how they store and access parameters.
Find below some narrative code which guides you through all of this.
Cheers,
FerdinandResult
The setup expects a material to be selected which has a layer shader in its color channel with a singular transform layer in it. It will then create this Xpresso setup which drives the angle of the transform layer with a user data field in the null object holding the Xpresso setup.
edit: lol, now I see that also the first example is working in my own screenshot. I have absolutely no idea why. I would still stay away from this. The layer shader internals are a mess.
Code
"""Explores concepts around driving layer shader parameters from XPresso. This script assumes that the active material has a layer shader in its color channel. As explained below, this code assumes the ID of the first parameter of the first layer in the shader, which does not seem to be entirely deterministic. When push comes to shove, you might have to comment out #Solution.InternalLayerShaderAccess(...) in #Solution.main() when it throws errors on your system. """ import c4d import mxutils from c4d.modules.graphview import GvNodeMaster, GvNode, GvPort, XPressoTag from mxutils import CheckType doc: c4d.documents.BaseDocument # The currently active document. op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`. class Solution: """Provides a dummy class so that we can organize our code in a more readable way. """ @staticmethod def main() -> None: """Called by Cinema 4D when the script is being executed. """ # Get the first material and get its color channel shader, assuming it is a layer shader. material: c4d.BaseMaterial = CheckType(doc.GetActiveMaterial()) shader: c4d.BaseShader = CheckType(material[c4d.MATERIAL_COLOR_SHADER], c4d.LayerShader) # Create the XPresso setup used by both examples below and also already add the user data # field to the null object used by the second example. null: c4d.BaseObject = CheckType(c4d.BaseObject(c4d.Onull)) doc.InsertObject(null) # Create the "Angle" user data field on the null object. bc: c4d.BaseContainer = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL) bc[c4d.DESC_NAME] = "Angle" bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_DEGREE null.AddUserData(bc) null[c4d.ID_USERDATA, 1] = c4d.utils.DegToRad(42.0) # Initial value. # Create the XPresso setup. tag: XPressoTag = CheckType(null.MakeTag(c4d.Texpresso)) master: GvNodeMaster = CheckType(tag.GetNodeMaster()) # Call this first example, you might have to comment this out, as it relies on the discussed # magic numbers. Solution.InternalLayerShaderAccess(shader, master) # Call the second example, which uses a Python node to access the layer shader. This will # always work. Solution.PythonNodeLayerShaderAccess(shader, null, master) c4d.EventAdd() @staticmethod def InternalLayerShaderAccess( shader: c4d.BaseShader, master: GvNodeMaster) -> None: """Explores the internal structure of the layer shader data type and how it interfaces with XPresso. Args: shader: The layer shader to inspect. master: The node master of the XPresso tag. """ # You claimed on the forum that this ID worked for you for animating the transform angle of # a layer shader. # # c4d.DescID(c4d.DescLevel(c4d.SLA_LAYER_BLEND), c4d.DescLevel(10120, c4d.DTYPE_REAL)) # Even when we ignore the concrete numeric offset and replace it with our own, it still does # not work for me. Could it be that you used abstracted GeListNode.__set/getitem__ access # instead, i.e., what the console drag and drop generates? Because there this works for me. print(shader[c4d.SLA_LAYER_BLEND, 10060]) # The problem with these IDs is that they are not deterministic from a public API point of # view. What holds true, is that data will placed with a stride of 20 (i.e., with # BLEND_DATA_STEP_SIZE) and starts in theory at 10000 (BLEND_DATA_OFFSET). # # But the elements in a layer then do not follow the symbols exposed in the C++ API for the # layer abstraction but are rather just sequentially numbered on a per layer type basis. E.g. # for the transform layer, a parameter set event looks like this: # # void BlendEffectTransform::SetParameter(Int32 lItem, const DescID &id, ...) # { # if (lItem == 0) # { # m_rAngle = t_data.GetFloat(); # flags |= DESCFLAGS_SET::PARAM_SET; # } # else if (lItem == 1) # { # m_bMirror = t_data.GetInt32() != 0; # flags |= DESCFLAGS_SET::PARAM_SET; # } # ... # # Where in the API the angle has actually the id LAYER_S_PARAM_TRANS_ANGLE (2000) and the # mirror has the id LAYER_S_PARAM_TRANS_MIRROR (2001). So, the internal values 0 and 1 are # hardcoded and absolutely ignore the public API symbols. We can of course make here the # observation that PUBLIC_SYMBOL - 2000 == INTERNAL_INDEX, but there is no guarantee at all # that this will always hold true. # # The bigger problem is that the the layer start index seems to be somewhat arbitrary. You # normally would expect that the first layer starts at 10000 (because BLEND_DATA_OFFSET and # that is what the code suggests), but this does not hold true. When I added a single # transform layer, its angle parameter had most of the time the ID 10060, i.e., this first # layer seems to start at what would be the third slot. Once I have also seen it starting at # 10040 without having a good explanation why. # So, internally, the layer shader is a bit irregular, or at least more complex than it looks # like. But let's continue for now and accept these magic numbers. # To more formally define the DescID for this parameter, we need to define its DescID. For # that we need the data type of SLA_LAYER_BLEND. It is not exposed as a symbol in the public # API (but you could look up its numeric value in the description of a layer shader). So, we # define it here. CUSTOMDATA_BLEND_LIST: int = 1011131 # Now we can define the DescID for the transform angle parameter of the first layer. This # assumes that it uses the magic number 10060 for the first property of the first layer and # that it is of type float. If it is a transform layer or not does not really matter. did: c4d.DescID = c4d.DescID( # First desc level is the datatype unexposed to the public API (both C++ and Python) c4d.DescLevel( c4d.SLA_LAYER_BLEND, # The parameter ID for the layer blend list. CUSTOMDATA_BLEND_LIST, # Its data type. shader.GetType()), # And the owner type of the parameter, i.e., the layer shader. # The sub-channel of the parameter, i.e., we reach here into the internals of the # unexposed data type. c4d.DescLevel( 10060, # Our magic number for the first property of the first layer. c4d.DTYPE_REAL, # The angle is a real number, you got that absolutely right. 0)) # For sub-channels, the owner type is usually 0. # With this DescID, we can now access the parameter value formally. if not shader.SetParameter(did, c4d.utils.DegToRad(30), c4d.DESCFLAGS_SET_NONE): raise RuntimeError("Could not set parameter value.") angle: float | None = shader.GetParameter(did, c4d.DESCFLAGS_GET_NONE) if angle is None: raise RuntimeError("Could not get parameter value.") print(f"{angle = }") # The bad news is that all this will not work in XPresso. The code below will run and create # a port but the port is absolutely garbage, as it is a "color profile" port. But I had a # look at how the C++ XPresso code creates the ports for #ID_OPERATOR_OBJECT nodes and there # is custom logic also covering layer shaders that is not replicable in the public API # (neither C++ nor Python). Which is likely why this fails with a nonsense port. # edit: okay, sometimes it seems to work. I would still avoid all this layer shader internals mess. root: GvNode = CheckType(master.GetRoot()) objNode: GvNode = CheckType(master.CreateNode(root, c4d.ID_OPERATOR_OBJECT, x=50, y=50)) objNode[c4d.GV_OBJECT_OBJECT_ID] = shader brokenAnglePort: GvPort | None = objNode.AddPort(c4d.GV_PORT_INPUT, did) if not brokenAnglePort: raise RuntimeError("Could not add port to node.") print(f"{brokenAnglePort = }") @staticmethod def PythonNodeLayerShaderAccess( shader: c4d.BaseShader, null: c4d.BaseObject,master: GvNodeMaster) -> None: """Creates a Python XPresso node that accesses the transform angle parameter of the first layer in the given layer shader. Args: shader: The layer shader to manipulate. null: The null object that hosts the XPresso tag and the 'Angle' user data field. master: The node master of the XPresso tag. """ # As you said yourself, an easy way out is to use a Python node. So, let's start by creating # a Python node. root: GvNode = CheckType(master.GetRoot()) pyNode: GvNode = CheckType(master.CreateNode(root, c4d.ID_OPERATOR_PYTHON, x=200, y=200)) # Create a parameter description for a link box named "Shader Link" and add it as a user # data field to the Python node. There are other ways to get "in" a BaseList2D into a Python # node, but it does not have a BaseLink port type, so we always have get a bit creative. # Another way could be to pass the UUID of the node, but this then always requires us # searching the node which costs performance. bc: c4d.BaseContainer = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BASELISTLINK) bc[c4d.DESC_NAME] = "Shader Link" pyNode.AddUserData(bc) # Now set the user data to our shader and the code to the code we define below. pyNode[c4d.ID_USERDATA, 1] = shader pyNode[c4d.GV_PYTHON_CODE] = Solution.CODE # Now remove all the default ports from the node. ports: list[GvPort] = CheckType((pyNode.GetInPorts() or []) + (pyNode.GetOutPorts() or [])) for p in ports: pyNode.RemovePort(p) # Now we are going to add in and output ports to the Python node. This works a bit differently # than for other nodes, see https://developers.maxon.net/forum/topic/16302/ for a more in # depth discussion of the subject. angleInPort: GvPort = CheckType(pyNode.AddPort( c4d.GV_PORT_INPUT, c4d.DescID( c4d.DescLevel(c4d.IN_REAL, c4d.DTYPE_SUBCONTAINER, pyNode.GetType()), c4d.DescLevel(1000, c4d.DTYPE_REAL, 0) ))) angleInPort.SetName("angle_in") angleOutPort: GvPort = CheckType(pyNode.AddPort( c4d.GV_PORT_OUTPUT, c4d.DescID( c4d.DescLevel(c4d.OUT_REAL, c4d.DTYPE_SUBCONTAINER, pyNode.GetType()), c4d.DescLevel(1000, c4d.DTYPE_REAL, 0) ))) angleOutPort.SetName("angle_out") # Now we create nodes for the null object and a result node and connect them to the Python. objNode: GvNode = CheckType(master.CreateNode(root, c4d.ID_OPERATOR_OBJECT, x=50, y=100)) resNode: GvNode = CheckType(master.CreateNode(root, c4d.ID_OPERATOR_RESULT, x=350, y=200)) # The output port for our "Angle" user data field on the null object. objOutPort: GvPort | None = objNode.AddPort( c4d.GV_PORT_OUTPUT, c4d.DescID( c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, objNode.GetType()), c4d.DescLevel(1, c4d.DTYPE_REAL, 0) )) if not objOutPort: raise RuntimeError("Could add user data port to object node.") # A result node can only have one input port, so we can also just do this without any danger # that future versions of Cinema 4D might break this code. We could also be more verbose. resInPort: GvPort | None = resNode.GetInPorts()[0] if resNode.GetInPorts() else None if not resInPort: raise RuntimeError("Could not get input port from result node.") # And we wire everything up and are done, yay! objOutPort.Connect(angleInPort) angleOutPort.Connect(resInPort) # The Python code used by the second, Python node based example, to set the code of the Python # node. CODE: str = '''import c4d op: c4d.modules.graphview.GvNode # The Python Xpresso node containing this code. def main() -> None: """Sets the value of the transform angle parameter of the first layer in the linked layer shader and outputs the value to the output port. """ global angle_out try: shader: c4d.BaseShader = op[c4d.ID_USERDATA, 1] # The shader linked in the user data field. if not isinstance(shader, c4d.LayerShader): angle_out = 0.0 print("The linked shader is not a layer shader.") return # It is of course up to you to define the details of this. My code does not make much sense. layer: c4d.LayerShaderLayer = shader.GetFirstLayer() if layer.GetType() != c4d.TypeTransform: angle_out = 0.0 print("The first layer is not a transform layer.") return if not layer.SetParameter(c4d.LAYER_S_PARAM_TRANS_ANGLE, angle_in): angle_out = 0.0 print("Could not set angle parameter.") return angle_out = angle_in except: angle_out = 0.0 ''' if __name__ == '__main__': Solution.main()
-
@ferdinand Thank you for your in-depth analysis. It would have taken me way too long to figure this out lol, especially finding "CUSTOMDATA_BLEND_LIST". The first example seems to work for me, but like you said, it may not be reliable. The Python node example also works great. Again, thank you for your time.