SOLVED Setting bitmap in Redshift dome light

I'm trying to create a Redshift dome light and add a bitmap to the 'Texture' link field. It all seems to work except the bitmap is not added. This is what I have so far:

Bool MyDialog::CreateRSDomeLight(BaseDocument* doc, BaseObject* parent, BaseObject* prev, Int32 domeType, Filename bitmapToSet)
{
	Bool success = false;
        GeData param;
	BaseObject* domeLight = nullptr;
	BaseContainer *data= nullptr, *shddata = nullptr;
	BaseShader* shd = nullptr;

	domeLight = BaseObject::Alloc(RSLight);
	if (domeLight != nullptr)
	{
		param.SetInt32(REDSHIFT_LIGHT_TYPE_DOME);
		domeLight->SetParameter(DescID(REDSHIFT_LIGHT_TYPE), param, DESCFLAGS_SET::NONE);
		doc->InsertObject(domeLight, parent, prev, false);
		doc->AddUndo(UNDOTYPE::NEWOBJ, domeLight);
		success = true;

		data = domeLight->GetDataInstance();
		shd = BaseShader::Alloc(Xbitmap);
		if (shd != nullptr)
		{
			shddata = shd->GetDataInstance();
			if (GeFExist(bitmapToSet), false))
			{
				shddata->SetFilename(BITMAPSHADER_FILENAME, bitmapToSet));
				shd->Message(MSG_UPDATE);
				data->SetLink(REDSHIFT_LIGHT_DOME_TEX0, shd);
				domeLight->Message(MSG_UPDATE);
			}
		}
	}

	return success;
}

This all works fine - the light is created, it's added to the scene, and is set as a dome light - but the texture link remains empty. This technique has worked for C4D materials and other objects but the Redshift link field doesn't seem to like it. I can see that in the description the field is actually an 'RSFILE' rather than a shader link - is that the problem? I've tried just adding a filename to the ljnk field but that doesn't work either.

Anyway, if I'm missing something obvious and there's a way to do this, I'd be grateful for any help.

Steve

Hello @spedler,

Thank you for reaching out to us. The problem you are facing is caused by multiple Redshift data types not being exposed in the public API. But these non-public types are then usually composed out of public types. So, while you cannot read or write the type as whole, you can do so for its levels (called sub-channels in the GUI). The problem has been discussed before on the forum here and here. I go there over the technical background in Python (I am sure you are aware of) and means how to discover the sub-channel IDs interactively in Cinema 4D with the "show sub-channels" option.

In your concrete case the correct DescLevel tuple to write to Texture.Path would be REDSHIFT_LIGHT_DOME_TEX0, REDSHIFT_FILE_PATH.

Cheers,
Ferdinand

Hi Ferdinand,

That's great, many thanks. I had a feeling it was a data type not (yet?) exposed in the API, but I'll read through those other posts and see if I can get it to work.

Thanks again,

Steve

Hey Steve,

I am sorry, I could have been a bit more verbose here. We are currently under release preparations, so time is a rare commodity. I assumed you would know what I was hinting at when mentioning multiple levels of a DescID. Find a small pseudo code example at the end of my posting which should get you going.

While writing the code example, I also saw that you are trying to write a shader link (i.e., treat the parameter like a BaseLink with the TexBoxGui), but that parameter subchannel is for a file path, i.e., effectively a string. Or am I overlooking something here and one can also somehow place a shader in that RS DomeLight parameter? I am not the biggest RS expert ...

Cheers,
Ferdinand

The code:

iferr_scope;
...
BaseObject* rsDomeLight; // A RS dome light instance pointer you somehow got hold of.
const maxon::String somefilePath; // A file path somehow provided by your code.
...
// We construct a two level DescID to write only the Path component of the Texture parameter, ...
const DescId rsDomeLightTexturePathId = DescId(DescLevel(REDSHIFT_LIGHT_DOME_TEX0), 
                                               DescLevel(REDSHIFT_FILE_PATH));
// and then write to that parameter component. Note that you cannot use BaseContainer access when
// writing sub-channel data, but must use C4DAtom::Get/SetParameter.
if (!rsDomeLight->SetParameter(rsDomeLightTexturePathId, GeData(somefilePath), DESCFLAGS_SET::NONE))
    return maxon::UnexpectedError(
        MAXON_SOURCE_LOCATION, "Could not write Texture.Path component in RS DomeLight."_s);

Hi Ferdinand,

Thank you very much for taking the time to post the pseudocode. I know you must be very busy right now!

Unfortunately I've tried this method numerous times and I just can't get it to work. Here's the code:

Bool MyDialog::CreateRSDomeLight(BaseDocument* doc, BaseObject* parent, BaseObject* prev, Filename fullPath)
{
	Bool success = false;
	BaseObject* domeLight = nullptr;

	domeLight = BaseObject::Alloc(RSLight);
	if (domeLight != nullptr)
	{
		param.SetInt32(REDSHIFT_LIGHT_TYPE_DOME);
		domeLight->SetParameter(DescID(REDSHIFT_LIGHT_TYPE), param, DESCFLAGS_SET::NONE);
		doc->InsertObject(domeLight, parent, prev, false);
		doc->AddUndo(UNDOTYPE::NEWOBJ, domeLight);
		success = true;

		// all working fine until now
		String pathToFile = fullPath.GetString();			// path is verified as correct
		param.SetString(pathToFile);
		const DescID rsDomeLightTexturePathId = DescID(DescLevel(REDSHIFT_LIGHT_DOME_TEX0),	DescLevel(REDSHIFT_FILE_PATH));
		if (!domeLight->SetParameter(rsDomeLightTexturePathId, param, DESCFLAGS_SET::NONE))              // <- Fails here
		{
			ApplicationOutput("Failed to write Texture.Path component in RS DomeLight.");
			success = false;
		}
	}

	return success;
}

It allworks fine up to the point when SetParameter() is called using the two-level DescID. It doesn't crash, just fails to set the bitmap into the link field. I must be doing something wrong but I can't see anything obvious. What am I missing?

Cheers,

Steve

I'm sorry to keep harping on about this but there's something odd here. In case the 'RSFILE' custom GUI was causing problems I tried to set sub-channels in the dome light colour. If you look at this code, having created a Redshift dome light I set the object's base color (in the Basic tab) using a two-level DescID method to set a sub-channel. This works correctly. Then I tried the same thing to set the dome light colour - but it doesn't work. Using a single-level DescID does work, however, to set the light intensity:

		// two-level DescID works on BaseObject parameter
		const DescID domeLightBaseColorId = DescID(DescLevel(ID_BASEOBJECT_COLOR), DescLevel(VECTOR_X));
		param.SetFloat(0.25);
		domeLight->SetParameter(domeLightBaseColorId, param, DESCFLAGS_SET::NONE);

		// single-level DescID works correctly on Redshift dome light parameter
		const DescID domeLightMultiplerId = DescID(DescLevel(REDSHIFT_LIGHT_DOME_MULTIPLIER));
		param.SetFloat(5.0);
		domeLight->SetParameter(domeLightMultiplerId, param, DESCFLAGS_SET::NONE);

		// two level DescID does not work on Redshift dome light parameter
		const DescID domeLightColorId = DescID(DescLevel(REDSHIFT_LIGHT_DOME_COLOR), DescLevel(VECTOR_Y));
		param.SetFloat(0.5);
		domeLight->SetParameter(domeLightColorId, param, DESCFLAGS_SET::NONE);

I also tried setting the dome light colour using a single-level DescID and this works without problems, but I couldn't set the individual sub-channels using the above method.

Either I'm being really dumb or there's something not working with this method when accessing a Redshift object.

Hello @spedler,

Please excuse the slight delay. Yes, DescID's can be nasty little fellas from time to time. But it is possible to write this Texture.Path sub-channel; find an example at the end of my posting. For me this successfully creates an RS dome light and sets the texture thingy. The underlying gotcha is here - which I did not respect in my hand-wavey 'pseudo-code' - that you must be quite precise from time to time when defining complex DescID instances for them to work in all places.

The example goes over some technical background and hints at a workflow (Python) for how to gather the here required magic numbers on your own.

Cheers,
Ferdinand

The result:
dc8e4246-45a1-484a-a98b-a554c315f856-image.png
The code:

#include "maxon/assets.h"
#include "c4d_general.h"
#include "lib_description.h"

#include "orslight.h"
#include "drsfile.h"

maxon::Result<void> CreateRSDomeLightWithTexture(BaseDocument* doc);
maxon::Result<void> CreateRSDomeLightWithTexture(BaseDocument* doc)
{
  iferr_scope;

  // This is just some boiler plate code to get a texture from the Asset API/Browser, you will
  // need an internet connection for this to run.

  // Get the Asset API user preferences repository to get a texture asset from it.
  maxon::AssetRepositoryRef repository = maxon::AssetInterface::GetUserPrefsRepository();
  if (!repository)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not retrieve user repository."_s);

  // Get the "bg-final.jpg" texture asset in Textures/Surfaces/Backgrounds, retrieve its URL, ...
  const maxon::Id assetId("file_ef50ab8cc676f624");
  const maxon::AssetDescription asset = repository.FindLatestAsset(
    maxon::Id(), assetId, maxon::Id(), maxon::ASSET_FIND_MODE::LATEST) iferr_return;
  
  // Get the texture URL string.
  const maxon::Url assetUrl = maxon::AssetInterface::GetAssetUrl(asset, true) iferr_return;
  const maxon::String filePath = assetUrl.GetUrl();

  // Allocate the Redshift light object, in S26 we still must use a hardcoded ID, with the next
  // SDK this will change, there we could use the symbol Orslight.
  BaseObject* rsLight = BaseObject::Alloc(1036751);
  if (!rsLight)
    return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate light object."_s);

  // The meat of the example, DescIDs. They can be tricky little fellas, as they can be defined
  // in different levels of verbosity.

  // The most simple form of defining a DescID, we simply wrap an integer, in this case for
  // the light type integer of an RS light. This form can only be used to define single
  // level desc ids.
  const DescID shortDescID = DescID(REDSHIFT_LIGHT_TYPE);

  // This is a slightly more verbose variant where we manually define the first DescLevel.
  const DescID mediumDescID = DescID(DescLevel(REDSHIFT_LIGHT_TYPE));

  // The most verbose variant of defining a DescID with one DescLevel.
  const DescID verboseDescID = DescID(
    DescLevel(REDSHIFT_LIGHT_TYPE,  // The ID of the thing which is being addressed.
              DTYPE_LONG,           // The data type ID of what is being addressed, long/int here.
              0                     // The creator ID of the parameter. This sort of expresses
                                    // which entity created the parameter, often enough you can
                                    // set this to 0 as I do here, and everything will still work
                                    // fine. For native elements this is often MAXON_CREATOR_ID.
    ));

  // All three these IDs are equivalent in this case, in the case of single level DescID you can
  // often get away with being less verbose.

  // We can use all three DescID definitions to write the light type (Dome). We write the type
  // in reverse DescID complexity order, so that it is actually least complex type which 'counts'.
  for (const DescID& did : { verboseDescID , mediumDescID, shortDescID })
  {
    if (!rsLight->SetParameter(did, REDSHIFT_LIGHT_TYPE_DOME, DESCFLAGS_SET::NONE))
      return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not set parameter."_s);
  }
  
  // For the composed multi-level parameter, we must make use of such very precise DescID definition,
  // as Cinema 4D otherwise refuses to write this parameter (SetParamter fails).
  const DescID texturePathId = DescID(
    // 1st DescLevel for the outer unexposed datatype of "Texture".
    DescLevel(
      REDSHIFT_LIGHT_DOME_TEX0, // The parameter ID (12000) for "Texture"
      1036765,                  // The datatype ID for for "Texture", this type is not exposed, 
                                // we must use the raw ID.
      1036751                   // The creator ID, Orslight in this case.
    ),
    // 2nd DescLevel for the inner exposed datatype of "Texture.Path". 
    DescLevel(
      REDSHIFT_FILE_PATH,       // The sub-channel parameter ID (1000) for "Texture.Path"
      DTYPE_STRING,             // The data type ID of this component, string in this case.
      0                         // The creator ID, I just went with undefined here.
    ));

  // You might now ask how I knew all these magic numbers? The answer is I used this little Python
  // script to inspect the description of an Orslight instance.

  /*
import c4d
import typing

op: typing.Optional[c4d.BaseObject]  # The active object, None if unselected

def main() -> None:
    """Will print out the description of op, which can be handy from time to
    time.
    """
    if op is None:
        return

    for data, descId, _ in op.GetDescription(0):
        print (f"{descId = }")
        for cid, value in data:
            print(f"\t{cid = }, {value = }")

if __name__ == '__main__':
    main()
  */

  // When we know the name of the parameter, or better is first DescLevel, it is easy to find this
  // line in the print out which contains all the relevant information.

  //(12000, 1036765, 1036751)
  //  1 Texture
  //  2 Texture
  //  3 3
  //  10 1
  //  21 1036766
  //  1920165492 0
  //  25 0
  //  999 REDSHIFT_LIGHT_DOME_TEX0

  if (!rsLight->SetParameter(texturePathId, filePath, DESCFLAGS_SET::NONE))
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not set parameter."_s);

  doc->InsertObject(rsLight, nullptr, nullptr);
  EventAdd();

  return maxon::OK;
}


// I am not sure how familiar you are with the maxon API, but here is an
// example for how you could call this function from the classic API, i.e.,
// something that is not a maxon::Result.

Bool DoStuff(BaseDocument* doc)
{
  iferr_scope_handler{
    ApplicationOutput("Error: @", err);
    return false;
  };

  CreateRSDomeLightWithTexture(doc) iferr_return;
  // ...
  return true;
};

Hi Ferdinand,

Thank you very much for this very long and helpful post. It's definitely going above and beyond the usual support. I think TBH it ought to become part of the DescID manual in the SDK docs, it's so useful.

I'll need to peruse it at length to be sure I understand how it all works, but with this I'm sure I can get it working. I'll mark this thread as solved when I do.

Thanks again,

Steve

Just tried the code and got it working now, no problems. Many thanks for taking the time to sort this out for me, very much appreciated!

Steve

Hey @spedler,

I am glad that this solved your problem. Regarding putting this in the docs or SDK: I have created a task for doing this, but there is other stuff in front of the queue, so it might take some time before I find the time to do so.

Cheers,
Ferdinand