アセットとは何か? アセットの「入れ物」を設計して学んだこと

最近アセットシステムを設計しているのですが、しばらくのあいだ議論が堂々巡りしていました。どの提案もそれ単体では筋が通っている。でも、どの提案も誰かのユースケースを壊してしまうのです。
やがて理由に気づきました。アセットには、根本的に異なる種類が少なくとも 2 つあります。マーケットプレイス・アセットと、プロダクション(パイプライン)・アセットです。私たちは片方を設計しながら、もう片方の話をしていました。
どちらも共有できるものです。違いはそこではありません。違いは、その箱が誰のために設計されているか。エンドユーザーのためか、パイプラインのためか、です。
両方にとってのベストな設計はありません。プロダクションの中でさえ、万能なベストはなく、具体的なケースにとってのベストがあるだけです。
まず用語から
議論の混乱の大部分は、2 つの言葉から生まれていました。どちらも 2 つの意味を持っていたからです。
リファレンス(reference)
- DCC のリファレンス(Maya、USD):あるファイルを開くと、ほかのファイルも読み込まれる仕組み。
- ファイルシステムのリンク:ディスク上の別の場所を指すパス。
アセット(asset)
- 制作中のアセット:部署から部署へ渡りながら変化し続ける、生きているもの。
- 完成品としてのアセット:誰かが使うための、完成したパッケージ。
ある人が前者の意味で「アセット」と言い、別の人が後者の意味で受け取ると、気づかないまま 1 時間議論できてしまいます。
アセットはお弁当
台湾のお弁当と日本のお弁当を思い浮かべてください。日本のお客さんに台湾の「鶏もも唐揚げ弁当」を渡しても、喜んでもらうのは難しいでしょう。逆に台湾のお客さんは、冷たい箱が「お弁当」を名乗ることを受け入れられません。
アセットはお弁当のようなものです。食べ物を入れる箱。どちらも納品物です。そして設計のロジックは一方向に流れます。
お客さんが求めるもの → 箱に入れるもの → 仕切りをどこに置くか → 各区画に何を入れるか
お弁当箱の仕切り方は、何が変わる要素なのかで決まります。私たちの世界では、それはフォーマット、DCC、部署、キャラクター、バージョンなどです。結局はどれもただのフォルダです。大事なのは、仕切りをどこに置くかです。
お弁当には明確な仕切りがあるべきです。ぐちゃぐちゃのお弁当が好きな人はいません。でも、同じおかずを、お客さんに合わせて違う箱に詰めることはできます。
お弁当と、お弁当工場
行き詰まりから抜け出せたのは、一歩引いて見たときでした。すぐ食べられるお弁当と、稼働中のお弁当工場は、別のものです。
すぐ食べられるお弁当は、お客さんをいちばん喜ばせることを中心に設計されます。
工場は違います。製造中は、部署と部署のあいだで食材を保持する容器があります。シンプルなものもあれば、構造化されたものもあります。
- にんじんは最初から最後まで同じ容器の中。千切りにされ、蒸され、保管される。構造は変わりません。
- お米は洗米用の容器、蒸し用の容器、保温用の容器へと移っていく。段階ごとに新しい容器です。
- 鶏肉は大きな容器から始まり、部位ごとの容器に分けられ、それぞれ別に下味をつけて調理される。
最後になって初めて、すべてが 1 つのお弁当に詰められます。
どれも同じ「箱」に見えますが、役割はまったく違います。パイプライン・アセットは工場の容器。プロダクト・アセットはお弁当です。 機能が違うので、別々に設計すべきです。
私たちが行き詰まっていたのはここでした。お弁当をどんな見た目にするかを決めながら、同時に工場の容器システムを設計しようとしていたのです。
3 つのパターン
この視点でアセットの構造を見直すと、これまで関わってきたシステムはすべて 3 つのパターンのどれかに当てはまりました。
以下の構造は簡略化したスケッチで、正確なスキーマではありません。
マーケットプレイス型:お客さんのための設計
シンプルなアセット。 必要最低限の形です。
{
"name": str, # "sword"
"type": str, # character, prop, motion...
"files": List[str]
}
フォーマットグループ型のアセット。 考え方は同じですが、フォーマットのキーが厳密に事前定義されています。中身が空でも、すべてのキーが存在しなければなりません。
{
"name": str,
"type": str,
"formats": {
"usd": List[str],
"fbx": List[str],
"unreal": List[str],
"maya": List[str]
}
}
Unreal Marketplace(Fab)。 フォーマットが先に来る構造で、お客さんが開いてすぐ使えるように作られています。
{
"name": str,
"type": str, # "/3d-model/characters-creatures"
"formats": ["unreal", "unity", "blender", "maya", "gltf"],
"additional_files": List[str] # 例:textures.zip
}
それでも、手動でダウンロードするとテクスチャの再リンクが必要になることがよくあります。マーケットプレイスのアセットはお客さんに最適化されていますが、魔法ではありません。
プロダクション型:パイプラインのための設計
バージョン管理型のアセット。 制作チームの分業から進化してきたことがわかる形です。部署ごとに読み取り専用のタスクフォルダがあり、その中にバージョンが並びます。
{
"name": str,
"type": str,
"tasks": [
{"name": "modeling", "versions": [{"name": "v001", "files": [...]}, ...]},
{"name": "rigging", "versions": [...]},
{"name": "animation", "versions": [...]}
]
}
入れ子のアセット。 実際の制作では、入れ子のアセットは必須です。キャラクターは剣を持っていて、その剣もまたアセットだからです。
{
"name": "hero",
"type": "character",
"folders": ["Model", "FBX", "Rig", "Texture"],
"children": [
{
"name": "sword",
"type": "prop",
"folders": ["FBX", "Rig", "Texture"]
}
]
}
AYON。 AYON は、work ファイル(アーティストが作業中のもの)と publish ファイル(ほかの部署が使ってよいもの)を分けています。
{
"name": "sphere",
"publish": [
{"product_type": "model", "versions": ["hero", "v006", ...]},
{"product_type": "workfile", "versions": ["v002", ...]}
],
"work": [
{"task": "modeling", "files": [...]},
{"task": "rigging", "files": [...]}
]
}
大手 VFX スタジオのリリースモデル。 私が見てきた大手スタジオでは、アセットシステムは作業ファイルをまったく管理していませんでした。アーティストは各自の DCC フォルダで作業します。システムが管理するのはリリース、つまり公開済みの読み取り専用バージョンで、それぞれがプロジェクト、シーン、ショットといったコンテキストに置かれます。
project/
library/ # 共有アセット:キャラクター、環境
sc010/
sh0010/
releases/ # 公開済み、読み取り専用
animated_character/
sh0010_hero/
v001/
maya/ # DCC の作業場所、アーティストごと
artist_a/
artist_b/
このモデルが面白いのは、アセットの種類ごとに独自のスキーマがコードで定義されていることです。
class Asset:
name: str
type: str
class Character(Asset):
model_file: str
cache: str
class AnimatedCharacter(Asset):
character: Character # 別のアセットのバージョンへのリンク
anim_cache: str
maya_file: str
# アセットの解決(疑似コード):
release = find_asset(context="proj/sc010/sh0010",
name="hero",
type="AnimatedCharacter").latest()
release.character # -> 別のリリース
release.maya_file # -> 共有ストレージ上のパス
私の最初の設計は、すべてのアセットが同じ汎用的なフィールドを共有するという前提でした。大手スタジオのモデルはその逆です。アニメーション付きのキャラクターとカメラパッケージは、そもそも同じフィールドを持たない。だから種類ごとに独自のスキーマを持たせるのです。
これは諸刃の剣です。
- 長所: すべてが期待どおりの形になります。バリデーション、オートコンプリート、予測可能性が手に入ります。
- 短所: 新しい要望が出るたびに、中央のコードベースでスキーマを変更しなければなりません。それが終わるまで、チームは回避策でしのぐことになります。
付け加えておくと、こうした大手スタジオの構造は、規模と厳密な分業に最適化されています。小規模なチームや動きの速いチームに合うとは限りません。
グラフ型:箱そのものがない
これはまったく別のパラダイムです。アセットとは何かを定義しません。どんなファイルが存在し、それらがどう関係しているかを追跡します。
{
"project": "proj",
"files": List[str],
"relationships": [
{"from": "wip/model/hero_model.ma",
"to": "release/model/hero_mesh.fbx",
"type": "derive"},
{"from": "wip/anim/sh0010_anim.mb",
"to": "release/rig/hero_rig_v020.mb",
"type": "reference"}
]
}
アセットの境界は暗黙的になり、グラフから導き出されます。クエリを決めるのは作り手ではなく、使い手です。
そのため、エージェントや自動化されたパイプラインには向いています。一方で、人間がフォルダ構造をたどるのには向いていません。少なくとも、上に良い UI がない限りは。
四角いスイカ
この話全体でいちばん深い問いは、「先に育ててから定義するのか、それとも先に枠を決めてから中身を埋めるのか」です。
育っている途中のスイカを四角い型に入れると、四角いスイカができます。棚にはぴったり収まります。ただしそれは、育つ前に正しい形を選んでいた場合だけです。棚の形が変わっても、スイカの形は変わりません。
大手スタジオの構造は、先に枠を決めます。規模が大きければそれでうまくいきます。小規模で動きの速い環境では、うまくいかないことが増えます。先に定義することの欠点は、必ず枠に合わないケースが出てくることです。問題は、それに出くわすかどうかだけです。
私がたどり着いたところ
そこで、両方の世界に使える 1 つの入れ物を設計するのはやめて、今は次の順番で問いを立てています。
1. 今設計しているのはどちらか:パイプライン(制作中)か、納品物(プロダクト)か?
両方やることはできますが、どちらか一方を主な制約にしなければなりません。1 つのレイアウトで両方を解決しようとすることこそが、複雑で壊れやすいフォルダ構造を生みます。
2. 納品物なら:お客さんは誰か?
納品の要件は比較的安定しています。アーティストごとの作業環境で毎日変わるものではないので、ここではキーを固定してかまいません。
- ゲームエンジン: Unreal Marketplace が良い参考になります。
- VFX パイプライン: リファレンス、バージョン、モジュール性が求められます。もっと調査が必要です。
- AI エージェント: プログラムからアクセスする、スキーマのないフラットなパスで十分かもしれません。
3. パイプラインなら:ワークフローはどれくらい動的か?
それによって、厳密なキーにするか、自由なフォルダにするかが決まります。
実際の現在のベースラインはシンプルです。AYON で出力の構造と命名を管理し、制作から出てくるデータを予測可能にする。そのうえで、すべてをリアルタイムに同期することはしません。プロジェクトが完了扱いになったときだけ軽量な同期を走らせ、その結果をお弁当に詰めます。制作は速いまま、リリースライブラリはきれいなままです。
線をどこに引くか
考えれば考えるほど、これはモノリスとマイクロサービスの議論に似ています。分け方はサイズの問題ではありません。何が独立して変わる必要があるかの問題です。
そして、いちばんシンプルな仕切りはファイルパスです。タイプ、バージョン、タスク、フォーマットといったほかのものはすべて、その上に重ねたメタデータにすぎません。より豊かな構造を足す前に、それが本当に必要なのか、それともただ受け継いできただけなのかを問う価値があります。
これは、「エンドツーエンド vs. ハイブリッド:AI 映像パイプライン」で投げかけた問いに私が何度も戻ってくる理由でもあります。アイデアと最終的な出力のあいだに、どれだけの構造を置くべきか。そしてその構造は、誰のためのものなのか。