Skip to content

create_farm_render_instances

CreateFarmRenderInstances

Bases: AbstractCollectRender

Source code in client/ayon_unreal/plugins/publish/create_farm_render_instances.py
 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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
class CreateFarmRenderInstances(publish.AbstractCollectRender):

    order = pyblish.api.CollectorOrder + 0.21
    label = "Create Farm Render Instances"
    families = ["render"]

    def preparing_rendering_instance(self, instance):
        context = instance.context

        data = instance.data
        data["remove"] = True

        ar = unreal.AssetRegistryHelpers.get_asset_registry()

        sequence = ar.get_asset_by_object_path(
            data.get("sequence")).get_asset()

        sequences = [{
            "sequence": sequence,
            "output": data.get("output"),
            "frame_range": (
                data.get("frameStart"), data.get("frameEnd"))
        }]

        for s in sequences:
            self.log.debug(f"Processing: {s.get('sequence').get_name()}")
            subscenes = pipeline.get_subsequences(s.get('sequence'))

            if subscenes:
                for ss in subscenes:
                    sequences.append({
                        "sequence": ss.get_sequence(),
                        "output": (f"{s.get('output')}/"
                                   f"{ss.get_sequence().get_name()}"),
                        "frame_range": (
                            ss.get_start_frame(), ss.get_end_frame() - 1)
                    })
            else:
                # Avoid creating instances for camera sequences
                if "_camera" not in s.get('sequence').get_name():
                    seq = s.get('sequence')
                    seq_name = seq.get_name()

                    product_type = "render"
                    new_product_name = f"{data.get('productName')}_{seq_name}"
                    new_instance = context.create_instance(
                        new_product_name
                    )
                    new_instance[:] = seq_name

                    new_data = new_instance.data

                    new_data["folderPath"] = instance.data["folderPath"]
                    new_data["setMembers"] = seq_name
                    new_data["productName"] = new_product_name
                    new_data["productType"] = product_type
                    new_data["family"] = product_type
                    new_data["families"] = [product_type, "review"]
                    new_data["parent"] = data.get("parent")
                    new_data["level"] = data.get("level")
                    new_data["output"] = s['output']
                    new_data["fps"] = seq.get_display_rate().numerator
                    new_data["frameStart"] = int(s.get('frame_range')[0])
                    new_data["frameEnd"] = int(s.get('frame_range')[1])
                    new_data["sequence"] = seq.get_path_name()
                    new_data["master_sequence"] = data["master_sequence"]
                    new_data["master_level"] = data["master_level"]
                    new_data["review"] = instance.data.get("review", False)
                    new_data["farm"] = instance.data.get("farm", False)

                    self.log.debug(f"new instance data: {new_data}")

    def get_instances(self, context):
        instances = []
        instances_to_remove = []

        current_file = context.data["currentFile"]
        version = 1  # TODO where to get this without change list

        project_name = context.data["projectName"]
        project_settings = context.data['project_settings']
        render_settings = project_settings["unreal"]["render_setup"]
        config_path, config = get_render_config(project_name, render_settings)
        if not config:
            raise RuntimeError("Please provide stored render config at path "
                "set in `ayon+settings://unreal/render_setup/render_config_path`")

        output_ext_from_settings = render_settings["render_format"]
        config = set_output_extension_from_settings(output_ext_from_settings,
                                                    config)

        ext = self._get_ext_from_config(config)
        if not ext:
            raise RuntimeError("Please provide output extension in config!")

        output_settings = config.find_or_add_setting_by_class(
            unreal.MoviePipelineOutputSetting)

        resolution = output_settings.output_resolution
        resolution_width = resolution.x
        resolution_height = resolution.y

        output_fps = output_settings.output_frame_rate
        fps = f"{output_fps.denominator}.{output_fps.numerator}"

        for inst in context:
            instance_families = inst.data.get("families", [])
            product_name = inst.data["productName"]

            if not inst.data.get("active", True):
                continue

            family = inst.data["family"]
            if family not in ["render"]:
                continue

            # skip if local render instances
            if "render.local" in instance_families:
                continue

            if not inst.data.get("farm", False):
                self.log.info("Skipping local render instance")
                continue

            render_queue_path = render_settings["render_queue_path"]
            if not unreal.EditorAssetLibrary.does_asset_exist(
                    render_queue_path):
                # TODO: temporary until C++ blueprint is created as it is not
                #   possible to create renderQueue. Also, we could
                #   use Render Graph from UE 5.4

                master_level = inst.data["master_level"]
                sequence = inst.data["sequence"]
                msg = (f"Please create `Movie Pipeline Queue` "
                       f"at `{render_queue_path}`. "
                       f"Set it Sequence to `{sequence}`, "
                       f"Map to `{master_level}` and "
                       f"Settings to `{config_path}` ")
                raise PublishError(msg)

            # Get current jobs
            jobs = unreal.EditorAssetLibrary.load_asset(
                render_settings["render_queue_path"]
            ).get_jobs()

            # backward compatibility
            task_name = inst.data.get("task") or inst.data.get("task_name")
            self.log.debug(f"Task name:{task_name}")

            ar = unreal.AssetRegistryHelpers.get_asset_registry()
            sequence = (ar.get_asset_by_object_path(inst.data["sequence"]).
                        get_asset())
            if not sequence:
                raise PublishError(f"Cannot find {inst.data['sequence']}")

            # Get current job
            job = next(
                (
                    job
                    for job in jobs
                    if job.sequence.export_text() == inst.data["sequence"]
                ),
                None,
            )
            if not job:
                raise PublishError(
                    f"Cannot find job with sequence {inst.data['sequence']}"
                )

            # current frame range - might be different from created
            frame_start = sequence.get_playback_start()
            # in Unreal 1 of 60 >> 0-59
            frame_end = sequence.get_playback_end() - 1

            inst.data["frameStart"] = frame_start
            inst.data["frameEnd"] = frame_end

            frame_placeholder = "#" * output_settings.zero_pad_frame_numbers
            version = (
                version
                if output_settings.auto_version
                else output_settings.version_number
            )

            exp_file_name = self._get_expected_file_name(
                output_settings.file_name_format,
                ext,
                frame_placeholder,
                job,
                version,
            )

            publish_attributes = {}

            try:
                review = bool(inst.data["creator_attributes"].get("review"))
            except KeyError:
                review = inst.data.get("review", False)

            new_instance = UnrealRenderInstance(
                family="render",
                families=["render.farm"],
                version=version,
                time="",
                source=current_file,
                label=f"{product_name} - {family}",
                productName=product_name,
                productType="render",
                folderPath=inst.data["folderPath"],
                task=task_name,
                attachTo=False,
                setMembers='',
                publish=True,
                name=product_name,
                resolutionWidth=resolution_width,
                resolutionHeight=resolution_height,
                pixelAspect=1,
                tileRendering=False,
                tilesX=0,
                tilesY=0,
                review=review,
                frameStart=frame_start,
                frameEnd=frame_end,
                frameStep=1,
                fps=fps,
                publish_attributes=publish_attributes,
                file_names=[exp_file_name],
                app_version=f"{UNREAL_VERSION.major}.{UNREAL_VERSION.minor}",
                output_settings=output_settings,
                config_path=config_path,
                master_level=inst.data["master_level"],
                render_queue_path=render_queue_path,
                deadline=inst.data.get("deadline"),
            )
            new_instance.farm = True

            instances.append(new_instance)
            instances_to_remove.append(inst)

        for instance in instances_to_remove:
            self.log.debug(f"Removing instance: {instance}")
            context.remove(instance)
        return instances

    def _get_expected_file_name(
        self,
        file_name_format,
        ext,
        frame_placeholder,
        job: unreal.MoviePipelineExecutorJob,
        version: int,
    ):
        """Calculate file name that should be rendered."""
        sequence_path = job.sequence.export_text()
        map_path = job.map.export_text()

        sequence_name = os.path.splitext(os.path.basename(sequence_path))[0]
        map_name = os.path.splitext(os.path.basename(map_path))[0]

        file_name_format = file_name_format.replace("{sequence_name}", sequence_name)
        file_name_format = file_name_format.replace("{level_name}", map_name)
        file_name_format = file_name_format.replace("{job_name}", job.job_name)
        file_name_format = file_name_format.replace("{version}", f"v{version:03d}")
        file_name_format = file_name_format.replace("{frame_number}", frame_placeholder)
        return f"{file_name_format}.{ext}"

    def get_expected_files(self, render_instance: UnrealRenderInstance):
        """
            Returns list of rendered files that should be created by
            Deadline. These are not published directly, they are source
            for later 'submit_publish_job'.

        Args:
            render_instance (UnrealRenderInstance): to pull anatomy and parts used
                in url

        Returns:
            (list) of absolute urls to rendered file
        """
        start = render_instance.frameStart
        end = render_instance.frameEnd

        base_dir = self._get_output_dir(render_instance)
        expected_files = []
        for file_name in render_instance.file_names:
            if "#" in file_name:
                _spl = file_name.split("#")
                _len = (len(_spl) - 1)
                placeholder = "#"*_len
                for frame in range(start, end+1):
                    new_file_name = file_name.replace(placeholder,
                                                      str(frame).zfill(_len))
                    path = os.path.join(base_dir, new_file_name)
                    expected_files.append(path)

        return expected_files

    def _get_output_dir(self, render_instance):
        """
            Returns dir path of rendered files, used in submit_publish_job
            for metadata.json location.
            Should be in separate folder inside work area.

        Args:
            render_instance (RenderInstance):

        Returns:
            (str): absolute path to rendered files
        """
        # render to folder of project
        output_dir = render_instance.output_settings.output_directory.path
        base_dir = os.path.dirname(render_instance.source)
        output_dir = output_dir.replace("{project_dir}", base_dir)

        return output_dir

    def _get_ext_from_config(self, config):
        """Get set extension in render config.

        Bit weird approach to loop through supported extensions and bail on
        found.
        Assumes that there would be only single extension!

        Arg:
            config (unreal.MoviePipelineMasterConfig): render config
        """
        for ext, cls in SUPPORTED_EXTENSION_MAP.items():
            current_sett = config.find_setting_by_class(cls)
            if current_sett:
                return ext

get_expected_files(render_instance)

Returns list of rendered files that should be created by
Deadline. These are not published directly, they are source
for later 'submit_publish_job'.

Parameters:

Name Type Description Default
render_instance UnrealRenderInstance

to pull anatomy and parts used in url

required

Returns:

Type Description

(list) of absolute urls to rendered file

Source code in client/ayon_unreal/plugins/publish/create_farm_render_instances.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def get_expected_files(self, render_instance: UnrealRenderInstance):
    """
        Returns list of rendered files that should be created by
        Deadline. These are not published directly, they are source
        for later 'submit_publish_job'.

    Args:
        render_instance (UnrealRenderInstance): to pull anatomy and parts used
            in url

    Returns:
        (list) of absolute urls to rendered file
    """
    start = render_instance.frameStart
    end = render_instance.frameEnd

    base_dir = self._get_output_dir(render_instance)
    expected_files = []
    for file_name in render_instance.file_names:
        if "#" in file_name:
            _spl = file_name.split("#")
            _len = (len(_spl) - 1)
            placeholder = "#"*_len
            for frame in range(start, end+1):
                new_file_name = file_name.replace(placeholder,
                                                  str(frame).zfill(_len))
                path = os.path.join(base_dir, new_file_name)
                expected_files.append(path)

    return expected_files