SOLVED Override material tag in take

I am porting a plugin to create takes based on different materials from python to C++. Everything works well until the moment when the overrideParam is added to the take. Nothing is added and the function returns null.

take->FindOrAddOverrideParam(td, *editTag, TEXTURETAG_MATERIAL, GetMaterial(*materialDescription),NULL,false);

I have noticed that instead of the type TextureTag that I got before from the material tags I now get Material as type. Do I need to change the TEXTURETAG_MATERIAL to something else now? Or how do I override the material of a MaterialTag in a Take using C++?

After some research by @ferdinand the issue was that the file was locked for take overrides. This can easily be missed since the overrides are locked when the button in the UI is disabled and not locked if its active:

notlocked.jpg locked.jpg

I added a check in the plugin to toggle the lock if its "active":

if (!IsCommandChecked(431000108)) {
    CallCommand(431000108);
}

Hello @krfft,

thank you for reaching out to us. While I do understand your question, I struggle to understand the technical details of your question. The line of code is very ambiguous, e.g. - what is the return type of your GetMaterial - and I also do not quite understand what you mean by '[...] [for] the type TextureTag that I got before from the material tags, I now get Material as type [...] '. Texture tags are still texture tags, nothing changed there. There is probably something going wrong in your GetMaterial.

I wrote a small example which demonstrates the steps which must be taken to add takes and overrides for a material assignment. I did not include the asset browser in my example, which I think is part of what you are working on, and instead relied on stuff that is already inside a document.

Cheers,
Ferdinand

The result:
mat_take.gif

The code:

// A simple example for how to create new takes for a given object and material which assign that
// material to the object.
//
// As discussed in:
//    https://plugincafe.maxon.net/topic/13658

#include "c4d_baselist.h"
#include "c4d_baseobject.h"
#include "c4d_basematerial.h"
#include "c4d_basedocument.h"
#include "lib_takesystem.h"
#include "ttexture.h"

/// A function which does create a new take with a material override for a given BaseObject
/// and BaseMaterial.
/// 
/// If there is no texture tag on the object, a new one will be created.
/// 
/// @param doc The document op and mat are part of.
/// @param op The object for which to add a material assignment override.
/// @param mat The material to override in the new take.
/// @return The material override for op and mat. 
static maxon::Result<BaseOverride*> AddMaterialAssignmentTake(
  BaseDocument* doc, BaseObject* op, BaseMaterial* mat)
{
  iferr_scope_handler
  {
    ApplicationOutput("Error in AddMaterialAssignmentTake(): @", err);
    return err;
  };

  // Get the take data of the document and create a new take for the material override.
  TakeData* const takeData = doc->GetTakeData();
  if (takeData == nullptr)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not retrieve take data."_s);

  String takeName = FormatString("@: @", op->GetName(), mat->GetName());
  BaseTake* const materialTake = takeData->AddTake(takeName, nullptr, nullptr);
  if (materialTake == nullptr)
    return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not add take."_s);

  // Get the first texture tag on the object or create a new one when there is none present.
  BaseTag* textureTag = op->GetTag(Ttexture);
  if (textureTag == nullptr)
    textureTag = op->MakeTag(Ttexture);
  if (textureTag == nullptr)
      return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not add texture tag."_s);

  // Add the override.
  DescID idMatrialAssignment = DescID(DescLevel(TEXTURETAG_MATERIAL));
  BaseOverride* result = materialTake->FindOrAddOverrideParam(
    takeData, textureTag, idMatrialAssignment, GeData(mat));
  if (result == nullptr)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not set override."_s);
  
  // Set the new take as the active one and return the override result.
  takeData->SetCurrentTake(materialTake);

  return result;
}

/// Runs the example.
static maxon::Result<void> PC13658(BaseDocument* doc)
{
  iferr_scope_handler
  {
    ApplicationOutput("Error in PC13658(): @", err);
    return err;
  };

  // Get the active object and material in the document.
  BaseObject* op = doc->GetActiveObject();
  if (op == nullptr)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Please select an object."_s);
  BaseMaterial* mat = doc->GetActiveMaterial();
  if (mat == nullptr)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Please select a material."_s);

  // And pass them to our function and then print out the returned override.
  BaseOverride* override = AddMaterialAssignmentTake(doc, op, mat) iferr_return;
  ApplicationOutput("Added overide: @", override);

  return maxon::OK;
}

@ferdinand Thank you for the swift response. I will try to elaborate on my question.
I am iterating over the entire scene to find tags matching a name and put them in a list:

maxon::BaseArray<BaseTag*> FindTagsContaining(BaseObject* topObject, maxon::String contains)
{
	maxon::BaseArray<BaseTag*> foundTags;

	BaseObject* child = topObject->GetDown();
	if (child == nullptr)
		return maxon::BaseArray<BaseTag*>();

	while (child != nullptr) {
		BaseTag* tag = child->GetTag(Ttexture);
		if(tag != nullptr)
		{
			maxon::String tagName = tag->GetName();
			if (tagName.Find(contains, 0)) {
				(void)foundTags.Append(tag);
			}
		}

		(void)foundTags.AppendAll(FindTagsContaining(child,contains));
		child = child->GetNext();
	}

	return foundTags;
}

I found a weird thing that if I print GetTypeName() on the tag returned by GetTag(Ttexture) it returns Material, that was the reason for my thinking that the term Texture tag was removed.

However with this list I then create a material override take with each tags material overridden:

maxon::BaseArray<BaseTag*>::Iterator editTagIterator;
   for (editTagIterator = editTags->Begin(); editTagIterator != editTags->End(); editTagIterator++)
   {
     DescID idMaterialAssignment = DescID(DescLevel(TEXTURETAG_MATERIAL));
     take->FindOrAddOverrideParam(td, *editTagIterator, idMaterialAssignment, GeData(GetMaterial(*materialDescription)));
   }

This is following the Python version (where it works) almost fully.
I have validated that the GetMaterial function returns a Material object and the function follows the example you posted in another of my threads:

BaseMaterial* GetMaterial(maxon::AssetDescription assetDescription) {
    iferr_scope_handler
    {
      ApplicationOutput("Stopped LoadMaterial() execution with the error: @", err);
      return nullptr;
    };

    BaseDocument* doc = GetActiveDocument();

    maxon::String matName = assetDescription.GetMetaString(maxon::OBJECT::BASE::NAME, maxon::LanguageRef()) iferr_return;
    BaseMaterial* material = doc->SearchMaterial(matName);

    if (material == nullptr) {
        maxon::Url url = assetDescription.GetUrl() iferr_return;

        if (url.IsEmpty())
            return nullptr;

        maxon::String* error = {};
        maxon::Bool didMerge = MergeDocument(doc, MaxonConvert(url), SCENEFILTER::MATERIALS, nullptr, error);

        material = doc->SearchMaterial(matName);
    }

    return material;
}

Hello @krfft,

@krfft said in Override material tag in take:

maxon::BaseArray<BaseTag*> FindTagsContaining(BaseObject* topObject, maxon::String contains)
{
	maxon::BaseArray<BaseTag*> foundTags;

	BaseObject* child = topObject->GetDown();
	if (child == nullptr)
		return maxon::BaseArray<BaseTag*>();

	while (child != nullptr) {
		BaseTag* tag = child->GetTag(Ttexture);
		if(tag != nullptr)
		{
			maxon::String tagName = tag->GetName();
			if (tagName.Find(contains, 0)) {
				(void)foundTags.Append(tag);
			}
		}

		(void)foundTags.AppendAll(FindTagsContaining(child,contains));
		child = child->GetNext();
	}

	return foundTags;
}

This does look correct. It will however never find the tags on the first object you pass in, since it immediately does go down in the hierarchy. Only for the ongoing recursion the parent call will have handled the parent object. It also not advisable to do stuff like (void)foundTags.Append(tag), you should properly handle our error system instead. If you want to step over errors, you can use iferr_ignore instead of iferr_return. But generally speaking, this function seems okay.

I found a weird thing that if I print GetTypeName() on the tag returned by GetTag(Ttexture) it returns Material, that was the reason for my thinking that the term Texture tag was removed.

This has unfortunately always been the case that Ttexture is being named a bit chaotically. I personally also always write Tmaterial first, before some interpreter/preprocessor reminds that this symbol does not exist.

maxon::BaseArray<BaseTag*>::Iterator editTagIterator;
   for (editTagIterator = editTags->Begin(); editTagIterator != editTags->End(); editTagIterator++)
   {
     DescID idMaterialAssignment = DescID(DescLevel(TEXTURETAG_MATERIAL));
     take->FindOrAddOverrideParam(td, *editTagIterator, idMaterialAssignment, GeData(GetMaterial(*materialDescription)));
   }

This also looks mostly fine. The iteration is a bit awfully explicit, you could also just write for (BaseTag* tag: editTags) to iterate over all items in the BaseArray<BaseTag*> editTags. What is missing here though, is the validation that the nodes td and editTagIterator are not null. You also do not show which BaseTake is referenced by take. Remember that you cannot add overrides to the main take (the take that is already present in a newly created document).

BaseMaterial* GetMaterial(maxon::AssetDescription assetDescription) {
    iferr_scope_handler
    {
      ApplicationOutput("Stopped LoadMaterial() execution with the error: @", err);
      return nullptr;
    };

    BaseDocument* doc = GetActiveDocument();

    maxon::String matName = assetDescription.GetMetaString(maxon::OBJECT::BASE::NAME, maxon::LanguageRef()) iferr_return;
    BaseMaterial* material = doc->SearchMaterial(matName);

    if (material == nullptr) {
        maxon::Url url = assetDescription.GetUrl() iferr_return;

        if (url.IsEmpty())
            return nullptr;

        maxon::String* error = {};
        maxon::Bool didMerge = MergeDocument(doc, MaxonConvert(url), SCENEFILTER::MATERIALS, nullptr, error);

        material = doc->SearchMaterial(matName);
    }

    return material;
}

This also looks mostly okay; I would point out two things though. A. It is not very desirable to return null pointers here since you must then deal later with them. B. That you do the merging of documents in an iterative fashion can throw a serious wrench into what you are doing.

So, since you do store pointers to tags and not BaseLink for these tags, merging documents could produce problems here since you do not assure that these pointers are still valid when you use them (Cinema does reallocate classic API nodes from time to time when necessary). What I would do, is first load all your material assets you need into the scene, then build some kind of collcetion for these new materials in the document, e.g., a BaseArray, and then carry out the linking in takes. This seems like a much less error prone route to me.

Cheers,
Ferdinand

@ferdinand Thank you for the input, I will look into the recommendations you have. I am quite new to working with C++ and get lost in pointers sometimes. Didn't know about BaseLink but it definately looks usable.

However, as you state most things looks fine but still I dont get any overrides on the tags in the takes (yes take is a child take). I tried the same code, but without the typing of what type of tag to find, on a scene where selectiontags are named as the searchterm instead of texturetags. Here it works as intended and the overrides show up on the take. Do you have any idea why this doesn't work when trying to override texturetags?

Hello @krfft,

I was probably a bit too polite here, your code is not fine, not checking for null pointers and stepping over errors are serious flaws you should fix. The stepping over errors is an unlikely culprit for your problem but will cause problems rather sooner than later. Not checking for null pointers before calling FindOrAddOverrideParam on the other hand is a likely culprit for it failing.

I cannot tell you why it works with selection tags and not with texture tags, I frankly do not even understand the statement, since these two are not interchangeable. There is then probably something wrong with your tag finding method. You are currently sub-string testing the name of material tags for some other string. Which is a bit odd, since the name of material tags is usually always just "Material".

Cheers,
Ferdinand

Hi @ferdinand,

I have spent some time rewriting the code incl using BaseLink instead of pointers. I am also validating all values put into the FindOrAddOverrideParam function. For background info we name the material tags to be able to know which are to have which material for the different variants of a product.

FindTags

Bool FindTagsContaining(BaseObject* topObject, BaseLinkArray* foundTags, maxon::String contains)
{
	iferr_scope_handler
	{
		ApplicationOutput("Iterating tags finished with error: @",err);
		return false;
	};

	BaseObject* child = topObject->GetDown();
	if (child == nullptr)
		return false;

	while (child != nullptr) {
		BaseTag* tag = child->GetTag(Ttexture);
		if(tag != nullptr)
		{
			maxon::String tagName = tag->GetName();
			if (tagName.Find(contains, 0)) {
				foundTags->Append(tag);
			}
		}

		FindTagsContaining(child,foundTags,contains);
		child = child->GetNext();
	}

	return true;
}

GetMaterial

BaseMaterial* GetMaterial(maxon::AssetDescription assetDescription) {
    iferr_scope_handler
    {
      ApplicationOutput("Stopped LoadMaterial() execution with the error: @", err);
      return nullptr;
    };

    BaseDocument* doc = GetActiveDocument();

    maxon::String matName = assetDescription.GetMetaString(maxon::OBJECT::BASE::NAME, maxon::LanguageRef()) iferr_return;
    BaseMaterial* material = doc->SearchMaterial(matName);

    if (material == nullptr) {
        maxon::Url url = assetDescription.GetUrl() iferr_return;

        if (url.IsEmpty())
            return nullptr;

        maxon::String* error = {};
        maxon::Bool didMerge = MergeDocument(doc, MaxonConvert(url), SCENEFILTER::MATERIALS, nullptr, error);

        material = doc->SearchMaterial(matName);
    }

    return material;
}

And finally the take creation:

BaseLinkArray editTags;
FindTagsContaining(productRoot, &editTags, "Edit_"_s);  

 // editTags is sent as a pointer to function below.

BaseTake* take = td->AddTake(takeName, parent, NULL);
BaseDocument* doc = GetActiveDocument();
DescID idMaterialAssignment = DescID(DescLevel(TEXTURETAG_MATERIAL));

for (Int i = 0; i < editTags->GetCount(); ++i)
{
    C4DAtomGoal* const goal = editTags->GetIndex((Int32)i, doc);
    if (goal == nullptr)
        return nullptr;

    BaseTag* tag = static_cast<BaseTag*>(goal);

    const maxon::AssetDescription* materialDescription = editMaterials->FindValue(tag->GetName());
    if (materialDescription == nullptr) {
        ApplicationOutput("Material description was null for name: @", editMaterials->FindValue(tag->GetName()));
        return nullptr;
    }
    
    BaseMaterial* mat = GetMaterial(*materialDescription);
    if (mat == nullptr) {
        ApplicationOutput("GetMaterial was null for name: @", editMaterials->FindValue(tag->GetName()));
        return nullptr;
    }

            ApplicationOutput("Adding tag override for " + tag->GetName() + " of type " + tag->GetTypeName() + " with " + mat->GetName());

    BaseOverride* overrideNode = take->FindOrAddOverrideParam(td, tag, idMaterialAssignment, GeData(mat));

    if (overrideNode == nullptr) {
        ApplicationOutput("Unable to create override for " + tag->GetName() + " with " + mat->GetName());
        return nullptr;
    }

    overrideNode->UpdateSceneNode(td, idMaterialAssignment);
}

The output of the ApplictionOutputs would be something like:

Adding tag override for Edit_StrapMetal of type Material with Aluminium_Anodised_Anthracite_0123
Unable to create override for Edit_StrapMetal with Aluminium_Anodised_Anthracite_0123

To me everything looks good, is there any way I could get some info on why the override is null or some error log from FindOrAddOverrideParam?

Hello @krfft,

yes, this is better. You are however still retrieving a tag first and then merging the document to import the material asset, potentially scrambling any object pointer you have established up to that point. As lined out in my previous posting, importing all assets first and then 'applying them' would be safer. If you use a BaseLink, you must resolve that into a BaseTag* as late as possible, as otherwise the link will be pointless. And links can also lose their target.

Regarding what can make FindOrAddOverrideParam fail: First of all, it is a classic API interface, so it has no error handling in the sense of concrete errors. The only error indication you get is a null pointer being returned. There are quite a few cases of how this function can fail, the most important one's are one of the input arguments being a null pointer and the parameter that should be overridden being locked from being overridden.

Have you run my example from above? It not, you might want to, just to see if you do everything correctly on the application side (you can lock overrides for example). If this all does not help, I would have to ask you to provide example code that can be complied as you so far only have provided snippets.

Cheers,
Ferdinand

@ferdinand Ok this is where it gets interesting, I tried your script on the C4D file where my plugin didn't work and I got the same behaviour in your script. The FindOrAddOverrideParam function returns nullptr. I tried to create a cube with a dummy material in the same project and it also returns nullptr. However if I create a new project, create the cube and run the script with a dummy material it works. Also both our scripts work in the other project i mentioned earlier in the thread, of course it had nothing to do with selection tags 🤦

The only difference I can find for the projects is that the non-functioning one was created in r23, it gives me a warning when saving it in r25 saying it will not be possible to open in r23 again. I do think that the other file also is created in a older version so should not be the issue but idk. Right now our workaround is to copy all geometry and materials into a new project, then the plugin works.

I will gladly share the two files and our plugin but since its confidential it needs to be in private, let me know where to send it if you want to have a look.

Hello @krfft,

hm, that is indeed weird, could you please provide such R23 file which does fail in R25 with my example from above, so that I could have a look? Because I can debug here against the source code of Cinema 4D and then see more clearly what is going wrong.

When you want your data to be treated confidentially, please follow the guidelines as lined out in the Forum Guidelines under Support Procedures: Confidential Data.

Cheers,
Ferdinand

@ferdinand I have sent the file to the supplied email address, looking forward to what you can find.

After some research by @ferdinand the issue was that the file was locked for take overrides. This can easily be missed since the overrides are locked when the button in the UI is disabled and not locked if its active:

notlocked.jpg locked.jpg

I added a check in the plugin to toggle the lock if its "active":

if (!IsCommandChecked(431000108)) {
    CallCommand(431000108);
}

Hey @krfft,

thanks for posting your detailed results.

Cheers,
Ferdinand