diff --git a/colorbleed/plugins/fusion/create/create_saver.py b/colorbleed/plugins/fusion/create/create_saver.py new file mode 100644 index 0000000000..75581da9cc --- /dev/null +++ b/colorbleed/plugins/fusion/create/create_saver.py @@ -0,0 +1,21 @@ +import avalon.api +from avalon import fusion + + +class CreateTiffSaver(avalon.api.Creator): + + name = "tiffDefault" + label = "Create Tiff Saver" + hosts = "fusion" + family = "colorbleed.imagesequence" + + def process(self): + + comp = fusion.get_current_comp() + with fusion.comp_lock_and_undo_chunk(comp): + args = (-32768, -32768) # Magical position numbers + saver = comp.AddTool("Saver", *args) + saver.SetAttrs({ + "TOOLS_Name": self.data.get("name", self.name), + 'TOOLST_Clip_FormatID': {1.0: 'TiffFormat'}, + }) diff --git a/colorbleed/plugins/fusion/load/load_alembic_camera.py b/colorbleed/plugins/fusion/load/load_alembic_camera.py new file mode 100644 index 0000000000..f2ed66fc9c --- /dev/null +++ b/colorbleed/plugins/fusion/load/load_alembic_camera.py @@ -0,0 +1,27 @@ +from avalon import api + + +class FusionLoadAlembicCamera(api.Loader): + """Load image sequence into Fusion""" + + families = ["colorbleed.camera"] + representations = ["ma"] + + label = "Load sequence" + order = -10 + icon = "play-circle" + color = "orange" + + def load(self, context, name, namespace, data): + """""" + + from avalon.fusion import (imprint_container, + get_current_comp, + comp_lock_and_undo_chunk) + + current_comp = get_current_comp() + with comp_lock_and_undo_chunk(current_comp): + tool = current_comp.SurfaceAlembicMesh() + tool.SetData("TOOLS_NameSet", ) + + pass diff --git a/colorbleed/plugins/fusion/publish/_validate_filenames.py b/colorbleed/plugins/fusion/publish/_validate_filenames.py new file mode 100644 index 0000000000..7f7764fb17 --- /dev/null +++ b/colorbleed/plugins/fusion/publish/_validate_filenames.py @@ -0,0 +1,52 @@ +import os + +import pyblish.api + + +class ValidateFileNames(pyblish.api.Validator): + """Ensure all file names follow the same structure + + Filename should have identifiable parts: + - name ( example: Avengers_shot010_preview ) + - frame ( example: #### ) + - extension ( example: tiff ) + + The result when rendering frame 1250 would be as follows: + Avengers_shot010_preview.1250.tiff + + When certain parts need to be rendered out separately for some reason it + is advisable to something all the lines of: + Avengers_shot010_character_beauty.1250.tiff + """ + + order = pyblish.api.ValidatorOrder + label = "Validate File Names (Saver)" + families = ["colorbleed.imagesequence"] + hosts = ["fusion"] + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + path = instance.data["path"] + basename = os.path.basename(path) + + parts = basename.split(".") + if len(parts) != 3: + invalid.append(instance) + cls.log.error("%s has %i parts, should be 3" + % (instance, len(parts))) + else: + is_numbers = all(i.isdigit() for i in parts[1]) + if len(parts[1]) != 4 or not is_numbers: + cls.log.error("Number padding is not four digits") + invalid.append(instance) + + return invalid + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Found %i instances with a wrong file name " + "structure" % len(invalid)) diff --git a/colorbleed/plugins/fusion/publish/_validate_frame_range.py b/colorbleed/plugins/fusion/publish/_validate_frame_range.py new file mode 100644 index 0000000000..134180d675 --- /dev/null +++ b/colorbleed/plugins/fusion/publish/_validate_frame_range.py @@ -0,0 +1,19 @@ +import pyblish.api + + +class ValidateFrameRange(pyblish.api.InstancePlugin): + """Validate the frame range of the current Saver""" + + order = pyblish.api.ValidatorOrder + label = "Validate Frame Range" + families = ["colorbleed.imagesequence"] + hosts = ["fusion"] + + @classmethod + def get_invalid(cls, instance): + return [] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Animation content is invalid. See log.") \ No newline at end of file diff --git a/colorbleed/plugins/fusion/publish/collect_comp.py b/colorbleed/plugins/fusion/publish/collect_comp.py new file mode 100644 index 0000000000..7dae0b908b --- /dev/null +++ b/colorbleed/plugins/fusion/publish/collect_comp.py @@ -0,0 +1,22 @@ +import pyblish.api + +from avalon import fusion + + +class CollectCurrentCompFusion(pyblish.api.ContextPlugin): + """Collect current comp""" + + order = pyblish.api.CollectorOrder - 0.4 + label = "Collect Current Comp" + hosts = ["fusion"] + + def process(self, context): + """Collect all image sequence tools""" + + current_comp = fusion.get_current_comp() + assert current_comp, "Must have active Fusion composition" + context.data["currentComp"] = current_comp + + # Store path to current file + attrs = current_comp.GetAttrs() + context.data['currentFile'] = attrs.get("COMPS_FileName", "") diff --git a/colorbleed/plugins/fusion/publish/collect_instances.py b/colorbleed/plugins/fusion/publish/collect_instances.py new file mode 100644 index 0000000000..e04fb1d432 --- /dev/null +++ b/colorbleed/plugins/fusion/publish/collect_instances.py @@ -0,0 +1,84 @@ +import os +import re + +import pyblish.api + + +def get_comp_render_range(comp): + """Return comp's start and end render range.""" + comp_attrs = comp.GetAttrs() + start = comp_attrs["COMPN_RenderStart"] + end = comp_attrs["COMPN_RenderEnd"] + + # Whenever render ranges are undefined fall back + # to the comp's global start and end + if start == -1000000000: + start = comp_attrs["COMPN_GlobalEnd"] + if end == -1000000000: + end = comp_attrs["COMPN_GlobalStart"] + + return start, end + + +class CollectInstances(pyblish.api.ContextPlugin): + """Collect Fusion saver instances""" + + order = pyblish.api.CollectorOrder + label = "Collect Instances" + hosts = ["fusion"] + + def process(self, context): + """Collect all image sequence tools""" + + comp = context.data["currentComp"] + + # Get all savers in the comp + tools = comp.GetToolList(False).values() + savers = [tool for tool in tools if tool.ID == "Saver"] + + start, end = get_comp_render_range(comp) + for tool in savers: + path = tool["Clip"][comp.TIME_UNDEFINED] + + if not path: + self.log.warning("Skipping saver because it " + "has no path set: {}".format(tool.Name)) + continue + + fname = os.path.basename(path) + _subset, ext = os.path.splitext(fname) + + # match all digits and character but no points + match = re.match("([\d\w]+)", _subset) + if not match: + self.log.warning("Skipping save because the file name is not" + "compatible") + subset = match.group(0) + + # Include start and end render frame in label + label = "{subset} ({start}-{end})".format(subset=subset, + start=int(start), + end=int(end)) + + instance = context.create_instance(subset) + instance.data.update({ + "asset": os.environ["AVALON_ASSET"], # todo: not a constant + "subset": subset, + "path": path, + "ext": ext, # todo: should be redundant + "label": label, + "families": ["colorbleed.imagesequence"], + "family": "colorbleed.imagesequence", + "tool": tool # keep link to the tool + }) + + self.log.info("Found: \"%s\" " % path) + + # Sort/grouped by family (preserving local index) + context[:] = sorted(context, key=self.sort_by_family) + + return context + + def sort_by_family(self, instance): + """Sort by family""" + return instance.data.get("families", instance.data.get("family")) diff --git a/colorbleed/plugins/fusion/publish/extract_image_sequence.py b/colorbleed/plugins/fusion/publish/extract_image_sequence.py new file mode 100644 index 0000000000..26264bfcad --- /dev/null +++ b/colorbleed/plugins/fusion/publish/extract_image_sequence.py @@ -0,0 +1,80 @@ +import os +import glob +import re + +import pyblish.api + +_frame_regex = re.compile("[0-9]") + + +class ExtractImageSequence(pyblish.api.Extractor): + """Extract result of saver by starting a comp render + + This will run the local render of Fusion, + """ + + order = pyblish.api.ExtractorOrder + label = "Extract Image Sequence" + families = ["colorbleed.imagesequence"] + hosts = ["fusion"] + + def process(self, context): + + current_comp = context.data["currentComp"] + start_frame = current_comp.GetAttrs("COMPN_RenderStart") + end_frame = current_comp.GetAttrs("COMPN_RenderEnd") + + # todo: read more about Render table form, page 84 + # todo: Think out strategy, create renderSettings instance? + # Build Fusion Render Job + + self.log.info("Starting render") + self.log.info("Start frame: {}".format(start_frame)) + self.log.info("End frame: {}".format(end_frame)) + + result = current_comp.Render() + if result: + + # Get all output paths after render was successful + # Note the .ID check, this is to ensure we only have savers + instances = [i for i in context[:] if i.data["tool"].ID == "Saver"] + for instance in instances: + # Ensure each instance has its files for the integrator + output_path = instance.data["path"] + query = self._create_qeury(output_path) + files = glob.glob(query) + + if "files" not in instance.data: + instance.data["files"] = list() + + print("{} files : {}".format(instance.data["subset"], + len(files))) + instance.data["files"].append(files) + + # Ensure the integrator has stagingDir + instance.data["stagingDir"] = os.path.dirname(output_path) + + def _create_qeury(self, instance): + """Create a queriable string for glob + + Args: + instance: instance of current context (comp) + + Returns: + str + """ + + clipname = instance.data["path"] + clip_dir = os.path.dirname(clipname) + basename = os.path.basename(clipname) + _, ext = os.path.splitext(basename) + + match = re.match("([0-9]{4})", basename) + if not match: + query_name = "{}.*.{}".format(instance.data["subset"], ext[1:]) + else: + query_name = basename.replace(match.group(0), ".*.") + + query = os.path.join(clip_dir, query_name) + + return query diff --git a/colorbleed/plugins/fusion/publish/validate_unique_name.py b/colorbleed/plugins/fusion/publish/validate_unique_name.py new file mode 100644 index 0000000000..6933729eb2 --- /dev/null +++ b/colorbleed/plugins/fusion/publish/validate_unique_name.py @@ -0,0 +1,29 @@ +import pyblish.api + + +class ValidateUniqueSubsetName(pyblish.api.InstancePlugin): + """Ensure all instances have a unique subset name""" + + order = pyblish.api.ValidatorOrder + label = "Validate Unique Subset Names" + families = ["colorbleed.imagesequence"] + hosts = ["fusion"] + + @classmethod + def get_invalid(cls, instance): + + context = instance.context + subset = instance.data["subset"] + for other_instance in context[:]: + if other_instance == instance: + continue + + if other_instance.data["subset"] == subset: + return [instance] # current instance is invalid + + return [] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Animation content is invalid. See log.") \ No newline at end of file