diff --git a/docs/media/pipeline-overview/shot-production/kitsu_asset_metadata.jpg b/docs/media/pipeline-overview/shot-production/kitsu_asset_metadata.jpg new file mode 100644 index 00000000..da785845 --- /dev/null +++ b/docs/media/pipeline-overview/shot-production/kitsu_asset_metadata.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:361c75dceb613b16448e9187a1872c4f76b38258861df29ec9afee28327009ba +size 13475 diff --git a/docs/media/pipeline-overview/shot-production/kitsu_asset_with_asset_pipeline.jpg b/docs/media/pipeline-overview/shot-production/kitsu_asset_with_asset_pipeline.jpg new file mode 100644 index 00000000..89ae37ca --- /dev/null +++ b/docs/media/pipeline-overview/shot-production/kitsu_asset_with_asset_pipeline.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9693c34b136872960465756bd6c14f6d21f0f2fd0a03da7ca2a57f528989d3d5 +size 13934 diff --git a/docs/media/pipeline-overview/shot-production/kitsu_set_asset.jpg b/docs/media/pipeline-overview/shot-production/kitsu_set_asset.jpg new file mode 100644 index 00000000..e5fac85c --- /dev/null +++ b/docs/media/pipeline-overview/shot-production/kitsu_set_asset.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf54ba1b1a99871c3b92fec372bbe30d4cd906829dfe9110f15fafb7e3914cc3 +size 17644 diff --git a/docs/user-guide/project_tools/usage-build-shot-core.md b/docs/user-guide/project_tools/usage-build-shot-core.md index dbd896fc..f2352b55 100644 --- a/docs/user-guide/project_tools/usage-build-shot-core.md +++ b/docs/user-guide/project_tools/usage-build-shot-core.md @@ -7,29 +7,33 @@ The next step is to create an asset and store that information into the Kitsu Se 3. Follow the [asset pipeline guide](https://studio.blender.org/pipeline/addons/asset_pipeline) to create a new asset collection, ensure these assets are marked as an [Asset in Blender](https://docs.blender.org/manual/en/latest/files/asset_libraries/introduction.html#creating-an-asset). 4. Save the above asset within the directory `your_project_name/svn/pro/assets/char` (or similar depending on type) +## Kitsu Casting +Casting is the process of associating a Kitsu Asset Entity with a given shot, this is how the Shot Builder knows what Assets to link into a given shot. +1. Please follow the [Kitsu Breakdown](https://kitsu.cg-wire.com/getting-started-production/) guide to Cast your assets to shots. + + ## Load Asset Data into Kitsu -To match Assets File to the data on the Kitsu server, we need to first enter the data into the Kitsu server and secondly create an Asset Index. This is a json file that contains the mapping of the asset's name to the asset's filepath. Any collection Marked as an Asset in Blender in the directory your_project/svn/pro/assets will be added to this index. +To match Assets File to the casting breakdown on the Kitsu server, we need to tag the Asset with a filepath and collection. This can be done via the Blender Kitsu Add-On. This data will be used to match a Kitsu Asset with a Collection within a .blend file, that will be linked into all shots that have that Asset "casted in it". -1. Create a matching entry in Kitsu for each asset via the [Kitsu Create Assets guide](https://kitsu.cg-wire.com/first_production/#create-assets) -2. Follow the [Kitsu Breakdown guide](https://kitsu.cg-wire.com/first_production/#create-a-breakdown-list) to assign/cast assets to shots. -3. Create a text [Metadata Column](https://kitsu.cg-wire.com/production_advanced/#create-custom-metadata-columns) with the exact name `slug`. -4. Populate the new `slug` column with the exact name of the asset's collection. -5. Use the [Index Assets Script](https://projects.blender.org/studio/blender-studio-pipeline/src/branch/main/scripts/index_assets) to create an `asset_index.json` file. +1. Open the file for a given Asset. +2. Under the Kitsu>Context Panel, check the following settings. + - **Type** is set to Asset. + - **Asset Type** is set to the correct Asset Type (Collection, Prop, etc) + - **Asset** Field is set to the matching entry on the Kitsu server for the current file. +3. Under the Kitsu>Context>Set Asset sub-panel... + - **Collecton** is set to the Asset's parent collection. + - Run the **Set Kitsu Asset** operator to send the current filepath and selected collection to the Kitsu Server. -**Example of `asset_index.json*`* -```json -{ - "CH-rain": { - "type": "Collection", - "filepath": "your_project/svn/pro/assets/chars/rain/rain.blend" - }, - "CH-snow": { - "type": "Collection", - "filepath": "your_project/svn/pro/assets/chars/snow/snow.blend" - } -} -``` +![Set Kitsu Asset](/media/pipeline-overview/shot-production/kitsu_set_asset.jpg) + +If you are using the Asset Pipeline, the latest publish file will be prompted to confirm using the latest Publish as the Asset target. + +![Publish Asset Pipeline with Set Kitsu Asset](/media/pipeline-overview/shot-production/kitsu_asset_with_asset_pipeline.jpg) + +You should now see the filepath and collection under the Asset's Metadata on the Kitsu. + +![Kitsu Asset Metadata](/media/pipeline-overview/shot-production/kitsu_asset_metadata.jpg) ## Building your First Shot Before building your first shot, you will need to customize your production's Shot Builder hooks. Inside your production’s `assets/scripts/shot-builder` directory the Shot Builder hook file should be stored, based on the [example](https://projects.blender.org/studio/blender-studio-pipeline/src/branch/main/scripts-blender/addons/blender_kitsu/shot_builder/hook_examples) included in the Add-On. This file can be automatically created at the correct directory using an operator in the **Blender Kitsu** Add-On preferences. Hooks are used to extend the functionality of the shot builder, and can be customized on a per project basis. diff --git a/scripts-blender/addons/blender_kitsu/README.md b/scripts-blender/addons/blender_kitsu/README.md index bc615836..d2f65a9f 100644 --- a/scripts-blender/addons/blender_kitsu/README.md +++ b/scripts-blender/addons/blender_kitsu/README.md @@ -296,31 +296,21 @@ The Shot Builder requires shot data including Name, Frame Rate, and Duration to #### Asset Setup ##### Kitsu Server -The Shot Builder requires all Asset to be stored on the Kitsu Server with a [Metadata Column](https://kitsu.cg-wire.com/production_advanced/#create-custom-metadata-columns) with the exact name `slug` exactly matching the name of the asset's collection. - Assets needs to be associated with each shot in your production. Please follow the [Kitsu Breakdown](https://kitsu.cg-wire.com/getting-started-production/) guide to Cast your assets to shots. -##### Asset Index -To match Assets File to the casting breakdown on the Kitsu server, we need to create an Asset Index. This is a json file that contains the mapping of the asset's name to the asset's filepath. Any collection Marked as an Asset in Blender in the directory `your_project/svn/pro/assets` will be added to this index. +##### Assets +To match Assets File to the casting breakdown on the Kitsu server, we need to tag the Asset with a filepath and collection. This can be done via the Blender Kitsu Add-On. This data will be used to match a Kitsu Asset with a Collection within a .blend file, that will be linked into all shots that have that Asset "casted in it". -##### Example `asset_index.json` -```json -{ - "CH-rain": { - "type": "Collection", - "filepath": "your_project/svn/pro/assets/chars/rain/rain.blend" - }, - "CH-snow": { - "type": "Collection", - "filepath": "your_project/svn/pro/assets/chars/snow/snow.blend" - } -} -``` +1. Open the file for a given Asset. +2. Under the Kitsu>Context Panel, check the following settings. + - **Type** is set to Asset. + - **Asset Type** is set to the correct Asset Type (Collection, Prop, etc) + - **Asset** Field is set to the matching entry on the Kitsu server for the current file. +3. Under the Kitsu>Context>Set Asset sub-panel... + - **Collecton** is set to the Asset's parent collection. + - Run the **Set Kitsu Asset** operator to send the current filepath and selected collection to the Kitsu Server. -To create/update the Asset Index: -1. Enter Asset Index directory `cd blender-studio-pipeline/scripts/index_assets` -2. Run using `./run_index_assets.py your_poduction` replace `your_production` with the path to your project's root directory -3. This will create an index file at `your_production/svn/pro/assets/asset_index.py` +You should now see the filepath and collection under the Asset's Metadata on the Kitsu Server. #### Hooks Setup Shot Builder uses hooks to extend the functionality of the shot builder. To create a hook file diff --git a/scripts-blender/addons/blender_kitsu/bkglobals.py b/scripts-blender/addons/blender_kitsu/bkglobals.py index 6ff12c10..559ff5af 100644 --- a/scripts-blender/addons/blender_kitsu/bkglobals.py +++ b/scripts-blender/addons/blender_kitsu/bkglobals.py @@ -87,8 +87,14 @@ MULTI_ASSETS = [ ASSET_COLL_PREFIXES = ["CH-", "PR-", "SE-", "FX-"] + +# Kitsu Constants KITSU_TV_PROJECT = 'tvshow' +# Kitsu Metadata Keys +KITSU_FILEPATH_KEY = "filepath" +KITSU_COLLECTION_KEY = "collection" + RES_DIR_PATH = Path(os.path.abspath(__file__)).parent.joinpath("res") SCENE_NAME_PLAYBLAST = "playblast_playback" diff --git a/scripts-blender/addons/blender_kitsu/context/ops.py b/scripts-blender/addons/blender_kitsu/context/ops.py index 60668eb0..3b58c470 100644 --- a/scripts-blender/addons/blender_kitsu/context/ops.py +++ b/scripts-blender/addons/blender_kitsu/context/ops.py @@ -26,6 +26,7 @@ import bpy from .. import bkglobals, cache, util, prefs from ..logger import LoggerFactory from ..types import TaskType, AssetType +from ..context import core as context_core logger = LoggerFactory.getLogger() @@ -240,11 +241,107 @@ class KITSU_OT_con_detect_context(bpy.types.Operator): return mapping[key] +class KITSU_OT_con_set_asset(bpy.types.Operator): + bl_idname = "kitsu.con_set_asset" + bl_label = "Set Kitsu Asset" + bl_description = ( + "Mark the current file & target collection as an Asset on Kitsu Server " + "Assets marked with this method will be automatically loaded by the " + "Shot Builder, if the Asset is casted to the buider's target shot" + ) + + _published_file_path: Path = None + + use_asset_pipeline_publish: bpy.props.BoolProperty( # type: ignore + name="Use Asset Pipeline Publish", + description=( + "Find the Publish of this file in the 'Publish' folder and use it's filepath for Kitsu Asset`" + "Selected Collection must be named exactly the same between current file and Publish" + ), + default=False, + ) + + @classmethod + def poll(cls, context): + kitsu_props = context.scene.kitsu + if bpy.data.filepath == "": + cls.poll_message_set("Blend file must be saved") + return False + if not bpy.data.filepath.startswith(str(prefs.project_root_dir_get(context))): + cls.poll_message_set("Blend file must be saved in project structure") + return False + if not context_core.is_asset_context(): + cls.poll_message_set("Kitsu Context panel must be set to 'Asset'") + return False + if kitsu_props.asset_type_active_name == "": + cls.poll_message_set("Asset Type must be set") + return False + if kitsu_props.asset_active_name == "": + cls.poll_message_set("Asset must be set") + return False + if not kitsu_props.asset_col: + cls.poll_message_set("Asset Collection must be set") + return False + return True + + def is_asset_pipeline_enabled(self, context) -> bool: + for addon in context.preferences.addons: + if addon.module == "asset_pipeline": + return True + return False + + def is_asset_pipeline_folder(self, context) -> bool: + current_folder = Path(bpy.data.filepath).parent + return current_folder.joinpath("task_layers.json").exists() + + def get_asset_pipeline_publish(self, context) -> Path: + from asset_pipeline.merge.publish import find_latest_publish + + return find_latest_publish(Path(bpy.data.filepath)) + + def invoke(self, context, event): + if self.is_asset_pipeline_enabled(context) and self.is_asset_pipeline_folder(context): + self._published_file_path = self.get_asset_pipeline_publish(context) + if self._published_file_path.exists(): + self.use_asset_pipeline_publish = True + wm = context.window_manager + return wm.invoke_props_dialog(self) + return self.execute(context) + + def draw(self, context): + layout = self.layout + relative_path = self._published_file_path.relative_to(Path(bpy.data.filepath).parent) + box = layout.box() + box.enabled = self.use_asset_pipeline_publish + box.label(text=f"//{str(relative_path)}") + layout.prop(self, "use_asset_pipeline_publish") + + def execute(self, context): + project_root = prefs.project_root_dir_get(context) + if self.use_asset_pipeline_publish: + relative_path = self._published_file_path.relative_to(project_root) + else: + relative_path = Path(bpy.data.filepath).relative_to(project_root) + blender_asset = context.scene.kitsu.asset_col + kitsu_asset = cache.asset_active_get() + if not kitsu_asset: + self.report({"ERROR"}, "Failed to find active Kitsu Asset") + return {"CANCELLED"} + + kitsu_asset.set_asset_path(str(relative_path), blender_asset.name) + self.report( + {"INFO"}, + f"Kitsu Asset '{kitsu_asset.name}' set to Collection '{blender_asset.name}' at path '{relative_path}'", + ) + return {"FINISHED"} + + # ---------REGISTER ----------. classes = [ KITSU_OT_con_productions_load, KITSU_OT_con_detect_context, + KITSU_OT_con_set_asset, ] diff --git a/scripts-blender/addons/blender_kitsu/context/ui.py b/scripts-blender/addons/blender_kitsu/context/ui.py index 9779fed6..40fdca0f 100644 --- a/scripts-blender/addons/blender_kitsu/context/ui.py +++ b/scripts-blender/addons/blender_kitsu/context/ui.py @@ -22,9 +22,7 @@ import bpy from ..context import core as context_core from .. import cache, prefs, ui, bkglobals -from ..context.ops import ( - KITSU_OT_con_detect_context, -) +from ..context.ops import KITSU_OT_con_detect_context, KITSU_OT_con_set_asset class KITSU_PT_vi3d_context(bpy.types.Panel): @@ -113,6 +111,34 @@ class KITSU_PT_vi3d_context(bpy.types.Panel): context_core.draw_task_type_selector(context, col) +class KITSU_PT_set_asset(bpy.types.Panel): + """ + Panel in 3dview that enables browsing through backend data structure. + Thought of as a menu to setup a context by selecting active production + active sequence, shot etc. + """ + + bl_category = "Kitsu" + bl_label = "Set Asset" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 25 + bl_parent_id = "KITSU_PT_vi3d_context" + + @classmethod + def poll(cls, context): + return context_core.is_asset_context() + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + col = layout.column() + col.prop(context.scene.kitsu, "asset_col") + col.operator(KITSU_OT_con_set_asset.bl_idname) + + class KITSU_PT_comp_context(KITSU_PT_vi3d_context): bl_space_type = "NODE_EDITOR" @@ -124,7 +150,12 @@ class KITSU_PT_editorial_context(KITSU_PT_vi3d_context): # ---------REGISTER ----------. # Classes that inherit from another need to be registered first for some reason. -classes = [KITSU_PT_comp_context, KITSU_PT_editorial_context, KITSU_PT_vi3d_context] +classes = [ + KITSU_PT_comp_context, + KITSU_PT_editorial_context, + KITSU_PT_vi3d_context, + KITSU_PT_set_asset, +] def register(): diff --git a/scripts-blender/addons/blender_kitsu/props.py b/scripts-blender/addons/blender_kitsu/props.py index 667534f1..f5fcb5f5 100644 --- a/scripts-blender/addons/blender_kitsu/props.py +++ b/scripts-blender/addons/blender_kitsu/props.py @@ -279,6 +279,8 @@ class KITSU_property_group_scene(bpy.types.PropertyGroup): NOTE: It would be nice to have searchable enums instead of doing all this work manually. """ + asset_col: bpy.props.PointerProperty(type=bpy.types.Collection, name="Collection") + ########### # Sequence ########### diff --git a/scripts-blender/addons/blender_kitsu/shot_builder/assets.py b/scripts-blender/addons/blender_kitsu/shot_builder/assets.py index e624b2bb..7c8a36bc 100644 --- a/scripts-blender/addons/blender_kitsu/shot_builder/assets.py +++ b/scripts-blender/addons/blender_kitsu/shot_builder/assets.py @@ -1,5 +1,5 @@ import bpy -from .. import prefs +from .. import prefs, bkglobals from pathlib import Path import json from ..types import Shot @@ -33,28 +33,32 @@ def get_shot_assets( asset_index = get_assset_index() if asset_index is None: return - assets = shot.get_all_assets() - asset_slugs = [asset.data.get("slug") for asset in assets if asset.data.get("slug") is not None] - if asset_slugs == []: - print("No asset slugs found on Kitsu Server. Assets will not be loaded") - for key, value in asset_index.items(): - if key in asset_slugs: - relative_path = value.get('filepath') - asset_dir = get_asset_dir() - filepath = Path(asset_dir).joinpath(relative_path).__str__() - data_type = value.get('type') - if config.ASSET_TYPE_TO_OVERRIDE.get(key.split('-')[0]): - if data_type != "Collection": - print(f"Cannot load {key} because it is not a collection") - continue - linked_collection = core.link_and_override_collection( - collection_name=key, file_path=filepath, scene=scene - ) - core.add_action_to_armature(linked_collection, shot) - print(f"'{key}': Succesfully Linked & Overriden") - else: - linked_collection = core.link_data_block( - file_path=filepath, data_block_name=key, data_block_type=data_type - ) - print(f"'{key}': Succesfully Linked") - output_collection.children.link(linked_collection) + kitsu_assets = shot.get_all_assets() + + for kitsu_asset in kitsu_assets: + asset_path = kitsu_asset.data.get(bkglobals.KITSU_FILEPATH_KEY) + collection_name = kitsu_asset.data.get(bkglobals.KITSU_COLLECTION_KEY) + if not asset_path or not collection_name: + print( + f"Asset '{kitsu_asset.name}' is missing filepath or collection metadata. Skipping" + ) + continue + + filepath = prefs.project_root_dir_get(bpy.context).joinpath(asset_path).absolute() + if not filepath.exists(): + print(f"Asset '{kitsu_asset.name}' filepath '{str(filepath)}' does not exist. Skipping") + + if config.ASSET_TYPE_TO_OVERRIDE.get(collection_name.split('-')[0]): + linked_collection = core.link_and_override_collection( + collection_name=collection_name, file_path=str(filepath), scene=scene + ) + core.add_action_to_armature(linked_collection, shot) + print(f"'{collection_name}': Succesfully Linked & Overriden") + else: + linked_collection = core.link_data_block( + file_path=str(filepath), + data_block_name=collection_name, + data_block_type="Collection", + ) + print(f"'{collection_name}': Succesfully Linked") + output_collection.children.link(linked_collection) diff --git a/scripts-blender/addons/blender_kitsu/types.py b/scripts-blender/addons/blender_kitsu/types.py index 88a56858..f8e34f2e 100644 --- a/scripts-blender/addons/blender_kitsu/types.py +++ b/scripts-blender/addons/blender_kitsu/types.py @@ -735,6 +735,24 @@ class Asset(Entity): asset_dict = gazu.asset.get_asset(asset_id) return cls.from_dict(asset_dict) + def set_asset_path(self, filepath: str, collection_name: str) -> None: + data = {} + filepath_key = bkglobals.KITSU_FILEPATH_KEY + collection_key = bkglobals.KITSU_COLLECTION_KEY + data[filepath_key] = filepath + data[collection_key] = collection_name + updated_asset = gazu.asset.update_asset_data(asdict(self), data) + self.data = updated_asset["data"] + + if not gazu.project.get_metadata_descriptor_by_field_name(self.project_id, filepath_key): + gazu.project.add_metadata_descriptor( + self.project_id, filepath_key, "Asset", data_type='string' + ) + if not gazu.project.get_metadata_descriptor_by_field_name(self.project_id, collection_key): + gazu.project.add_metadata_descriptor( + self.project_id, collection_key, "Asset", data_type='string' + ) + def get_all_task_types(self) -> List[TaskType]: return [TaskType.from_dict(t) for t in gazu.task.all_task_types_for_asset(asdict(self))]