Thank you for reaching out to us. And thanks again for the community answers. This is a tricky one to answer, at least in the current state of Python Asset API. In general, the answer is easy:
- You cannot retrieve the selection state of the Asset Browser as there can be multiple Asset Browser instances open at the same time.
- While 1. might not be the strongest reasoning, the way how such things are intended to be done, is with maxon.AssetManagerInterface.OpenPopup. So, you open a little Asset Browser popup, as for example how it is done by the Node Editor or the Load Preset button, let the user select something and then retrieve the selection state in that Asset Browser instance.
But in Python there unfold a few problems:
- Accessing the asset descriptions passed to
outSelection in form of an
maxon.DragAndDropDataAssetArray instance will crash Cinema 4D. The culprit is
DragAndDropDataAssetArray.GetAssetDescriptions(), not the delegate itself. So, while you can open a popup and let the user select things, you cannot get hold of the selection state at the moment.
- The same thing applies to asset drag and drop events which are managed by the same data type.
But what you could do, is use a custom GUI which has implicit asset drag and drop support, as for example
CUSTOMGUI_FILENAME, to drag and drop assets into them to get the URL of the asset. But there are also quite a few hoops to jump through there. I have provided an example below.
|Please note that when evaluated conservatively, what you want to be done is not possible in Python at the moment and you should switch to C++. What I am providing here is very much a workaround.
edit: @m_adam suggested kindly that one could monkey patch
maxon.AssetInterface.GetAssetUrl oneself inside the script, which makes this a lot less dicey. I have adopted the code below which will now operate on the proper
asset scheme Urls.
"""Provides a temporary work around for letting a user select a set of texture assets to create a
This can be run as a Script Manager script and will let the user drag two (texture) assets into
CUSTOMGUI_FILENAME gadgets in order to create a material.
The Asset API does not provide access to the Asset Browser in an extended fashion, which is why
there maxon.AssetManagerInterface.GetSelectedAssets(). The intended way is to use maxon.
AssetManagerInterface.OpenPopup(), which will open a new popup Asset Browser instance. maxon.
DragAndDropDataAssetArray.GetAssetDescriptions(), a method required by .OpenPopup() will however
currently crash Cinema 4D.
This file provides a work around. It is not pretty but it gets the job done. As soon as we fix
the relevant methods, you should adopt them.
doc: c4d.documents.BaseDocument # The active document
# --- AssetInterface monkey patching fix for AssetInterface.GetAssetUrl() --------------------------
def GetAssetUrl(self, asset, isLatest):
maxon.AssetInterface.GetAssetUrl = AssetInterfaceFix.GetAssetUrl
# --- End of fix -----------------------------------------------------------------------------------
class MaterialCreationDialog (c4d.gui.GeDialog):
"""Implements a dialog into which one can drag media assets to create a material.
# The gadget IDs.
ID_GROUP: int = 1000
ID_LBL_COLOR: int = 1001
ID_FLE_COLOR: int = 1002
ID_LBL_BUMP: int = 1003
ID_FLE_BUMP: int = 1004
ID_BTN_CREATE: int = 1005
def CreateLayout(self) -> bool:
"""Called to populate the dialog.
self.SetTitle("Material Creation Dialog")
self.GroupBegin(self.ID_GROUP, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=2)
self.GroupBorderSpace(5, 5, 5, 5)
# The static text and CUSTOMGUI_FILENAME for the color channel asset. CUSTOMGUI_FILENAME is
# a custom GUI which can implicitly deal with asset drag and drop events. I.e., you can drag
# an asset onto it, and it will create a link.
self.AddStaticText(self.ID_LBL_COLOR, c4d.BFH_FIT, name="Color")
self.AddCustomGui(self.ID_FLE_COLOR, c4d.CUSTOMGUI_FILENAME, "", c4d.BFH_SCALEFIT, 0, 0,
# The static text and CUSTOMGUI_FILENAME for the bump channel asset.
self.AddStaticText(self.ID_LBL_BUMP, c4d.BFH_FIT, name="Bump")
self.AddCustomGui(self.ID_FLE_BUMP, c4d.CUSTOMGUI_FILENAME, "", c4d.BFH_SCALEFIT, 0, 0,
self.AddButton(self.ID_BTN_CREATE, c4d.BFH_FIT, name="Create Material")
def Command(self, cid: int, msg: c4d.BaseContainer) -> bool:
"""Called on gadget interaction events.
# The create button has been clicked, CreateMaterial() is being called with the strings
# shown in the filename GUIs.
if cid == self.ID_BTN_CREATE:
return super().Command(cid, msg)
def CreateMaterial(urlColor: str, urlBump: str) -> bool:
"""Creates a material for the assets referenced in the two filename custom GUI gadgets.
# Bail when any of the asset URL strings is empty.
if not isinstance(urlColor, str) or urlColor == "":
if not isinstance(urlBump, str) or urlBump == "":
# Get the true asset Url, or at least one which is working. What does this mean? There are
# in principal three Urls associated with an asset:
# 1. The human readable asset Url in the assetdb scheme, e.g.:
# assetdb:///tex/Surfaces/Dirt Scratches & Smudges/RustPaint0291_M.jpg
# The catch here is that this is not a Url in the more strict sense, it is just smoke
# and mirrors and the AssetApi has no clue what to do with it. In C++ there are
# methods attached to UrlInterface with which one can convert such faux Url to the true
# Url, but they are missing in Python. When one would write such Url to a bitmap shader,
# Cinema 4D would not recognize it (without some major message trickery). In the user
# interface one can use assetdb Urls to reference assets, in the API not.
# 2. The true asset Url in the asset scheme, e.g.,
# This is the "true" asset Url one should use for linking a texture asset in a material.
# In the Ui, Cinema 4D will show it as (1.). It can be retrieved with
# maxon.AssetInterface.GetAssetUrl(asset, True), but the
# method is broken.
# 3. The raw asset url, usually in the ramdisk scheme, e.g.,:
# This is the true physical file of the asset. Usually one should not touch it. Write
# operations to it are off limits.
urlColor = MaterialCreationDialog.GetTrueAssetUrl(urlColor)
urlBump = MaterialCreationDialog.GetTrueAssetUrl(urlBump)
if None in (urlColor, urlBump):
# At this point we have replaced the assetdb scheme asset Urls with asset scheme Urls.
# This is now standard material creation.
# Get the active document, create a material, and two bitmap shaders, one for the color, and
# one for the bump channel.
doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
material: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial)
colorShader: c4d.BaseShader = c4d.BaseShader(c4d.Xbitmap)
bumpShader: c4d.BaseShader = c4d.BaseShader(c4d.Xbitmap)
if None in (material, colorShader, bumpShader):
raise MemoryError("Could not allocate material or shader.")
# Link the two assets in the filename properties of the bitmap shaders.
colorShader[c4d.BITMAPSHADER_FILENAME] = urlColor
bumpShader[c4d.BITMAPSHADER_FILENAME] = urlBump
# Insert the shaders into the material and link them in the color and bump channel, and
# enable the bump channel.
material[c4d.MATERIAL_COLOR_SHADER] = colorShader
material[c4d.MATERIAL_BUMP_SHADER] = bumpShader
material[c4d.MATERIAL_USE_BUMP] = True
# Insert the material into the document, set it as active and push an update event.
def GetTrueAssetUrl(assetDbUrl: str) -> typing.Optional[str]:
"""Returns the true asset Url in the asset scheme for an asset Url in the assetdb scheme.
This is very much a hack as lined out above.
# Bail on illegal inputs.
if not isinstance(assetDbUrl, str) or not assetDbUrl.startswith("assetdb:///"):
# Get the user preferences repository as a lookup repo.
repo: maxon.AssetRepositoryRef = maxon.AssetInterface.GetUserPrefsRepository()
if repo is None:
# Massage #assetDbUrl into form: Cut of the scheme, split by / and pop off the last element
# as the file name. For assetdb:///tex/Surfaces/Dirt Scratches & Smudges/RustPaint0291_M.jpg,
# the values would be:
# categories = ["tex", "Surfaces", "Dirt Scratches & Smudges"]
# filename = "RustPaint0291_M.jpg"
categories: list[str] = assetDbUrl[11:].split("/")
filename: str = categories.pop()
if filename is None or filename == "":
# Find the Id of all asset categories which match the name of the last category in the
# assetdb Url. E.g. for assetdb:///tex/Surfaces/Dirt Scratches & Smudges/RustPaint0291_M.jpg
# it would search for all categories named "Dirt Scratches & Smudges".
categoryIds: list[maxon.Id] = 
if len(categories) > 0:
lastCategory: str = categories.pop()
categoryIds = MaterialCreationDialog.FindCategoryByName(repo, lastCategory)
# Get all asset descriptions for assets of type file, which includes texture assets.
fileAssets: list[maxon.AssetDescription] = 
repo.FindAssets(maxon.AssetTypes.File(), maxon.Id(), maxon.Id(),
# Iterate over them and get the metadata of each of them while doing so.
for asset in fileAssets:
metadata: maxon.AssetMetaData = asset.GetMetaData()
if metadata is None:
# Retrieve the subtype of the asset, when it is not SubType_ENUM_MediaImage, i.e., a
# texture file asset, we skip the asset.
subType: maxon.Id = metadata.Get(maxon.ASSETMETADATA.SubType, None)
if (subType is None or
maxon.InternedId(subType) != maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
# Get the name of the asset, e.g., "RustPaint0291_M.jpg". When it does not align with
# #filename, skip the asset.
name: str = asset.GetMetaString(
maxon.OBJECT.BASE.NAME, maxon.Resource.GetCurrentLanguage(), "")
if name != filename:
# This asset aligns in file name with the file name we extracted form the assetdb Url.
# Now we test if also the asset Id of the category asset of this asset aligns with what
# could be found in the assetdb Url. I am testing here only the first category, which can
# lead to false positives. I the C++ docs  I have shown how to recursively find asset
# categories by a path, but I did not want to make this example even longer.
categoryId: maxon.Id = maxon.CategoryAssetInterface.GetParentCategory(asset)
if len(categoryIds) == 0 or categoryId in categoryIds:
# Get the true asset Url, this only works with the monkey patch provided at the
# top of the file.
trueAssetUrl: maxon.Url = maxon.AssetInterface.GetAssetUrl(asset, True)
# Return the string wrapped by the maxon.Url, e.g.
def FindCategoryByName(repo: maxon.AssetRepositoryRef, name: str) -> list[maxon.Id]:
"""Finds all asset categories with the given #name in the given repository #repo and returns
# Get all asset category assets from the repository.
categoryAssets: list[maxon.AssetDescription] = 
repo.FindAssets(maxon.AssetTypes.Category(), maxon.Id(), maxon.Id(),
# And find all categories whose name matches #name, and return them.
result: list[maxon.Id]= 
for asset in categoryAssets:
thisName: str = asset.GetMetaString(
maxon.OBJECT.BASE.NAME, maxon.Resource.GetCurrentLanguage(), "")
if thisName == name:
# Hack to keep an async dialog alive in a Script Manger environment, please do not do this in a
# production environment.
dlg: MaterialCreationDialog = MaterialCreationDialog()
def main() -> None:
"""Runs the example.
dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=500, defaulth=200)
if __name__ == '__main__':