1
$\begingroup$

I want to render objects from nested collections and save them to a folder created using the collection name.

If the collection has a child collection e.g. Suzanne then it should create a folder structure similar to the collection hierarchy.

So in this case the folder structure will be Cube/Suzanne/

Now there are two mode of rendering.

  1. COMBINED It should render all objects from a particular collection and save them to their corresponding folder.

  2. INDIVIDUAL It should render each object from a particular collection and save it to its corresponding folder.

Example:

enter image description here

import bpy
from bpy.types import Operator
from bpy.props import *
import os


class RENDER_OT_render_frames(Operator):
    bl_label = "Render Frames"
    bl_idname = "redner.render_frames"

    type : EnumProperty(
        items = (
            ('ANIMATION', 'Animation', ''),
            ('VIEW', 'View', ''),
        )
    )

    mode : EnumProperty(
        items = (
            ('COMBINED', 'Combined', 'Render all object together from a collection'),
            ('INDIVIDUAL', 'Individual', 'Render individual object from a collection'),
        )
    )

    def create_output_folder(self, path):
        if os.path.isdir(path):
            return
        os.makedirs(path)

    def render_combined_objects(self, context, collection: bpy.types.Collection, filepath: str, name: str):
        for object in bpy.data.objects:
            if object.type not in {"CAMERA", "LIGHT"}:
                object.hide_render = True
                object.hide_viewport = True

        if collection.objects:
            if self.type == 'ANIMATION':
                for obj in bpy.data.objects:
                    if obj in list(collection.objects):
                        obj.hide_render = False

                context.scene.render.filepath = filepath + name.strip() + '-'
                bpy.ops.render.render("INVOKE_DEFAULT")

            elif self.type == 'VIEW':
                for obj in bpy.data.objects:
                    if obj in list(collection.objects):
                        obj.hide_viewport = False

                context.scene.render.filepath = filepath + name.strip() + '-'
                bpy.ops.render.opengl()

        for col in collection.children:
            if not col.hide_viewport:
                path = bpy.path.abspath(os.path.join(filepath, col.name.strip(), ''))
                self.create_output_folder(path)
                self.render_combined_objects(context, collection=col, filepath=path, name=col.name)

    def render_individual_object(self, context, collection: bpy.types.Collection, filepath: str):
        for object in bpy.data.objects:
            if object.type not in {"CAMERA", "LIGHT"}:
                object.hide_render = True
                object.hide_viewport = True

        if collection.objects:
            for object in collection.objects:
                if self.type == 'ANIMATION':
                    object.hide_render = False
                    context.scene.render.filepath = filepath + object.name.strip() + '-'
                    bpy.ops.render.render("INVOKE_DEFAULT")
                    object.hide_render = True
                elif self.type == 'VIEW':
                    object.hide_viewport = False
                    context.scene.render.filepath = filepath + object.name.strip() + '-'
                    bpy.ops.render.opengl()
                    object.hide_viewport = True

        for col in collection.children:
            if not col.hide_viewport:
                path = bpy.path.abspath(os.path.join(filepath, col.name.strip(), ''))
                self.create_output_folder(path)
                self.render_individual_object(context, collection=col, filepath=path)

    def execute(self, context):
        self.rendering = True
        self.collection = bpy.data.collections['Cube']
        self.path = bpy.app.tempdir

        self._timer = context.window_manager.event_timer_add(0.5, window=context.window)
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        if event.type == 'TIMER':
            if self.rendering:

                filepath = os.path.join(self.path, self.collection.name.strip(), '')
                self.create_output_folder(filepath)

                if self.mode == 'COMBINED':
                    self.render_combined_objects(context, collection=self.collection, filepath=filepath, name=self.collection.name)
                else:
                    self.render_individual_object(context, collection=self.collection, filepath=filepath)

                for object in bpy.data.objects:
                    if object.type not in {"CAMERA", "LIGHT"}:
                        object.hide_render = False
                        object.hide_viewport = False

                self.rendering = False

            else:
                context.window_manager.event_timer_remove(self._timer)
                return {"FINISHED"} 

        return {"PASS_THROUGH"}


def register():
    bpy.utils.register_class(RENDER_OT_render_frames)


def unregister():
    bpy.utils.unregister_class(RENDER_OT_render_frames)


if __name__ == "__main__":
    register()

But when I try to render child collection i.e. Suzanne it crashes blender.

enter image description here

Please provide a better solution.

$\endgroup$

1 Answer 1

0
$\begingroup$

your code crashes blender because of a EXCEPTION_ACCESS_VIOLATION. For further information, see https://docs.blender.org/api/3.3/info_gotcha.html#help-my-script-crashes-blender.

Here is a solution to your problem using bpy.app.handlers.render_complete. Also see https://docs.blender.org/api/3.3/bpy.app.handlers.html.

import bpy
from bpy.types import Operator
from bpy.props import *
import os
from .utils import *
from contextlib import suppress



class RENDER_OT_render_frames(Operator):
    bl_label = "Render Frames"
    bl_idname = "redner.render_frames"

    type : EnumProperty(
        items = (
            ('ANIMATION', 'Animation', ''),
            ('VIEW', 'View', ''),
        )
    )
    
    mode : EnumProperty(
        items = (
            ('COMBINED', 'Combined', 'Render all object together from a collection'),
            ('INDIVIDUAL', 'Individual', 'Render individual object from a collection'),
        )
    )
    
    def create_output_folder(self, path):
        if os.path.isdir(path):
            return
        os.makedirs(path)

    @classmethod
    def poll(cls, context):
        mass_export = context.scene.mass_export
        return mass_export.source_collection and mass_export.path

    # somehow it needs 3 inputs, instead of expected 2 for self, scene
    def render_complete(self, dummy1, dummy2):
        self.rendering_completed = True

    def render(self, context, collection, filepath):
        self.rendering_completed = False
        mass_export = context.scene.mass_export
        self.create_output_folder(filepath)

        if mass_export.type == 'COMBINED':
            self.render_combined_objects(context, collection=collection, filepath=filepath, name=collection.name)
        elif mass_export.type == 'INDIVIDUAL':
            self.render_individual_object(context, collection=collection, filepath=filepath)
    
    def render_combined_objects(self, context, collection: bpy.types.Collection, filepath: str, name: str):
        include_objects = [bpy.data.objects[name] for name in self.include_objects_name]
        for object in bpy.data.objects:
            if object.type not in {"CAMERA", "LIGHT"}:
                object.hide_render = True
                object.hide_viewport = True

            if object in include_objects:
                object.hide_render = False
                object.hide_viewport = False

        for col in collection.children:
            if not col.hide_viewport:
                path = bpy.path.abspath(os.path.join(filepath, col.name.strip(), ''))
                self.collection_names.append((col.name, path))
                
        if not collection.objects:
            return
        
        if self.type == 'ANIMATION':
            for obj in collection.objects:
                obj.hide_render = False

            context.scene.render.filepath = filepath + name.strip() + '-'
            bpy.ops.render.render("INVOKE_DEFAULT")
            
        elif self.type == 'VIEW':
            for obj in collection.objects:
                obj.hide_viewport = False

            context.scene.render.filepath = filepath + name.strip() + '-'
            bpy.ops.render.opengl()

    def render_individual_object(self, context, collection: bpy.types.Collection, filepath: str):
        for object in bpy.data.objects:
            if object.type not in {"CAMERA", "LIGHT"}:
                object.hide_render = True
                object.hide_viewport = True

            if object in self.include_objects:
                object.hide_render = False
                object.hide_viewport = False

        for col in collection.children:
            if not col.hide_viewport:
                path = bpy.path.abspath(os.path.join(filepath, col.name.strip(), ''))
                self.collection_names.append((col.name, path))

        if not collection.objects:
            return
        
        for object in collection.objects:
            if self.type == 'ANIMATION':
                object.hide_render = False
                context.scene.render.filepath = filepath + object.name.strip() + '-'
                bpy.ops.render.render("INVOKE_DEFAULT")
                object.hide_render = True
            elif self.type == 'VIEW':
                object.hide_viewport = False
                context.scene.render.filepath = filepath + object.name.strip() + '-'
                bpy.ops.render.opengl()
                object.hide_viewport = True

    def execute(self, context):
        self.rendering = True
        self.collection_names = []

        collection = bpy.data.collections['Cube']
        path = bpy.app.tempdir

        self.timer = context.window_manager.event_timer_add(0.1, window=context.window)
        context.window_manager.modal_handler_add(self)
        bpy.app.handlers.render_complete.append(self.render_complete)
        
        filepath = bpy.path.abspath(os.path.join(path, collection.name.strip(), ''))
        self.render(context, collection, filepath)
        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        if event.type != 'TIMER':
            return {'PASS_THROUGH'}
        
        # starts a new rendering part if possible
        if self.rendering and self.rendering_completed:
            if len(self.collection_names) == 0:
                self.clear(context)
                return {"RUNNING_MODAL"}
            collection_name, filepath = self.collection_names.pop(0)
            collection = bpy.data.collections[collection_name]
            self.render(context, collection, filepath)
            return {"RUNNING_MODAL"}
        
        if self.rendering:
            return {"PASS_THROUGH"}
        
        # processed after rendering is done
        # add some code if you need
        return {"FINISHED"}

    def clear(self, context):
        with suppress(ValueError):
            bpy.app.handlers.render_complete.remove(self.render_complete)
        self.rendering = False
        with suppress(AttributeError):
            context.window_manager.event_timer_remove(self.timer)
            
def register():
    bpy.utils.register_class(RENDER_OT_render_frames)


def unregister():
    bpy.utils.unregister_class(RENDER_OT_render_frames)


if __name__ == "__main__":
    register()

This will render the first collection and set up a handler to wait for one collection rendering to be finished. If the handler indicates that rendering is completed, the next collection gets rendered until the list collection_names is empty. The list collection_names is used to store a tuple of (collection_name, filepath) for the collection to be rendered to the given filepath. Therefore, a new collection is added to the rendering process with collection_names.append((collection.name, filepath)).

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .