SOLVED How to change render setting before add BatchRender

Is it possible to change the rendering settings before AddFile to BatchRender? I could not find in the documentation how to change the rendering settings.

I want to switch the rendering settings included in the c4d file.
I would appreciate it if you could tell me.

I am not good at English, so please let me know if it is difficult to understand.

Thank you.

    file_paths = glob.glob(f'{DIR}/**/*.c4d')

    br = c4d.documents.GetBatchRender()
    br.Open()

    for i, file in enumerate(file_paths):
        // "**I want to switch the rendering settings included in the c4d file.**"
        br.AddFile(file, i)

Hey @tash,

you got the right idea and your code

rd = doc.GetFirstRenderData()

while True:    
    if rd.GetName() == "typeB":
        break
    rd = rd.GetNext()

is not a loop without an exit condition, it is only that it is a bit ugly one 😉 At some point rd will be None, and then rd.GetName() will raise an AttributeError because the type None has no such method. So, you could write instead:

rd = doc.GetFirstRenderData()

while rd:   # or a bit more verbose as: while isinstance(rd, c4d.documents.RenderData):    
    if rd.GetName() == "typeB":
        break
    rd = rd.GetNext()

The problem would then however be, that this code cannot reach nested render data, e.g., RenderData_1.1 and RenderData_1.2 in the example below.

RenderData_1
    RenderData_1.1
    RenderData_1.2
RenderData_2

Your code also does not have an answer to what happens when there is no "typeB" to be found at all. Find an example of how I would write something like this below. Please keeo in mind that this is still an example that must be finished by yourself.

Cheers,
Ferdinand

"""Provides an example for discovering, loading, modifying, and then saving documents, to be loaded
by the batch renderer.
"""

import c4d
import os
import typing

def Iterate(node: c4d.GeListNode) -> c4d.GeListNode:
    """Walks #node depth first.
    """
    if node is None:
        return

    while node:
        yield node
        for descendant in Iterate(node.GetDown()):
            yield descendant
        node = node.GetNext()

def ModifyDocument(doc: c4d.documents.BaseDocument, renderDataTargetName: str,
                   fallbackRenderData: typing.Optional[c4d.documents.RenderData] = None) -> None:
    """Modifies a document according to what you want to do.

    This is just an example, the function could do anything.

    Args:
        doc: The document to modify.
        renderDataTargetName: The name of the render data to load in the document.
        fallbackRenderData: The render data to use when #renderDataTargetName is not present. Will
        be ignored when None. Defaults to None.
    """
    def _createNewRenderData():
        """Little helper function which inserts #fallbackRenderData into #doc.
        """
        # Cloning is technically not necessary in this case, since we know #fallbackRenderData to
        # be unique for each passed #doc, but if we weren't, we must clone the render data since a
        # node can only be attached to one document (RenderData is also a BaseList2D, i.e., node).
        clone: c4d.documents.RenderData = fallbackRenderData.GetClone(c4d.COPYFLAGS_0)
        doc.InsertRenderDataLast(clone)
        doc.SetActiveRenderData(clone)

    if not isinstance(doc, c4d.documents.BaseDocument):
        raise TypeError(f"Not a document: {doc}")

    # Get the first render data in the document. If there is None, either create the default data 
    # and bail, or just bail when there is no default data.
    rData: c4d.documents.RenderData = doc.GetFirstRenderData()
    if rData is None:
        if fallbackRenderData:
            _createNewRenderData()
        return

    # Loop over all render data in the document.
    for rData in Iterate(rData):
        # We found a match, set the data as the active one and bail.
        if rData.GetName() == renderDataTargetName:
            doc.SetActiveRenderData(rData)
            return

    # When we reach this point it means that #renderDataTargetName was not contained in #doc.
    if fallbackRenderData:
        _createNewRenderData()


def main() -> None:
    """
    """
    # Let the user select a directory to scan for documents to load.
    directory: str = c4d.storage.LoadDialog(
        title="Please select the scene directory", flags=c4d.FILESELECT_DIRECTORY)
    if directory is None:
        return

    # Your render settings to look for and a list to put files into for the Batch render.
    targetRenderSettings: str = "typeB"
    batchRenderFiles: list[str] = []

    # Walk that directory and find everything that is a Cinema 4D file.
    for root, _, files in os.walk(directory):
        for f in files:
            if not f.endswith("c4d"):
                continue
            
            # Load the file into a Cinema 4D document and make up a new path for the modified 
            # document.
            filePath: str = os.path.join(root, f)
            doc: c4d.BaseDocument = c4d.documents.LoadDocument(filePath, c4d.SCENEFILTER_NONE)
            if not isinstance(doc, c4d.documents.BaseDocument):
                raise OSError(f"Failed loading into a c4d document: '{filePath}'")

            name: str = os.path.splitext(doc.GetDocumentName())[0]
            newPath: str = os.path.join(doc.GetDocumentPath(), f"modified_{name}.c4d")

            # doc could not contain #targetRenderSettings, so we build some fallback data to use
            # instead. They could also be loaded from another document, etc.
            fallback: c4d.documents.RenderData = c4d.documents.RenderData()
            fallback[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE
            fallback[c4d.RDATA_PATH] = f"render_{name}"

            # Modify #doc to whatever you want to do.
            ModifyDocument(doc, targetRenderSettings, fallback)

            # Save the document and append the file to the to be rendered files.
            if not c4d.documents.SaveDocument(doc, newPath, c4d.SAVEDOCUMENTFLAGS_NONE, 
                                              c4d.FORMAT_C4DEXPORT):
                raise OSError(f"Could not save {doc} to '{newPath}'.")
            batchRenderFiles.append(newPath)


    # Get the batch renderer.
    batchRender: c4d.documents.BatchRender = c4d.documents.GetBatchRender()

    # Start adding the newly crated files as render jobs and then open the batch render dialog.
    for i, file in enumerate(batchRenderFiles):
        batchRender.AddFile(file, 0)

    batchRender.Open()


if __name__ == '__main__':
    main()

Hello @tash,

welcome to the forum and thank you for reaching out to us.

Is it possible to change the rendering settings before AddFile to BatchRender?

It depends a bit on how you mean that. c4d.documents.BatchRender is inherently file based.

  1. You can certainly change something about a file, e.g., its render settings, but you must then save these changes to disk (to the old or a new location) before you can render them with the batch renderer. So, you would have to iterate over your file paths, load each of them into a BaseDocument with c4d.documents.LoadDocument, carry out your modifications of the document, and then save the document to disk with c4d.documents.SaveDocument. Then you would have to add the file path fm for the modified document to the Batch Renderer. Once the rendering is done, you could optionally remove these documents from disk again.
  2. You can also render BaseDocument instances directly with c4d.documents.RenderDocument. If you would want any sort of queue/batch thing, you would have to implement it yourself in this case though.
  3. Or you hook directly into c4d.modules.net.NetRenderService, the Teams Renderer backend. There you can also add BaseDocument instances as renders.

I personally would go with option 1, as it is the most economical solution. RenderDocument is quite limited and NetRenderService is quite powerful but also nontrivial to use.

Cheers,
Ferdinand

Hi @ferdinand

Thanks for your prompt reply.
I gave you a simple code example, but I was imagining method 1.

carry out your modifications of the document,

doc = c4d.documents.LoadDocument(FILE_NAME, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS)

// Process changes to render settings here

c4d.documents.SaveDocument(doc, f"{DIR}/FOOBAR.c4d", c4d.FORMAT_C4DEXPORT)

I understand that the code will be like this. But... How should this be changed?
For example, if the document render settings exist as typeA, typeB, I don't know how to change from typeA to typeB...

https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d.documents/BaseDocument/index.html#BaseDocument.SetActiveRenderData
I think this is a little different (is this useful when changing advanced settings?)
However, the BaseDocument class doesn't seem to have any other rendering settings.

Thank you for teaching me.
スクリーンショット 2022-08-07 2.44.33.png

@ferdinand

Ah. I found it.
Found hint in C++ SDK Documents.

https://developers.maxon.net/docs/Cinema4DCPPSDK/html/page_manual_renderdata.html

doc = c4d.documents.LoadDocument(FILE_NAME, [ANY_SETTINGS])   

rd = doc.GetFirstRenderData()

while True:    
  if rd.GetName() == "typeB":
    break

  rd = rd.GetNext()

doc.SetActiveRenderData(rd)

// save

I was able to do what I wanted to do!
But please let me know if there is a better way.
This code concerns an infinite loop if the "if" condition is not met.
I'm still new to Python.

thank you for your kindness 😌

Hey @tash,

you got the right idea and your code

rd = doc.GetFirstRenderData()

while True:    
    if rd.GetName() == "typeB":
        break
    rd = rd.GetNext()

is not a loop without an exit condition, it is only that it is a bit ugly one 😉 At some point rd will be None, and then rd.GetName() will raise an AttributeError because the type None has no such method. So, you could write instead:

rd = doc.GetFirstRenderData()

while rd:   # or a bit more verbose as: while isinstance(rd, c4d.documents.RenderData):    
    if rd.GetName() == "typeB":
        break
    rd = rd.GetNext()

The problem would then however be, that this code cannot reach nested render data, e.g., RenderData_1.1 and RenderData_1.2 in the example below.

RenderData_1
    RenderData_1.1
    RenderData_1.2
RenderData_2

Your code also does not have an answer to what happens when there is no "typeB" to be found at all. Find an example of how I would write something like this below. Please keeo in mind that this is still an example that must be finished by yourself.

Cheers,
Ferdinand

"""Provides an example for discovering, loading, modifying, and then saving documents, to be loaded
by the batch renderer.
"""

import c4d
import os
import typing

def Iterate(node: c4d.GeListNode) -> c4d.GeListNode:
    """Walks #node depth first.
    """
    if node is None:
        return

    while node:
        yield node
        for descendant in Iterate(node.GetDown()):
            yield descendant
        node = node.GetNext()

def ModifyDocument(doc: c4d.documents.BaseDocument, renderDataTargetName: str,
                   fallbackRenderData: typing.Optional[c4d.documents.RenderData] = None) -> None:
    """Modifies a document according to what you want to do.

    This is just an example, the function could do anything.

    Args:
        doc: The document to modify.
        renderDataTargetName: The name of the render data to load in the document.
        fallbackRenderData: The render data to use when #renderDataTargetName is not present. Will
        be ignored when None. Defaults to None.
    """
    def _createNewRenderData():
        """Little helper function which inserts #fallbackRenderData into #doc.
        """
        # Cloning is technically not necessary in this case, since we know #fallbackRenderData to
        # be unique for each passed #doc, but if we weren't, we must clone the render data since a
        # node can only be attached to one document (RenderData is also a BaseList2D, i.e., node).
        clone: c4d.documents.RenderData = fallbackRenderData.GetClone(c4d.COPYFLAGS_0)
        doc.InsertRenderDataLast(clone)
        doc.SetActiveRenderData(clone)

    if not isinstance(doc, c4d.documents.BaseDocument):
        raise TypeError(f"Not a document: {doc}")

    # Get the first render data in the document. If there is None, either create the default data 
    # and bail, or just bail when there is no default data.
    rData: c4d.documents.RenderData = doc.GetFirstRenderData()
    if rData is None:
        if fallbackRenderData:
            _createNewRenderData()
        return

    # Loop over all render data in the document.
    for rData in Iterate(rData):
        # We found a match, set the data as the active one and bail.
        if rData.GetName() == renderDataTargetName:
            doc.SetActiveRenderData(rData)
            return

    # When we reach this point it means that #renderDataTargetName was not contained in #doc.
    if fallbackRenderData:
        _createNewRenderData()


def main() -> None:
    """
    """
    # Let the user select a directory to scan for documents to load.
    directory: str = c4d.storage.LoadDialog(
        title="Please select the scene directory", flags=c4d.FILESELECT_DIRECTORY)
    if directory is None:
        return

    # Your render settings to look for and a list to put files into for the Batch render.
    targetRenderSettings: str = "typeB"
    batchRenderFiles: list[str] = []

    # Walk that directory and find everything that is a Cinema 4D file.
    for root, _, files in os.walk(directory):
        for f in files:
            if not f.endswith("c4d"):
                continue
            
            # Load the file into a Cinema 4D document and make up a new path for the modified 
            # document.
            filePath: str = os.path.join(root, f)
            doc: c4d.BaseDocument = c4d.documents.LoadDocument(filePath, c4d.SCENEFILTER_NONE)
            if not isinstance(doc, c4d.documents.BaseDocument):
                raise OSError(f"Failed loading into a c4d document: '{filePath}'")

            name: str = os.path.splitext(doc.GetDocumentName())[0]
            newPath: str = os.path.join(doc.GetDocumentPath(), f"modified_{name}.c4d")

            # doc could not contain #targetRenderSettings, so we build some fallback data to use
            # instead. They could also be loaded from another document, etc.
            fallback: c4d.documents.RenderData = c4d.documents.RenderData()
            fallback[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE
            fallback[c4d.RDATA_PATH] = f"render_{name}"

            # Modify #doc to whatever you want to do.
            ModifyDocument(doc, targetRenderSettings, fallback)

            # Save the document and append the file to the to be rendered files.
            if not c4d.documents.SaveDocument(doc, newPath, c4d.SAVEDOCUMENTFLAGS_NONE, 
                                              c4d.FORMAT_C4DEXPORT):
                raise OSError(f"Could not save {doc} to '{newPath}'.")
            batchRenderFiles.append(newPath)


    # Get the batch renderer.
    batchRender: c4d.documents.BatchRender = c4d.documents.GetBatchRender()

    # Start adding the newly crated files as render jobs and then open the batch render dialog.
    for i, file in enumerate(batchRenderFiles):
        batchRender.AddFile(file, 0)

    batchRender.Open()


if __name__ == '__main__':
    main()

Thank you for your very kindness support.🥺

There are methods I didn't know about Python, so I learned a lot!
I've already assembled the other parts, but I'm going to update it with the code you wrote.
Please let me know if there is anything else!

Cheers!