Hello @thomasb,
thank you for reaching out to us. There are several problems with your code and there is a major problem with our API.
- Your code is not correct when it comes parsing the file names of the files you have provided. Or in other words, the files you have provided have incorrect file names.
- You must insert the shader once when you create it.
- There are some other minor problems as not checking returns values and using
OverrideNode()
(twice) which I do not really understand why you are doing this.
There are in principle two ways how to do this:
- Use
BaseTake.FindOrAddOverrideParam()
to override a single parameter, the BITMAPSHADER_FILENAME
parameter.
- Use
BaseTake.AutoTake()
to pass the actual node (the shader) and a copy of it where parameters have been changed (e.g., BITMAPSHADER_FILENAME
) and let Cinema 4D figure out the rest.
The problem is however that this does not work (which is the major problem of our API). I have provided [1] a script which implements what you want to do cleanly, but the result is always the same: The take data structure is identical to the one created by Cinema 4D when such setup is created manually, it is only that the actual value of BITMAPSHADER_FILENAME
is not written, but instead is a string (as shown in the screen below).

- Not shown here, but also tried by me was the
AutoTake
approach, which yields the same results.
- This is also not tied to
BITMAPSHADER_FILENAME
, but it happens for example also for the Sampling parameter of the Xbitmap
shader.
- My code does however run fine on a
XColor
shader and creating overrides for its COLORSHADER_COLOR
parameter.
Which is all a bit odd, to say the least. In principle using FindOrAddOverrideParam
is pretty straight forward and at least we at SDK do not see anything wrong with the code I have provided (which does not help you much of course). I have reached out to the developer of the take system to ask if there is anything special about the take system regarding bitmap shaders. This could either be some undocumented steps which must be taken or a bug in the take system or its Python bindings (I have not tried C++ yet).
I cannot give you an ETA when we will come back with a final answer, as we are currently very busy with release preparations. In the mean time I can only recommend taking another approach. What you could do is the following:
- Create materials for each of your textures
- Then create overrides for the texture tags that reference your "Main-Material" and create overrides for the
TEXTURETAG_MATERIAL
parameter. There have been C++ and Python posts in this forum which discuss that topic.
Cheers,
Ferdinand
[1] The Python code which I used (I replaced the name of the target material with "Mat"):
"""Creates overrides for the file name of bitmap shaders.
Note:
THIS CODE DOES NOT WORK AS OF R25SP1.
"""
import c4d
import os
import typing
# The ID of the lock overrides command.
CID_LOCK_OVERRIDES = 431000108
# The ID of the BITMAPSHADER_FILENAME parameter as it is required by overrides.
DID_BITMAPSHADER_FILENAME = c4d.DescID(
c4d.DescLevel(c4d.BITMAPSHADER_FILENAME, c4d.DTYPE_FILENAME, c4d.Xbitmap))
def AssertType(item: any, t: typing.Type, label: str) -> None:
"""Asserts that #item is of type #t. When the assertion fails, an assertion error will be
raised with #label referring to #item.
"""
if not isinstance(item, t):
raise AssertionError(f"Expected {t} for '{label}'. Received: {type(item)}")
def GetTargetMaterial(doc: c4d.documents.BaseDocument) -> c4d.BaseMaterial:
"""Returns the material which is the target of the take operations for the script.
"""
AssertType(doc, c4d.documents.BaseDocument, "doc")
material = doc.SearchMaterial("Mat")
if material is None:
raise RuntimeError(f"Could not find target material in: {doc}")
return material
def GetBitmapShaders(material: c4d.BaseMaterial,
channels: list[int]) -> list[c4d.BaseShader(c4d.Xbitmap)]:
"""Returns bitmap shaders for all the passed #channels in #material.
Will raise error when the target parameters are not of type BaseLink and create shaders for
BaseLink parameters where no shader does exist.
"""
AssertType(material, c4d.BaseMaterial, "material")
AssertType(channels, (list, tuple), "channels")
data = material.GetDescription(c4d.DESCFLAGS_DESC_NONE)
results = []
for descID in channels:
AssertType(descID, int, "descID")
# Get the data container of the material and assert that the ID points is to a BASELISTLINK
data = material.GetDataInstance()
if data.GetType(descID) != c4d.DTYPE_BASELISTLINK:
raise TypeError(
f"The id {descID} does not point to a 'DTYPE_BASELISTLINK' in {material}")
# Get the linked shader.
shader = data.GetLink(descID)
# There is no link established yet. Create a new shader.
if shader is None:
shader = c4d.BaseShader(c4d.Xbitmap)
if shader is None:
raise MemoryError(f"Could not allocate new shader.")
material.InsertShader(shader)
material[descID] = shader
# All other cases.
else:
AssertType(shader, c4d.BaseShader, "shader")
if not shader.CheckType(c4d.Xbitmap):
raise TypeError(
f"Found shader in target channel that is not a bitmap shader: {descID}")
# Append the shader to the results.
results.append(shader)
return results
def GetDocumentTakes(doc: c4d.documents.BaseDocument) -> c4d.modules.takesystem.BaseTake:
"""Yields all non main-take takes in #doc.
"""
AssertType(doc, c4d.documents.BaseDocument, "doc")
takeData = doc.GetTakeData()
mainTake = takeData.GetMainTake()
def iterate(node):
"""Walks #node depth first.
"""
if node is None:
return
while node:
yield node
for descendant in iterate(node.GetDown()):
yield descendant
node = node.GetNext()
for take in iterate(mainTake.GetDown()):
yield take
def GetTextureData() -> dict[str: str]:
"""Returns a dictionary of identifier and file-path tuples for a texture directory.
"""
textureDirectory = c4d.storage.LoadDialog(type=c4d.FILESELECTTYPE_ANYTHING,
title="Choose Folder for textures",
flags=c4d.FILESELECT_DIRECTORY)
if textureDirectory is None:
return None
result = {}
for root, _, files in os.walk(textureDirectory):
for f in files:
# Massage the file names into a meaningful form by cutting of the extension and then
# the prefix which is not reflected in your document. The bit in square brackets is
# what we are interested in.
#
# 1647228063839-34348.webp -> 1647228063839, [34348], webp
#
name, _ = os.path.splitext(f)
if not name.find("-"):
raise RuntimeError(
f"Encountered texture name not following naming conventions: {f}")
key = name.split("-")[1]
# And store the absolute path under that identifier key.
if key in result:
raise RuntimeError(f"Ambiguous file identifier: '{result[key]}' and '{f}' collide.")
result[key] = os.path.join(root, f)
return result
def main(doc: c4d.documents.BaseDocument) -> None:
"""Runs the example.
"""
AssertType(doc, c4d.documents.BaseDocument, "doc")
# Get the textures. There were multiple problems with how you handled the file names, ranging
# from assuming incorrect file types to not matching the file names. This could all done less
# hastily compared to how I did implement it, but that would be up to you.
textureData = GetTextureData()
if textureData is None or textureData == {}:
print ("User aborted operation or directory does not contain files.")
return None
# Get the target material and all relevant bitmap shaders for it.
material = GetTargetMaterial(doc)
# Ensure the "lock" overrides button is checked when the script is invoked.
if not c4d.IsCommandChecked(CID_LOCK_OVERRIDES):
c4d.CallCommand(CID_LOCK_OVERRIDES)
# Iterate over all takes in the passed document and update them.
takeData = doc.GetTakeData()
for take in GetDocumentTakes(doc):
# Get the take name and the imbedded texture identifier.
takeName = take[c4d.ID_BASELIST_NAME].split(" -")
if len(takeName) != 2:
raise RuntimeError(
f"Take naming convention violated for take: {take[c4d.ID_BASELIST_NAME]}")
tid = takeName[0]
# Try to find the matching texture in the texture data.
if tid not in textureData:
raise RuntimeError(
f"No matching texture found for identifier '{tid}' for take '{takeName}'.")
texturePath = textureData[tid]
# Cerate a parameter override for each shader for that texture. This might be in
# practice more complicated than shown here, as you might want to provide different
# texture files for different channels.
clone = material.GetClone(c4d.COPYFLAGS_NONE)
shader = shader = c4d.BaseShader(c4d.Xbitmap)
if None in (clone, shader):
raise MemoryError("Could not allocate clone material or shader.")
clone.InsertShader(shader)
clone[c4d.MATERIAL_COLOR_SHADER] = shader
clone.Message(c4d.MSG_UPDATE)
shader[c4d.BITMAPSHADER_FILENAME] = texturePath
shader.Message(c4d.MSG_UPDATE)
take.AutoTake(takeData, material, clone)
print (f"Added override in {take} with the value: '{texturePath}'.")
c4d.EventAdd()
# Execute main()
if __name__=='__main__':
main(doc)
[2] This is some testing code I did use to compare the data of manually and programmatically generated takes:
import c4d
def GetDocumentTakes(doc: c4d.documents.BaseDocument) -> c4d.modules.takesystem.BaseTake:
"""Yields all non main-take takes in #doc.
"""
takeData = doc.GetTakeData()
mainTake = takeData.GetMainTake()
def iterate(node):
"""Walks #node depth first.
"""
if node is None:
return
while node:
yield node
for descendant in iterate(node.GetDown()):
yield descendant
node = node.GetNext()
for take in iterate(mainTake.GetDown()):
yield take
# Main function
def main():
for take in GetDocumentTakes(doc):
print (f"Take: {take}")
print (f"\tGroups: {take.GetOverrideGroups()}")
print (f"\tOverrides:")
for override in take.GetOverrides():
print (f"\t\tOverride: {override}")
print (f"\t\t\tNode: {override.GetSceneNode()}")
print (f"\t\t\tParameter: {override.GetAllOverrideDescID()}")
print("")
# Execute main()
if __name__=='__main__':
main()
[3] And finally, here is one of the AutoTake()
variants I tried, here I do operate intentionally on the material and not the shader level.
"""Creates overrides for the file name of bitmap shaders.
Note:
THIS CODE DOES NOT WORK AS OF R25SP1.
"""
import c4d
import os
import typing
# The ID of the lock overrides command.
CID_LOCK_OVERRIDES = 431000108
# The ID of the BITMAPSHADER_FILENAME parameter as it is required by overrides.
DID_BITMAPSHADER_FILENAME = c4d.DescID(
c4d.DescLevel(c4d.BITMAPSHADER_FILENAME, c4d.DTYPE_FILENAME, c4d.Xbitmap))
def AssertType(item: any, t: typing.Type, label: str) -> None:
"""Asserts that #item is of type #t. When the assertion fails, an assertion error will be
raised with #label referring to #item.
"""
if not isinstance(item, t):
raise AssertionError(f"Expected {t} for '{label}'. Received: {type(item)}")
def GetTargetMaterial(doc: c4d.documents.BaseDocument) -> c4d.BaseMaterial:
"""Returns the material which is the target of the take operations for the script.
"""
AssertType(doc, c4d.documents.BaseDocument, "doc")
material = doc.SearchMaterial("Mat")
if material is None:
raise RuntimeError(f"Could not find target material in: {doc}")
return material
def GetBitmapShaders(material: c4d.BaseMaterial,
channels: list[int]) -> list[c4d.BaseShader(c4d.Xbitmap)]:
"""Returns bitmap shaders for all the passed #channels in #material.
Will raise error when the target parameters are not of type BaseLink and create shaders for
BaseLink parameters where no shader does exist.
"""
AssertType(material, c4d.BaseMaterial, "material")
AssertType(channels, (list, tuple), "channels")
data = material.GetDescription(c4d.DESCFLAGS_DESC_NONE)
results = []
for descID in channels:
AssertType(descID, int, "descID")
# Get the data container of the material and assert that the ID points is to a BASELISTLINK
data = material.GetDataInstance()
if data.GetType(descID) != c4d.DTYPE_BASELISTLINK:
raise TypeError(
f"The id {descID} does not point to a 'DTYPE_BASELISTLINK' in {material}")
# Get the linked shader.
shader = data.GetLink(descID)
# There is no link established yet. Create a new shader.
if shader is None:
shader = c4d.BaseShader(c4d.Xbitmap)
if shader is None:
raise MemoryError(f"Could not allocate new shader.")
material.InsertShader(shader)
material[descID] = shader
# All other cases.
else:
AssertType(shader, c4d.BaseShader, "shader")
if not shader.CheckType(c4d.Xbitmap):
raise TypeError(
f"Found shader in target channel that is not a bitmap shader: {descID}")
# Append the shader to the results.
results.append(shader)
return results
def GetDocumentTakes(doc: c4d.documents.BaseDocument) -> c4d.modules.takesystem.BaseTake:
"""Yields all non main-take takes in #doc.
"""
AssertType(doc, c4d.documents.BaseDocument, "doc")
takeData = doc.GetTakeData()
mainTake = takeData.GetMainTake()
def iterate(node):
"""Walks #node depth first.
"""
if node is None:
return
while node:
yield node
for descendant in iterate(node.GetDown()):
yield descendant
node = node.GetNext()
for take in iterate(mainTake.GetDown()):
yield take
def GetTextureData() -> dict[str: str]:
"""Returns a dictionary of identifier and file-path tuples for a texture directory.
"""
textureDirectory = c4d.storage.LoadDialog(type=c4d.FILESELECTTYPE_ANYTHING,
title="Choose Folder for textures",
flags=c4d.FILESELECT_DIRECTORY)
if textureDirectory is None:
return None
result = {}
for root, _, files in os.walk(textureDirectory):
for f in files:
# Massage the file names into a meaningful form by cutting of the extension and then
# the prefix which is not reflected in your document. The bit in square brackets is
# what we are interested in.
#
# 1647228063839-34348.webp -> 1647228063839, [34348], webp
#
name, _ = os.path.splitext(f)
if not name.find("-"):
raise RuntimeError(
f"Encountered texture name not following naming conventions: {f}")
key = name.split("-")[1]
# And store the absolute path under that identifier key.
if key in result:
raise RuntimeError(f"Ambiguous file identifier: '{result[key]}' and '{f}' collide.")
result[key] = os.path.join(root, f)
return result
def main(doc: c4d.documents.BaseDocument) -> None:
"""Runs the example.
"""
AssertType(doc, c4d.documents.BaseDocument, "doc")
# Get the textures. There were multiple problems with how you handled the file names, ranging
# from assuming incorrect file types to not matching the file names. This could all done less
# hastily compared to how I did implement it, but that would be up to you.
textureData = GetTextureData()
if textureData is None or textureData == {}:
print ("User aborted operation or directory does not contain files.")
return None
# Get the target material and all relevant bitmap shaders for it.
material = GetTargetMaterial(doc)
# Ensure the "lock" overrides button is checked when the script is invoked.
if not c4d.IsCommandChecked(CID_LOCK_OVERRIDES):
c4d.CallCommand(CID_LOCK_OVERRIDES)
# Iterate over all takes in the passed document and update them.
takeData = doc.GetTakeData()
for take in GetDocumentTakes(doc):
# Get the take name and the imbedded texture identifier.
takeName = take[c4d.ID_BASELIST_NAME].split(" -")
if len(takeName) != 2:
raise RuntimeError(
f"Take naming convention violated for take: {take[c4d.ID_BASELIST_NAME]}")
tid = takeName[0]
# Try to find the matching texture in the texture data.
if tid not in textureData:
raise RuntimeError(
f"No matching texture found for identifier '{tid}' for take '{takeName}'.")
texturePath = textureData[tid]
# Cerate a parameter override for each shader for that texture. This might be in
# practice more complicated than shown here, as you might want to provide different
# texture files for different channels.
clone = material.GetClone(c4d.COPYFLAGS_NONE)
shader = shader = c4d.BaseShader(c4d.Xbitmap)
if None in (clone, shader):
raise MemoryError("Could not allocate clone material or shader.")
clone.InsertShader(shader)
clone[c4d.MATERIAL_COLOR_SHADER] = shader
clone.Message(c4d.MSG_UPDATE)
shader[c4d.BITMAPSHADER_FILENAME] = texturePath
shader.Message(c4d.MSG_UPDATE)
take.AutoTake(takeData, material, clone)
print (f"Added override in {take} with the value: '{texturePath}'.")
c4d.EventAdd()
# Execute main()
if __name__=='__main__':
main(doc)