Blender Kitsu: New Assset Tagging System #286

Merged
Nick Alberelli merged 8 commits from TinyNick/blender-studio-pipeline:feature/asset-tagging into main 2024-04-30 21:43:07 +02:00
11 changed files with 231 additions and 70 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 productions `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.

View File

@ -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

View File

@ -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"

View File

@ -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,
]

View File

@ -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():

View File

@ -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
###########

View File

@ -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")
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=key, file_path=filepath, scene=scene
collection_name=collection_name, file_path=str(filepath), scene=scene
)
core.add_action_to_armature(linked_collection, shot)
print(f"'{key}': Succesfully Linked & Overriden")
print(f"'{collection_name}': Succesfully Linked & Overriden")
else:
linked_collection = core.link_data_block(
file_path=filepath, data_block_name=key, data_block_type=data_type
file_path=str(filepath),
data_block_name=collection_name,
data_block_type="Collection",
)
print(f"'{key}': Succesfully Linked")
print(f"'{collection_name}': Succesfully Linked")
output_collection.children.link(linked_collection)

View File

@ -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))]