Blender Kitsu: New Assset Tagging System #286
BIN
docs/media/pipeline-overview/shot-production/kitsu_asset_metadata.jpg
(Stored with Git LFS)
Normal file
BIN
docs/media/pipeline-overview/shot-production/kitsu_asset_metadata.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/media/pipeline-overview/shot-production/kitsu_asset_with_asset_pipeline.jpg
(Stored with Git LFS)
Normal file
BIN
docs/media/pipeline-overview/shot-production/kitsu_asset_with_asset_pipeline.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/media/pipeline-overview/shot-production/kitsu_set_asset.jpg
(Stored with Git LFS)
Normal file
BIN
docs/media/pipeline-overview/shot-production/kitsu_set_asset.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
###########
|
||||
|
@ -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)
|
||||
|
@ -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))]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user