Populating a Bitmap Shader without a file
-
Hi,
In our plugin we generate images which we would like to use as texture maps in our modifiers. I've been following theBaseShader
manual and can see that I can create a Bitmap Shader and populate the bitmap with a file:// configure the bitmap shader and the material bitmapShader>SetParameter(ConstDescID(DescLevel(BITMAPSHADER_FILENAME)), imageFile, DESCFLAGS_SET::NONE);
(This is from the C4D documentation). I was wondering if it's possible to populate the
BaseShader
bitmap without saving the image to the disk.Thank you very much,
Daniel -
Hi @danniccs just to let you know that your topic, was not forgotten.
I doubt I will have the time to really look into it, but the correct answers to that is that you should provide an maxon::Url for the filename. There is the RamDiskInterface that have the duty to represent a File System but in the memory. So the idea would be to load your picture data (e.g. your TIFF) in a RamDisk and use this ramdisk within the BITMAPSHADER_FILENAME. But in any case it will need to have the same data structure than the supported picture file format from Cinema 4D.
I will try to come with an example next week.
Cheers,
Maxime. -
Hi Maxime,
Thank you very much for the answer! Sorry I haven't responded in a while, some other issues came up. I'll try using the method you mentioned, the image structure should not be a problem because we save images already generated in Cinema 4D. I'll get back to you once I get a chance to implement it.
Thanks again,
Daniel -
Hi @m_adam,
I've been trying to use a
RamDiskInterface
to store the images I need in memory, but I can't see a way to create a file using it. I can see the CreateLazyFile method, but that's not available before C4D 2024, and I need support back to R25. I am looking into MemoryFileStruct and it seems promising, I was just wondering if theRamDiskInterface
is preferred when creating multiple files in memory.Cheers,
Daniel Bauer -
Hey @danniccs,
first of all, please excuse the long radio silence from our side that should not have happened.
RamDiskInterface
is an option here, but it will make things more complicated.MemoryFileStruct
is indeed an easier option which I would pick when I know that I do not want to reload files in this manner. But what I think is also and even more important to point out, is that all "virtual" bitmap shaders in a scene would be volatile. I.e., you cannot save a shader with such implicitly in memory stored bitmap. Well, you can save the scene and with it the shader, but the shader URL be it "mfs" or "ramdisk" will then point into the void when loading the scene again.In general we do this all the time internally, that we load files in memory into shaders. All
asset:///
urls are effectivelyramdisk:///
URLs under the hood which get loaded on demand. I have written a simplemfs:///
URL scheme based example find it below.Cheers,
FerdinandResult:
Code:
// Provides a simple example for populating a bitmap shader with a file held in memory only. #include "xbitmap.h" static maxon::Result<Bool> InMemoryBitmapShader(BaseDocument* const doc) { // Declare the material and the bitmap shader we will load the virtual file into. BaseMaterial* material = nullptr; BaseShader* shader = nullptr; // The scope handler to clean up in case of an error. iferr_scope_handler { if (shader) { shader->Remove(); BaseShader::Free(shader); } if (material) { material->Remove(); BaseMaterial::Free(material); } return err; }; // Let the user chose an image file from disk, the format does not matter as long as Cinema 4D can read it. Filename source; if (!source.FileSelect(FILESELECTTYPE::IMAGES, FILESELECT::LOAD, "Select the image file to load into ram."_s)) return false; // Read the file into a buffer. AutoAlloc<BaseFile> readHandler; if (readHandler == nullptr) return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate file handler."_s); if (!readHandler->Open(source, FILEOPEN::READ, FILEDIALOG::ANY, BYTEORDER::V_INTEL, MACTYPE_CINEMA, MACCREATOR_CINEMA)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not open file."_s); maxon::Int64 size = readHandler->GetLength(); if (size == 0) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "File is empty."_s); maxon::BaseArray<maxon::Char> readBuffer; readBuffer.Resize(size) iferr_return; if (readHandler->ReadBytes(readBuffer.GetFirst(), size) != size) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not read file."_s); ApplicationOutput("Read file into buffer: @ bytes", size); // Now we are going to write the buffer into a virtual file. The setup is a bit complicated here. The shader expects a // Filename (which these days is just a thin wraper around maxon::Url). To this Filename we then attach a // MemoryFileStruct which is a memory based file handler. We could also use maxon::IoMemoryInterface, instead which is // the modern maxon API counterpart to MemoryFileStruct, just as maxon::UrlInterface is the modern counterpart to // Filename. But I kept things simple here. The actual write access is then done by a BaseFile handler. The would be // again more "modern" ways to do this, but this is the most straight forward way to do it. Filename virtualFile; AutoAlloc<MemoryFileStruct> mfs; if (mfs == nullptr) return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate memory file struct."_s); // Set the mfs as the data of the virtual file and set the file name of the virtual file to the source file name. THIS // IS IMPORTANT. As we will otherwise end up with "mfs:///" as the file name of the file in the shader. Which would // not only be confusing for users, but also means that we would only have one "slot". Under the hood, things are cached by // Cinema 4D, and if we would run this code multiple times, all materials would point to the same bitmap at // "mfs:///" which has been built with the first invocation of the code. I.e., all materials would use the same bitmap even // though the user might have chosen different bitmaps for each material. We can prevent this by making the URLs unique. // This is also the flaw of the mfs approach, one cannot so easily invalidate the cache (other than restarting Cinema 4D). virtualFile.SetMemoryWriteMode(mfs); virtualFile.SetFile(source.GetFile()); AutoAlloc<BaseFile> writeHandler; if (writeHandler == nullptr) return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate file handler."_s); if (!writeHandler->Open(virtualFile, FILEOPEN::WRITE, FILEDIALOG::ANY, BYTEORDER::V_INTEL, MACTYPE_CINEMA, MACCREATOR_CINEMA)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not open file."_s); if (!writeHandler->WriteBytes(readBuffer.GetFirst(), size)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not write file."_s); // Use the virtual file as the source for the shader. // // WARNING: What we do here is obviously volatile, you cannot save a bitmap in this manner with the scene. material = BaseMaterial::Alloc(Mmaterial); if (material == nullptr) return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate material."_s); shader = BaseShader::Alloc(Xbitmap); if (shader == nullptr) return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate shader."_s); if (!shader->SetParameter(ConstDescID(DescLevel(BITMAPSHADER_FILENAME)), virtualFile, DESCFLAGS_SET::NONE)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not set parameter."_s); material->InsertShader(shader); if (!material->SetParameter(ConstDescID(DescLevel(MATERIAL_COLOR_SHADER)), shader, DESCFLAGS_SET::NONE)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not set parameter."_s); doc->InsertMaterial(material); EventAdd(); return true; }
-
Hi Ferdinand,
Thank you very much for the answer! Don't worry about it, I was also dealing with other tasks in the interim. The volatility might be an issue, we might have to write manual code to save scenes if necessary. That would just involve saving the image file somewhere on the actual disk and changing the where the shader points to, right?
Thank you for the example, I'll work on implementing something similar in our code and get back to you if anything comes up.
Cheers,
Daniel -
Hey @danniccs,
I think you misunderstood me a little bit, as I was probably too ambiguous.
As already mentioned,
Filename
is just a wrapper around maxon::UrlInterface that just adds some UI functions like opening a file dialog to the underlying URL system. Amaxon::Url
is an abstraction for uniform resource locators as its name implies. The type supports a wide range of UrlScheme's, like for example,https
,ftps
, orfile
. You can try it yourself in Cinema 4D by enteringhttps://developers.maxon.net/src/media/maxon.png
as the bitmap for a bitmap shader. But aside from these 'common' schemes we also support many specialized schemes that allow you to address things in memory, in an SQL database, in an Asset database, inside a zip file, and many more.
Fig.I: Anything that is a parameter of typeFilename
, i.e., effectively amaxon::Url
, lets you interface with resources over the full width of resource schemes supported by our IO/Url API. Here we load the bitmap for a shader directly from a web resource.Such URL is then only an URL as any other. So, the memory file URL
mfs:///foo.bar
either resolves or not, and is not any different in that aspect than the file URLfile:///c:/foo.bar
. But other than afile
orhttps
or most other schemes, resources for in-memory scheme URLs are volatile. So, when you shut Cinema 4D down, such in-memory resource disappears, while a file on a webserver or your local disk will not. You can also quite easily test this by running the code example shown above, and then just then just copying thatmfs
URL into a different shader or anything else that accepts a file. It will still resolve, because the resource does exist.
Fig. II: The URLmfs:///maxon.jpg
does resolve in a manually created material because I before ran the code example which actually created that resource by me picking a file of that name on my disk.So, you can serialize a scene with in memory URLs such as
mfs
and the URL will save just fine, as it just that, a resource locator. But your resources will disappear once you shut down Cinema 4D.ramdisk
can actually cache things to disk, so it will not happen every time there, but the ram disk cache can also be wiped, and when that happens, you will also need there a way to reestablish your in-memory-data.There are many ways how you can do that, you could for example write a plugin which puts some files into memory each time Cinema 4D is being booted, so other things can address them. You could also develop an on-demand pattern like we use for assets, so that things are actually only loaded/constructed when they are concretely required.
I just blindly answered your technical question here as this thread is owned by Maxime. In general, I would question a bit the necessity of what you are trying to do. There can be cases where you have to do something custom, but I somehow doubt that this is the case here, you would have to discuss with Maxime what your best way is and more importantly explain what you want to achieve. I assume you want to get rid of textures which must be saved next to a document or be shipped with a document, possible options are:
- Just store them on an
https
orftps
file server when you can afford the traffic costs. - Store them as assets inside the asset database which is attached to each document.
- Do something super custom with a plugin and some custom in-memory file system using
MemoryFileStruct
,RamDiskInterface
, or directly their common baseIoMemoryInterface
.
Cheers,
Ferdinand - Just store them on an
-
Hi Ferdinand and Maxime,
I think I was unclear when asking the last question, my bad. I get that the issue would be that we would essentially have a
Filename
that does not resolve if the image was in working memory (using aRamDisk
or aMemoryFileStruct
) and then shut down and re-launched Cinema 4D. I was originally wondering if we could trigger a function (probably using the modifierMessage
function) when saving the scene to save the image on the physical disk at that point, and change theFilename
in the shader accordingly. However we've decided not to try putting the image in memory at all, and directly save it to the disk. I'll try to explain what we're trying to achieve so the situation is a bit clearer, I would love your input on this.We currently have an object consisting of a stack that performs computations. We are implementing functionality so that the entire stack can be baked into a single object, which helps speed up computations. The collapse is reversible, so we want the user to be able to "unbake" the object. We also want the user to be able to save the stack to the physical disk as a "baked" object. If one of the objects in the stack (let's call it object A) uses a texture, we save the texture as an image with the baked object. We essentially serialize the image and use it in the calculations, then save it to the physical disk. However, if the user "unbakes" the object, we need to take the serialized image and generate a texture for object A to use. The texture could be an image found on the disk, a gradient created in C4D, some procedurally generated image, etc.
What we are currently considering is to simply save the texture as an image at the moment that the stack is baked, and use a
Filename
pointing to it if the stack is unbaked. However, if the scene had not been saved before, we would have to create the image as a temp file and copy it to wherever the scene is saved on the disk afterwards. Is there something I might be missing in the Cinema 4D SDK that could improve this approach, or even be a better way to handle the situation?Thanks a lot for the help,
Daniel -
Hey @danniccs,
I am still somewhat doubtful about the usefulness of what you are trying to do. When I boil it down, you basically want to postpone saving a texture file next to a document when the document itself has not been saved yet and you therefore cannot yet derive your texture save path from the document, right? I personally would simply put a check into my code which forces the user to save the document, or provide a centralized "cache" path where such files could be stored.
Cinema 4D does not have the mechanism you seem to be looking for, a texture which resides in RAM until its document is saved to disk, to then serialize itself next to that document and update all URLs in the document.
MemoryFileStruct
is not the right choice here and the whole "in-memory-approach" is neither IMHO. If anything, you could useRamDiskInterface
. It is not just an in-memory file system but also realizes promises/lazy-loading with its method RamDiskInterface::CreateLazyFile. You pass there a delegate function, thecreator
which is called when something tries to actually access the pointed resource. Our Asset API makes heavy use of that concept, under the hood allasset:///
URLs areramdisk:///
URLs which only get resolved, i.e., downloaded from the server into a local cache, when the user actually tries to access them. The Asset Browser somewhat obfuscates that at it always downloads things once a user adds them to a scene. But that is not an intrinsic quality ofasset:///
URLs, they by default wait to the very last moment with downloading their primary content. In the Asset API Examples: Load File Asset Manually I once showed some lower levelRamDiskInterface
handling (but not exactly what you need here). So,RamDiskInterface
is somewhat similar what you are trying to do, but there are question marks for me if it is possible to do what you actually want to do. I would have to try myself. I could unpack things here now in detail, but that is not worth the effort IMHO.There are three sensible approaches I see here:
- For unsaved documents, first store the files in a centralized location. Then, when the document is saved, update all paths in the document to a path next to the document. You could do this:
- Either as a custom
SceneHookData
plugin which listens forMSG_DOCUMENTINFO
of typeMSG_DOCUMENTINFO_TYPE_SAVE_BEFORE
orMSG_DOCUMENTINFO_TYPE_SAVEPROJECT_BEFORE
and then scans the document for elements of your plugin type X and updates all paths. The save path should already have been established when these messages are emitted. For clarity, allMSG_
messages are atom messages, i.e., messages emitted to scene elements viaC4DAtom::Message
or::MultiMessage
and received viaNodeData::Message
. This is why you need aSceneHookData
plugin and not aMessageData
plugin (which can only receive core messages). I.e., you would here have a "spider in the net" which manually shuffles things into place. With this workflow you probably have the most freedoms but also the most work and danger of hitting a technical limitation. - Or implement
MSG_GETALLASSETS
andMSG_RENAMETEXTURES
, i.e., the Asset Inspector workflow (has nothing to do with the Asset API or the Asset Browser). Opposed to the "spider in the net" scene hook approach of (1.), here the management is decomposed into the scene elements that "own" the URLs, e.g., aShaderData
,ObjectData
, orTagData
implementation. The relevant method is againNodeData::Message
. The big disadvantage is here that this whole pipeline is only runs when the user invokes 'Save Project with Assets ..' and not on normal save events.
- Either as a custom
- A more modern and IMHO better approach would be to save your data in the asset repository of the document and then just leave it there. This also works when the document has not yet been saved. The primary disadvantage would be here size. It is okay to attach a few (hundred) MB in this manner to a document. But I would say, for example, that it would be not such a good idea to dump a 5GB cache of some VDB data into the document repo (as your c4d files would then become very large).
Find below a simple sketch for (2.) in Python. We have a quite extensive Asset API documentation which should help you with translating the example.
Cheers,
FerdinandResult
Top: The script ran on a MacOS Cinema 4D instance and created a texture in the document database and then a material which uses this texture. later this document was saved as
test.c4d
. Bottom: The filetest.c4d
was copied to a Windows machine and loaded, the texture file was saved and migrated with the document attached asset database.
Code
"""Demonstrates how to create a media asset stored in the asset repository which is attached to the (loaded) document. """ import c4d import maxon import os import itertools from mxutils import CheckType, CheckIterable doc: c4d.documents.BaseDocument # The currently active document. op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`. def CreateMediaAsset(path: str, doc: c4d.documents.BaseDocument) -> maxon.Url: """Stores the given texture file at #path as a media asset in the document repository of #doc. """ if not os.path.exists(CheckType(path, str)): raise FileNotFoundError(f"File not found: {path}") # Get the asset repository which is associated with the document #doc. repo: maxon.AssetRepositoryRef = doc.GetSceneRepository(create=True) if not repo: raise RuntimeError("Could not access the documents repository.") # Save the texture as a media asset in the document repository. The second return value signals # if the asset already existed in the repository or not, we do not care here. We could also make # it so here that the URL is more descriptive by passing/creating a meaningful asset category, # so that we end up with an URL like `assetdb:///blah/cache/gradient_texture.png` or something like # that. I pass here the empty Id() to StoreAssetStruct() to store the asset in the uncategorized # section of the repository. We end up with the URL `assetdb:///gradient_texture.png`. # # Note: "assetdb" is actually not a scheme/protocol for URLs, but just smoke and mirrors for the # user. The actual asset URL scheme is "asset" and it does not have this path like structure. # The URL will look something like this: # # asset:///file_ae4d716acaccbe88~.png?name=gradient_texture.png&db=Scene (Untitled 1) # # In C++, we can translate between "asset" and "assetdb" URLs using the methods UrlInterface:: # ConvertToUiName and UrlInterface::ConvertFromUiName. Python does not have these methods but # there is somewhere a thread in the forum where I posted a Python approximation for them. description, _ = maxon.AssetCreationInterface.SaveTextureAsset( maxon.Url(path), os.path.basename(path), maxon.StoreAssetStruct(maxon.Id(), repo, repo), (), True) return maxon.AssetInterface.GetAssetUrl(description, True) def CreateTexture(size: tuple[int, int]) -> c4d.bitmaps.BaseBitmap: """Creates a new texture with the given size and fills it with a gradient. """ CheckIterable(size, int, tuple, minCount=2, maxCount=2) bmp: c4d.bitmaps.BaseBitmap = CheckType(c4d.bitmaps.BaseBitmap()) if bmp.Init(size[0], size[1]) != c4d.IMAGERESULT_OK: raise RuntimeError("Failed to initialize bitmap.") for x, y in itertools.product(range(size[0]), range(size[1])): color: c4d.Vector = c4d.Vector(x / size[0], y / size[1], 0) bmp.SetPixel(x, y, int(color.x * 254.99), int(color.y * 254.99), int(color.z * 254.99)) return bmp def main() -> None: """Called by Cinema 4D when the script is being executed. """ if not os.path.exists(__file__): raise FileNotFoundError("You must save the script to disk before running it.") # Create a new texture and save it to disk next to this script. We could also save the # file to RAM with MemoryFileStruct, RamDiskInterface, and so on. But I was too lazy to # implement it here. bmp: c4d.bitmaps.BaseBitmap = CreateTexture((512, 512)) path: str = os.path.join(os.path.dirname(__file__), "gradient_texture.png") if bmp.Save(path, c4d.FILTER_PNG) != c4d.IMAGERESULT_OK: raise RuntimeError("Failed to save the texture to disk.") # Now create a media asset in the document repository of #doc, get its URL, and finally delete # the physical file on disk. url: maxon.Url = CreateMediaAsset(path, doc) os.remove(path) # Create a new material and assign our asset texture stored in the document repository to it. material: c4d.BaseMaterial = CheckType(c4d.BaseMaterial(c4d.Mmaterial)) shader: c4d.BaseShader = CheckType(c4d.BaseShader(c4d.Xbitmap)) material[c4d.MATERIAL_COLOR_SHADER] = shader shader[c4d.BITMAPSHADER_FILENAME] = url.ToString() material.InsertShader(shader) doc.InsertMaterial(material) c4d.EventAdd() if __name__ == '__main__': main()
- For unsaved documents, first store the files in a centralized location. Then, when the document is saved, update all paths in the document to a path next to the document. You could do this:
-
Hey Ferdinand, thanks again for the quick reply.
What we're going to do is basically have a "cache" location, as you said, where we will save the users' textures when they bake the stack. I would also prefer the approach you showed using the document's asset repository, but since the textures might be very heavy, it would probably lead to very large document sizes for a lot of users, especially if they use several textures/have several stacks with textures. I'm working on implementing solution 1.1, using the Message system to move things around. However, thank you very much for the code example, I will probably use something similar with other assets that are not as large as textures.
Cheers,
Daniel