Blender Kitsu: Refactor Shot Builder #183
1
scripts-blender/addons/blender_kitsu/.gitattributes
vendored
Normal file
1
scripts-blender/addons/blender_kitsu/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.blend filter=lfs diff=lfs merge=lfs -text
|
1
scripts-blender/addons/blender_kitsu/.gitignore
vendored
Normal file
1
scripts-blender/addons/blender_kitsu/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.blend1
|
@ -7,28 +7,45 @@ blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Installation](#installation)
|
||||
- [How to get started](#how-to-get-started)
|
||||
- [**Setup Login Data**](#setup-login-data)
|
||||
- [**Setup Project Settings**](#setup-project-settings)
|
||||
- [**Setup Animation Tools**](#setup-animation-tools)
|
||||
- [**Setup Lookdev Tools**](#setup-lookdev-tools)
|
||||
- [**Setup Media Search Paths**](#setup-media-search-paths)
|
||||
- [**Setup Miscellaneous**](#setup-miscellaneous)
|
||||
- [Features](#features)
|
||||
- [Sequence Editor](#sequence-editor)
|
||||
- [Metadata](#metadata)
|
||||
- [Push](#push)
|
||||
- [Pull](#pull)
|
||||
- [Multi Edit](#multi-edit)
|
||||
- [Shot as Image Sequence](#shot-as-image-sequence)
|
||||
- [General Sequence Editor Tools](#general-sequence-editor-tools)
|
||||
- [Context](#context)
|
||||
- [Animation Tools](#animation-tools)
|
||||
- [Lookdev Tools](#lookdev-tools)
|
||||
- [Error System](#error-system)
|
||||
- [Sequence Editor](#sequence-editor)
|
||||
- [Metastrips](#metastrips)
|
||||
- [Create a Metastrip](#create-a-metastrip)
|
||||
- [Initialize a Shot](#initialize-a-shot)
|
||||
- [Metadata](#metadata)
|
||||
- [Push](#push)
|
||||
- [Pull](#pull)
|
||||
- [Multi Edit](#multi-edit)
|
||||
- [Shot as Image Sequence](#shot-as-image-sequence)
|
||||
- [Advanced Settings](#advanced-settings)
|
||||
- [General Sequence Editor Tools](#general-sequence-editor-tools)
|
||||
- [Context](#context)
|
||||
- [Animation Tools](#animation-tools)
|
||||
- [Lookdev Tools](#lookdev-tools)
|
||||
- [Error System](#error-system)
|
||||
- [Shot Builder](#shot-builder)
|
||||
- [Features](#features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Shot Setup](#shot-setup)
|
||||
- [Asset Setup](#asset-setup)
|
||||
- [Kitsu Server](#kitsu-server)
|
||||
- [Asset Index](#asset-index)
|
||||
- [Example asset_index.json](#example-asset_indexjson)
|
||||
- [Hooks Setup](#hooks-setup)
|
||||
- [Editorial Exports](#editorial-exports)
|
||||
- [Run Shot Builder](#run-shot-builder)
|
||||
- [Development](#development)
|
||||
- [Update Dependencies](#update-dependencies)
|
||||
- [Troubleshoot](#troubleshoot)
|
||||
- [Credits](#credits)
|
||||
|
||||
<!-- /TOC -->
|
||||
blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It also has features that are not directly related to Kitsu but support certain aspects of the Blender Studio Pipeline.
|
||||
|
||||
[Blender-Kitsu blogpost](https://studio.blender.org/blog/kitsu-addon-for-blender/)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@ -274,250 +291,83 @@ blender-kitsu has different checks that are performed during file load or during
|
||||
![image info](/media/addons/blender_kitsu/error_animation.jpg)
|
||||
|
||||
## Shot Builder
|
||||
Shot Builder is an Add-on that helps studios to work with task specific
|
||||
Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are
|
||||
Shot Builder is a Feature of the Blender Kitsu Add-on To automatically build shot files, using the data from Kitsu server and the file structures defined on the [Blender Studio](https://studio.blender.org/pipeline/naming-conventions/svn-folder-structure) website.
|
||||
|
||||
* Build blend files for a specific task and shot.
|
||||
* Sync data back from work files to places like kitsu, or `edit.blend`.
|
||||
### Features
|
||||
- Saves a 'Shot' File for each of the Shot's Task Types on Kitsu Server.
|
||||
- Automatically Names Scenes based on Shot and Task Type names
|
||||
- Creates output collections for certain Task Types (anim, fx, layout, lighting, previz, storyboard)
|
||||
- Links output collections between Task Types based on [Shot Assembly](https://studio.blender.org/pipeline/pipeline-overview/shot-production/shot-assembly) specifications
|
||||
- Loads Editorial Export (defined in preferences) into Shot file's VSE area (if available)
|
||||
- Loads Assets via `asset_index.json` file stored at `your_production/svn/pro/assets/asset_index.json`
|
||||
- Executes a `hook.py` file stored at `your_production/svn/pro/assets/shot-builder/hooks.py`
|
||||
|
||||
### Design Principles
|
||||
### Getting Started
|
||||
#### Shot Setup
|
||||
The Shot Builder requires shot data including Name, Frame Rate, and Duration to be stored on a Kitsu Server. Please follow the [Sequence Editor](#sequence-editor) guide to create metastrips and Push that data to the Kitsu server or follow the [Kitsu First Production](https://kitsu.cg-wire.com/first_production/) to manually enter this data into the Kitsu Server.
|
||||
|
||||
The main design principles are:
|
||||
#### 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.
|
||||
|
||||
* The core-tool can be installed as an add-on, but the (production specific)
|
||||
configuration should be part of the production repository.
|
||||
* The configuration files are a collection of python files. The API between
|
||||
the configuration files and the add-on should be easy to use as pipeline
|
||||
TDs working on the production should be able to work with it.
|
||||
* TDs/artists should be able to handle issues during building without looking
|
||||
at how the add-on is structured.
|
||||
* The tool contains connectors that can be configured to read/write data
|
||||
from the system/file that is the main location of the data. For example
|
||||
The start and end time of a shot could be stored in an external production tracking application.
|
||||
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.
|
||||
|
||||
### Connectors
|
||||
##### 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.
|
||||
|
||||
Connectors are components that can be used to read or write to files or
|
||||
systems. The connectors will add flexibility to the add-on so it could be used
|
||||
in multiple productions or studios.
|
||||
|
||||
In the configuration files the TD can setup the connectors that are used for
|
||||
the production. Possible connectors would be:
|
||||
|
||||
* Connector for text based config files (json/yaml).
|
||||
* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html).
|
||||
* Connector for blend files.
|
||||
|
||||
### Layering & Hooks
|
||||
|
||||
The configuration of the tool is layered. When building a work file for a sequence
|
||||
there are multiple ways to change the configuration.
|
||||
|
||||
* Configuration for the production.
|
||||
* Configuration for the asset that is needed.
|
||||
* Configuration for the asset type of the loaded asset.
|
||||
* Configuration for the sequence.
|
||||
* Configuration for the shot.
|
||||
* Configuration for the task type.
|
||||
|
||||
For any combination of these configurations hooks can be defined.
|
||||
|
||||
```
|
||||
@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A')
|
||||
def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides when Spring is loaded in 02_020A.
|
||||
"""
|
||||
|
||||
@shot_tools.hook(match_task_type='anim')
|
||||
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides for any animation task.
|
||||
"""
|
||||
```
|
||||
|
||||
#### Data
|
||||
|
||||
All hooks must have Python’s `**kwargs` parameter. The `kwargs` contains
|
||||
the context at the moment the hook is invoked. The context can contain the
|
||||
following items.
|
||||
|
||||
* `production`: `shot_tools.Production`: Include the name of the production
|
||||
and the location on the filesystem.
|
||||
* `task`: `shot_tools.Task`: The task (combination of task_type and shot)
|
||||
* `task_type`: `shot_tools.TaskType`: Is part of the `task`.
|
||||
* `sequence`: `shot_tools.Sequence`: Is part of `shot`.
|
||||
* `shot`: `shot_tools.Shot` Is part of `task`.
|
||||
* `asset`: `shot_tools.Asset`: Only available during asset loading phase.
|
||||
* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase.
|
||||
|
||||
#### Execution Order
|
||||
|
||||
The add-on will internally create a list containing the hooks that needs to be
|
||||
executed for the command in a sensible order. It will then execute them in that
|
||||
order.
|
||||
|
||||
By default the next order will be used:
|
||||
|
||||
* Production wide hooks
|
||||
* Asset Type hooks
|
||||
* Asset hooks
|
||||
* Sequence hooks
|
||||
* Shot hooks
|
||||
* Task type hooks
|
||||
|
||||
A hook with a single ‘match’ rule will be run in the corresponding phase. A hook with
|
||||
multiple ‘match’ rules will be run in the last matching phase. For example, a hook with
|
||||
‘asset’ and ‘task type’ match rules will be run in the ‘task type’ phase.
|
||||
|
||||
###### Events
|
||||
|
||||
Order of execution can be customized by adding the optional `run_before`
|
||||
or `run_after` parameters.
|
||||
|
||||
```
|
||||
@shot_tools.hook(match_task_type='anim',
|
||||
requires={shot_tools.events.AssetsLoaded, hook_task_other_anim},
|
||||
is_required_by={shot_tools.events.ShotOverrides})
|
||||
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides for any animation task run after all assets have been loaded.
|
||||
"""
|
||||
```
|
||||
|
||||
Events could be:
|
||||
|
||||
* `shot_tools.events.BuildStart`
|
||||
* `shot_tools.events.ProductionSettingsLoaded`
|
||||
* `shot_tools.events.AssetsLoaded`
|
||||
* `shot_tools.events.AssetTypeOverrides`
|
||||
* `shot_tools.events.SequenceOverrides`
|
||||
* `shot_tools.events.ShotOverrides`
|
||||
* `shot_tools.events.TaskTypeOverrides`
|
||||
* `shot_tools.events.BuildFinished`
|
||||
* `shot_tools.events.HookStart`
|
||||
* `shot_tools.events.HookEnd`
|
||||
|
||||
During usage we should see which one of these or other events are needed.
|
||||
|
||||
`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded`
|
||||
and `shot_tools.events.HookStart` can only be used in the `run_after`
|
||||
parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished`
|
||||
can only be used in the `run_before` parameter.
|
||||
|
||||
|
||||
### API
|
||||
|
||||
The shot builder has an API between the add-on and the configuration files. This
|
||||
API contains convenience functions and classes to hide complexity and makes
|
||||
sure that the configuration files are easy to maintain.
|
||||
|
||||
```
|
||||
register_task_type(task_type="anim")
|
||||
register_task_type(task_type="lighting")
|
||||
```
|
||||
|
||||
```
|
||||
# shot_tool/characters.py
|
||||
class Asset(shot_tool.some_module.Asset):
|
||||
asset_file = "/{asset_type}/{name}/{name}.blend"
|
||||
collection = “{class_name}”
|
||||
name = “{class_name}”
|
||||
|
||||
class Character(Asset):
|
||||
asset_type = ‘char’
|
||||
|
||||
|
||||
class Ellie(Character):
|
||||
collection = “{class_name}-{variant_name}”
|
||||
variants = {‘default’, ‘short_hair’}
|
||||
|
||||
class Victoria(Character): pass
|
||||
class Rex(Character): pass
|
||||
|
||||
# shot_tool/shots.py
|
||||
class Shot_01_020_A(shot_tool.some_module.Shot):
|
||||
shot_id = ‘01_020_A’
|
||||
assets = {
|
||||
characters.Ellie(variant=”short_hair”),
|
||||
characters.Rex,
|
||||
sets.LogOverChasm,
|
||||
##### 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"
|
||||
}
|
||||
|
||||
class AllHumansShot(shot_tool.some_module.Shot):
|
||||
assets = {
|
||||
characters.Ellie(variant=”short_hair”),
|
||||
characters.Rex,
|
||||
characters.Victoria,
|
||||
}
|
||||
|
||||
class Shot_01_035_A(AllHumansShot):
|
||||
assets = {
|
||||
sets.Camp,
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
This API is structured/implemented in a way that it keeps track of what
|
||||
is being done. This will be used when an error occurs so a descriptive
|
||||
error message can be generated that would help the TD to solve the issue more
|
||||
quickly. The goal would be that the error messages are descriptive enough to
|
||||
direct the TD into the direction where the actual cause is. And when possible
|
||||
propose several solutions to fix it.
|
||||
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`
|
||||
|
||||
### Setting up the tool
|
||||
#### Hooks Setup
|
||||
Shot Builder uses hooks to extend the functionality of the shot builder. To create a hook file
|
||||
1. Open `Edit>Preferences>Add-Ons`
|
||||
2. Search for the `Blender Kitsu` Add-On
|
||||
3. In the `Blender Kitsu` Add-On preferences find the `Shot Builder` section
|
||||
4. Run the Operator `Save Shot Builder Hook File`
|
||||
5. Edit the file `your_project/svn/pro/assets/scripts/shot-builder/hooks.py` to customize your hooks.
|
||||
|
||||
The artist/TD can configure their current local project directory in the add-on preferences.
|
||||
This can then be used for new blend files. The project associated with an opened (so existing)
|
||||
blend file can be found automatically by iterating over parent directories until a Shot Builder
|
||||
configuration file is found. Project-specific settings are not configured/stored in the add-on,
|
||||
but in this configuration file.
|
||||
|
||||
The add-on will look in the root of the production repository to locate the
|
||||
main configuration file `/project_root_directory/pro/shot-builder/config.py`. This file contains general
|
||||
settings about the production, including:
|
||||
|
||||
* The name of the production for reporting back to the user when needed.
|
||||
* Naming standards to test against when reporting deviations.
|
||||
* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production.
|
||||
* Configuration of the needed connectors.
|
||||
|
||||
#### Directory Layout
|
||||
``` bash
|
||||
└── project-name/ # Project Root Directory
|
||||
└── pro/
|
||||
├── assets/
|
||||
├── shot-builder/
|
||||
│ ├── assets.py
|
||||
│ ├── config.py
|
||||
│ ├── hooks.py
|
||||
│ └── shots.py
|
||||
└── shots/
|
||||
```
|
||||
Arguments to use in hooks
|
||||
scene: bpy.types.Scene # current scene
|
||||
shot: Shot class from blender_kitsu.types.py
|
||||
prod_path: str # path to production root dir (your_project/svn/)
|
||||
shot_path: str # path to shot file (your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_task_name}.blend)
|
||||
|
||||
Notes
|
||||
matching_task_type = ['anim', 'lighting', 'fx', 'comp'] # either use list or just one string
|
||||
output_col_name = shot.get_output_collection_name(task_type_short_name="anim")
|
||||
|
||||
### Usage
|
||||
```
|
||||
#### Editorial Exports
|
||||
Shot Builder can load Exports from Editorial to the .blend's VSE for reference.
|
||||
|
||||
Any artist can open a shot file via the `File` menu. A modal panel appears
|
||||
where the user can select the task type and sequence/shot. When the file
|
||||
already exists, it will be opened. When the file doesn't exist, the file
|
||||
will be built.
|
||||
1. Open `Edit>Preferences>Add-Ons`
|
||||
2. Search for the `Blender Kitsu` Add-On
|
||||
3. In the `Blender Kitsu` Add-On preferences find the `Shot Builder` section
|
||||
4. Set your `Editorial Export Directory` to `your_project/shared/editorial/export/`
|
||||
5. Set your `Editorial File Pattern` to `your_project_v\d\d\d.mp4` where `\d` represents a digit. This pattern matches a file named `your_movie_v001.mp4`.
|
||||
|
||||
In the future other use cases will also be accessible, such as:
|
||||
|
||||
* Syncing data back from a work file to the source of the data.
|
||||
* Report of errors/differences between the shot file and the configuration.
|
||||
|
||||
### Open Issues
|
||||
|
||||
#### Security
|
||||
|
||||
* Security keys needed by connectors need to be stored somewhere. The easy
|
||||
place is to place inside the production repository, but that isn't secure
|
||||
Anyone with access to the repository could misuse the keys to access the
|
||||
connector. Other solution might be to use the OS key store or retrieve the
|
||||
keys from an online service authenticated by the blender cloud add-on.
|
||||
|
||||
We could use `keyring` to access OS key stores.
|
||||
#### Run Shot Builder
|
||||
1. Open Blender
|
||||
2. Select File>New
|
||||
3. From dialogue box, select the desired Sequence/Shot/Task Type
|
||||
4. Hit `ok` to run the tool. The tool will create a new file in the directory `your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_name}+{task_type_name}.blend`
|
||||
|
||||
## Development
|
||||
### Update Dependencies
|
||||
|
@ -116,7 +116,6 @@ def unregister():
|
||||
lookdev.unregister()
|
||||
playblast.unregister()
|
||||
shot_builder.unregister()
|
||||
|
||||
LoggerLevelManager.restore_levels()
|
||||
|
||||
|
||||
|
@ -29,7 +29,7 @@ SHOT_DIR_NAME = "shots"
|
||||
SEQ_DIR_NAME = "sequences"
|
||||
ASSET_DIR_NAME = "assets"
|
||||
|
||||
FILE_DELIMITER = '-'
|
||||
FILE_DELIMITER = "-"
|
||||
|
||||
ASSET_TASK_MAPPING = {
|
||||
"geometry": "Geometry",
|
||||
@ -90,3 +90,54 @@ RES_DIR_PATH = Path(os.path.abspath(__file__)).parent.joinpath("res")
|
||||
|
||||
SCENE_NAME_PLAYBLAST = "playblast_playback"
|
||||
PLAYBLAST_DEFAULT_STATUS = "Todo"
|
||||
|
||||
###########################
|
||||
# Shot Builder Properties
|
||||
###########################
|
||||
|
||||
# TODO add documentation and move other shot builder props here
|
||||
|
||||
OUTPUT_COL_CREATE = {
|
||||
"anim": True,
|
||||
"comp": False,
|
||||
"fx": True,
|
||||
"layout": True,
|
||||
"lighting": True,
|
||||
"previz": True,
|
||||
"rendering": False,
|
||||
"smear_to_mesh": False,
|
||||
"storyboard": True,
|
||||
}
|
||||
|
||||
OUTPUT_COL_LINK_MAPPING = {
|
||||
"anim": None,
|
||||
"comp": ['anim', 'fx', 'lighting'],
|
||||
"fx": ['anim', 'lighting'],
|
||||
"layout": None,
|
||||
"lighting": ['anim'],
|
||||
"previz": None,
|
||||
"rendering": None,
|
||||
"smear_to_mesh": None,
|
||||
"storyboard": None,
|
||||
}
|
||||
|
||||
LOAD_EDITORIAL_REF = {
|
||||
"anim": True,
|
||||
"comp": False,
|
||||
"fx": False,
|
||||
"layout": True,
|
||||
"lighting": False,
|
||||
"previz": False,
|
||||
"rendering": False,
|
||||
"smear_to_mesh": False,
|
||||
"storyboard": False,
|
||||
}
|
||||
|
||||
ASSET_TYPE_TO_OVERRIDE = {
|
||||
"CH": True, # Character
|
||||
"PR": True, # Rigged Prop
|
||||
"LI": True, # Library/Environment Asset
|
||||
"SE": False, # Set
|
||||
"LG": True, # Lighting Rig
|
||||
"CA": True, # Camera Rig
|
||||
}
|
||||
|
@ -269,6 +269,18 @@ def get_shots_enum_for_active_seq(
|
||||
return _shot_enum_list
|
||||
|
||||
|
||||
def get_shots_enum_for_seq(
|
||||
self: bpy.types.Operator, context: bpy.types.Context, sequence: Sequence
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _shot_enum_list
|
||||
|
||||
_shot_enum_list.clear()
|
||||
_shot_enum_list.extend(
|
||||
[(s.id, s.name, s.description or "") for s in sequence.get_all_shots()]
|
||||
)
|
||||
return _shot_enum_list
|
||||
|
||||
|
||||
def get_assetypes_enum_list(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
@ -343,6 +355,20 @@ def get_shot_task_types_enum(
|
||||
return _task_types_shots_enum_list
|
||||
|
||||
|
||||
def get_shot_task_types_enum_for_shot( # TODO Rename
|
||||
self: bpy.types.Operator, context: bpy.types.Context, shot: Shot
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
# TODO why do we have global variables here can't I just return the items directly? We clear the list every time how is this a cahe?
|
||||
global _task_types_shots_enum_list
|
||||
|
||||
items = [(t.id, t.name, "") for t in shot.get_all_task_types()]
|
||||
|
||||
_task_types_shots_enum_list.clear()
|
||||
_task_types_shots_enum_list.extend(items)
|
||||
|
||||
return _task_types_shots_enum_list
|
||||
|
||||
|
||||
def get_all_task_statuses_enum(
|
||||
self: bpy.types.Operator, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
|
@ -40,7 +40,7 @@ from .auth.ops import (
|
||||
)
|
||||
from .context.ops import KITSU_OT_con_productions_load
|
||||
from .lookdev.prefs import LOOKDEV_preferences
|
||||
from .shot_builder.editorial.core import editorial_export_check_latest
|
||||
from .shot_builder.editorial import editorial_export_check_latest
|
||||
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
@ -339,12 +339,6 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences):
|
||||
default="ANI-",
|
||||
)
|
||||
|
||||
user_exec_code: bpy.props.StringProperty( # type: ignore
|
||||
name="Post Execution Command",
|
||||
description="Run this command after shot_builder is complete, but before the file is saved.",
|
||||
default="",
|
||||
)
|
||||
|
||||
session: Session = Session()
|
||||
|
||||
tasks: bpy.props.CollectionProperty(type=KITSU_task)
|
||||
@ -439,7 +433,7 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences):
|
||||
start_frame_row.prop(self, "shot_builder_frame_offset", text="")
|
||||
box.row().prop(self, "shot_builder_armature_prefix")
|
||||
box.row().prop(self, "shot_builder_action_prefix")
|
||||
box.row().prop(self, "user_exec_code")
|
||||
box.operator("kitsu.save_shot_builder_hooks", icon='FILE_SCRIPT')
|
||||
|
||||
# Misc settings.
|
||||
box = layout.box()
|
||||
@ -527,6 +521,11 @@ def addon_prefs_get(context: bpy.types.Context) -> bpy.types.AddonPreferences:
|
||||
return context.preferences.addons["blender_kitsu"].preferences
|
||||
|
||||
|
||||
def project_root_dir_get(context: bpy.types.Context):
|
||||
addon_prefs = addon_prefs_get(context)
|
||||
return Path(addon_prefs.project_root_dir).resolve()
|
||||
|
||||
|
||||
def session_auth(context: bpy.types.Context) -> bool:
|
||||
"""
|
||||
Shortcut to check if zession is authorized
|
||||
|
@ -1,62 +1,13 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
from .ui import *
|
||||
from .connectors.kitsu import *
|
||||
from .operators import *
|
||||
import bpy
|
||||
from .anim_setup import ops as anim_setup_ops # TODO Fix Registraion
|
||||
from .editorial import ops as editorial_ops # TODO Fix Registraion
|
||||
|
||||
# import logging
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
# bl_info = {
|
||||
# 'name': 'Shot Builder',
|
||||
# "author": "Jeroen Bakker",
|
||||
# 'version': (0, 1),
|
||||
# 'blender': (2, 90, 0),
|
||||
# 'location': 'Addon Preferences panel and file new menu',
|
||||
# 'description': 'Shot builder production tool.',
|
||||
# 'category': 'Studio',
|
||||
# }
|
||||
|
||||
|
||||
classes = (
|
||||
KitsuPreferences,
|
||||
SHOTBUILDER_OT_NewShotFile,
|
||||
)
|
||||
from . import ops
|
||||
from .ui import topbar_file_new_draw_handler
|
||||
|
||||
|
||||
def register():
|
||||
anim_setup_ops.register()
|
||||
editorial_ops.register()
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.TOPBAR_MT_file_new.append(topbar_file_new_draw_handler)
|
||||
ops.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
anim_setup_ops.unregister()
|
||||
editorial_ops.unregister()
|
||||
bpy.types.TOPBAR_MT_file_new.remove(topbar_file_new_draw_handler)
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
ops.unregister()
|
||||
|
@ -1,34 +0,0 @@
|
||||
import bpy
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
from ... import prefs
|
||||
from ... import cache
|
||||
|
||||
|
||||
def animation_workspace_vse_area_add(context: bpy.types.Context):
|
||||
"""Split smallest 3D View in current workspace"""
|
||||
for workspace in [
|
||||
workspace for workspace in bpy.data.workspaces if workspace.name == "Animation"
|
||||
]:
|
||||
context.window.workspace = workspace
|
||||
context.view_layer.update()
|
||||
areas = workspace.screens[0].areas
|
||||
view_3d_areas = sorted(
|
||||
[area for area in areas if area.ui_type == "VIEW_3D"],
|
||||
key=lambda x: x.width,
|
||||
reverse=False,
|
||||
)
|
||||
small_view_3d = view_3d_areas[0]
|
||||
with context.temp_override(window=context.window, area=small_view_3d):
|
||||
bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.5)
|
||||
small_view_3d.ui_type = "SEQUENCE_EDITOR"
|
||||
small_view_3d.spaces[0].view_type = "PREVIEW"
|
||||
|
||||
|
||||
def animation_workspace_delete_others():
|
||||
"""Delete any workspace that is not an animation workspace"""
|
||||
for ws in bpy.data.workspaces:
|
||||
if ws.name != "Animation":
|
||||
with bpy.context.temp_override(workspace=ws):
|
||||
bpy.ops.workspace.delete()
|
@ -1,35 +0,0 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from .core import animation_workspace_delete_others, animation_workspace_vse_area_add
|
||||
class ANIM_SETUP_OT_setup_workspaces(bpy.types.Operator):
|
||||
bl_idname = "anim_setup.setup_workspaces"
|
||||
bl_label = "Setup Workspace"
|
||||
bl_description = "Sets up the workspaces for the animation task"
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
animation_workspace_delete_others(self, context)
|
||||
self.report({"INFO"}, "Deleted non Animation workspaces")
|
||||
return {"FINISHED"}
|
||||
|
||||
class ANIM_SETUP_OT_animation_workspace_vse_area_add(bpy.types.Operator):
|
||||
bl_idname = "anim_setup.animation_workspace_vse_area_add"
|
||||
bl_label = "Split Viewport"
|
||||
bl_description = "Split smallest 3D View in current workspace"
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
animation_workspace_vse_area_add(self, context)
|
||||
return {"FINISHED"}
|
||||
|
||||
classes = [
|
||||
ANIM_SETUP_OT_setup_workspaces,
|
||||
ANIM_SETUP_OT_animation_workspace_vse_area_add
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
@ -1,50 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
|
||||
class Asset:
|
||||
"""
|
||||
Container to hold data where the asset can be located in the production repository.
|
||||
|
||||
path: absolute path to the blend file containing this asset.
|
||||
|
||||
"""
|
||||
|
||||
asset_type = ""
|
||||
code = ""
|
||||
name = ""
|
||||
path = "{production.path}/assets/{asset.asset_type}/{asset.code}/{asset.code}.blend"
|
||||
collection = "{asset.code}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class AssetRef:
|
||||
"""
|
||||
Reference to an asset from an external system.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "", code: str = ""):
|
||||
self.name = name
|
||||
self.code = code
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
60
scripts-blender/addons/blender_kitsu/shot_builder/assets.py
Normal file
60
scripts-blender/addons/blender_kitsu/shot_builder/assets.py
Normal file
@ -0,0 +1,60 @@
|
||||
import bpy
|
||||
from .. import prefs
|
||||
from pathlib import Path
|
||||
import json
|
||||
from ..types import Shot
|
||||
from .core import link_and_override_collection, link_data_block
|
||||
from .. import bkglobals
|
||||
|
||||
|
||||
def get_asset_index_file() -> str:
|
||||
svn_project_root_dir = prefs.project_root_dir_get(bpy.context)
|
||||
asset_index_file = (
|
||||
Path(svn_project_root_dir)
|
||||
.joinpath("pro")
|
||||
.joinpath("assets")
|
||||
.joinpath("asset_index.json")
|
||||
)
|
||||
if asset_index_file.exists():
|
||||
return asset_index_file.__str__()
|
||||
|
||||
|
||||
def get_assset_index() -> dict:
|
||||
asset_index_file = get_asset_index_file()
|
||||
if asset_index_file is None:
|
||||
return
|
||||
return json.load(open(asset_index_file))
|
||||
|
||||
|
||||
def get_shot_assets(
|
||||
scene: bpy.types.Scene,
|
||||
output_collection: bpy.types.Collection,
|
||||
shot: Shot,
|
||||
):
|
||||
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:
|
||||
filepath = value.get('filepath')
|
||||
data_type = value.get('type')
|
||||
if bkglobals.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 = link_and_override_collection(
|
||||
collection_name=key, file_path=filepath, scene=scene
|
||||
)
|
||||
print(f"'{key}': Succesfully Linked & Overriden")
|
||||
else:
|
||||
linked_collection = 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)
|
@ -1,100 +0,0 @@
|
||||
from ..project import Production
|
||||
from ..task_type import TaskType
|
||||
from ..asset import Asset, AssetRef
|
||||
from .build_step import BuildStep, BuildContext
|
||||
from .init_asset import InitAssetStep
|
||||
from .init_shot import InitShotStep
|
||||
from .set_render_settings import SetRenderSettingsStep
|
||||
from .new_scene import NewSceneStep
|
||||
from .invoke_hook import InvokeHookStep
|
||||
|
||||
import bpy
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShotBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
production: Production,
|
||||
task_type: TaskType,
|
||||
shot_name: str,
|
||||
):
|
||||
self._steps: typing.List[BuildStep] = []
|
||||
|
||||
shot = production.get_shot(context, shot_name)
|
||||
assert shot
|
||||
render_settings = production.get_render_settings(context, shot)
|
||||
self.build_context = BuildContext(
|
||||
context=context,
|
||||
production=production,
|
||||
shot=shot,
|
||||
render_settings=render_settings,
|
||||
task_type=task_type,
|
||||
)
|
||||
|
||||
def __find_asset(self, asset_ref: AssetRef) -> typing.Optional[Asset]:
|
||||
for asset_class in self.build_context.production.assets:
|
||||
asset = typing.cast(Asset, asset_class())
|
||||
logger.debug(f"{asset_ref.name}, {asset.name}")
|
||||
if asset_ref.name == asset.name:
|
||||
return asset
|
||||
return None
|
||||
|
||||
def create_build_steps(self) -> None:
|
||||
self._steps.append(InitShotStep())
|
||||
self._steps.append(NewSceneStep())
|
||||
self._steps.append(SetRenderSettingsStep())
|
||||
|
||||
production = self.build_context.production
|
||||
task_type = self.build_context.task_type
|
||||
|
||||
# Add global hooks.
|
||||
for hook in production.hooks.filter():
|
||||
self._steps.append(InvokeHookStep(hook))
|
||||
|
||||
# Add task specific hooks.
|
||||
for hook in production.hooks.filter(match_task_type=task_type.name):
|
||||
self._steps.append(InvokeHookStep(hook))
|
||||
|
||||
context = self.build_context.context
|
||||
shot = self.build_context.shot
|
||||
|
||||
# Collect assets that should be loaded.
|
||||
asset_refs = production.get_assets_for_shot(context, shot)
|
||||
assets = []
|
||||
for asset_ref in asset_refs:
|
||||
asset = self.__find_asset(asset_ref)
|
||||
if asset is None:
|
||||
logger.warning(f"cannot determine repository data for {asset_ref}")
|
||||
continue
|
||||
assets.append(asset)
|
||||
|
||||
# Sort the assets on asset_type and asset.code).
|
||||
assets.sort(key=lambda asset: (asset.asset_type, asset.code))
|
||||
|
||||
# Build asset specific build steps.
|
||||
for asset in assets:
|
||||
self._steps.append(InitAssetStep(asset))
|
||||
# Add asset specific hooks.
|
||||
for hook in production.hooks.filter(
|
||||
match_task_type=task_type.name, match_asset_type=asset.asset_type
|
||||
):
|
||||
self._steps.append(InvokeHookStep(hook))
|
||||
|
||||
def build(self) -> None:
|
||||
num_steps = len(self._steps)
|
||||
step_number = 1
|
||||
build_context = self.build_context
|
||||
window_manager = build_context.context.window_manager
|
||||
window_manager.progress_begin(min=0, max=num_steps)
|
||||
for step in self._steps:
|
||||
logger.info(f"Building step [{step_number}/{num_steps}]: {step} ")
|
||||
step.execute(build_context=build_context)
|
||||
window_manager.progress_update(value=step_number)
|
||||
step_number += 1
|
||||
window_manager.progress_end()
|
@ -1,38 +0,0 @@
|
||||
import bpy
|
||||
import typing
|
||||
|
||||
from ..project import Production
|
||||
from ..shot import Shot
|
||||
from ..task_type import TaskType
|
||||
from ..render_settings import RenderSettings
|
||||
from ..asset import Asset
|
||||
|
||||
|
||||
class BuildContext:
|
||||
def __init__(self, context: bpy.types.Context, production: Production, shot: Shot, render_settings: RenderSettings, task_type: TaskType):
|
||||
self.context = context
|
||||
self.production = production
|
||||
self.shot = shot
|
||||
self.task_type = task_type
|
||||
self.render_settings = render_settings
|
||||
self.asset: typing.Optional[Asset] = None
|
||||
self.scene: typing.Optional[bpy.types.Scene] = None
|
||||
|
||||
def as_dict(self) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'context': self.context,
|
||||
'scene': self.scene,
|
||||
'production': self.production,
|
||||
'shot': self.shot,
|
||||
'task_type': self.task_type,
|
||||
'render_settings': self.render_settings,
|
||||
'asset': self.asset,
|
||||
}
|
||||
|
||||
|
||||
class BuildStep:
|
||||
def __str__(self) -> str:
|
||||
return "unnamed build step"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
raise NotImplementedError()
|
@ -1,25 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..asset import *
|
||||
from ..project import *
|
||||
from ..shot import *
|
||||
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitAssetStep(BuildStep):
|
||||
def __init__(self, asset: Asset):
|
||||
self.__asset = asset
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"init asset \"{self.__asset.name}\""
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
build_context.asset = self.__asset
|
||||
self.__asset.path = self.__asset.path.format_map(build_context.as_dict())
|
||||
self.__asset.collection = self.__asset.collection.format_map(
|
||||
build_context.as_dict()
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..asset import *
|
||||
from ..project import *
|
||||
from ..shot import *
|
||||
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitShotStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return "init shot"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
shot = build_context.shot
|
||||
shot.file_path = shot.file_path_format.format_map(build_context.as_dict())
|
@ -1,21 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..hooks import HookFunction
|
||||
import bpy
|
||||
|
||||
import typing
|
||||
import types
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InvokeHookStep(BuildStep):
|
||||
def __init__(self, hook: HookFunction):
|
||||
self._hook = hook
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"invoke hook [{self._hook.__name__}]"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
params = build_context.as_dict()
|
||||
self._hook(**params) # type: ignore
|
@ -1,30 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..render_settings import RenderSettings
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewSceneStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return f"new scene"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
production = build_context.production
|
||||
scene_name = production.scene_name_format.format_map(
|
||||
build_context.as_dict())
|
||||
logger.debug(f"create scene with name {scene_name}")
|
||||
scene = bpy.data.scenes.new(name=scene_name)
|
||||
|
||||
bpy.context.window.scene = scene
|
||||
build_context.scene = scene
|
||||
|
||||
self.__remove_other_scenes(build_context)
|
||||
|
||||
def __remove_other_scenes(self, build_context: BuildContext) -> None:
|
||||
for scene in bpy.data.scenes:
|
||||
if scene != build_context.scene:
|
||||
logger.debug(f"remove scene {scene.name}")
|
||||
bpy.data.scenes.remove(scene)
|
@ -1,30 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
from ..asset import *
|
||||
from ..project import *
|
||||
from ..shot import *
|
||||
import pathlib
|
||||
|
||||
import bpy
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def save_shot_builder_file(file_path: str):
|
||||
"""Save Shot File within Folder of matching name.
|
||||
Set Shot File to relative Paths."""
|
||||
dir_path = pathlib.Path(file_path)
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_mainfile(filepath=file_path, relative_remap=True)
|
||||
|
||||
|
||||
class SaveFileStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return "save file"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
shot = build_context.shot
|
||||
file_path = pathlib.Path(shot.file_path)
|
||||
save_shot_builder_file(file_path)
|
||||
logger.info(f"save file {shot.file_path}")
|
@ -1,27 +0,0 @@
|
||||
from ..builder.build_step import BuildStep, BuildContext
|
||||
import bpy
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetRenderSettingsStep(BuildStep):
|
||||
def __str__(self) -> str:
|
||||
return f"set render settings"
|
||||
|
||||
def execute(self, build_context: BuildContext) -> None:
|
||||
scene = typing.cast(bpy.types.Scene, build_context.scene)
|
||||
render_settings = build_context.render_settings
|
||||
logger.debug(
|
||||
f"set render resolution to {render_settings.width}x{render_settings.height}")
|
||||
scene.render.resolution_x = render_settings.width
|
||||
scene.render.resolution_y = render_settings.height
|
||||
scene.render.resolution_percentage = 100
|
||||
|
||||
shot = build_context.shot
|
||||
scene.frame_start = shot.frame_start
|
||||
scene.frame_current = shot.frame_start
|
||||
scene.frame_end = shot.frame_start + shot.frames -1
|
||||
logger.debug(f"set frame range to ({scene.frame_start}-{scene.frame_end})")
|
@ -1,19 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
@ -1,108 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
"""
|
||||
This module contains the Connector class. It is an abstract base class for concrete connectors.
|
||||
"""
|
||||
|
||||
from ..shot import Shot, ShotRef
|
||||
from..asset import Asset, AssetRef
|
||||
from..task_type import TaskType
|
||||
from..render_settings import RenderSettings
|
||||
from typing import *
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from..project import Production
|
||||
from..properties import ShotBuilderPreferences
|
||||
|
||||
|
||||
class Connector:
|
||||
"""
|
||||
A Connector is used to retrieve data from a source. This source can be an external system.
|
||||
|
||||
Connectors can be configured for productions in its `shot-builder/config.py` file.
|
||||
|
||||
# Members
|
||||
|
||||
_production: reference to the production that we want to read data for.
|
||||
_preference: reference to the add-on preference to read settings for.
|
||||
Connectors can add settings to the add-on preferences.
|
||||
|
||||
# Class Members
|
||||
|
||||
PRODUCTION_KEYS: Connectors can register production configuration keys that will be loaded from the production config file.
|
||||
When keys are added the content will be read and stored in the production.
|
||||
|
||||
# Usage
|
||||
|
||||
Concrete connectors only overrides methods that they support. All non-overridden methods will raise an
|
||||
NotImplementerError.
|
||||
|
||||
|
||||
Example of using predefined connectors in a production config file:
|
||||
```shot-builder/config.py
|
||||
from ..connectors.default import DefaultConnector
|
||||
from ..connectors.kitsu import KitsuConnector
|
||||
|
||||
PRODUCTION_NAME = DefaultConnector
|
||||
TASK_TYPES = KitsuConnector
|
||||
KITSU_PROJECT_ID = "...."
|
||||
```
|
||||
"""
|
||||
PRODUCTION_KEYS: Set[str] = set()
|
||||
|
||||
def __init__(self, production: 'Production', preferences: 'ShotBuilderPreferences'):
|
||||
self._production = production
|
||||
self._preferences = preferences
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""
|
||||
Retrieve the production name using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of production name")
|
||||
|
||||
def get_task_types(self) -> List[TaskType]:
|
||||
"""
|
||||
Retrieve the task types using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of task types")
|
||||
|
||||
def get_shots(self) -> List[ShotRef]:
|
||||
"""
|
||||
Retrieve the shots using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of shots")
|
||||
|
||||
def get_assets_for_shot(self, shot: Shot) -> List[AssetRef]:
|
||||
"""
|
||||
Retrieve the sequences using the connector.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of assets for a shot")
|
||||
|
||||
def get_render_settings(self, shot: Shot) -> RenderSettings:
|
||||
"""
|
||||
Retrieve the render settings for the given shot.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support retrieval of render settings for a shot")
|
@ -1,49 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
from ..shot import Shot, ShotRef
|
||||
from ..asset import Asset, AssetRef
|
||||
from ..task_type import TaskType
|
||||
from ..render_settings import RenderSettings
|
||||
from ..connectors.connector import Connector
|
||||
from typing import *
|
||||
|
||||
|
||||
class DefaultConnector(Connector):
|
||||
"""
|
||||
Default connector is a connector that returns the defaults for the shot builder add-on.
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "unnamed production"
|
||||
|
||||
def get_shots(self) -> List[ShotRef]:
|
||||
return []
|
||||
|
||||
def get_assets_for_shot(self, shot: Shot) -> List[AssetRef]:
|
||||
return []
|
||||
|
||||
def get_task_types(self) -> List[TaskType]:
|
||||
return [TaskType("anim"), TaskType("lighting"), TaskType("comp"), TaskType("fx")]
|
||||
|
||||
def get_render_settings(self, shot: Shot) -> RenderSettings:
|
||||
"""
|
||||
Retrieve the render settings for the given shot.
|
||||
"""
|
||||
return RenderSettings(width=1920, height=1080, frames_per_second=24.0)
|
@ -1,248 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
import bpy
|
||||
from .. import vars
|
||||
from ..shot import Shot, ShotRef
|
||||
from ..asset import Asset, AssetRef
|
||||
from ..task_type import TaskType
|
||||
from ..render_settings import RenderSettings
|
||||
from ..connectors.connector import Connector
|
||||
import requests
|
||||
from ... import cache
|
||||
import gazu
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KitsuException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KitsuPreferences(bpy.types.PropertyGroup):
|
||||
backend: bpy.props.StringProperty( # type: ignore
|
||||
name="Server URL",
|
||||
description="Kitsu server address",
|
||||
default="https://kitsu.blender.cloud/api",
|
||||
)
|
||||
|
||||
username: bpy.props.StringProperty( # type: ignore
|
||||
name="Username",
|
||||
description="Username to connect to Kitsu",
|
||||
)
|
||||
|
||||
password: bpy.props.StringProperty( # type: ignore
|
||||
name="Password",
|
||||
description="Password to connect to Kitsu",
|
||||
subtype='PASSWORD',
|
||||
)
|
||||
|
||||
def draw(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.label(text="Kitsu")
|
||||
layout.prop(self, "backend")
|
||||
layout.prop(self, "username")
|
||||
layout.prop(self, "password")
|
||||
|
||||
def _validate(self):
|
||||
if not (self.backend and self.username and self.password):
|
||||
raise KitsuException(
|
||||
"Kitsu connector has not been configured in the add-on preferences"
|
||||
)
|
||||
|
||||
|
||||
class KitsuDataContainer:
|
||||
def __init__(self, data: typing.Dict[str, typing.Optional[str]]):
|
||||
self._data = data
|
||||
|
||||
def get_parent_id(self) -> typing.Optional[str]:
|
||||
return self._data['parent_id']
|
||||
|
||||
def get_id(self) -> str:
|
||||
return str(self._data['id'])
|
||||
|
||||
def get_name(self) -> str:
|
||||
return str(self._data['name'])
|
||||
|
||||
def get_code(self) -> typing.Optional[str]:
|
||||
return self._data['code']
|
||||
|
||||
def get_description(self) -> str:
|
||||
result = self._data['description']
|
||||
if result is None:
|
||||
return ""
|
||||
return result
|
||||
|
||||
|
||||
class KitsuProject(KitsuDataContainer):
|
||||
def get_resolution(self) -> typing.Tuple[int, int]:
|
||||
"""
|
||||
Get the resolution and decode it to (width, height)
|
||||
"""
|
||||
res_str = str(self._data['resolution'])
|
||||
splitted = res_str.split("x")
|
||||
return (int(splitted[0]), int(splitted[1]))
|
||||
|
||||
|
||||
class KitsuSequenceRef(ShotRef):
|
||||
def __init__(self, kitsu_id: str, name: str, code: str):
|
||||
super().__init__(name=name, code=code)
|
||||
self.kitsu_id = kitsu_id
|
||||
|
||||
def sync_data(self, shot: Shot) -> None:
|
||||
shot.sequence_code = self.name
|
||||
|
||||
|
||||
class KitsuShotRef(ShotRef):
|
||||
def __init__(
|
||||
self,
|
||||
kitsu_id: str,
|
||||
name: str,
|
||||
code: str,
|
||||
frame_start: int,
|
||||
frames: int,
|
||||
frame_end: int,
|
||||
frames_per_second: float,
|
||||
sequence: KitsuSequenceRef,
|
||||
):
|
||||
super().__init__(name=name, code=code)
|
||||
self.kitsu_id = kitsu_id
|
||||
self.frame_start = frame_start
|
||||
self.frames = frames
|
||||
self.frame_end = frame_end
|
||||
self.frames_per_second = frames_per_second
|
||||
self.sequence = sequence
|
||||
|
||||
def sync_data(self, shot: Shot) -> None:
|
||||
shot.name = self.name
|
||||
shot.code = self.code
|
||||
shot.kitsu_id = self.kitsu_id
|
||||
shot.frame_start = self.frame_start
|
||||
shot.frames = self.frames
|
||||
shot.frame_end = self.frame_end
|
||||
shot.frames_per_second = self.frames_per_second
|
||||
self.sequence.sync_data(shot)
|
||||
|
||||
|
||||
class KitsuConnector(Connector):
|
||||
# PRODUCTION_KEYS = {'KITSU_PROJECT_ID'}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __get_production_data(self) -> KitsuProject:
|
||||
production = cache.project_active_get()
|
||||
project = KitsuProject(typing.cast(typing.Dict[str, typing.Any], production))
|
||||
return project
|
||||
|
||||
def get_name(self) -> str:
|
||||
production = self.__get_production_data()
|
||||
return production.get_name()
|
||||
|
||||
def get_task_types(self) -> typing.List[TaskType]:
|
||||
project = cache.project_active_get()
|
||||
task_types = project.task_types
|
||||
import pprint
|
||||
|
||||
pprint.pprint(task_types)
|
||||
return []
|
||||
|
||||
def get_shots(self) -> typing.List[ShotRef]:
|
||||
project = cache.project_active_get()
|
||||
kitsu_sequences = gazu.shot.all_sequences_for_project(project.id)
|
||||
|
||||
sequence_lookup = {
|
||||
sequence_data['id']: KitsuSequenceRef(
|
||||
kitsu_id=sequence_data['id'],
|
||||
name=sequence_data['name'],
|
||||
code=sequence_data['code'],
|
||||
)
|
||||
for sequence_data in kitsu_sequences
|
||||
}
|
||||
|
||||
kitsu_shots = gazu.shot.all_shots_for_project(project.id)
|
||||
shots: typing.List[ShotRef] = []
|
||||
|
||||
for shot_data in kitsu_shots:
|
||||
# Initialize default values
|
||||
frame_start = vars.DEFAULT_FRAME_START
|
||||
frame_end = 0
|
||||
|
||||
# shot_data['data'] can be None
|
||||
if shot_data['data']:
|
||||
# If 3d_start key not found use default start frame.
|
||||
frame_start = int(
|
||||
shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START)
|
||||
)
|
||||
frame_end = (
|
||||
int(shot_data['data'].get('3d_start', vars.DEFAULT_FRAME_START))
|
||||
+ shot_data['nb_frames']
|
||||
- 1
|
||||
)
|
||||
|
||||
# If 3d_start and 3d_out available use that to calculate frames.
|
||||
# If not try shot_data['nb_frames'] or 0 -> invalid.
|
||||
frames = int(
|
||||
(frame_end - frame_start + 1)
|
||||
if frame_end
|
||||
else shot_data['nb_frames'] or 0
|
||||
)
|
||||
if frames < 0:
|
||||
logger.error(
|
||||
"%s duration is negative: %i. Check frame range information on Kitsu",
|
||||
shot_data['name'],
|
||||
frames,
|
||||
)
|
||||
frames = 0
|
||||
|
||||
shots.append(
|
||||
KitsuShotRef(
|
||||
kitsu_id=shot_data['id'],
|
||||
name=shot_data['name'],
|
||||
code=shot_data['code'],
|
||||
frame_start=frame_start,
|
||||
frames=frames,
|
||||
frame_end=frame_end,
|
||||
frames_per_second=24.0,
|
||||
sequence=sequence_lookup[shot_data['parent_id']],
|
||||
)
|
||||
)
|
||||
|
||||
return shots
|
||||
|
||||
def get_assets_for_shot(self, shot: Shot) -> typing.List[AssetRef]:
|
||||
kitsu_assets = gazu.asset.all_assets_for_shot(shot.kitsu_id)
|
||||
|
||||
return [
|
||||
AssetRef(name=asset_data['name'], code=asset_data['code'])
|
||||
for asset_data in kitsu_assets
|
||||
]
|
||||
|
||||
def get_render_settings(self, shot: Shot) -> RenderSettings:
|
||||
"""
|
||||
Retrieve the render settings for the given shot.
|
||||
"""
|
||||
project = cache.project_active_get()
|
||||
return RenderSettings(
|
||||
width=int(project.resolution.split('x')[0]),
|
||||
height=int(project.resolution.split('x')[1]),
|
||||
frames_per_second=project.fps,
|
||||
)
|
202
scripts-blender/addons/blender_kitsu/shot_builder/core.py
Normal file
202
scripts-blender/addons/blender_kitsu/shot_builder/core.py
Normal file
@ -0,0 +1,202 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
from .. import bkglobals
|
||||
from ..types import (
|
||||
Sequence,
|
||||
Shot,
|
||||
TaskType,
|
||||
)
|
||||
|
||||
from ..cache import Project
|
||||
|
||||
from .. import prefs
|
||||
|
||||
#################
|
||||
# Constants
|
||||
#################
|
||||
CAMERA_NAME = 'CAM-camera'
|
||||
|
||||
|
||||
def get_file_dir(seq: Sequence, shot: Shot, task_type: TaskType) -> Path:
|
||||
"""Returns Path to Directory for Current Shot, will ensure that
|
||||
file path exists if it does not.
|
||||
|
||||
Args:
|
||||
seq (Sequence): Sequence Class from blender_kitsu.types
|
||||
shot (Shot): Shot Class from blender_kitsu.types
|
||||
task_type TaskType Class from blender_kitsu.types
|
||||
|
||||
Returns:
|
||||
Path: Returns Path for Shot Directory
|
||||
"""
|
||||
project_root_dir = prefs.project_root_dir_get(bpy.context)
|
||||
all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots')
|
||||
shot_dir = all_shots_dir.joinpath(seq.name).joinpath(shot.name)
|
||||
if not shot_dir.exists():
|
||||
shot_dir.mkdir(parents=True)
|
||||
return shot_dir
|
||||
|
||||
|
||||
def set_render_engine(scene: bpy.types.Scene, engine='CYCLES'):
|
||||
"""
|
||||
By default we set Cycles as the renderer.
|
||||
"""
|
||||
scene.render.engine = engine
|
||||
|
||||
|
||||
def remove_all_data():
|
||||
for lib in bpy.data.libraries:
|
||||
bpy.data.libraries.remove(lib)
|
||||
|
||||
for col in bpy.data.collections:
|
||||
bpy.data.collections.remove(col)
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
bpy.ops.outliner.orphans_purge(
|
||||
do_local_ids=True, do_linked_ids=True, do_recursive=True
|
||||
)
|
||||
|
||||
|
||||
def set_shot_scene(context: bpy.types.Context, scene_name: str) -> bpy.types.Scene:
|
||||
print(f"create scene with name {scene_name}")
|
||||
for scene in bpy.data.scenes:
|
||||
scene.name = 'REMOVE-' + scene.name
|
||||
|
||||
keep_scene = bpy.data.scenes.new(name=scene_name)
|
||||
for scene in bpy.data.scenes:
|
||||
if scene.name == scene_name:
|
||||
continue
|
||||
print(f"remove scene {scene.name}")
|
||||
bpy.data.scenes.remove(scene)
|
||||
|
||||
context.window.scene = keep_scene
|
||||
return keep_scene
|
||||
|
||||
|
||||
def set_resolution_and_fps(project: Project, scene: bpy.types.Scene):
|
||||
scene.render.fps = int(project.fps) # set fps
|
||||
resolution = project.resolution.split('x')
|
||||
scene.render.resolution_x = int(resolution[0])
|
||||
scene.render.resolution_y = int(resolution[1])
|
||||
scene.render.resolution_percentage = 100
|
||||
|
||||
|
||||
def get_3d_start(shot: Shot):
|
||||
if shot.data and shot.data.get("3d_start"): # shot.data and
|
||||
return int(shot.data.get("3d_start"))
|
||||
else:
|
||||
return bkglobals.FRAME_START
|
||||
|
||||
|
||||
def set_frame_range(shot: Shot, scene: bpy.types.Scene):
|
||||
start_3d = get_3d_start(shot)
|
||||
scene.frame_start = start_3d
|
||||
if not shot.nb_frames:
|
||||
raise Exception(f"{shot.name} has missing frame duration information")
|
||||
scene.frame_end = start_3d + shot.nb_frames - 1
|
||||
|
||||
|
||||
def link_data_block(file_path: str, data_block_name: str, data_block_type: str):
|
||||
bpy.ops.wm.link(
|
||||
filepath=file_path,
|
||||
directory=file_path + "/" + data_block_type,
|
||||
filename=data_block_name,
|
||||
instance_collections=False,
|
||||
)
|
||||
# TODO This doesn't return anything but collections
|
||||
return bpy.data.collections.get(data_block_name)
|
||||
|
||||
|
||||
def link_and_override_collection(
|
||||
file_path: str, collection_name: str, scene: bpy.types.Scene
|
||||
) -> bpy.types.Collection:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
file_path (str): File Path to .blend file to link from
|
||||
collection_name (str): Name of collection to link from given filepath
|
||||
scene (bpy.types.Scene): Current Scene to link collection to
|
||||
|
||||
Returns:
|
||||
bpy.types.Collection: Overriden Collection linked to Scene Collection
|
||||
"""
|
||||
camera_col = link_data_block(file_path, collection_name, "Collection")
|
||||
override_camera_col = camera_col.override_hierarchy_create(
|
||||
scene, bpy.context.view_layer, do_fully_editable=True
|
||||
)
|
||||
scene.collection.children.unlink(camera_col)
|
||||
# Make library override.
|
||||
return override_camera_col
|
||||
|
||||
|
||||
def link_camera_rig(
|
||||
scene: bpy.types.Scene,
|
||||
output_collection: bpy.types.Collection,
|
||||
):
|
||||
"""
|
||||
Function to load the camera rig. The rig will be added to the output collection
|
||||
of the shot and the camera will be set as active camera.
|
||||
"""
|
||||
# Load camera rig.
|
||||
project_path = prefs.project_root_dir_get(bpy.context)
|
||||
path = f"{project_path}/pro/assets/cam/camera_rig.blend"
|
||||
|
||||
if not Path(path).exists():
|
||||
camera_data = bpy.data.cameras.new(name=CAMERA_NAME)
|
||||
camera_object = bpy.data.objects.new(name=CAMERA_NAME, object_data=camera_data)
|
||||
scene.collection.objects.link(camera_object)
|
||||
output_collection.objects.link(camera_object)
|
||||
return
|
||||
|
||||
collection_name = (
|
||||
"CA-camera_rig" # TODO Rename the asset itself, this breaks convention
|
||||
)
|
||||
|
||||
override_camera_col = link_and_override_collection(
|
||||
file_path=path, collection_name=collection_name, scene=scene
|
||||
)
|
||||
output_collection.children.link(override_camera_col)
|
||||
|
||||
# Set the camera of the camera rig as active scene camera.
|
||||
camera = override_camera_col.objects.get(CAMERA_NAME)
|
||||
scene.camera = camera
|
||||
|
||||
|
||||
def create_task_type_output_collection(
|
||||
scene: bpy.types.Scene, shot: Shot, task_type: TaskType
|
||||
) -> bpy.types.Collection:
|
||||
collections = bpy.data.collections
|
||||
output_col_name = shot.get_output_collection_name(task_type.get_short_name())
|
||||
|
||||
if not collections.get(output_col_name):
|
||||
bpy.data.collections.new(name=output_col_name)
|
||||
output_collection = collections.get(output_col_name)
|
||||
|
||||
output_collection.use_fake_user = True
|
||||
if not scene.collection.children.get(output_col_name):
|
||||
scene.collection.children.link(output_collection)
|
||||
|
||||
for view_layer in scene.view_layers:
|
||||
view_layer_output_collection = view_layer.layer_collection.children.get(
|
||||
output_col_name
|
||||
)
|
||||
view_layer_output_collection.exclude = True
|
||||
return output_collection
|
||||
|
||||
|
||||
def link_task_type_output_collections(shot: Shot, task_type: TaskType):
|
||||
task_type_short_name = task_type.get_short_name()
|
||||
if bkglobals.OUTPUT_COL_LINK_MAPPING.get(task_type_short_name) == None:
|
||||
return
|
||||
for short_name in bkglobals.OUTPUT_COL_LINK_MAPPING.get(task_type_short_name):
|
||||
external_filepath = shot.get_shot_filepath(bpy.context, short_name)
|
||||
if not Path(external_filepath).exists():
|
||||
print(
|
||||
f"Unable to link output collection for {Path(external_filepath).name}"
|
||||
)
|
||||
file_path = external_filepath.__str__()
|
||||
colection_name = shot.get_output_collection_name(short_name)
|
||||
link_data_block(file_path, colection_name, 'Collection')
|
@ -1,246 +0,0 @@
|
||||
# Project Description (DRAFT)
|
||||
|
||||
Shot Builder is an Add-on that helps studios to work with task specific
|
||||
Blend-files. The shot builder is part of the shot-tools repository. The main functionalities are
|
||||
|
||||
* Build blend files for a specific task and shot.
|
||||
* Sync data back from work files to places like kitsu, or `edit.blend`.
|
||||
|
||||
## Design Principles
|
||||
|
||||
The main design principles are:
|
||||
|
||||
* The core-tool can be installed as an add-on, but the (production specific)
|
||||
configuration should be part of the production repository.
|
||||
* The configuration files are a collection of python files. The API between
|
||||
the configuration files and the add-on should be easy to use as pipeline
|
||||
TDs working on the production should be able to work with it.
|
||||
* TDs/artists should be able to handle issues during building without looking
|
||||
at how the add-on is structured.
|
||||
* The tool contains connectors that can be configured to read/write data
|
||||
from the system/file that is the main location of the data. For example
|
||||
The start and end time of a shot could be stored in an external production tracking application.
|
||||
|
||||
## Connectors
|
||||
|
||||
Connectors are components that can be used to read or write to files or
|
||||
systems. The connectors will add flexibility to the add-on so it could be used
|
||||
in multiple productions or studios.
|
||||
|
||||
In the configuration files the TD can setup the connectors that are used for
|
||||
the production. Possible connectors would be:
|
||||
|
||||
* Connector for text based config files (json/yaml).
|
||||
* Connector for kitsu (https://www.cg-wire.com/en/kitsu.html).
|
||||
* Connector for blend files.
|
||||
|
||||
## Layering & Hooks
|
||||
|
||||
The configuration of the tool is layered. When building a work file for a sequence
|
||||
there are multiple ways to change the configuration.
|
||||
|
||||
* Configuration for the production.
|
||||
* Configuration for the asset that is needed.
|
||||
* Configuration for the asset type of the loaded asset.
|
||||
* Configuration for the sequence.
|
||||
* Configuration for the shot.
|
||||
* Configuration for the task type.
|
||||
|
||||
For any combination of these configurations hooks can be defined.
|
||||
|
||||
```
|
||||
@shot_tools.hook(match_asset_name='Spring', match_shot_code='02_020A')
|
||||
def hook_Spring_02_020A(asset: shot_tools.Asset, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides when Spring is loaded in 02_020A.
|
||||
"""
|
||||
|
||||
@shot_tools.hook(match_task_type='anim')
|
||||
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides for any animation task.
|
||||
"""
|
||||
```
|
||||
|
||||
### Data
|
||||
|
||||
All hooks must have Python’s `**kwargs` parameter. The `kwargs` contains
|
||||
the context at the moment the hook is invoked. The context can contain the
|
||||
following items.
|
||||
|
||||
* `production`: `shot_tools.Production`: Include the name of the production
|
||||
and the location on the filesystem.
|
||||
* `task`: `shot_tools.Task`: The task (combination of task_type and shot)
|
||||
* `task_type`: `shot_tools.TaskType`: Is part of the `task`.
|
||||
* `sequence`: `shot_tools.Sequence`: Is part of `shot`.
|
||||
* `shot`: `shot_tools.Shot` Is part of `task`.
|
||||
* `asset`: `shot_tools.Asset`: Only available during asset loading phase.
|
||||
* `asset_type`: `shot_tools.AssetType`: Only available during asset loading phase.
|
||||
|
||||
### Execution Order
|
||||
|
||||
The add-on will internally create a list containing the hooks that needs to be
|
||||
executed for the command in a sensible order. It will then execute them in that
|
||||
order.
|
||||
|
||||
By default the next order will be used:
|
||||
|
||||
* Production wide hooks
|
||||
* Asset Type hooks
|
||||
* Asset hooks
|
||||
* Sequence hooks
|
||||
* Shot hooks
|
||||
* Task type hooks
|
||||
|
||||
A hook with a single ‘match’ rule will be run in the corresponding phase. A hook with
|
||||
multiple ‘match’ rules will be run in the last matching phase. For example, a hook with
|
||||
‘asset’ and ‘task type’ match rules will be run in the ‘task type’ phase.
|
||||
|
||||
#### Events
|
||||
|
||||
Order of execution can be customized by adding the optional `run_before`
|
||||
or `run_after` parameters.
|
||||
|
||||
```
|
||||
@shot_tools.hook(match_task_type='anim',
|
||||
requires={shot_tools.events.AssetsLoaded, hook_task_other_anim},
|
||||
is_required_by={shot_tools.events.ShotOverrides})
|
||||
def hook_task_anim(task: shot_tools.Task, shot: shot_tools.Shot, **kwargs) -> None:
|
||||
"""
|
||||
Specific overrides for any animation task run after all assets have been loaded.
|
||||
"""
|
||||
```
|
||||
|
||||
Events could be:
|
||||
|
||||
* `shot_tools.events.BuildStart`
|
||||
* `shot_tools.events.ProductionSettingsLoaded`
|
||||
* `shot_tools.events.AssetsLoaded`
|
||||
* `shot_tools.events.AssetTypeOverrides`
|
||||
* `shot_tools.events.SequenceOverrides`
|
||||
* `shot_tools.events.ShotOverrides`
|
||||
* `shot_tools.events.TaskTypeOverrides`
|
||||
* `shot_tools.events.BuildFinished`
|
||||
* `shot_tools.events.HookStart`
|
||||
* `shot_tools.events.HookEnd`
|
||||
|
||||
During usage we should see which one of these or other events are needed.
|
||||
|
||||
`shot_tools.events.BuildStart`, `shot_tools.events.ProductionSettingsLoaded`
|
||||
and `shot_tools.events.HookStart` can only be used in the `run_after`
|
||||
parameter. `shot_tools.events.BuildFinished`, `shot_tools.events.HookFinished`
|
||||
can only be used in the `run_before` parameter.
|
||||
|
||||
|
||||
## API
|
||||
|
||||
The shot builder has an API between the add-on and the configuration files. This
|
||||
API contains convenience functions and classes to hide complexity and makes
|
||||
sure that the configuration files are easy to maintain.
|
||||
|
||||
```
|
||||
register_task_type(task_type="anim")
|
||||
register_task_type(task_type="lighting")
|
||||
```
|
||||
|
||||
```
|
||||
# shot_tool/characters.py
|
||||
class Asset(shot_tool.some_module.Asset):
|
||||
asset_file = "/{asset_type}/{name}/{name}.blend"
|
||||
collection = “{class_name}”
|
||||
name = “{class_name}”
|
||||
|
||||
class Character(Asset):
|
||||
asset_type = ‘char’
|
||||
|
||||
|
||||
class Ellie(Character):
|
||||
collection = “{class_name}-{variant_name}”
|
||||
variants = {‘default’, ‘short_hair’}
|
||||
|
||||
class Victoria(Character): pass
|
||||
class Rex(Character): pass
|
||||
|
||||
# shot_tool/shots.py
|
||||
class Shot_01_020_A(shot_tool.some_module.Shot):
|
||||
shot_id = ‘01_020_A’
|
||||
assets = {
|
||||
characters.Ellie(variant=”short_hair”),
|
||||
characters.Rex,
|
||||
sets.LogOverChasm,
|
||||
}
|
||||
|
||||
class AllHumansShot(shot_tool.some_module.Shot):
|
||||
assets = {
|
||||
characters.Ellie(variant=”short_hair”),
|
||||
characters.Rex,
|
||||
characters.Victoria,
|
||||
}
|
||||
|
||||
class Shot_01_035_A(AllHumansShot):
|
||||
assets = {
|
||||
sets.Camp,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This API is structured/implemented in a way that it keeps track of what
|
||||
is being done. This will be used when an error occurs so a descriptive
|
||||
error message can be generated that would help the TD to solve the issue more
|
||||
quickly. The goal would be that the error messages are descriptive enough to
|
||||
direct the TD into the direction where the actual cause is. And when possible
|
||||
propose several solutions to fix it.
|
||||
|
||||
## Setting up the tool
|
||||
|
||||
The artist/TD can configure their current local project directory in the add-on preferences.
|
||||
This can then be used for new blend files. The project associated with an opened (so existing)
|
||||
blend file can be found automatically by iterating over parent directories until a Shot Builder
|
||||
configuration file is found. Project-specific settings are not configured/stored in the add-on,
|
||||
but in this configuration file.
|
||||
|
||||
The add-on will look in the root of the production repository to locate the
|
||||
main configuration file `/project_root_directory/pro/shot-builder/config.py`. This file contains general
|
||||
settings about the production, including:
|
||||
|
||||
* The name of the production for reporting back to the user when needed.
|
||||
* Naming standards to test against when reporting deviations.
|
||||
* Location of other configuration (`tasks.py`, `assets.py`) relative to the `shot-builder` directory of the production.
|
||||
* Configuration of the needed connectors.
|
||||
|
||||
### Directory Layout
|
||||
``` bash
|
||||
└── project-name/ # Project Root Directory
|
||||
└── pro/
|
||||
├── assets/
|
||||
├── shot-builder/
|
||||
│ ├── assets.py
|
||||
│ ├── config.py
|
||||
│ ├── hooks.py
|
||||
│ └── shots.py
|
||||
└── shots/
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Any artist can open a shot file via the `File` menu. A modal panel appears
|
||||
where the user can select the task type and sequence/shot. When the file
|
||||
already exists, it will be opened. When the file doesn't exist, the file
|
||||
will be built.
|
||||
|
||||
In the future other use cases will also be accessible, such as:
|
||||
|
||||
* Syncing data back from a work file to the source of the data.
|
||||
* Report of errors/differences between the shot file and the configuration.
|
||||
|
||||
## Open Issues
|
||||
|
||||
### Security
|
||||
|
||||
* Security keys needed by connectors need to be stored somewhere. The easy
|
||||
place is to place inside the production repository, but that isn't secure
|
||||
Anyone with access to the repository could misuse the keys to access the
|
||||
connector. Other solution might be to use the OS key store or retrieve the
|
||||
keys from an online service authenticated by the blender cloud add-on.
|
||||
|
||||
We could use `keyring` to access OS key stores.
|
@ -1,4 +0,0 @@
|
||||
# Example configuration files
|
||||
|
||||
This folder contains an example shot builder configuration. It shows the part
|
||||
that a TD would do to incorporate the shot builder in a production.
|
@ -1,30 +0,0 @@
|
||||
from blender_kitsu.shot_builder.asset import Asset
|
||||
|
||||
|
||||
class ProductionAsset(Asset):
|
||||
path = "{production.path}/assets/{asset.asset_type}/{asset.code}/{asset.code}.blend" # Path to most assets
|
||||
color_tag = "NONE"
|
||||
|
||||
|
||||
# Categories
|
||||
class Character(ProductionAsset):
|
||||
asset_type = "chars"
|
||||
collection = "CH-{asset.code}" # Prefix for characters
|
||||
|
||||
|
||||
class Prop(ProductionAsset):
|
||||
asset_type = "props"
|
||||
collection = "PR-{asset.code}" # Prefix for props
|
||||
|
||||
|
||||
# Assets
|
||||
class MyCharacter(Character):
|
||||
name = "My Character" # Name on Kitsu Server
|
||||
code = "mycharacter" # Name of Collection without prefix (e.g. CH-mycharacter)
|
||||
path = "{production.path}/assets/{asset.asset_type}/mycharacter/publish/mycharacter.v001.blend" # This asset has a custom path
|
||||
color_tag = "COLOR_01"
|
||||
|
||||
|
||||
class MyProp(Prop):
|
||||
name = "MyProp"
|
||||
code = "myprop"
|
@ -1,13 +0,0 @@
|
||||
from blender_kitsu.shot_builder.connectors.kitsu import KitsuConnector
|
||||
|
||||
PRODUCTION_NAME = KitsuConnector
|
||||
SHOTS = KitsuConnector
|
||||
ASSETS = KitsuConnector
|
||||
RENDER_SETTINGS = KitsuConnector
|
||||
|
||||
# Formatting rules
|
||||
# ----------------
|
||||
|
||||
# The name of the scene in blender where the shot is build in.
|
||||
SCENE_NAME_FORMAT = "{shot.name}.{task_type}"
|
||||
SHOT_NAME_FORMAT = "{shot.name}"
|
@ -1,170 +0,0 @@
|
||||
import bpy
|
||||
from blender_kitsu.shot_builder.hooks import hook, Wildcard
|
||||
from blender_kitsu.shot_builder.asset import Asset
|
||||
from blender_kitsu.shot_builder.shot import Shot
|
||||
from blender_kitsu.shot_builder.project import Production
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------- Global Hook ----------
|
||||
|
||||
|
||||
CAMERA_NAME = 'CAM-camera'
|
||||
|
||||
|
||||
@hook()
|
||||
def set_cycles_render_engine(scene: bpy.types.Scene, **kwargs):
|
||||
"""
|
||||
By default we set Cycles as the renderer.
|
||||
"""
|
||||
scene.render.engine = 'CYCLES'
|
||||
|
||||
|
||||
# ---------- Overrides for animation files ----------
|
||||
|
||||
|
||||
@hook(match_task_type='anim')
|
||||
def task_type_anim_set_workbench(scene: bpy.types.Scene, **kwargs):
|
||||
"""
|
||||
Override of the render engine to Workbench when building animation files.
|
||||
"""
|
||||
scene.render.engine = 'BLENDER_WORKBENCH'
|
||||
|
||||
|
||||
# ---------- Create output collection for animation files ----------
|
||||
|
||||
|
||||
def _add_camera_rig(
|
||||
scene: bpy.types.Scene,
|
||||
production: Production,
|
||||
shot: Shot,
|
||||
):
|
||||
"""
|
||||
Function to load the camera rig. The rig will be added to the output collection
|
||||
of the shot and the camera will be set as active camera.
|
||||
"""
|
||||
# Load camera rig.
|
||||
path = f"{production.path}/assets/cam/camera_rig.blend"
|
||||
|
||||
if not Path(path).exists():
|
||||
camera_data = bpy.data.cameras.new(name=CAMERA_NAME)
|
||||
camera_object = bpy.data.objects.new(name=CAMERA_NAME, object_data=camera_data)
|
||||
shot.output_collection.objects.link(camera_object)
|
||||
return
|
||||
|
||||
collection_name = "CA-camera_rig"
|
||||
bpy.ops.wm.link(
|
||||
filepath=path,
|
||||
directory=path + "/Collection",
|
||||
filename=collection_name,
|
||||
)
|
||||
# Keep the active object name as this would also be the name of the collection after enabling library override.
|
||||
active_object_name = bpy.context.active_object.name
|
||||
|
||||
# Make library override.
|
||||
bpy.ops.object.make_override_library()
|
||||
|
||||
# Add camera collection to the output collection
|
||||
asset_collection = bpy.data.collections[active_object_name]
|
||||
shot.output_collection.children.link(asset_collection)
|
||||
|
||||
# Set the camera of the camera rig as active scene camera.
|
||||
camera = bpy.data.objects[CAMERA_NAME]
|
||||
scene.camera = camera
|
||||
|
||||
|
||||
@hook(match_task_type='anim')
|
||||
def task_type_anim_output_collection(
|
||||
scene: bpy.types.Scene, production: Production, shot: Shot, task_type: str, **kwargs
|
||||
):
|
||||
"""
|
||||
Animations are stored in an output collection. This collection will be linked
|
||||
by the lighting file.
|
||||
|
||||
Also loads the camera rig.
|
||||
"""
|
||||
output_collection = bpy.data.collections.new(
|
||||
name=shot.get_output_collection_name(shot=shot, task_type=task_type)
|
||||
)
|
||||
shot.output_collection = output_collection
|
||||
output_collection.use_fake_user = True
|
||||
scene.collection.children.link(output_collection)
|
||||
|
||||
_add_camera_rig(scene, production, shot)
|
||||
|
||||
|
||||
@hook(match_task_type='lighting')
|
||||
def link_anim_output_collection(
|
||||
scene: bpy.types.Scene, production: Production, shot: Shot, **kwargs
|
||||
):
|
||||
"""
|
||||
Link in the animation output collection from the animation file.
|
||||
"""
|
||||
anim_collection = bpy.data.collections.new(name="animation")
|
||||
scene.collection.children.link(anim_collection)
|
||||
anim_file_path = shot.get_anim_file_path(production, shot)
|
||||
anim_output_collection_name = shot.get_output_collection_name(
|
||||
shot=shot, task_type="anim"
|
||||
)
|
||||
result = bpy.ops.wm.link(
|
||||
filepath=anim_file_path,
|
||||
directory=anim_file_path + "/Collection",
|
||||
filename=anim_output_collection_name,
|
||||
)
|
||||
assert result == {'FINISHED'}
|
||||
|
||||
# Move the anim output collection from scene collection to the animation collection.
|
||||
anim_output_collection = bpy.data.objects[anim_output_collection_name]
|
||||
anim_collection.objects.link(anim_output_collection)
|
||||
scene.collection.objects.unlink(anim_output_collection)
|
||||
|
||||
# Use animation camera as active scene camera.
|
||||
camera = bpy.data.objects['CAM-camera']
|
||||
scene.camera = camera
|
||||
|
||||
|
||||
# ---------- Asset loading and linking ----------
|
||||
|
||||
|
||||
@hook(match_task_type='anim', match_asset_type=['chars', 'props'])
|
||||
def link_char_prop_for_anim(scene: bpy.types.Scene, shot: Shot, asset: Asset, **kwargs):
|
||||
"""
|
||||
Loading a character or prop for an animation file.
|
||||
"""
|
||||
collection_names = []
|
||||
if asset.code == 'notepad_pencil':
|
||||
collection_names.append("PR-pencil")
|
||||
collection_names.append("PR-notepad")
|
||||
else:
|
||||
collection_names.append(asset.collection)
|
||||
|
||||
for collection_name in collection_names:
|
||||
logger.info("link asset")
|
||||
bpy.ops.wm.link(
|
||||
filepath=str(asset.path),
|
||||
directory=str(asset.path) + "/Collection",
|
||||
filename=collection_name,
|
||||
)
|
||||
# Keep the active object name as this would also be the name of the collection after enabling library override.
|
||||
active_object_name = bpy.context.active_object.name
|
||||
|
||||
# Make library override.
|
||||
bpy.ops.object.make_override_library()
|
||||
|
||||
# Add overridden collection to the output collection.
|
||||
asset_collection = bpy.data.collections[active_object_name]
|
||||
shot.output_collection.children.link(asset_collection)
|
||||
|
||||
|
||||
@hook(match_task_type=Wildcard, match_asset_type='sets')
|
||||
def link_set(asset: Asset, **kwargs):
|
||||
"""
|
||||
Load the set of the shot.
|
||||
"""
|
||||
bpy.ops.wm.link(
|
||||
filepath=str(asset.path),
|
||||
directory=str(asset.path) + "/Collection",
|
||||
filename=asset.collection,
|
||||
)
|
@ -1,6 +0,0 @@
|
||||
# Example configuration files
|
||||
|
||||
This folder contains an example shot builder configuration. It shows the part
|
||||
that a TD would do to incorporate the shot builder in a production.
|
||||
|
||||
|
@ -1,41 +0,0 @@
|
||||
from blender_kitsu.shot_builder.shot import Shot
|
||||
from blender_kitsu.shot_builder.project import Production
|
||||
|
||||
|
||||
class ProductionShot(Shot):
|
||||
def get_anim_file_path(self, production: Production, shot: Shot) -> str:
|
||||
"""Get the animation file path for this given shot."""
|
||||
return self.file_path_format.format_map(
|
||||
{'production': production, 'shot': shot, 'task_type': "anim"}
|
||||
)
|
||||
|
||||
def get_lighting_file_path(self, production: Production, shot: Shot) -> str:
|
||||
"""Get the lighting file path for this given shot."""
|
||||
return self.file_path_format.format_map(
|
||||
{'production': production, 'shot': shot, 'task_type': "lighting"}
|
||||
)
|
||||
|
||||
def get_output_collection_name(self, shot: Shot, task_type: str) -> str:
|
||||
"""Get the collection name where the output is stored."""
|
||||
return f"{shot.name}.{task_type}.output"
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if this shot contains all data, so it could be selected
|
||||
for shot building.
|
||||
"""
|
||||
if not super().is_valid():
|
||||
return False
|
||||
return True
|
||||
|
||||
# Assuming path to file is in `project_name/svn/pro/shot/sequence_name/shot_name`
|
||||
# Render Ouput path should be `project_name/shared/shot_frames/sequence_name/shot_name/`
|
||||
|
||||
def get_render_output_dir(self) -> str:
|
||||
return f"//../../../../../shared/shot_frames/{self.sequence_code}/{self.name}/{self.name}.lighting"
|
||||
|
||||
def get_comp_output_dir(self) -> str:
|
||||
return f"//../../../../../shared/shot_frames/{self.sequence_code}/{self.name}/{self.name}.comp"
|
||||
|
||||
|
||||
class GenericShot(ProductionShot):
|
||||
is_generic = True
|
@ -1,9 +1,8 @@
|
||||
import bpy
|
||||
import re
|
||||
from .. import prefs
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
from ... import prefs
|
||||
from ... import cache
|
||||
import re
|
||||
from .core import get_3d_start
|
||||
|
||||
|
||||
def editorial_export_get_latest(
|
||||
@ -16,7 +15,7 @@ def editorial_export_get_latest(
|
||||
if not latest_file:
|
||||
return None
|
||||
# Check if Kitsu server returned empty shot
|
||||
if shot.get("id") == '':
|
||||
if shot.id == '':
|
||||
return None
|
||||
strip_filepath = latest_file.as_posix()
|
||||
strip_frame_start = addon_prefs.shot_builder_frame_offset
|
||||
@ -41,8 +40,8 @@ def editorial_export_get_latest(
|
||||
new_strips = [movie_strip, sound_strip]
|
||||
|
||||
# Update shift frame range prop.
|
||||
frame_in = shot["data"].get("frame_in")
|
||||
frame_3d_start = shot["data"].get("3d_start")
|
||||
frame_in = shot.data.get("frame_in")
|
||||
frame_3d_start = get_3d_start(shot)
|
||||
frame_3d_offset = frame_3d_start - addon_prefs.shot_builder_frame_offset
|
||||
edit_export_offset = addon_prefs.edit_export_frame_offset
|
||||
|
@ -1,30 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
from . import ops
|
||||
|
||||
|
||||
def register():
|
||||
ops.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
ops.unregister()
|
@ -1,42 +0,0 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from .core import editorial_export_get_latest
|
||||
from ... import cache
|
||||
import gazu
|
||||
|
||||
|
||||
class ANIM_SETUP_OT_load_latest_editorial(bpy.types.Operator):
|
||||
bl_idname = "asset_setup.load_latest_editorial"
|
||||
bl_label = "Load Editorial Export"
|
||||
bl_description = (
|
||||
"Loads latest edit from shot_preview_folder "
|
||||
"Shifts edit so current shot starts at 3d_start metadata shot key from Kitsu"
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
cache_shot = cache.shot_active_get()
|
||||
shot = gazu.shot.get_shot(cache_shot.id) # TODO INEFFICENT TO LOAD SHOT TWICE
|
||||
strips = editorial_export_get_latest(context, shot)
|
||||
if strips is None:
|
||||
self.report(
|
||||
{"ERROR"}, f"No valid editorial export in editorial export path."
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.report({"INFO"}, f"Loaded latest edit: {strips[0].name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = [
|
||||
ANIM_SETUP_OT_load_latest_editorial,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
@ -0,0 +1,12 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
|
||||
def save_shot_builder_file(file_path: str):
|
||||
"""Save Shot File within Folder of matching name.
|
||||
Set Shot File to relative Paths."""
|
||||
if Path(file_path).exists():
|
||||
raise Exception(f"Cannot Overwrite Existing Shot File {file_path}")
|
||||
dir_path = Path(file_path).parent
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_mainfile(filepath=file_path, relative_remap=True)
|
@ -0,0 +1,51 @@
|
||||
import bpy
|
||||
|
||||
from blender_kitsu.shot_builder.hooks import hook
|
||||
from blender_kitsu.types import Shot, Asset
|
||||
|
||||
import logging
|
||||
|
||||
'''
|
||||
Arguments to use in hooks
|
||||
scene: bpy.types.Scene # current scene
|
||||
shot: Shot class from blender_kitsu.types.py
|
||||
prod_path: str # path to production root dir (your_project/svn/)
|
||||
shot_path: str # path to shot file (your_project/svn/pro/shots/{sequence_name}/{shot_name}/{shot_task_name}.blend})
|
||||
|
||||
Notes
|
||||
matching_task_type = ['anim', 'lighting', 'fx', 'comp'] # either use list or just one string
|
||||
output_col_name = shot.get_output_collection_name(task_type_short_name="anim")
|
||||
|
||||
|
||||
|
||||
|
||||
'''
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------- Global Hook ----------
|
||||
|
||||
|
||||
@hook()
|
||||
def set_eevee_render_engine(scene: bpy.types.Scene, **kwargs):
|
||||
"""
|
||||
By default, we set EEVEE as the renderer.
|
||||
"""
|
||||
scene.render.engine = 'BLENDER_EEVEE'
|
||||
print("HOOK SET RENDER ENGINE")
|
||||
|
||||
|
||||
# ---------- Overrides for animation files ----------
|
||||
|
||||
|
||||
@hook(match_task_type='anim')
|
||||
def test_args(
|
||||
scene: bpy.types.Scene, shot: Shot, prod_path: str, shot_path: str, **kwargs
|
||||
):
|
||||
"""
|
||||
Set output parameters for animation rendering
|
||||
"""
|
||||
print(f"Scene = {scene.name}")
|
||||
print(f"Shot = {shot.name}")
|
||||
print(f"Prod Path = {prod_path}")
|
||||
print(f"Shot Path = {shot_path}")
|
@ -1,8 +1,14 @@
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import *
|
||||
|
||||
import typing
|
||||
import types
|
||||
from collections.abc import Iterable
|
||||
|
||||
import importlib
|
||||
from .. import prefs
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -44,6 +50,8 @@ HookFunction = typing.Callable[[typing.Any], None]
|
||||
def _match_hook_parameter(
|
||||
hook_criteria: MatchCriteriaType, match_query: typing.Optional[str]
|
||||
) -> bool:
|
||||
if hook_criteria == None:
|
||||
return True
|
||||
if hook_criteria == DoNotMatch:
|
||||
return match_query is None
|
||||
if hook_criteria == Wildcard:
|
||||
@ -60,10 +68,6 @@ class Hooks:
|
||||
def __init__(self):
|
||||
self._hooks: typing.List[HookFunction] = []
|
||||
|
||||
def register(self, func: HookFunction) -> None:
|
||||
logger.info(f"registering hook '{func.__name__}'")
|
||||
self._hooks.append(func)
|
||||
|
||||
def matches(
|
||||
self,
|
||||
hook: HookFunction,
|
||||
@ -73,7 +77,6 @@ class Hooks:
|
||||
) -> bool:
|
||||
assert not kwargs
|
||||
rules = typing.cast(MatchingRulesType, getattr(hook, '_shot_builder_rules'))
|
||||
|
||||
return all(
|
||||
(
|
||||
_match_hook_parameter(rules['match_task_type'], match_task_type),
|
||||
@ -86,32 +89,54 @@ class Hooks:
|
||||
if self.matches(hook=hook, **kwargs):
|
||||
yield hook
|
||||
|
||||
def execute_hooks(
|
||||
self, match_task_type: str = None, match_asset_type: str = None, *args, **kwargs
|
||||
) -> None:
|
||||
for hook in self._hooks:
|
||||
if self.matches(
|
||||
hook, match_task_type=match_task_type, match_asset_type=match_asset_type
|
||||
):
|
||||
hook(*args, **kwargs)
|
||||
|
||||
def _register_hook(func: types.FunctionType) -> None:
|
||||
from .project import get_active_production
|
||||
def load_hooks(self, context):
|
||||
root_dir = prefs.project_root_dir_get(context)
|
||||
shot_builder_config_dir = root_dir.joinpath("pro/assets/scripts/shot-builder")
|
||||
if not shot_builder_config_dir.exists():
|
||||
raise Exception("Shot Builder Hooks directory does not exist")
|
||||
paths = [shot_builder_config_dir.resolve().__str__()]
|
||||
with SystemPathInclude(paths) as _include:
|
||||
try:
|
||||
import hooks as production_hooks
|
||||
|
||||
production = get_active_production()
|
||||
production.hooks.register(func)
|
||||
importlib.reload(production_hooks)
|
||||
self.register_hooks(production_hooks)
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("Production has no `hooks.py` configuration file")
|
||||
|
||||
return False
|
||||
|
||||
def register_hooks(module: types.ModuleType) -> None:
|
||||
"""
|
||||
Register all hooks inside the given module.
|
||||
"""
|
||||
for module_item_str in dir(module):
|
||||
module_item = getattr(module, module_item_str)
|
||||
if not isinstance(module_item, types.FunctionType):
|
||||
continue
|
||||
if module_item.__module__ != module.__name__:
|
||||
continue
|
||||
if not hasattr(module_item, "_shot_builder_rules"):
|
||||
continue
|
||||
_register_hook(module_item)
|
||||
def register(self, func: HookFunction) -> None:
|
||||
logger.info(f"registering hook '{func.__name__}'")
|
||||
self._hooks.append(func)
|
||||
|
||||
def register_hooks(self, module: types.ModuleType) -> None:
|
||||
"""
|
||||
Register all hooks inside the given module.
|
||||
"""
|
||||
for module_item_str in dir(module):
|
||||
module_item = getattr(module, module_item_str)
|
||||
if not isinstance(module_item, types.FunctionType):
|
||||
continue
|
||||
if module_item.__module__ != module.__name__:
|
||||
continue
|
||||
if not hasattr(module_item, "_shot_builder_rules"):
|
||||
continue
|
||||
self.register(module_item)
|
||||
|
||||
|
||||
def hook(
|
||||
match_task_type: MatchCriteriaType = DoNotMatch,
|
||||
match_asset_type: MatchCriteriaType = DoNotMatch,
|
||||
match_task_type: MatchCriteriaType = None,
|
||||
match_asset_type: MatchCriteriaType = None,
|
||||
) -> typing.Callable[[types.FunctionType], types.FunctionType]:
|
||||
"""
|
||||
Decorator to add custom logic when building a shot.
|
||||
@ -128,3 +153,41 @@ def hook(
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SystemPathInclude:
|
||||
"""
|
||||
Resource class to temporary include system paths to `sys.paths`.
|
||||
|
||||
Usage:
|
||||
```
|
||||
paths = [pathlib.Path("/home/guest/my_python_scripts")]
|
||||
with SystemPathInclude(paths) as t:
|
||||
import my_module
|
||||
reload(my_module)
|
||||
```
|
||||
|
||||
It is possible to nest multiple SystemPathIncludes.
|
||||
"""
|
||||
|
||||
def __init__(self, paths_to_add: List[pathlib.Path]):
|
||||
# TODO: Check if all paths exist and are absolute.
|
||||
self.__paths = paths_to_add
|
||||
self.__original_sys_path: List[str] = []
|
||||
|
||||
def __enter__(self):
|
||||
self.__original_sys_path = sys.path
|
||||
new_sys_path = []
|
||||
for path_to_add in self.__paths:
|
||||
# Do not add paths that are already in the sys path.
|
||||
# Report this to the logger as this might indicate wrong usage.
|
||||
path_to_add_str = str(path_to_add)
|
||||
if path_to_add_str in self.__original_sys_path:
|
||||
logger.warn(f"{path_to_add_str} already added to `sys.path`")
|
||||
continue
|
||||
new_sys_path.append(path_to_add_str)
|
||||
new_sys_path.extend(self.__original_sys_path)
|
||||
sys.path = new_sys_path
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
sys.path = self.__original_sys_path
|
||||
|
@ -1,298 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
import pathlib
|
||||
from typing import *
|
||||
import bpy
|
||||
import gazu
|
||||
from .shot import ShotRef
|
||||
from .project import (
|
||||
ensure_loaded_production,
|
||||
get_active_production,
|
||||
)
|
||||
from .builder import ShotBuilder
|
||||
from .task_type import TaskType
|
||||
from .. import prefs, cache
|
||||
from .anim_setup.core import (
|
||||
animation_workspace_delete_others,
|
||||
animation_workspace_vse_area_add,
|
||||
)
|
||||
from .editorial.core import editorial_export_get_latest
|
||||
from .builder.save_file import save_shot_builder_file
|
||||
|
||||
|
||||
_production_task_type_items: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def production_task_type_items(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_task_type_items
|
||||
return _production_task_type_items
|
||||
|
||||
|
||||
_production_seq_id_items: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def production_seq_id_items(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_seq_id_items
|
||||
return _production_seq_id_items
|
||||
|
||||
|
||||
_production_shots: List[ShotRef] = []
|
||||
|
||||
|
||||
def production_shots(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_shots
|
||||
return _production_shots
|
||||
|
||||
|
||||
_production_shot_id_items_for_seq: List[Tuple[str, str, str]] = []
|
||||
|
||||
|
||||
def production_shot_id_items_for_seq(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global _production_shot_id_items_for_seq
|
||||
global _production_shot_id_items
|
||||
|
||||
if not self.seq_id or not _production_shots:
|
||||
return []
|
||||
|
||||
shots_for_seq: List[Tuple(str, str, str)] = [
|
||||
(s.name, s.name, "")
|
||||
for s in _production_shots
|
||||
if s.sequence.name == self.seq_id
|
||||
]
|
||||
|
||||
_production_shot_id_items_for_seq.clear()
|
||||
_production_shot_id_items_for_seq.extend(shots_for_seq)
|
||||
|
||||
return _production_shot_id_items_for_seq
|
||||
|
||||
|
||||
def reset_shot_id_enum(self: Any, context: bpy.types.Context) -> None:
|
||||
production_shot_id_items_for_seq(self, context)
|
||||
global _production_shot_id_items_for_seq
|
||||
if _production_shot_id_items_for_seq:
|
||||
self.shot_id = _production_shot_id_items_for_seq[0][0]
|
||||
|
||||
|
||||
class SHOTBUILDER_OT_NewShotFile(bpy.types.Operator):
|
||||
"""Build a new shot file"""
|
||||
|
||||
bl_idname = "shotbuilder.new_shot_file"
|
||||
bl_label = "New Production Shot File"
|
||||
|
||||
_timer = None
|
||||
_built_shot = False
|
||||
_add_vse_area = False
|
||||
_file_path = ''
|
||||
|
||||
production_root: bpy.props.StringProperty( # type: ignore
|
||||
name="Production Root", description="Root of the production", subtype='DIR_PATH'
|
||||
)
|
||||
|
||||
production_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Production",
|
||||
description="Name of the production to create a shot file for",
|
||||
options=set(),
|
||||
)
|
||||
|
||||
seq_id: bpy.props.EnumProperty( # type: ignore
|
||||
name="Sequence ID",
|
||||
description="Sequence ID of the shot to build",
|
||||
items=production_seq_id_items,
|
||||
update=reset_shot_id_enum,
|
||||
)
|
||||
|
||||
shot_id: bpy.props.EnumProperty( # type: ignore
|
||||
name="Shot ID",
|
||||
description="Shot ID of the shot to build",
|
||||
items=production_shot_id_items_for_seq,
|
||||
)
|
||||
|
||||
task_type: bpy.props.EnumProperty( # type: ignore
|
||||
name="Task",
|
||||
description="Task to create the shot file for",
|
||||
items=production_task_type_items,
|
||||
)
|
||||
auto_save: bpy.props.BoolProperty(
|
||||
name="Save after building.",
|
||||
description="Automatically save build file after 'Shot Builder' is complete.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == 'TIMER' and not self._add_vse_area:
|
||||
# Show Storyboard/Animatic from VSE
|
||||
"""Running as Modal Event because functions within execute() function like
|
||||
animation_workspace_delete_others() changed UI context that needs to be refreshed.
|
||||
https://docs.blender.org/api/current/info_gotcha.html#no-updates-after-changing-ui-context
|
||||
"""
|
||||
# TODO this is a hack, should be inherient to above builder
|
||||
# TODO fix during refactor
|
||||
if self.task_type == 'anim':
|
||||
animation_workspace_vse_area_add(context)
|
||||
self._add_vse_area = True
|
||||
|
||||
if self._built_shot and self._add_vse_area:
|
||||
if self.auto_save:
|
||||
file_path = pathlib.Path()
|
||||
try:
|
||||
save_shot_builder_file(self._file_path)
|
||||
self.report(
|
||||
{"INFO"}, f"Saved Shot{self.shot_id} at {self._file_path}"
|
||||
)
|
||||
return {'FINISHED'}
|
||||
except FileExistsError:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Cannot create a file/folder when that file/folder already exists {file_path}",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
self.report({"INFO"}, f"Built Shot {self.shot_id}, file is not saved!")
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
project = cache.project_active_get()
|
||||
|
||||
if addon_prefs.session.is_auth() is False:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Must be logged into Kitsu to continue. \nCheck login status in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if project.id == "":
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the Kitsu production's name. \nCheck project is selected in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the project root directory. \nCheck project root directiory is configured in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.production_root = addon_prefs.project_root_dir
|
||||
self.production_name = project.name
|
||||
|
||||
if not ensure_loaded_production(context):
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Shot builder configuration files not found in current project directory. \nCheck addon preferences to ensure project root contains shot_builder config.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
production = get_active_production()
|
||||
|
||||
global _production_task_type_items
|
||||
_production_task_type_items = production.get_task_type_items(context=context)
|
||||
|
||||
global _production_seq_id_items
|
||||
_production_seq_id_items = production.get_seq_items(context=context)
|
||||
|
||||
global _production_shots
|
||||
_production_shots = production.get_shots(context=context)
|
||||
|
||||
return cast(
|
||||
Set[str], context.window_manager.invoke_props_dialog(self, width=400)
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
addon_prefs = bpy.context.preferences.addons["blender_kitsu"].preferences
|
||||
wm = context.window_manager
|
||||
self._timer = wm.event_timer_add(0.1, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
if not self.production_root:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Shot builder can only be started from the File menu. Shortcuts like CTRL-N don't work",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self._built_shot:
|
||||
return {'RUNNING_MODAL'}
|
||||
ensure_loaded_production(context)
|
||||
production = get_active_production()
|
||||
shot_builder = ShotBuilder(
|
||||
context=context,
|
||||
production=production,
|
||||
shot_name=self.shot_id,
|
||||
task_type=TaskType(self.task_type),
|
||||
)
|
||||
shot_builder.create_build_steps()
|
||||
shot_builder.build()
|
||||
|
||||
active_project = cache.project_active_get()
|
||||
|
||||
# Build Kitsu Context
|
||||
sequence = gazu.shot.get_sequence_by_name(active_project.id, self.seq_id)
|
||||
shot = gazu.shot.get_shot_by_name(sequence, self.shot_id)
|
||||
|
||||
# TODO this is a hack, should be inherient to above builder
|
||||
# TODO fix during refactor
|
||||
if self.task_type == 'anim':
|
||||
# Load EDIT
|
||||
editorial_export_get_latest(context, shot)
|
||||
# Load Anim Workspace
|
||||
animation_workspace_delete_others()
|
||||
|
||||
# Initilize armatures
|
||||
for obj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]:
|
||||
base_name = obj.name.split(addon_prefs.shot_builder_armature_prefix)[-1]
|
||||
new_action = bpy.data.actions.new(
|
||||
f"{addon_prefs.shot_builder_action_prefix}{base_name}.{self.shot_id}.v001"
|
||||
)
|
||||
new_action.use_fake_user = True
|
||||
obj.animation_data.action = new_action
|
||||
|
||||
# Set Shot Frame Range
|
||||
context.scene.frame_start = int(shot["data"].get("3d_start"))
|
||||
context.scene.frame_end = (
|
||||
int(shot["data"].get("3d_start")) + shot.get('nb_frames') - 1
|
||||
)
|
||||
|
||||
# Run User Script
|
||||
exec(addon_prefs.user_exec_code)
|
||||
|
||||
self._file_path = shot_builder.build_context.shot.file_path
|
||||
self._built_shot = True
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
layout.prop(self, "seq_id")
|
||||
layout.prop(self, "shot_id")
|
||||
layout.prop(self, "task_type")
|
||||
layout.prop(self, "auto_save")
|
271
scripts-blender/addons/blender_kitsu/shot_builder/ops.py
Normal file
271
scripts-blender/addons/blender_kitsu/shot_builder/ops.py
Normal file
@ -0,0 +1,271 @@
|
||||
import bpy
|
||||
from .. import bkglobals
|
||||
from pathlib import Path
|
||||
from typing import List, Any, Tuple, Set, cast
|
||||
from .. import prefs, cache
|
||||
from .core import (
|
||||
set_render_engine,
|
||||
link_camera_rig,
|
||||
create_task_type_output_collection,
|
||||
set_shot_scene,
|
||||
set_resolution_and_fps,
|
||||
set_frame_range,
|
||||
link_task_type_output_collections,
|
||||
remove_all_data,
|
||||
)
|
||||
|
||||
from .editorial import editorial_export_get_latest
|
||||
from .file_save import save_shot_builder_file
|
||||
from .template import replace_workspace_with_template
|
||||
from .assets import get_shot_assets
|
||||
from .hooks import Hooks
|
||||
|
||||
active_project = None
|
||||
|
||||
|
||||
def get_shots_for_seq(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
if self.seq_id != '':
|
||||
seq = active_project.get_sequence(self.seq_id)
|
||||
shot_enum = cache.get_shots_enum_for_seq(self, context, seq)
|
||||
if shot_enum != []:
|
||||
return shot_enum
|
||||
return [('NONE', "No Shots Found", '')]
|
||||
|
||||
|
||||
def get_tasks_for_shot(
|
||||
self: Any, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
global active_project
|
||||
if not (self.shot_id == '' or self.shot_id == 'NONE'):
|
||||
shot = active_project.get_shot(self.shot_id)
|
||||
task_enum = cache.get_shot_task_types_enum_for_shot(self, context, shot)
|
||||
if task_enum != []:
|
||||
return task_enum
|
||||
return [('NONE', "No Tasks Found", '')]
|
||||
|
||||
|
||||
class KITSU_OT_save_shot_builder_hooks(bpy.types.Operator):
|
||||
bl_idname = "kitsu.save_shot_builder_hooks"
|
||||
bl_label = "Save Shot Builder Hook File"
|
||||
bl_description = "Save hook.py file to `your_project/svn/pro/assets/scripts/shot-builder` directory. Hooks are used to customize shot builder behaviour."
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
addon_prefs = prefs.addon_prefs_get(context)
|
||||
project = cache.project_active_get()
|
||||
if addon_prefs.session.is_auth() is False:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Must be logged into Kitsu to continue. \nCheck login status in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if project.id == "":
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the Kitsu production's name. \nCheck project is selected in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the project root directory. \nCheck project root directiory is configured in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
root_dir = prefs.project_root_dir_get(context)
|
||||
config_dir = root_dir.joinpath("pro/assets/scripts/shot-builder")
|
||||
if not config_dir.exists():
|
||||
config_dir.mkdir(parents=True)
|
||||
hook_file_path = config_dir.joinpath("hooks.py")
|
||||
if hook_file_path.exists():
|
||||
self.report(
|
||||
{'WARNING'},
|
||||
"File already exists, cannot overwrite",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
config_dir = Path(__file__).parent
|
||||
example_hooks_path = config_dir.joinpath("hook_examples/hooks.py")
|
||||
if not example_hooks_path.exists():
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Cannot find example hook file",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
with example_hooks_path.open() as source:
|
||||
# Read contents
|
||||
contents = source.read()
|
||||
|
||||
# Write contents to target file
|
||||
with hook_file_path.open('w') as target:
|
||||
target.write(contents)
|
||||
self.report({'INFO'}, f"Hook File saved to {hook_file_path}")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class KITSU_OT_build_new_shot(bpy.types.Operator):
|
||||
bl_idname = "kitsu.build_new_shot"
|
||||
bl_label = "Build New Shot"
|
||||
bl_description = "Build a New Shot file, based on infromation from KITSU Server"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
_timer = None
|
||||
_built_shot = False
|
||||
_add_vse_area = False
|
||||
_file_path = ''
|
||||
production_name: bpy.props.StringProperty( # type: ignore
|
||||
name="Production",
|
||||
description="Name of the production to create a shot file for",
|
||||
options=set(),
|
||||
)
|
||||
|
||||
seq_id: bpy.props.EnumProperty(
|
||||
name="Sequence ID",
|
||||
description="Sequence ID of the shot to build",
|
||||
items=cache.get_sequences_enum_list,
|
||||
)
|
||||
|
||||
shot_id: bpy.props.EnumProperty(
|
||||
name="Shot ID",
|
||||
description="Shot ID of the shot to build",
|
||||
items=get_shots_for_seq,
|
||||
)
|
||||
|
||||
task_type: bpy.props.EnumProperty(
|
||||
name="Task",
|
||||
description="Task to create the shot file for",
|
||||
items=get_tasks_for_shot,
|
||||
)
|
||||
|
||||
save_file: bpy.props.BoolProperty(
|
||||
name="Save after building.",
|
||||
description="Automatically save build file after 'Shot Builder' is complete.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.prop(self, "production_name")
|
||||
layout.prop(self, "seq_id")
|
||||
layout.prop(self, "shot_id")
|
||||
layout.prop(self, "task_type")
|
||||
layout.prop(self, "save_file")
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
global active_project
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
project = cache.project_active_get()
|
||||
active_project = project
|
||||
|
||||
if addon_prefs.session.is_auth() is False:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Must be logged into Kitsu to continue. \nCheck login status in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if project.id == "":
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the Kitsu production's name. \nCheck project is selected in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not addon_prefs.is_project_root_valid:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Operator is not able to determine the project root directory. \nCheck project root directiory is configured in 'Blender Kitsu' addon preferences.",
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.production_name = project.name
|
||||
|
||||
return cast(
|
||||
Set[str], context.window_manager.invoke_props_dialog(self, width=400)
|
||||
)
|
||||
|
||||
def _get_task_type_for_shot(self, context, shot):
|
||||
for task_type in shot.get_all_task_types():
|
||||
if task_type.id == self.task_type:
|
||||
return task_type
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
# Get Properties
|
||||
global active_project
|
||||
seq = active_project.get_sequence(self.seq_id)
|
||||
shot = active_project.get_shot(self.shot_id)
|
||||
task_type = self._get_task_type_for_shot(context, shot)
|
||||
task_type_short_name = task_type.get_short_name()
|
||||
shot_file_path_str = shot.get_shot_filepath(context, task_type_short_name)
|
||||
|
||||
# Open Template File
|
||||
replace_workspace_with_template(context, task_type_short_name)
|
||||
|
||||
# Set Up Scene + Naming
|
||||
shot_task_name = shot.get_shot_task_name(task_type.get_short_name())
|
||||
scene = set_shot_scene(context, shot_task_name)
|
||||
remove_all_data()
|
||||
set_resolution_and_fps(active_project, scene)
|
||||
set_frame_range(shot, scene)
|
||||
|
||||
# Set Render Settings
|
||||
if task_type_short_name == 'anim': # TODO get anim from a constant instead
|
||||
set_render_engine(context.scene, 'BLENDER_WORKBENCH')
|
||||
else:
|
||||
set_render_engine(context.scene)
|
||||
|
||||
# Create Output Collection & Link Camera
|
||||
if bkglobals.OUTPUT_COL_CREATE.get(task_type_short_name):
|
||||
output_col = create_task_type_output_collection(
|
||||
context.scene, shot, task_type
|
||||
)
|
||||
if task_type_short_name == 'anim':
|
||||
link_camera_rig(context.scene, output_col)
|
||||
|
||||
# Load Assets
|
||||
get_shot_assets(scene=scene, output_collection=output_col, shot=shot)
|
||||
|
||||
# Link External Output Collections
|
||||
link_task_type_output_collections(shot, task_type)
|
||||
|
||||
if bkglobals.LOAD_EDITORIAL_REF.get(task_type_short_name):
|
||||
editorial_export_get_latest(context, shot)
|
||||
|
||||
# Run Hooks
|
||||
hooks_instance = Hooks()
|
||||
hooks_instance.load_hooks(context)
|
||||
hooks_instance.execute_hooks(
|
||||
match_task_type=task_type_short_name,
|
||||
scene=context.scene,
|
||||
shot=shot,
|
||||
prod_path=prefs.project_root_dir_get(context),
|
||||
shot_path=shot_file_path_str,
|
||||
)
|
||||
|
||||
# Save File
|
||||
if self.save_file:
|
||||
save_shot_builder_file(file_path=shot_file_path_str)
|
||||
|
||||
self.report(
|
||||
{"INFO"}, f"Successfully Built Shot:`{shot.name}` Task: `{task_type.name}`"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = (KITSU_OT_build_new_shot, KITSU_OT_save_shot_builder_hooks)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
@ -1,460 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import importlib
|
||||
from collections import defaultdict
|
||||
|
||||
import bpy
|
||||
|
||||
from .task_type import *
|
||||
from .shot import Shot, ShotRef
|
||||
from .render_settings import RenderSettings
|
||||
from .asset import Asset, AssetRef
|
||||
from .sys_utils import *
|
||||
from .hooks import Hooks, register_hooks
|
||||
from .connectors.default import DefaultConnector
|
||||
from .connectors.connector import Connector
|
||||
import os
|
||||
|
||||
from .. import prefs
|
||||
from pathlib import Path
|
||||
|
||||
from typing import *
|
||||
import types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Production:
|
||||
"""
|
||||
Class containing data and methods for a production.
|
||||
|
||||
# Data members #
|
||||
path: contains the path to the root of the production.
|
||||
task_types: contains a list of `TaskType`s or a Connector to retrieve that are defined for this
|
||||
production. By default the task_types are prefilled with anim and light.
|
||||
name: human readable name of the production.
|
||||
|
||||
"""
|
||||
|
||||
__ATTRNAMES_SUPPORTING_CONNECTOR = ['task_types', 'shots', 'name']
|
||||
|
||||
def __init__(self, production_path: pathlib.Path):
|
||||
self.path = production_path
|
||||
self.task_types: List[TaskType] = []
|
||||
self.task_types_connector = DefaultConnector
|
||||
self.shots_connector = DefaultConnector
|
||||
self.assets: List[type] = []
|
||||
self.shots: List[Shot] = []
|
||||
self.name = ""
|
||||
self.name_connector = DefaultConnector
|
||||
self.render_settings_connector = DefaultConnector
|
||||
self.config: Dict[str, Any] = {}
|
||||
self.__shot_lookup: Dict[str, Shot] = {}
|
||||
self.hooks: Hooks = Hooks()
|
||||
self.shot_data_synced = False
|
||||
|
||||
self.scene_name_format = "{shot.sequence_code}_{shot.code}.{task_type}"
|
||||
self.shot_name_format = "{shot.sequence_code}_{shot.code}"
|
||||
self.file_name_format = (
|
||||
"{production.path}shots/{shot.code}/{shot.code}.{task_type}.blend"
|
||||
)
|
||||
|
||||
def __create_connector(
|
||||
self, connector_cls: Type[Connector], context: bpy.types.Context
|
||||
) -> Connector:
|
||||
# TODO: Cache connector
|
||||
preferences = context.preferences.addons["blender_kitsu"].preferences
|
||||
return connector_cls(production=self, preferences=preferences)
|
||||
|
||||
def __format_shot_name(self, shot: Shot) -> str:
|
||||
return self.shot_name_format.format(shot=shot)
|
||||
|
||||
def get_task_type_items(
|
||||
self, context: bpy.types.Context
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Get the list of task types items to be used in an item function of a
|
||||
`bpy.props.EnumProperty`
|
||||
"""
|
||||
if not self.task_types:
|
||||
connector = self.__create_connector(
|
||||
self.task_types_connector, context=context
|
||||
)
|
||||
self.task_types = connector.get_task_types()
|
||||
return [
|
||||
(task_type.name, task_type.name, task_type.name)
|
||||
for task_type in self.task_types
|
||||
]
|
||||
|
||||
def get_assets_for_shot(
|
||||
self, context: bpy.types.Context, shot: Shot
|
||||
) -> List[AssetRef]:
|
||||
connector = self.__create_connector(self.shots_connector, context=context)
|
||||
|
||||
return connector.get_assets_for_shot(shot)
|
||||
|
||||
def get_shots(self, context: bpy.types.Context) -> List[ShotRef]:
|
||||
connector = self.__create_connector(self.shots_connector, context=context)
|
||||
return connector.get_shots()
|
||||
|
||||
def get_shot(self, context: bpy.types.Context, shot_name: str) -> Optional[Shot]:
|
||||
self._ensure_shot_data(context)
|
||||
|
||||
for shot in self.shots:
|
||||
if shot.name == shot_name:
|
||||
return shot
|
||||
return None
|
||||
|
||||
def _ensure_shot_data(self, context: bpy.types.Context) -> None:
|
||||
if self.shot_data_synced:
|
||||
return
|
||||
# Find a generic shot definition. This class will be used as template
|
||||
# when no specific shot definition could be found.
|
||||
generic_shot_class = None
|
||||
for shot in self.shots:
|
||||
if shot.is_generic:
|
||||
generic_shot_class = shot.__class__
|
||||
break
|
||||
|
||||
shot_refs = self.get_shots(context)
|
||||
for shot_ref in shot_refs:
|
||||
logger.debug(f"Finding shot definition for {shot_ref.name}")
|
||||
for shot in self.shots:
|
||||
if shot.name == shot_ref.name:
|
||||
logger.debug(f"Shot definition found for {shot_ref.name}")
|
||||
shot_ref.sync_data(shot)
|
||||
break
|
||||
else:
|
||||
logger.info(f"No shot definition found for {shot_ref.name}")
|
||||
if generic_shot_class:
|
||||
logger.info(f"Using generic shot class")
|
||||
shot = generic_shot_class()
|
||||
shot_ref.sync_data(shot)
|
||||
shot.is_generic = False
|
||||
self.shots.append(shot)
|
||||
|
||||
self.shot_data_synced = True
|
||||
|
||||
def get_render_settings(
|
||||
self, context: bpy.types.Context, shot: Shot
|
||||
) -> RenderSettings:
|
||||
connector = self.__create_connector(self.shots_connector, context=context)
|
||||
return connector.get_render_settings(shot)
|
||||
|
||||
def get_shot_items(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Get the list of shot items to be used in an item function of a
|
||||
`bpy.props.EnumProperty` to select a shot.
|
||||
"""
|
||||
result = []
|
||||
self._ensure_shot_data(context)
|
||||
sequences: Dict[str, List[Shot]] = defaultdict(list)
|
||||
for shot in self.shots:
|
||||
if not shot.is_valid():
|
||||
continue
|
||||
sequences[shot.sequence_code].append(shot)
|
||||
|
||||
sorted_sequences = sorted(sequences.keys())
|
||||
for sequence in sorted_sequences:
|
||||
result.append(("", sequence, sequence))
|
||||
for shot in sorted(sequences[sequence], key=lambda x: x.name):
|
||||
result.append((shot.name, self.__format_shot_name(shot), shot.name))
|
||||
|
||||
return result
|
||||
|
||||
def get_seq_items(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Get the list of seq items to be used in an item function of a
|
||||
`bpy.props.EnumProperty` to select a shot.
|
||||
"""
|
||||
shots = self.get_shots(context)
|
||||
sequences = list(set([s.sequence for s in shots]))
|
||||
sequences.sort(key=lambda seq: seq.name)
|
||||
|
||||
return [(seq.name, seq.name, "") for seq in sequences]
|
||||
|
||||
def get_name(self, context: bpy.types.Context) -> str:
|
||||
"""
|
||||
Get the name of the production
|
||||
"""
|
||||
if not self.name:
|
||||
connector = self.__create_connector(self.name_connector, context=context)
|
||||
self.name = connector.get_name()
|
||||
return self.name
|
||||
|
||||
# TODO: Use visitor pattern.
|
||||
def __load_name(self, main_config_mod: types.ModuleType) -> None:
|
||||
name = getattr(main_config_mod, "PRODUCTION_NAME", None)
|
||||
if name is None:
|
||||
return
|
||||
|
||||
# Extract task types from a list of strings
|
||||
if isinstance(name, str):
|
||||
self.name = name
|
||||
return
|
||||
|
||||
if issubclass(name, Connector):
|
||||
self.name = ""
|
||||
self.name_connector = name
|
||||
return
|
||||
|
||||
logger.warn(
|
||||
"Skip loading of production name. Incorrect configuration detected."
|
||||
)
|
||||
|
||||
def __load_task_types(self, main_config_mod: types.ModuleType) -> None:
|
||||
task_types = getattr(main_config_mod, "TASK_TYPES", None)
|
||||
if task_types is None:
|
||||
return
|
||||
|
||||
# Extract task types from a list of strings
|
||||
if isinstance(task_types, list):
|
||||
self.task_types = [TaskType(task_type) for task_type in task_types]
|
||||
return
|
||||
|
||||
if issubclass(task_types, Connector):
|
||||
self.task_types = task_types
|
||||
|
||||
logger.warn("Skip loading of task_types. Incorrect configuration detected.")
|
||||
|
||||
def __load_shots_connector(self, main_config_mod: types.ModuleType) -> None:
|
||||
shots = getattr(main_config_mod, "SHOTS", None)
|
||||
if shots is None:
|
||||
return
|
||||
|
||||
# Extract task types from a list of strings
|
||||
if issubclass(shots, Connector):
|
||||
self.shots_connector = shots
|
||||
return
|
||||
|
||||
logger.warn("Skip loading of shots. Incorrect configuration detected.")
|
||||
|
||||
def __load_connector_keys(self, main_config_mod: types.ModuleType) -> None:
|
||||
connectors = set()
|
||||
for attrname in Production.__ATTRNAMES_SUPPORTING_CONNECTOR:
|
||||
connector = getattr(self, f"{attrname}_connector")
|
||||
connectors.add(connector)
|
||||
|
||||
connector_keys = set()
|
||||
for connector in connectors:
|
||||
for key in connector.PRODUCTION_KEYS:
|
||||
connector_keys.add(key)
|
||||
|
||||
for connector_key in connector_keys:
|
||||
if hasattr(main_config_mod, connector_key):
|
||||
self.config[connector_key] = getattr(main_config_mod, connector_key)
|
||||
|
||||
def __load_render_settings(self, main_config_mod: types.ModuleType) -> None:
|
||||
render_settings = getattr(main_config_mod, "RENDER_SETTINGS", None)
|
||||
if render_settings is None:
|
||||
return
|
||||
|
||||
if issubclass(render_settings, Connector):
|
||||
self.render_settings_connector = render_settings
|
||||
return
|
||||
|
||||
logger.warn("Skip loading of render settings. Incorrect configuration detected")
|
||||
|
||||
def __load_formatting_strings(self, main_config_mod: types.ModuleType) -> None:
|
||||
self.shot_name_format = getattr(
|
||||
main_config_mod, "SHOT_NAME_FORMAT", self.scene_name_format
|
||||
)
|
||||
self.scene_name_format = getattr(
|
||||
main_config_mod, "SCENE_NAME_FORMAT", self.scene_name_format
|
||||
)
|
||||
self.file_name_format = getattr(
|
||||
main_config_mod, "FILE_NAME_FORMAT", self.file_name_format
|
||||
)
|
||||
|
||||
def _load_config(self, main_config_mod: types.ModuleType) -> None:
|
||||
self.__load_name(main_config_mod)
|
||||
self.__load_task_types(main_config_mod)
|
||||
self.__load_shots_connector(main_config_mod)
|
||||
self.__load_connector_keys(main_config_mod)
|
||||
self.__load_render_settings(main_config_mod)
|
||||
self.__load_formatting_strings(main_config_mod)
|
||||
|
||||
def _load_asset_definitions(self, asset_mod: types.ModuleType) -> None:
|
||||
"""
|
||||
Load all assets from the given module.
|
||||
"""
|
||||
self.assets = []
|
||||
for module_item_str in dir(asset_mod):
|
||||
module_item = getattr(asset_mod, module_item_str)
|
||||
if module_item.__class__ != type:
|
||||
continue
|
||||
if not issubclass(module_item, Asset):
|
||||
continue
|
||||
if not hasattr(module_item, "name"):
|
||||
continue
|
||||
logger.info(f"loading asset config {module_item}")
|
||||
self.assets.append(module_item)
|
||||
# TODO: only add assets that are leaves
|
||||
|
||||
def _load_shot_definitions(self, shot_mod: types.ModuleType) -> None:
|
||||
"""
|
||||
Load all assets from the given module.
|
||||
"""
|
||||
self.shots = []
|
||||
for module_item_str in dir(shot_mod):
|
||||
module_item = getattr(shot_mod, module_item_str)
|
||||
if module_item.__class__ != type:
|
||||
continue
|
||||
if not issubclass(module_item, Shot):
|
||||
continue
|
||||
if not hasattr(module_item, "name"):
|
||||
continue
|
||||
logger.info(f"loading shot config {module_item}")
|
||||
self.shots.append(module_item())
|
||||
|
||||
|
||||
_PRODUCTION: Optional[Production] = None
|
||||
|
||||
|
||||
def is_valid_production_root(path: pathlib.Path) -> bool:
|
||||
"""
|
||||
Test if the given project path is configured correctly.
|
||||
|
||||
A valid project path contains a subfolder with the name `shot-builder`
|
||||
holding configuration files.
|
||||
"""
|
||||
if not path.is_absolute():
|
||||
return False
|
||||
if not path.exists():
|
||||
return False
|
||||
if not path.is_dir():
|
||||
return False
|
||||
config_file_path = get_production_config_file_path(path)
|
||||
return config_file_path.exists()
|
||||
|
||||
|
||||
def get_production_config_dir_path(path: pathlib.Path) -> pathlib.Path:
|
||||
"""
|
||||
Get the production configuration dir path.
|
||||
"""
|
||||
return path / "shot-builder"
|
||||
|
||||
|
||||
def get_production_config_file_path(path: pathlib.Path) -> pathlib.Path:
|
||||
"""
|
||||
Get the production configuration file path.
|
||||
"""
|
||||
return get_production_config_dir_path(path) / "config.py"
|
||||
|
||||
|
||||
def _find_production_root(path: pathlib.Path) -> Optional[pathlib.Path]:
|
||||
"""
|
||||
Given a path try to find the production root
|
||||
"""
|
||||
if is_valid_production_root(path):
|
||||
return path
|
||||
try:
|
||||
parent_path = path.parents[0]
|
||||
return _find_production_root(parent_path)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
# TODO: return type is optional
|
||||
def get_production_root(context: bpy.types.Context) -> Optional[pathlib.Path]:
|
||||
"""
|
||||
Determine the project root based on the current file.
|
||||
When current file isn't part of a project the project root
|
||||
configured in the add-on will be used.
|
||||
"""
|
||||
current_file = pathlib.Path(bpy.data.filepath)
|
||||
production_root = _find_production_root(current_file)
|
||||
if production_root:
|
||||
return production_root
|
||||
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
production_root = Path(addon_prefs.project_root_dir)
|
||||
if is_valid_production_root(production_root):
|
||||
return production_root
|
||||
return None
|
||||
|
||||
|
||||
def ensure_loaded_production(context: bpy.types.Context) -> bool:
|
||||
"""
|
||||
Ensure that the production of the current context is loaded.
|
||||
|
||||
Returns if the production of for the given context is loaded.
|
||||
"""
|
||||
global _PRODUCTION
|
||||
addon_prefs = prefs.addon_prefs_get(bpy.context)
|
||||
base_path = Path(addon_prefs.project_root_dir)
|
||||
production_root = os.path.join(
|
||||
base_path, "pro"
|
||||
) # TODO Fix during refactor should use base_path
|
||||
if is_valid_production_root(Path(production_root)):
|
||||
logger.debug(f"loading new production configuration from '{production_root}'.")
|
||||
__load_production_configuration(context, Path(production_root))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def __load_production_configuration(
|
||||
context: bpy.types.Context, production_path: pathlib.Path
|
||||
) -> bool:
|
||||
global _PRODUCTION
|
||||
_PRODUCTION = Production(production_path)
|
||||
paths = [production_path / "shot-builder"]
|
||||
with SystemPathInclude(paths) as _include:
|
||||
try:
|
||||
import config as production_config
|
||||
|
||||
importlib.reload(production_config)
|
||||
_PRODUCTION._load_config(production_config)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `config.py` configuration file")
|
||||
|
||||
try:
|
||||
import shots as production_shots
|
||||
|
||||
importlib.reload(production_shots)
|
||||
_PRODUCTION._load_shot_definitions(production_shots)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `shots.py` configuration file")
|
||||
|
||||
try:
|
||||
import assets as production_assets
|
||||
|
||||
importlib.reload(production_assets)
|
||||
_PRODUCTION._load_asset_definitions(production_assets)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `assets.py` configuration file")
|
||||
|
||||
try:
|
||||
import hooks as production_hooks
|
||||
|
||||
importlib.reload(production_hooks)
|
||||
register_hooks(production_hooks)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning("Production has no `hooks.py` configuration file")
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_active_production() -> Production:
|
||||
global _PRODUCTION
|
||||
assert _PRODUCTION
|
||||
return _PRODUCTION
|
@ -1,29 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
from ..shot_builder.asset import Asset
|
||||
from typing import *
|
||||
|
||||
|
||||
class RenderSettings:
|
||||
def __init__(self, width: int, height: int, frames_per_second: float):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.frames_per_second = frames_per_second
|
@ -1,64 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
class Shot:
|
||||
is_generic = False
|
||||
kitsu_id = ""
|
||||
sequence_code = ""
|
||||
name = ""
|
||||
code = ""
|
||||
frame_start = 0
|
||||
frames = 0
|
||||
# Frame_end will be stored for debugging only.
|
||||
frame_end = 0
|
||||
frames_per_second = 24.0
|
||||
file_path_format = "{production.path}/shots/{shot.sequence_code}/{shot.name}/{shot.name}.{task_type}.blend"
|
||||
file_path = ""
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
Check if this shot contains all data so it could be selected
|
||||
for shot building.
|
||||
|
||||
When not valid it won't be shown in the shot selection field.
|
||||
"""
|
||||
if not self.name:
|
||||
return False
|
||||
|
||||
if self.frames <= 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ShotRef:
|
||||
"""
|
||||
Reference to an asset from an external system.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "", code: str = ""):
|
||||
self.name = name
|
||||
self.code = code
|
||||
|
||||
def sync_data(self, shot: Shot) -> None:
|
||||
pass
|
@ -1,64 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
import logging
|
||||
from typing import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemPathInclude:
|
||||
"""
|
||||
Resource class to temporary include system paths to `sys.paths`.
|
||||
|
||||
Usage:
|
||||
```
|
||||
paths = [pathlib.Path("/home/guest/my_python_scripts")]
|
||||
with SystemPathInclude(paths) as t:
|
||||
import my_module
|
||||
reload(my_module)
|
||||
```
|
||||
|
||||
It is possible to nest multiple SystemPathIncludes.
|
||||
"""
|
||||
|
||||
def __init__(self, paths_to_add: List[pathlib.Path]):
|
||||
# TODO: Check if all paths exist and are absolute.
|
||||
self.__paths = paths_to_add
|
||||
self.__original_sys_path: List[str] = []
|
||||
|
||||
def __enter__(self):
|
||||
self.__original_sys_path = sys.path
|
||||
new_sys_path = []
|
||||
for path_to_add in self.__paths:
|
||||
# Do not add paths that are already in the sys path.
|
||||
# Report this to the logger as this might indicate wrong usage.
|
||||
path_to_add_str = str(path_to_add)
|
||||
if path_to_add_str in self.__original_sys_path:
|
||||
logger.warn(f"{path_to_add_str} already added to `sys.path`")
|
||||
continue
|
||||
new_sys_path.append(path_to_add_str)
|
||||
new_sys_path.extend(self.__original_sys_path)
|
||||
sys.path = new_sys_path
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
sys.path = self.__original_sys_path
|
@ -1,27 +0,0 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
|
||||
class TaskType:
|
||||
def __init__(self, task_name: str):
|
||||
self.name = task_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
@ -0,0 +1,59 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from .core import link_data_block
|
||||
|
||||
|
||||
# TODO add ability for custom templates
|
||||
def get_template_dir() -> Path:
|
||||
return Path(__file__).absolute().parent.joinpath("templates")
|
||||
|
||||
|
||||
def get_template_files() -> list[Path]:
|
||||
dir = get_template_dir()
|
||||
return list(dir.glob('*.blend'))
|
||||
|
||||
|
||||
def get_template_for_task_type(task_type_short_name: str) -> Path:
|
||||
for file in get_template_files():
|
||||
if file.stem == task_type_short_name:
|
||||
return file
|
||||
|
||||
|
||||
def replace_workspace_with_template(
|
||||
context: bpy.types.Context, task_type_short_name: str
|
||||
):
|
||||
file_path = get_template_for_task_type(task_type_short_name).resolve().absolute()
|
||||
remove_prefix = "REMOVE-"
|
||||
if not file_path.exists():
|
||||
return
|
||||
|
||||
# Mark Existing Workspaces for Removal
|
||||
for workspace in bpy.data.workspaces:
|
||||
if workspace.name.startswith(remove_prefix):
|
||||
continue
|
||||
workspace.name = remove_prefix + workspace.name
|
||||
|
||||
file_path_str = file_path.__str__()
|
||||
with bpy.data.libraries.load(file_path_str) as (data_from, data_to):
|
||||
for workspace in data_from.workspaces:
|
||||
bpy.ops.wm.append(
|
||||
filepath=file_path_str,
|
||||
directory=file_path_str + "/" + 'WorkSpace',
|
||||
filename=str(workspace),
|
||||
)
|
||||
|
||||
for lib in bpy.data.libraries:
|
||||
if lib.filepath == file_path_str:
|
||||
bpy.data.libraries.remove(bpy.data.libraries.get(lib.name))
|
||||
break
|
||||
|
||||
workspaces_to_remove = []
|
||||
for workspace in bpy.data.workspaces:
|
||||
if workspace.name.startswith(remove_prefix):
|
||||
workspaces_to_remove.append(workspace)
|
||||
|
||||
# context.window.workspace = workspace
|
||||
for workspace in workspaces_to_remove:
|
||||
with context.temp_override(workspace=workspace):
|
||||
bpy.ops.workspace.delete()
|
||||
return True
|
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/anim.blend
(Stored with Git LFS)
Normal file
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/anim.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/comp.blend
(Stored with Git LFS)
Normal file
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/comp.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/fx.blend
(Stored with Git LFS)
Normal file
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/fx.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/lighting.blend
(Stored with Git LFS)
Normal file
BIN
scripts-blender/addons/blender_kitsu/shot_builder/templates/lighting.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -1,27 +1,7 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
import bpy
|
||||
from typing import *
|
||||
from .operators import *
|
||||
from typing import Any
|
||||
|
||||
|
||||
def topbar_file_new_draw_handler(self: Any, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
op = layout.operator(SHOTBUILDER_OT_NewShotFile.bl_idname, text="Shot File")
|
||||
op = layout.operator("kitsu.build_new_shot", text="Shot File")
|
||||
|
@ -1 +0,0 @@
|
||||
DEFAULT_FRAME_START: int = 101
|
@ -22,9 +22,11 @@ from __future__ import annotations
|
||||
import inspect
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Union, Tuple, TypeVar
|
||||
|
||||
from pathlib import Path
|
||||
import gazu
|
||||
from .logger import LoggerFactory
|
||||
from . import bkglobals
|
||||
from . import prefs
|
||||
|
||||
logger = LoggerFactory.getLogger()
|
||||
|
||||
@ -558,6 +560,11 @@ class Shot(Entity):
|
||||
def get_all_tasks(self) -> List[Task]:
|
||||
return [Task.from_dict(t) for t in gazu.task.all_tasks_for_shot(asdict(self))]
|
||||
|
||||
def get_all_assets(self) -> List[Asset]:
|
||||
return [
|
||||
Asset.from_dict(t) for t in gazu.asset.all_assets_for_shot(asdict(self))
|
||||
]
|
||||
|
||||
def get_sequence(self) -> Sequence:
|
||||
return Sequence.from_dict(gazu.shot.get_sequence_from_shot(asdict(self)))
|
||||
|
||||
@ -565,6 +572,23 @@ class Shot(Entity):
|
||||
gazu.shot.update_shot(asdict(self))
|
||||
return self
|
||||
|
||||
def get_shot_task_name(self, task_type_short_name: str) -> str: #
|
||||
return f"{self.name}{bkglobals.FILE_DELIMITER}{task_type_short_name}"
|
||||
|
||||
def get_output_collection_name(self, task_type_short_name: str) -> str:
|
||||
return f"{self.get_shot_task_name(task_type_short_name)}{bkglobals.FILE_DELIMITER}output"
|
||||
|
||||
def get_shot_dir(self, context) -> str:
|
||||
project_root_dir = prefs.project_root_dir_get(context)
|
||||
all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots')
|
||||
seq = self.get_sequence()
|
||||
shot_dir = all_shots_dir.joinpath(seq.name).joinpath(self.name)
|
||||
return shot_dir.__str__()
|
||||
|
||||
def get_shot_filepath(self, context, task_type_short_name: str) -> str:
|
||||
file_name = self.get_shot_task_name(task_type_short_name) + '.blend'
|
||||
return Path(self.get_shot_dir(context)).joinpath(file_name).__str__()
|
||||
|
||||
def update_data(self, data: Dict[str, Any]) -> Shot:
|
||||
gazu.shot.update_shot_data(asdict(self), data=data)
|
||||
if not self.data:
|
||||
@ -714,6 +738,11 @@ class TaskType(Entity):
|
||||
if t["for_entity"] == "Sequence"
|
||||
]
|
||||
|
||||
def get_short_name(self) -> str:
|
||||
for key, value in bkglobals.SHOT_TASK_MAPPING.items():
|
||||
if value == self.name:
|
||||
return key
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.id)
|
||||
|
||||
|
10
scripts/index_assets/README.md
Normal file
10
scripts/index_assets/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Index Assets
|
||||
|
||||
This tool will create a dictionary of all data-blocks (collections only) [marked as an Asset](https://docs.blender.org/manual/en/latest/files/asset_libraries/introduction.html#asset-create) and stores this information in a JSON file at `you_project/svn/pro/assets/asset_index.json`. This JSON File is used by other tools like the Blender Kitsu Add-On to quickly discover assets to use in the shot builder.
|
||||
|
||||
To run this tool use the following command, followed by the path to your project's root folder
|
||||
|
||||
```bash
|
||||
./run_index_assets.py your_project/
|
||||
```
|
||||
|
66
scripts/index_assets/blender_index_assets.py
Normal file
66
scripts/index_assets/blender_index_assets.py
Normal file
@ -0,0 +1,66 @@
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def load_json_file(json_file_path: str) -> dict:
|
||||
"""Finds JSON file with asset index if any exist
|
||||
|
||||
Args:
|
||||
json_file_path (str): Path to existing JSON File
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with the existing JSON File, else blank
|
||||
"""
|
||||
asset_index_json = Path(json_file_path)
|
||||
if asset_index_json.exists():
|
||||
return json.load(open(asset_index_json))
|
||||
return {}
|
||||
|
||||
|
||||
def dump_json_file(asset_dict: dict, json_file_path: str) -> None:
|
||||
"""Save Asset Index to JSON File at provided path
|
||||
|
||||
Args:
|
||||
asset_dict (dict): Dictionary of Asset Items
|
||||
json_file_path (str): Path to Save JSON File
|
||||
"""
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(asset_dict, json_file, indent=4)
|
||||
|
||||
|
||||
def find_save_assets():
|
||||
"""Find all collections marked as asset in the current
|
||||
.blend file, and add them to a dictionary, saved as a JSON"""
|
||||
|
||||
argv = sys.argv
|
||||
json_file_path = argv[argv.index("--") + 1 :][0]
|
||||
|
||||
asset_dict = load_json_file(json_file_path)
|
||||
for col in bpy.data.collections:
|
||||
if col.asset_data:
|
||||
print(f"Found Asset {col.name}")
|
||||
asset_dict[col.name] = {
|
||||
'type': type(col).bl_rna.name,
|
||||
'filepath': bpy.data.filepath,
|
||||
}
|
||||
print('Asset Index Completed')
|
||||
dump_json_file(asset_dict, json_file_path)
|
||||
|
||||
|
||||
find_save_assets()
|
110
scripts/index_assets/run_index_assets.py
Executable file
110
scripts/index_assets/run_index_assets.py
Executable file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def cancel_program(message: str) -> None:
|
||||
"""Cancel Execution of this file"""
|
||||
print(message)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"path",
|
||||
help="Path to a file(s) or folder(s) on which to perform crawl. In format of '{my_project}/'",
|
||||
)
|
||||
|
||||
|
||||
def get_bbatch_script_path() -> str:
|
||||
"""Returns path to script that runs with bbatch"""
|
||||
dir = Path(__file__).parent.absolute()
|
||||
return dir.joinpath("blender_index_assets.py").__str__()
|
||||
|
||||
|
||||
def get_blender_path(project_path: Path) -> str:
|
||||
"""Get the path to a project's blender executable
|
||||
|
||||
Args:
|
||||
project_path (Path): Path Object, containing project's root path
|
||||
|
||||
Returns:
|
||||
str: Path to blender executable as a string
|
||||
"""
|
||||
# TODO get this from the run_blender.py script instead (new logic needs tobe added to run_blender.py first)
|
||||
local_blender_path = project_path.joinpath('local').joinpath('blender')
|
||||
system_name = platform.system().lower()
|
||||
blender_path_base = local_blender_path / system_name
|
||||
if system_name == 'linux':
|
||||
blender_path = blender_path_base / 'blender'
|
||||
elif system_name == 'darwin':
|
||||
blender_path = (
|
||||
blender_path_base / 'Blender.app' / 'Contents' / 'MacOS' / 'Blender'
|
||||
)
|
||||
elif system_name == 'windows':
|
||||
blender_path = blender_path_base / 'blender.exe'
|
||||
return blender_path.absolute().__str__()
|
||||
|
||||
|
||||
def index_assets():
|
||||
"""Crawl the Asset Library of a provided Blender Studio Pipeline Project and
|
||||
index all assets into a dictionary using a script executed by bbatch"""
|
||||
args = parser.parse_args()
|
||||
project_path = Path(args.path)
|
||||
if not project_path.exists():
|
||||
cancel_program("Provided Path does not exist")
|
||||
asset_dir = (
|
||||
project_path.joinpath("svn").joinpath("pro").joinpath("assets").absolute()
|
||||
)
|
||||
if not asset_dir.exists():
|
||||
cancel_program("Asset Library does not exist at provided path")
|
||||
asset_dir_path = asset_dir.__str__()
|
||||
json_file_path = asset_dir.joinpath("asset_index.json").__str__()
|
||||
script_path = get_bbatch_script_path()
|
||||
project_blender = get_blender_path(project_path)
|
||||
print(project_blender)
|
||||
os.chdir("../bbatch")
|
||||
cmd_list = (
|
||||
'python',
|
||||
'-m',
|
||||
'bbatch',
|
||||
asset_dir_path,
|
||||
'--ask',
|
||||
'--exec',
|
||||
project_blender,
|
||||
"--nosave",
|
||||
"--recursive",
|
||||
'--script',
|
||||
script_path,
|
||||
"--args",
|
||||
f'{json_file_path}',
|
||||
)
|
||||
process = subprocess.Popen(cmd_list, shell=False)
|
||||
if process.wait() != 0:
|
||||
cancel_program(f"Asset Index Failed!")
|
||||
print("Asset Index Completed Successfully")
|
||||
print(f"Index File: '{json_file_path}'")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
index_assets()
|
Loading…
Reference in New Issue
Block a user