Skip to content

create_render

Create render.

CreateRender

Bases: BlenderCreator

Create render from Compositor File Output node

Source code in client/ayon_blender/plugins/create/create_render.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class CreateRender(plugin.BlenderCreator):
    """Create render from Compositor File Output node"""

    identifier = "io.ayon.creators.blender.render"
    label = "Render"
    description = __doc__
    product_type = "render"
    icon = "eye"

    def _find_compositor_node_from_create_render_setup(self) -> Optional["bpy.types.CompositorNodeOutputFile"]:
        tree = bpy.context.scene.node_tree
        for node in tree.nodes:
            if (
                    node.bl_idname == "CompositorNodeOutputFile"
                    and node.name == "AYON File Output"
            ):
                return node
        return None

    def create(
        self, product_name: str, instance_data: dict, pre_create_data: dict
    ):
        # Force enable compositor
        if not bpy.context.scene.use_nodes:
            bpy.context.scene.use_nodes = True

        variant: str = instance_data.get("variant", self.default_variant)

        if pre_create_data.get("create_render_setup", False):
            # TODO: Prepare rendering setup should always generate a new
            #  setup, and return the relevant compositor node instead of
            #  guessing afterwards
            node = render_lib.prepare_rendering(variant_name=variant)
        else:
            # Create a Compositor node
            tree = bpy.context.scene.node_tree
            node: bpy.types.CompositorNodeOutputFile = tree.nodes.new(
                "CompositorNodeOutputFile"
            )
            project_settings = (
                self.create_context.get_current_project_settings()
            )
            node.format.file_format = "OPEN_EXR_MULTILAYER"
            node.base_path = render_lib.get_base_render_output_path(
                variant_name=variant,
                # For now enforce multi-exr here since we are not connecting
                # any inputs and it at least ensures a full path is set.
                multi_exr=True,
                project_settings=project_settings,
            )

        node.name = variant
        node.label = variant

        self.set_instance_data(product_name, instance_data)
        instance = CreatedInstance(
            self.product_type, product_name, instance_data, self
        )
        instance.transient_data["instance_node"] = node
        self._add_instance_to_context(instance)

        self.imprint(node, instance_data)

        return instance

    def collect_instances(self):
        if not bpy.context.scene.use_nodes:
            # Compositor is not enabled, so no render instances should be found
            return

        super().collect_instances()

        # TODO: Collect all Compositor nodes - even those that are not
        #   imprinted with any data.
        collected_nodes = {
            created_instance.transient_data.get("instance_node")
            for created_instance in self.create_context.instances
        }
        collected_nodes.discard(None)

        # Convert legacy instances that did not yet imprint on the
        # compositor node itself
        for instance in self.create_context.instances:
            instance: CreatedInstance

            # Ignore instances from other creators
            if instance.creator_identifier != self.identifier:
                continue

            # Check if node type is the old object type
            node = instance.transient_data["instance_node"]

            if not isinstance(node, bpy.types.Collection):
                # Already new-style node
                continue

            self.log.info(f"Converting legacy render instance: {node}")
            # Find the related compositor node
            # TODO: Find the actual relevant compositor node instead of just
            #  any
            comp_node = self._find_compositor_node_from_create_render_setup()
            if not comp_node:
                raise RuntimeError("No compositor node found")

            instance.transient_data["instance_node"] = comp_node
            self.imprint(comp_node, instance.data_to_store())

            # Delete the original object
            bpy.data.collections.remove(node)

        # Collect all remaining compositor output nodes
        unregistered_output_nodes = [
            node for node in bpy.context.scene.node_tree.nodes
            if node.bl_idname == "CompositorNodeOutputFile"
            and node not in collected_nodes
        ]
        if not unregistered_output_nodes:
            return

        project_name = self.create_context.get_current_project_name()
        project_entity = self.create_context.get_current_project_entity()
        folder_entity = self.create_context.get_current_folder_entity()
        task_entity = self.create_context.get_current_task_entity()
        for node in unregistered_output_nodes:
            self.log.info("Found unregistered render output node: %s",
                          node.name)
            variant = clean_name(node.name)
            product_name = self.get_product_name(
                project_name=project_name,
                project_entity=project_entity,
                folder_entity=folder_entity,
                task_entity=task_entity,
                variant=variant,
                host_name=self.create_context.host_name,
            )
            instance_data = self.read(node)
            instance_data.update({
                "folderPath": folder_entity["path"],
                "task": task_entity["name"],
                "productName": product_name,
                "variant": variant,
            })

            instance = CreatedInstance(
                self.product_type,
                product_name,
                data=instance_data,
                creator=self,
                transient_data={
                    "instance_node": node
                }
            )
            self._add_instance_to_context(instance)

    def get_instance_attr_defs(self):
        defs = lib.collect_animation_defs(self.create_context)
        defs.extend([
            BoolDef("review",
                    label="Review",
                    tooltip="Mark as reviewable",
                    default=True
            )
        ])
        return defs

    def get_pre_create_attr_defs(self):
        return [
            BoolDef(
                "create_render_setup",
                label="Create Render Setup",
                default=False,
                tooltip="Create Render Setup",
            ),

        ]

    def imprint(self, node: bpy.types.CompositorNodeOutputFile, data: dict):
        # Use the node `mute` state to define the active state of the instance.
        active = data.pop("active", True)
        node.mute = not active
        super().imprint(node, data)

    def read(self, node: bpy.types.CompositorNodeOutputFile) -> dict:
        # Read the active state from the node `mute` state.
        data = super().read(node)

        # On super().collect_instances() it may collect legacy render instances
        # that are not Compositor nodes but Collection objects.
        if isinstance(node, bpy.types.CompositorNodeOutputFile):
            data["active"] = not node.mute

        return data

clean_name(name)

Ensure variant name is valid, e.g. strip spaces from name

Source code in client/ayon_blender/plugins/create/create_render.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def clean_name(name: str) -> str:
    """Ensure variant name is valid, e.g. strip spaces from name"""
    # Entity name regex taken from server code which also applies to
    # product names (which usually
    name_regex = r"^[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$"

    # Replace space with underscore
    clean = name.replace(" ", "")
    # Strip out any remaining invalid characters
    clean = re.sub(r"[^a-zA-Z0-9_.-]", "", clean)
    # Ensure start and end characters are not a dot or dash
    clean = clean.strip(".-")
    # Ensure name is at least 1 character long
    if not clean:
        # Fallback to a default name
        clean = "Main"

    if not re.match(name_regex, clean):
        raise ValueError(f"Failed to create valid name for {name}")
    return clean