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
|