C++ Shader Plug-In Best Practices



  • Hello there,

    I've taken my first leap into the C++ SDK and experienced some early triumphs and defeats. I'm working slowly on a shader starting from the Xbitmapdistortion SDK example. I have the SDK documentation open in about 20 tabs but still feel unsure about how I'm approaching what I'd like to accomplish.

    I'm hoping to write a bare bones Ptex shader. I don't need to deal with most of the issues that surround Ptex as my only goal is to get a shader that I can bake into UDIMs 😁 I figured I'd start the effort by generating per-polygon UV coordinates. It took me a while but I came up with a working solution which exists mostly in the Output method of my young shader:

    Vector PtexData::Output(BaseShader* chn, ChannelData* cd)
    {
    	if (cd->vd == nullptr) // It took me a while to realize I needed this here :)
    		return Vector(0.0);
    
    	RayPolyWeight weight;
    	RayHitID hit = cd->vd->lhit;
    	Int32 faceid = hit.GetPolygon();
    	/* I'm not currently using the faceid, but I will need it to index into the sub-images
    	of the Ptex file; is this a bad idea? Is there another way?*/
    
    	cd->vd->GetWeights(hit, cd->vd->p, &weight);
    
    	Vector coord = Vector(weight.wa + weight.wb, weight.wb + weight.wc, 0.0);
    
    	return coord;
    }
    

    poly_uvw.png

    So first off I suppose I'm wondering: have I done anything horribly wrong or inefficient in the above code? That leads me to my next question: what is the best practice for loading an external texture (in this case Ptex)? I was not planning to build a Ptex file handler but instead to 'brute force' this and load the required Ptex images into a PtexCache in my InitRender() and use Ptex's getPixel() method to 'sample' directly using the coordinates I've generated. I have several concerns with this approach (do I need to consider MIP levels? even if my only goal is to bake the shader?). Unfortunately I don't even know enough to know whether they're valid concerns or not 🤷

    Does anyone have a suggestion for an acceptable (not necessarily best) approach for doing this?

    Finally, could anyone recommend additional avenues for getting up to speed on modern C++ more generally (this is the first time I've touched C++ since 2001) and the Cinema 4D SDK in particular?



  • Hello,

    Char is just a typedef for maxon::Char, which itself is an alias for char. Using the MAXON types makes your code independent from changes to the underlying type and independent of different types on different platforms.

    If you want to check if the string is empty, why don't you simply check the original?

    String filename = GetString();
    if (filename.IsPopulated())
    {
    	// do stuff
    }
    else
    {
    	// do something else
    }
    

    There is no correct or un-correct way of handling the conversion. The question is, what is the safest way of handling this (memory leaks, nullptrs, etc.). You take ownership of the array returned by GetCStringCopy(). There are different ways of handling that ownership.

    maxon::String has the member function GetCString() which returns an array, that stores the C-array:

    const Filename fn { "C:\\some\\file\\somewhere.txt" };
    
    const maxon::String fileString = fn.GetString();
    
    const maxon::BaseArray<maxon::Char> cstr = fileString.GetCString() iferr_return;
    
    const char* arg = cstr.GetFirst();
    TheOtherFunction(arg);
    

    But to use that, you have to deal with error handling, since GetCString() returns a maxon::Result<>.



  • Hello,

    Loading your texture in your InitRender function is a good approche.
    The Output function is called for each pixel in the scene, you should avoid calculation/process as much as possible to avoid your render time to be too slow.

    About how to create such a shader i'm afraid we are somehow a bit limited in that area (in the SDK team i mean)

    Be sure to handle the message MSG_GETALLASSET properly. This will allow the texture to be collected if needed.

    If you have more specific questions, we can probably ask the dev team.

    About the modern c++ question we don't have too much about this in our documentation :

    Cheers,
    Manuel



  • @m_magalhaes

    Thanks Manuel!

    I realized that there are a couple of specific pieces of the puzzle that I'm not sure how to handle.

    I need to pass file paths between Cinema 4D and Ptex. I think the most straightforward way to do this is to use a CString to go between them, but I ran across several potential ways of approaching this. What is the preferred method? I'm currently trying this in InitRender():

    String filename = data->GetFilename(PTEX_FILE_PATH).GetString();
    Char* filestring = filename.GetCStringCopy();    // I noticed on a page in the docs that Char* was used as opposed to char* what is the reason for that?
    
    if (filestring[0] != '\0') // is this the best way to check for an empty CString?
    {
        ptexTexture->open(filestring, err, true);
        // ...set up additional data based on loaded texture
    }
    
    DeleteMem(filestring); // The SDK says this is necessary when using the GetCStringCopy() method
    

    Is this correct?

    Additionally, I've realized that in my Output() function I will need access to the object's name in order to determine which Ptex file to sample from. Would this be the way to get that information in the Output() function?

    String name;
    BaseObject* obj = cd->vd->op->link;
    
    if (obj != nullptr)    // If I'm checking for nullptr could I just use if(obj) as in Python?
        name = obj->GetName();
    

    Thanks as always for your awesome support! 😁



  • Hello,

    Char is just a typedef for maxon::Char, which itself is an alias for char. Using the MAXON types makes your code independent from changes to the underlying type and independent of different types on different platforms.

    If you want to check if the string is empty, why don't you simply check the original?

    String filename = GetString();
    if (filename.IsPopulated())
    {
    	// do stuff
    }
    else
    {
    	// do something else
    }
    

    There is no correct or un-correct way of handling the conversion. The question is, what is the safest way of handling this (memory leaks, nullptrs, etc.). You take ownership of the array returned by GetCStringCopy(). There are different ways of handling that ownership.

    maxon::String has the member function GetCString() which returns an array, that stores the C-array:

    const Filename fn { "C:\\some\\file\\somewhere.txt" };
    
    const maxon::String fileString = fn.GetString();
    
    const maxon::BaseArray<maxon::Char> cstr = fileString.GetCString() iferr_return;
    
    const char* arg = cstr.GetFirst();
    TheOtherFunction(arg);
    

    But to use that, you have to deal with error handling, since GetCString() returns a maxon::Result<>.



  • @PluginStudent

    Thanks for the suggestions. I will definitely use your super obvious (in hindsight) recommendation to check the emptiness of the original String!

    Could you encapsulate briefly why I might or might not want to take ownership of the CString array? I have a gut feeling that using the GetCString() method you suggested is safer but I have no knowledge to back that up 😁 Error handling is definitely something I need to better familiarize myself with.

    Lastly, when would I need to use the maxon namespace? I sort of assumed that these two would be equivalent:

    const String fileString = fn.GetString();
    const maxon::String fileString = fn.GetString(); // significance of namespace here?
    

    Is it a situation where if String also exists in another namespace then not providing the explicit namespace would result in ambiguity for the compiler?



  • hello,

    thanks @PluginStudent for the answer here :)

    if (obj != nullptr)    // If I'm checking for nullptr could I just use if(obj) as in Python?
    

    The correct way is to check against nullptr, that's a c++11 standard.
    if (obj) could lead to false positive and bugs.

    String is not the same thing as maxon::String. String is the classic API while maxon::String is the Maxon API. You can read more about that in the manual about strings

    Error handling is super eady to use, we got every thing to do so, you should definitely use it. Check Our manual about Error Handling

    Last but not least you can probably use our Url class to handle file or filename. Give it a look

    Cheers,
    Manuel