Get all textures for a material
-
Hi- I'm a bit stumped and looking for some direction. I have a file with multiple objects in it. I'd like to select 10 of those objects and export each one as an individual fbx file that has only a single object in it. 10 selected objects equates to 10 fbx files. That part I have working.
I'd also like to collect the textures associated with the single material that has been applied to the single object in the fbx.
I tried using doc.GetAllAssets() and set the flag to textures only but I could not find a way to link the single models to the correct texture files in the list. Maybe there is an identifier in that list that I'm not seeing that will allow me to find only the texture files I want versus the list of all textures used?
Since I couldn't figure that out I moved on to doc.GetAllTextures() . I feel like the optional [List[c4d.C4DAtom]] is the thing I need to use in order to get the texture file list for the single, selected model instead of the all of the textures for the entire document.
models = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE) for obj in (models): #theAssets = c4d.documents.GetAllAssets(doc, False, lastPath, c4d.ASSETDATA_FLAG_TEXTURESONLY) theTextures = doc.GetAllTextures(False, [obj]) print(theTextures)
The script returns a c4d.BaseContainer. I'm scratching my head on what to do with that. Am I on the right path or am I missing something?
Any tips on where to look next or reference material on how to get my texture file names out of that BaseContainer will be greatly appreciated.
thanks,
.del -
I found a way around what I want to do by using this -
c4d.documents.SaveProject(newDoc, c4d.SAVEPROJECT_NONE|c4d.SAVEPROJECT_ASSETS|c4d.SAVEPROJECT_DIALOGSALLOWED, targetPath, theAssets)I didn't realize that I could skip saving the actual project file but instead use the call just to gather textures. It's probably not very clean creating a new document and isolating my selected models to it just for the purpose of texture collection but it gets me through this jam while I learn more about how to do it the right way.
-
Hello @del,
Thank you for reaching out to us. I am glad that you found your solution! I am however a bit confused regarding what you exactly what you want to do, but this might help:
- c4d.documents.IsolateObjects should be the way to go for the first part of your task.
- c4d.documents.GetAllAssets is deprecated since S22, you must use c4d.documents.GetAllAssetsNew instead.
- In C++ the actual mechanism to update assets is the message
MSG_GETALLASSETS
. In Python the message is however only supported as a received message and one must use the methods mentioned above to overwrite asset paths. I have provided an example below. - This task comes with an inherent complexity when dealt with manually instead of using
c4d.documents.SaveProject
. This happens when the (texture) assets are buried within node systems. We talked in this thread in more detail about the subject. I also provided a code example there. In the example given here, I ignore the Nodes API case.
Cheers,
FerdinandResult:
Code:
"""Demonstrates isolating objects into FBX files while also copying their texture assets to a new custom location. This example does not deal with assets used in node graphs. See [1] for doing this. References: [1] Change textures path from GetAllAssetNew in the new node editor. url: https://developers.maxon.net/forum/topic/14388 """ import c4d import os import shutil doc: c4d.documents.BaseDocument # The active document TARGET_PATH: str = "E:\\temp\\scenes\\" # The path to copy objects and textures to. if not os.path.exists(TARGET_PATH): os.makedirs(TARGET_PATH) def main() -> None: """Runs the example. """ # For each selected object in #doc ... for obj in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN): # Determine a path name which is still free in #TARGET_PATH based on the name of the object. name: str = obj.GetName() i: int = 1 while name in os.listdir(TARGET_PATH): name = f"{obj.GetName()}_{i}" i += 1 docPath: str = os.path.join(TARGET_PATH, name) os.makedirs(docPath) # Isolate the object and iterate over all texture assets in that document. isoDoc: c4d.documents.BaseDocument = c4d.documents.IsolateObjects(doc, [obj]) assetCollection: list[dict[str, any]] = [] res: int = c4d.documents.GetAllAssetsNew( isoDoc, False, "", c4d.ASSETDATA_FLAG_TEXTURESONLY, assetCollection) if res == c4d.GETALLASSETSRESULT_FAILED: raise RuntimeError("Could not access texture assets for scene.") for item in assetCollection: assetExists: bool = item.get("exists", False) nodePath: str = item.get("nodePath", "") oldPath: str = item.get("filename", "") owner: c4d.BaseList2D | None = item.get("owner", None) paramId: int = item.get("paramId", c4d.NOTOK) # We skip over all non-existing assets. We also skip Asset API assets, i.e., files which # are in the "asset" scheme. We could also localize them, but that would be up to you to # do. Finally we skip all assets which have a node path, i.e., where the asset is used # inside a node graph. See the other thread for the last case. if not assetExists or oldPath.startswith("asset:") or nodePath != "": print ("Skipping over non-existing, Asset API, or Nodes API related asset.") continue # Copy the file when it has not already been copied. This assumes that there are no # name collisions between textures in a document, i.e., that there is not a material # which links to "foo/color.jpg" and one that links to "bar/color.jpg". You could # make this copying more sophisticated to deal with that problem. textureFile: str = os.path.split(oldPath)[1] newPath: str = os.path.join(docPath, textureFile) if not os.path.exists(newPath): shutil.copy(oldPath, newPath) print (f"Copying texture '{textureFile}' to new path.") # Overwrite the texture path in the owning node. owner[paramId] = newPath # Save the document as an FBX file. fbxPath: str = os.path.join(docPath, f"{name}.fbx") c4d.documents.SaveDocument( isoDoc, fbxPath, c4d.SAVEDOCUMENTFLAGS_NONE, c4d.FORMAT_FBX_EXPORT) print (f"Saving '{name}' to '{fbxPath}'") if __name__ == "__main__": main()
-
Thanks @ferdinand for the reply and the example.
You are correct that my current solution uses IsolateObjects and creates a new doc. Since that doc only has the single mesh and materials in it I can use SaveProject to collect the textures.
In reference to GetAllAssets vs GetAllAssetsNew - I had originally written an exporter in R19 and pulled some of my script forward to begin this new version. I saw the new version for this but I kept getting an error and the old command still worked. I didn't understand what the last parameter was supposed to be. The documentation shows this - assetList (List[Dict]). After awhile I gave up because the old command still worked. I know that isn't the right thing to do but when the clock is ticking I have to go with what I can get working. Your example sheds more light on how I have to format that last parameter. I'm going to give it a try.
Ultimately I was trying to create a different solution that didn't require creating an isolation doc. I feel like I should be able to iterate through a list of selected objects and export them as fbx files without creating temporary documents. I have that working. The next step is to collect the textures for those objects. I can get the material for each object and crawl through the shader tree looking for bitmaps but I was hoping for something that was going to be quicker to set up. GetAllTextures() looked promising but I struggled to get it to work and I haven't been able to find any threads or examples that.
thanks,
.del -
Hey @del,
Well, my example shows you how to do it without crawling. The larger example in the other thread also shows how to deal with node graphs. I am not quite sure what you mean with 'I feel like I should be able to iterate through a list of selected objects and export them as fbx files without creating temporary documents'. When you want to eat a cake, you must either acquire one first. The same goes for documents, when you want to save a document containing only once object, you must first create it. Not quite sure what could be easier than using
.IsolateObjects
although I might misunderstand you here.Cheers,
Ferdinand -
That makes total sense @ferdinand.
Kind of have to start at the beginning to understand where I'm coming from. I found myself exporting individual .fbx files on a more regular basis but only a couple at a time so it was not a problem to do it in the interface. I added the Export Selected Objects As to my toolbar for ease of use as it fills in the fbx file name with the selected objects name by default. I have my fbx export preferences preset to Selection Only. This work flow has gotten me into a habit of clicking a button to open the export panel and then clicking ok. Easy.
That brings us to this week where I have to export 160 objects. Writing a script to do exactly what I was doing was easy enough. But then I got greedy and wanted to collect the textures as well. We don't use embedded textures in our fbx so collecting them for transfer has been a manual process. That's when I was faced with two options.
- Get a cake (I love that analogy by the way) I do this for many of my other exports like .c4d, .obj and .gltf
- Leverage the built in Selection Only aspect of the fbx exporter and then simply use Python to copy some texture files from my tex folder to my delivery folder. It felt like it would be a simpler, lower overhead solution versus creating a 'donor' doc. Or at least it feels like it should be simple. There's even a function called GetAllTextures and an option to have it restricted to a single model rather than from the entire doc.
I'm ok with how I had to make it in the end but now I'm really curious how the GetAllTextures() works in case I can leverage it for this or something else down the road.
The FBX exporter is unique in Cinema since Maxon added the Selection Only option to it. If it weren't for that thing I'd be at the bakery....all.....day.....long
import c4d import os from c4d import gui from c4d import documents from c4d import plugins def listModels(): selected = [] rawSelection = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE) for item in rawSelection: if item.CheckType(5100) or item.CheckType(5140): # PolygonalObject or Null selected.append(item) rawSelection = [] return selected models = listModels() if models == []: message = "You must select at least one object" c4d.gui.MessageDialog(message, c4d.GEMB_OK) else: gui.SetMousePointer(c4d.MOUSE_BUSY) c4d.StatusSetSpin() # Get fbx export plugin, plug = plugins.FindPlugin(c4d.FORMAT_FBX_EXPORT, c4d.PLUGINTYPE_SCENESAVER) if plug is None: print("noPlug") op = {} # Send MSG_RETRIEVEPRIVATEDATA to fbx export plugin if plug.Message(c4d.MSG_RETRIEVEPRIVATEDATA, op): # BaseList2D object stored in "imexporter" key holds the settings fbxExport = op["imexporter"] # Define the settings fbxExport[c4d.FBXEXPORT_FBX_VERSION] = c4d.FBX_EXPORTVERSION_NATIVE fbxExport[c4d.FBXEXPORT_SELECTION_ONLY] = True fbxExport[c4d.FBXEXPORT_TRIANGULATE] = True fbxExport[c4d.FBXEXPORT_SAVE_NORMALS] = True fbxExport[c4d.FBXEXPORT_UP_AXIS] = c4d.FBXEXPORT_UP_AXIS_Y fbxExport[c4d.FBXEXPORT_FLIP_Z_AXIS] = False fbxExport[c4d.FBXEXPORT_MATERIALS] = True fbxExport[c4d.FBXEXPORT_EMBED_TEXTURES] = False fbxExport[c4d.FBXEXPORT_BAKE_MATERIALS] = False #check for target directory targetPath = os.path.join(doc.GetDocumentPath(), "FBX_Exports") if os.path.exists(targetPath) == False: #makes a new directory to store the converted files in os.mkdir(targetPath) for obj in (models): fbxName = c4d.BaseList2D.GetName(obj) + ".fbx" #get the name of the model fbxPath = os.path.join(targetPath,fbxName) # Get the texture files and a save a c4d file for rendering theTextures = doc.GetAllTextures(False, [obj]) print(theTextures) # if I can get the textures I will write some shutil.copyFile type of stuff here #export the fbx if c4d.documents.SaveDocument(newDoc, fbxPath, c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST, c4d.FORMAT_FBX_EXPORT) == False: if gui.MessageDialog("Export failed!" + " - " + fbxName, c4d.GEMB_OKCANCEL) == c4d.GEMB_R_CANCEL: break c4d.gui.SetMousePointer(c4d.MOUSE_NORMAL) gui.MessageDialog("Export Complete", c4d.GEMB_OK) c4d.StatusClear()
-
Hey @del,
well, you can use
GetAllTextures
but the method does indeed what it says, it gets you all the texture paths for a given set of scene elements. But other thanGetAllAssetsNew
,GetAllTextures
will throw the sematic relations to the owning scene elements and parameters out of the window. So, you can use it to find the textures elements, but for replacement tasks it cannot be used. E.g., this:import c4d doc: c4d.documents.BaseDocument # The active document def main() -> None: """ """ # Get the texture paths for all materials in the scene #doc. textureData: c4d.BaseContainer = doc.GetAllTextures(ar=doc.GetMaterials()) for key, value in textureData: print (key, value) if __name__ == '__main__': main()
Will spit out this:
1 E:\misc\images\particles.png 2 E:\misc\images\Maxon-Computer-Logo.jpg
On this scene:
I.e., it will find the bitmap in the color channel of the
M1
material, as well as the bitmap buried in a Filter shader in the bump channel of theM2
material.Cheers,
Ferdinand -
Thanks Ferdinand. I'll take a stab at it again. I have to start by learning more about your Python syntax.
Did I mention I'm a 3D/mograph guy that pretends to understand python and the c4d sdk?
Thanks for taking the time to help people like me out. I try to do what I can with the documentation, forums and search but sometimes it's the syntax in the documentation that I don't quite understand. That's the downside for me. I learn a little more each time though and that helps.
Thanks again for the example. I'll see if I can get GetAllTextures() to work for me.
-
Hello @del,
without further questions or postings, we will consider this topic as solved by Wednesday 31/05/2023 and flag it accordingly.
Thank you for your understanding,
Maxime. -