EMG 0.5 リファレンス
.emg は、組み合わせ可能な2Dスプライトの配布形式です。ZIP の中に data.json
とテクスチャアトラスの PNG が入っています。
本書は参考文書です。 規範は
emg-json-spec.md(v0.3.0)、
emg-json-spec-0.4.0.md、
emg-json-spec-0.5.0.md の 3 文書にあり、
食い違った場合はそちらが優先します。各項目の「導入」列が、どの版で入った規定かを示します。
概要
v0.5.0 の全体像を、まず 1 画面で。
1 つの .emg は パーツの集まりです。
パーツは static(レイヤーを重ねて描く。体、背景、部屋の壁)か
switch(フレームを 1 つだけ描く。目、口、看板の表示内容、エフェクトのコマ)の
どちらかで、この区別が全実装の分岐点になっています。
キャラクター専用の形式ではありません。
data.json に人物を前提とした構造はどこにもなく、あるのは
「重ねるレイヤー」と「差し替えるコマ」だけです。背景、小物、UI パーツ、シーンの切り替え、
エフェクトのコマ送りも同じ仕組みで表せます。
人物を前提にしているのは、まばたき・口パク・表情という意味を与える
任意のコンパニオンファイル mapping.json の側だけです。
本書がキャラクターを例に出すのは、それが最も作り込まれた用途だからで、唯一の用途だからではありません。
// data.json の骨格 { "version": "0.5.4", "requiredExtensions": [], // 0.4.0 "baseCanvasWidth": 2894, "baseCanvasHeight": 4093, "textures": [ { … } ], // アトラス PNG の宣言 "parts": [ { … } ], // パーツとレイヤー "sprites": [ { … } ], // フレーム切り替え・変換 "presets": [ { … } ] // 0.5.0 }
v0.5.0 で増えたもの
| 追加 | 何ができるようになるか |
|---|---|
layers[].frameName | 複数レイヤーを 1 つの差分として同時に切り替える |
parts[].control | UI 側での出し分けのヒント(描画には影響しない) |
parts[].defaultVisible | 初期非表示のトグル。switch では「どれも出さない」 |
presets[] | 複数パーツの状態をまとめて名前で呼ぶ |
sequence.keys[] | コマごとに表示時間が違うアニメーション |
sprites[].tracks[] | 移動・回転・拡縮・不透明度のアニメーション |
コンテナ
.emg は拡張子を変えた ZIP です。
| エントリ | 必須 | 内容 |
|---|---|---|
data.json | 必須 | 定義本体。model.json という名前のファイルも実在するため、読み込みは末尾一致で探す |
texture.png | 必須 | テクスチャアトラス。分割された場合は texture_0.png, texture_1.png… |
mapping.json | 任意 | まばたき・口パク・表情の意味づけ(後述) |
LICENSE.txt | 任意 | 素材のライセンス |
アトラスの分割
全素材を 1 枚のテクスチャに詰めます。1 辺 8192px に収まらない場合に限り複数枚へ分割し、
レイヤーはそれぞれ textureFile で参照先を指します。
PNG は無圧縮で格納します。 PNG は既に deflate 済みなので、
ZIP 側で再圧縮しても 0.3% 程度しか縮まず、読み込みのたびに展開コストがかかります。
JSZip なら compression を指定しない(STORE)のが正解です。
フレーム識別子 0.5.0
v0.5.0 の中心概念。ここを理解すると残りが繋がります。
switch パーツが「今どれを表示するか」を指す値をフレーム識別子と呼びます。
フレーム識別子 = layer.frameName ?? layer.textureID
parts[].default、sequence.frames[]、
sequence.keys[].frame、presets[].parts、
mapping.json の参照 — すべてがこの値で解決されます。
frameName を持たないファイルでは textureID と同一になるため、
v0.3.0 / v0.4.0 のファイルの意味は一切変わりません。これが、既存ファイルを壊さずに
「複数レイヤーで 1 つの差分」を表現できる理由です。
{ "textureID": "上着", "frameName": "制服" } // ┐ 同時に表示される
{ "textureID": "スカート", "frameName": "制服" } // ┘
{ "textureID": "シャツ", "frameName": "私服" }
{ "textureID": "ズボン", "frameName": "私服" }
1 フレームに 2 枚以上が属する場合、requiredExtensions に
EMG_frame_name の宣言が必須です。
未対応の実装は default を textureID として探し、
一致するレイヤーが無いため何も描画できません。しかもそれを失敗として検知できません。
root
data.json の最上位オブジェクト。
| フィールド | 型 | 必須 | 不在時 | 導入 |
|---|---|---|---|---|
version | string | 必須 | — | 0.3.0 |
baseCanvasWidth | number | 必須 | — | 0.3.0 |
baseCanvasHeight | number | 必須 | — | 0.3.0 |
textures | Texture[] | 必須 | — | 0.3.0 |
parts | Part[] | 必須 | — | 0.3.0 |
sprites | Sprite[] | 必須 | — | 0.3.0 |
requiredExtensions | string[] | 任意 | [] | 0.4.0 |
presets | Preset[] | 任意 | [] | 0.5.0 |
version を解釈の分岐に使ってはいけません(規則 B1)。
構造の判定はフィールドの有無で行います。実データには version が
"0.2.2" と書かれているのに v0.2 以前の平坦な構造を持つファイルが存在します。
textures[]
アトラス PNG の宣言。1 枚に収まらない場合のみ複数。
| フィールド | 型 | 必須 | 説明 | 導入 |
|---|---|---|---|---|
textureFile | string | 必須 | ZIP 内のエントリ名 | 0.3.0 |
width | number | 必須 | アトラスの幅(px) | 0.3.0 |
height | number | 必須 | アトラスの高さ(px) | 0.3.0 |
{ "textures": [
{ "textureFile": "texture.png", "width": 4096, "height": 8192 }
] }
8 bit / sRGB。1 辺の上限は 8192px です 0.4.0。
parts[]
描画の単位。type が全実装の分岐点です。
| フィールド | 型 | 必須 | 不在時 | 導入 |
|---|---|---|---|---|
partID | string | 必須 | — | 0.3.0 |
type | "static" | "switch" | 必須 | — | 0.3.0 |
default | string | switch では必須 | 先頭フレーム | 0.3.0 |
layers | Layer[] | 必須 | — | 0.3.0 |
control | "animated" | "user" | 任意 | "animated" | 0.5.0 |
defaultVisible | boolean | 任意 | true | 0.5.0 |
type の意味
| 値 | 描画 | 例 |
|---|---|---|
static | layers をすべて描く | 体、背景、髪 |
switch | フレームを1 つだけ描く | 目、口、眉 |
// static — 体は 3 枚が重なって 1 つの体になる { "partID": "Body", "type": "static", "layers": [ { "textureID": "胴", … }, { "textureID": "腕", … }, { "textureID": "脚", … } ] } // switch — 目は 6 枚のうち 1 枚だけが出る { "partID": "Eyes", "type": "switch", "default": "01", // 初期表示のフレーム識別子 "control": "animated", "layers": [ { "textureID": "01", … }, { "textureID": "02", … }, … ] }
表示フレームの決定順序
優先順に、次で決めます。
- 外部制御(ホストの指定、再生中の
sprites[]、mapping.jsonの解決結果) defaultと一致するフレーム識別子を持つレイヤー- いずれも解決できなければ
layersの先頭
defaultVisible 0.5.0 switch は 0.5.1
型によって意味が変わります。
| 型 | false のときの意味 | 用途 |
|---|---|---|
static | パーツ全体が初期非表示。独立して on/off できる | 眼鏡、帽子、小物 |
switch | 初期状態でどのフレームも表示しない | チーク、青ざめ、汗、涙 |
switch の場合も default は必須のままで、意味が変わります —
「表示状態にされたときに選ばれるフレーム」です。
switch で defaultVisible: false を使う場合、
EMG_switch_none の宣言が必須です。
未対応の実装は default のフレームを描いてしまい、
出ないはずのチークが出続けます。static のみでの使用では
宣言してはいけません(常に表示されるだけで絵は成立するため)。
control 0.5.0
| 値 | 意味 | 例 |
|---|---|---|
animated |
主に sprites[] や mapping.json が時間軸に沿って切り替える |
目、口 |
user |
主に利用者が選ぶ | 衣装、髪型 |
制約ではなくヒントです。 control: "user" のパーツを
sprites[] が切り替えても、animated のパーツを
利用者が固定してもかまいません。制約にすると「演出で衣装を切り替える」といった
正当な用途を禁じることになるためです。
実装は無視してよく、描画には影響しません。用途は UI で、 「衣装」を表情の切り替え欄ではなく設定欄に出す、といった提示の分岐に使います。
parts[].layers[]
アトラスの矩形 1 つと、それをキャンバスのどこへ描くか。
| フィールド | 型 | 必須 | 不在時 | 導入 |
|---|---|---|---|---|
textureID | string | 必須 | — | 0.3.0 |
textureFile | string | 必須 | — | 0.3.0 |
x / y | number | 必須 | — | 0.3.0 |
width / height | number | 必須 | — | 0.3.0 |
basePosition_x / _y | number | 必須 | — | 0.3.0 |
textureZIndex | number | 必須 | — | 0.3.0 |
opacity | number | 任意 | 1.0 | 0.3.0 |
blendMode | string | 任意 | "normal" | 0.3.0 値は 0.4.0 加算は 0.5.4 |
anchor_x / _y | number | 任意 | basePosition_* | 0.4.0 |
frameName | string | 任意 | textureID | 0.5.0 |
サンプル
目パーツの layers[]。開き目と閉じ目が 1 枚ずつ入っています。
"layers": [ { "textureID": "01", // パーツ内で一意 "textureFile": "texture.png", "x": 194, "y": 497, // ← アトラスのどこを切るか "width": 238, "height": 82, "basePosition_x": 244, // ← キャンバスのどこへ置くか "basePosition_y": 342, "textureZIndex": 24, // 大きいほど手前 "opacity": 1, "blendMode": "normal" }, { "textureID": "04", "textureFile": "texture.png", "x": 194, "y": 579, "width": 238, "height": 82, "basePosition_x": 244, // 同じ位置に重なるので値は同じ "basePosition_y": 342, "textureZIndex": 24 } ]
座標系が 2 つあります。
x / y / width / height
はアトラス上の切り出し位置、
basePosition_x / _y は
キャンバス上の描画位置です。混同すると絵が崩れます。
frameName を使う場合 0.5.0
同じ frameName を持つレイヤーは同時に表示されます。
textureID は引き続きパーツ内で一意である必要があります。
"layers": [ { "textureID": "上着", "frameName": "制服", "textureZIndex": 30, … }, { "textureID": "スカート", "frameName": "制服", "textureZIndex": 28, … }, { "textureID": "シャツ", "frameName": "私服", "textureZIndex": 30, … }, { "textureID": "ズボン", "frameName": "私服", "textureZIndex": 28, … } ] // この場合 parts[].default は "制服" / "私服"(textureID ではない)
textureZIndex
ファイル全体で 1 本の順序です。パーツ単位ではありません。
値が大きいほど手前で、描画は昇順(奥から手前)に行います。
同じ値が複数ある場合は宣言順(parts[] の順、
その中の layers[] の順)で描きます 0.5.2。
blendMode 0.4.0
許容値は CSS の mix-blend-mode と同じ集合です —
normal, multiply, screen,
overlay, darken, lighten,
color-dodge, color-burn,
hard-light, soft-light,
difference, exclusion,
hue, saturation,
color, luminosity。
未知の値は normal として描画します。
0.5.4 で plus-lighter(加算)が加わり、許容値は 17 語になりました。
上の 16 語は CSS Blending Level 1 の集合で、加算に当たる値を含んでいなかったためです。
色は min(1, Cb + Cs)、アルファは通常どおり source-over です
(Photoshop の「覆い焼き(リニア)— 加算」に一致)。
Porter-Duff の PLUS(canvas の lighter)ではありません —
あちらはアルファまで足すため、半透明の部分が重ねるたびに不透明へ寄ってしまいます。
normal 以外の実装は任意です(0.4.0 §5.3)。未対応の実装は
normal として描いて構いません。
anchor_x / anchor_y 0.4.0
回転と拡縮の基準点(キャンバス座標)。tracks[] でのみ使います。
不在時は basePosition_* と同値です。
アンカーはレイヤーごとに独立しています 0.5.3 で明文化。
トランスフォームは、処理中のレイヤー自身のアンカーを軸に適用されます。パーツ共通の 1 点ではありません。
そのため 1 本の rotation トラックで、髪の房のそれぞれを自分の根元を軸に回せます。
パーツ全体を 1 点で回したい場合は、所属レイヤー全部に同じ値を書きます。
textureID はパーツ内でのみ一意です。
実データでは "01" が Mouth・
Eyes・Eyebrows に同時に存在します。
レイヤーを一意に指すには (partID, フレーム識別子) の組が必要です。
裸の textureID を保存すると、列挙順で先に来たパーツが選ばれます
(「眉がまばたきする」不具合の原因です)。
sprites[]
フレームの切り替え(sequence)と座標変換(tracks)。
| フィールド | 型 | 必須 | 不在時 | 導入 |
|---|---|---|---|---|
spriteID | string | 必須 | — | 0.3.0 |
targetPartID | string | 必須 | — | 0.3.0 |
sequence | Sequence | 任意 | — | 0.3.0 |
fps | number | 任意 | 12 | 0.4.0 で任意化 |
trigger | Trigger | 任意 | 自律再生しない | 0.3.0 |
targetLayer | string | 任意 | パーツ全体 | 0.5.3 |
tracks | Track[] | 任意 | — | 0.5.0 |
duration | number | tracks では必須 | — | 0.5.0 |
loop | "once" | "loop" | "pingpong" | 任意 | "loop" | 0.5.0 |
phaseOffset | number | 任意 | 0 | 0.5.0 |
trigger が無ければ、プレイヤーは自律再生してはいけません。
外部から spriteID で明示的に再生させます。
loop 0.5.0
tracks の再生の仕方。duration を 1 周期として扱います。
| 値 | 挙動 | 使いどころ |
|---|---|---|
once | duration まで再生して末尾の値で止まる | 登場演出 |
loop | 末尾まで行ったら先頭へ飛ぶ(不連続になりうる) | 回転、点滅 |
pingpong | 末尾まで行ったら逆再生で戻る(つなぎ目が滑らか) | 揺れ、呼吸 |
phaseOffset は再生開始位置のずれ(秒)です。
同じ動きを複数パーツに付けるとき、値をずらすと揃って動く不自然さを避けられます。
targetPartID の制約
| sprite の中身 | 対象にできるパーツ |
|---|---|
sequence を持つ | switch のみ |
tracks のみ | static / switch どちらでも 0.5.0 |
mapping.json が掌握するパーツは自律発火してはいけません。
blinkPartKey / blinkParts /
lipSyncPartKey / lipSyncParts で
明示的に指定された partID を
targetPartID に持つ sprite が対象です。
キーワード一致で解決されただけのものは該当しません。
sequence
フレームの並びと再生順。frames と keys は排他です。
| フィールド | 型 | 必須 | 説明 | 導入 |
|---|---|---|---|---|
type | "ordered" | "random_hold" | 必須 | 再生順の種別 | 0.3.0 |
frames | string[] | 任意 | 等間隔。fps の間隔で 1 つずつ進む | 0.3.0 |
keys | Key[] | 任意 | 不等間隔。{ t, frame } の配列 | 0.5.0 |
type | 挙動 |
|---|---|
ordered | 先頭から順に再生する |
random_hold | 1 つをランダムに選んで表示し続ける。trigger のたびに再抽選 |
keys[] 0.5.0
t は再生開始からの累積秒数(各コマの長さではありません)。
昇順に並んでいる必要があります。時刻 t の表示フレームは
key.t <= t を満たす最後のキーです。
{ "spriteID": "blink", "targetPartID": "Eyes",
"sequence": { "type": "ordered", "keys": [
{ "t": 0.00, "frame": "01" },
{ "t": 0.06, "frame": "03" },
{ "t": 0.10, "frame": "04" },
{ "t": 0.18, "frame": "03" },
{ "t": 0.24, "frame": "01" }
] } }
keys を使う場合、fps は不要です。書くと解釈が二重になります。
trigger
いつ発火するか。不在なら自律再生しません。
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
type | "auto_loop" | "random_interval" | "external" | 必須 | 発火の種別 |
intervalMin | number | random_interval では必須 | 最小の間隔(秒) |
intervalMax | number | random_interval では必須 | 最大の間隔(秒) |
| 値 | 挙動 |
|---|---|
auto_loop | 再生が終わったらすぐ次を始める |
random_interval | intervalMin〜intervalMax 秒のランダムな間隔で発火 |
external | 自律発火しない。spriteID で外部から再生させる |
sprites[].tracks[] 0.5.0
移動・回転・拡縮・不透明度のアニメーション。
| フィールド | 型 | 必須 | 不在時 |
|---|---|---|---|
path | string | 必須 | — |
keys | { t, v }[] | 必須 | — |
interpolation | "step" | "linear" | "cubic" | 任意 | "linear" |
path — 対象プロパティ
この 6 種のみです。 メッシュ変形・色調補正・textureZIndex の変更は範囲外です。
path | 単位 | 既定値 |
|---|---|---|
translate_x | px(キャンバス座標) | 0 |
translate_y | px(キャンバス座標) | 0 |
rotation | 度。時計回りが正 | 0 |
scale_x | 倍率 | 1.0 |
scale_y | 倍率 | 1.0 |
opacity | 0.0〜1.0 | 1.0 |
この集合に限ることで、CSS transform・OS コンポジタ・GPU・CPU 合成の どの経路でも実現できます。これを超えると、いずれの経路でも自前描画が必要になります。
適用順序
対象となる各レイヤーへ、この順で適用します。1 と 4 で使うのは、そのレイヤー自身のアンカーです。
1. アンカー点(anchor_x, anchor_y)を原点へ移動 2. scale_x / scale_y 3. rotation 4. アンカー点を元の位置へ戻す 5. translate_x / translate_y 6. opacity を layer.opacity に乗じる
targetLayer — パーツの一部だけを動かす 0.5.3
トランスフォームの対象を、targetPartID のパーツ内の
特定のフレーム識別子に絞ります。不在ならこれまでどおりパーツ全体が対象です。
{
"spriteID": "hair_sway",
"targetPartID": "Body",
"targetLayer": "Back_hair", // 0.5.3
"duration": 3.0,
"loop": "pingpong",
"tracks": [ { "path": "rotation", "keys": [ … ] } ]
}
| 規則 | 内容 |
|---|---|
| 1 | sequence と併用してはいけません。sequence はパーツ内のフレームを切り替えるものなので、対象を 1 つに絞ることと両立しません |
| 2 | 値は対象パーツに実在するフレーム識別子であること |
| 3 | 同じフレーム識別子を持つレイヤーが複数ある場合、そのすべてが対象になります |
| 4 | パーツのレイヤーが 1 枚しかない場合は書くべきではありません(効果が同じで宣言義務だけ増える) |
targetLayer を使うファイルは
EMG_layer_transform の宣言が必須です。
理解しない実装はこのフィールドを無視してパーツ全体を動かします。
髪だけが揺れるはずの絵で体ごと揺れるので、「動かない」ではなく「違う絵」になります。
同じことは動かしたい部分を独立した parts[] に切り出しても表現できます。
それでもこの機能があるのは、パーツが描画の単位であると同時に状態と意味づけの単位でもあるためです。
presets[] の parts / toggles、
mapping.json の blinkPartKey などはすべて partID で働くので、
動きの都合で房を独立させると、衣装の着せ替えプリセットが体を指しても房には及ばなくなります。
interpolation — キーとキーの間をどう埋めるか
| 値 | 挙動 | 使いどころ |
|---|---|---|
step |
直前のキーの値を保持する。次のキーで不連続に飛ぶ | 点滅、パラパラした動き |
linear |
2 つのキーを直線でつなぐ。既定値 | ほとんどの用途 |
cubic |
Catmull-Rom で滑らかにつなぐ。キーの前後を見て曲線を作る | 揺れ、加減速のある動き |
cubic を Catmull-Rom に固定しているのは、
制御点を持たないため追加データが不要で、実装間で結果が一意に定まるからです。
端点は最初/最後のキーを複製して計算します。
キーは t の昇順である必要があります。
範囲外の時刻では端のキーの値を保持します。
{ "spriteID": "hair_sway", "targetPartID": "hair_back",
"duration": 3.0, "loop": "pingpong",
"tracks": [
{ "path": "rotation",
"keys": [ { "t": 0.0, "v": 0 },
{ "t": 1.5, "v": 2.5 },
{ "t": 3.0, "v": 0 } ],
"interpolation": "linear" }
] }
§7 は分離可能です。 tracks を実装せず
§2〜§6 のみを実装しても v0.5.0 として整合します。emg-renpy は
方針として非対応です。
presets[] 0.5.0
複数パーツの状態をまとめて名前で呼ぶ。
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
presetID | string | 必須 | 参照キー |
label | string | 任意 | UI に出す名前 |
parts | { partID: frameID } | 任意 | switch パーツのフレーム指定 |
toggles | { partID: boolean } | 任意 | パーツの表示 / 非表示 |
プリセットに現れない partID の状態は変更しません。
「この表情のときはこれだけ変える」を表現するためで、全パーツを列挙する必要はありません。
{ "presetID": "happy_look",
"label": "にこやか",
"parts": { "Eyebrows": "01", "Blushs": "01" },
"toggles": { "arms": true } }
状態の使い分け
差し替え・on/off・まとめ指定を、どの機構で表現するか。
v0.5 には状態を表す機構が 4 つあります。似て見えますが排他性の扱いが違い、 選び間違えると後から破綻します。
| やりたいこと | 使う機構 | 排他性 |
|---|---|---|
| 衣装の切り替え(制服/私服) | switch + frameName |
自動で排他。1 スロットに 1 フレーム |
| 小物の on/off(帽子・眼鏡) | static + defaultVisible |
無し。自由に組み合わせられる |
| 複数パーツをまとめて指定 | presets[] |
無し。差分として適用される |
| 表情(まばたき・口パクを含む) | mapping.json の expressions |
無し |
着せ替えは frameName が本命
「上着とスカートが同時に切り替わる」は presets[].toggles でも書けますが、
排他性を自分で管理することになります — 制服を選んだら私服の全パーツを
false にする、という列挙が要り、衣装を 1 つ足すたびに
既存プリセット全部の書き換えが必要になります。
switch + frameName なら排他は自動です。
z が非連続な組(上着 z=20 / 体 z=10 / スカート z=5)も 1 フレームにできるのが
この機構の存在理由なので、「1 枚の画像に焼き込む」で代替できない組み合わせもここで扱えます。
presets[] が要るのは、衣装の切り替えに小物の on/off が伴う場合です
(制服のときだけ帽子を出す、など)。switch パーツと
static のトグルにまたがるため、まとめる側が要ります。
表情はどこに置くか
presets[](data.json) | expressions(mapping.json) | |
|---|---|---|
| 役割 | 構造的な状態の組 | 意味づけ |
| 目・口 | 指定できる | 指定できない(後述) |
| まばたき/口パクの差し替え | 不可 | overrides.blink / overrides.lipSync |
| 相互参照 | — | presetID でプリセットを呼べる |
avatarId は識別用のラベルで、解決には使いません。
mapping.json は独自のバージョンを持たず、同梱される
data.json の version に従います
0.5.2。
表情から目・口のパーツを直接指定することはできません。
解決は expressions[].parts を先に適用し、そのあと blink と lipSync が
自分のパーツを上書きします。parts に Eyes を
書いても黙って無効になります。目と口は
overrides.blink / overrides.lipSync を使ってください。
したがって想定される分担は、プリセットが「どのフレーム・どのトグル」という構造を持ち、
表情がそれを presetID で参照しつつ、目・口だけを
overrides で足す形です。
現状の制約
0.5 系列では次を扱いません。回避策とあわせて把握しておいてください。
| できないこと | 起きること | 回避策 |
|---|---|---|
| プリセット同士の排他 | 衣装 A を適用しても衣装 B が外れない | 排他が要るものは frameName で 1 パーツにまとめる |
| 絶対指定(差分ではなく全体を決める) | シーン A → B で、B が触れないパーツに A の残骸が残る | シーンに関わるパーツを全プリセットで列挙する |
| 直交性の宣言 | 同じパーツに触れる 2 つのプリセットが衝突しても検知されず後勝ち | 衣装用と表情用で触るパーツを分けておく |
いずれも conflicts / requires のような依存グラフを
導入すれば解けますが、組み合わせ検証の実装を全消費側に要求するため、
必要性が実データで確認されるまで見送られています(0.5.0 §4.4)。
mapping.json
任意のコンパニオンファイル。まばたき・口パク・表情の意味づけ。
data.json が構造を定めるのに対し、
mapping.json は意味を与えます —
「どのパーツが目か」「どのフレームが閉じ目か」。
存在しなくても .emg は有効で、対応しない実装は無視してかまいません。
{
"avatarId": "avatar",
"baseMapping": {
"blinkPartKey": "Eyes",
"blink": { "open": "01", "half": "03", "closed": "04" },
"lipSyncPartKey": "Mouth",
"lipSync": { "a": "02", "i": "03", "u": "04",
"e": "05", "o": "06", "n": "01" }
},
"expressions": {
"default": {},
"angry": { "parts": { "Eyebrows": ["04"] },
"overrides": { "blink": { "open": "05", "half": "06", "closed": "04" } } }
}
}
avatarId は識別用のラベルで、解決には使いません。
mapping.json は独自のバージョンを持たず、同梱される
data.json の version に従います
0.5.2。
表情から目・口のパーツを直接指定することはできません。
解決は expressions[].parts を先に適用し、
そのあと blink と lipSync が自分のパーツを上書きします。
parts に Eyes を書いても黙って無効になります。
目と口は overrides.blink / overrides.lipSync を使ってください。
描画モデル
1 フレーム分の描画手順(規範)。
- 対象を集める。
staticは全レイヤー、switchは解決した 1 フレーム分のみ textureZIndexの昇順に並べ替える。 パーツ単位でまとめて描いてはいけません(パーツをまたぐ重なりが壊れます)- 順に描く。 アトラスの矩形をキャンバスの位置へ、
opacityを乗じて合成
draw = [] for part in data.parts: if part.defaultVisible == False: continue # 0.5.0 if part.type == "static": draw += part.layers else: fid = resolve_switch(part) draw += [l for l in part.layers if frame_id(l) == fid] for layer in sorted(draw, key=lambda l: l.textureZIndex): blit(src=(layer.x, layer.y, layer.width, layer.height), dst=(layer.basePosition_x, layer.basePosition_y, layer.width, layer.height), alpha=layer.opacity)
キャンバスの背景は透明です。.emg は背景色を持ちません。
結果を一致させる規定 0.5.2
「複数の実装が同じ絵を出す」ために必要な前提。0.5.1 まで未定義だったもの。
座標系
キャンバスもアトラスも 左上が原点、X は右、Y は下。単位は px。
rotation の時計回りが正は、この向きでの右回りです。
Y 軸を上向きに扱うエンジン(Unity 等)へ移す場合は、 読み込み時に一度だけ変換してください。仕様側は画像フォーマットとして自然な向きに固定しています。
色空間とアルファ
| 項目 | 規定 | 理由 |
|---|---|---|
| 合成の色空間 | sRGB のまま。リニアへ変換しない | CSS の mix-blend-mode と同じ値集合を使うため。変換を挟むと multiply / screen の結果が実装ごとに変わる |
| アルファ | straight(非乗算済み) | PNG の規定どおり。実装が内部で premultiply するのは自由だが、結果は straight 合成と一致すること |
sequence と tracks
1 つの sprite が両方を持てます。それぞれ独立に評価します。
| 時間の基準 | 繰り返し | |
|---|---|---|
sequence | fps または keys[].t | trigger が決める |
tracks | duration | loop が決める |
loop は sequence に効きません。
sequence が末尾に達したとき、auto_loop なら先頭から再開し、
それ以外(random_interval / external / 不在)は
末尾のフレームを保持します。
keys があれば fps は無視します。
両方を書いてはいけませんが、遭遇した場合の解釈はこれで一意に定まります。
同じパーツを複数の sprite が対象にしたとき
同一パーツを自律発火する sequence を複数書いてはいけません。
表示できるフレームは 1 つなので取り合いになります。
違反したファイルに遭遇した場合、および外部制御で複数が同時に有効になった場合は、
sprites[] の宣言順で後にあるものが勝ちます。
tracks も同じで、同一パーツの同一 path に
複数のトラックが効く場合は宣言順で後のものが上書きします。
加算してはいけません — 加算にすると評価順の違いがそのまま値の違いになります。
再生中の sprite に同じ spriteID の再生要求が来たら、
先頭から再生し直します(無視もキューイングもしない)。
参照の欠落と異常系
| 状況 | 挙動 |
|---|---|
presets[] が存在しない partID / フレームを指す |
その項目のみ無視し、残りを適用する |
textures[] のファイルが ZIP に無い | 読み込みを失敗させる |
| PNG が復号できない | 読み込みを失敗させる |
| メイン JSON が見つからない | 読み込みを失敗させる |
一部のレイヤーを黙って描かずに続行してはいけません。 沈黙して誤った絵を出すより、検知できる状態を優先します。 ZIP のエントリ名は大文字小文字を区別します。
数値
- 座標・寸法・
textureZIndexは整数 opacityが 0.0〜1.0 の範囲外ならクランプする- 内部精度は実装に委ねる。ピクセル完全一致は要求しない — 求めるのは「同じ絵に見えること」
アトラスの滲み(参考)
アトラスは矩形を密に詰めておりパディングを持ちません。等倍では問題ありませんが、
scale_* で拡大したりバイリニアでサンプリングすると、
隣接領域の色が滲むことがあります。0.5 系列ではパディングを要求しません。
拡大を前提とする素材では、生成側が 1〜2px の余白を入れると回避できます。
互換性の規則 0.4.0
v0.3.0 にはこれに相当する規定がありませんでした。
前方互換 — 古い実装が新しいファイルを読む
| # | 規則 |
|---|---|
F1 | 未知のキー・未知のフィールドは無視して読み進める。 存在を理由にエラーとしてはならない |
F2 | parts[].type が未知の値なら、default を持つなら switch、持たないなら static として扱う |
F3 | sequence.type が未知の値なら ordered として扱う |
F4 | trigger.type が未知の値なら自律発火してはならない(external と同じ扱い) |
F5 | requiredExtensions[] に未知の識別子があれば読み込みを拒否する |
F1 と F5 は矛盾しません。 対象が違います。 知らなくても表示が成立する追加は無視して読み進め(F1)、 知らないと表示が成立しない追加は明示的に失敗する(F5)。 沈黙して誤った絵を出すより、検知できる状態を優先します。
後方互換 — 新しい実装が古いファイルを読む
| # | 規則 |
|---|---|
B1 | version を解釈の分岐に使わない。構造の判定はフィールドの有無で行う |
B2 | 既存キーの意味を変更しない。異なる意味で再利用もしない |
B3 | 任意のフィールドを必須へ変更しない |
B4 | 新規フィールドには必ず「不在時の意味」を定義する |
B5 | 読み込みの対応は維持する。引退させるのは書き出し側のみ |
requiredExtensions[] 0.4.0
理解できない実装に読ませてはならない機能の宣言。
宣言してよいのは「その機能を理解しない実装が、誤った絵を描いてしまう」場合だけです。 動かない・切り替えられない・初期状態のまま、といった劣化に対して宣言してはいけません。 不必要に古い実装を締め出すことになります。
| 識別子 | 対象 | 宣言が必要な条件 |
|---|---|---|
EMG_frame_name 0.5.0 | layers[].frameName |
1 つのフレーム識別子に 2 枚以上のレイヤーが属する場合 |
EMG_switch_none 0.5.1 | parts[].defaultVisible |
type: "switch" のパーツに defaultVisible: false を指定した場合 |
EMG_layer_transform 0.5.3 | sprites[].targetLayer |
targetLayer を持つ sprite が 1 つでもある場合 |
宣言が不要なもの
control、presets[]、
sequence.keys[]、tracks[]、
anchor_*、static での
defaultVisible — いずれも無視されても絵は成立します。
実装対応表
2026-08-25 時点。
| 実装 | frameName |
control〜presets |
switch none | keys |
tracks |
|---|---|---|---|---|---|
| emg-ymm4(Emg.Core) | ✓ | ✓ | ✓ | ✓ | ✓ |
| emg-cdn プレイヤー | ✓ | ✓ | ✓ | ✓ | ✓ |
| emg-godot | ✓ | ✓ | ✓ | ✓ | ✓ |
| emg-unity-importer | ✓ | 一部 | ✓ | — | 評価器 |
| emg-web-runtime | ✓ | 一部 | ✓ | — | 評価器 |
| emg-renpy | ✓ | 一部 | ✓ | — | 方針として非対応 |
「一部」は presets 未実装を指します。
「評価器」は値の解決まで実装済みで、描画への適用はホスト側に委ねている状態です。
落とし穴
実データで実際に踏んだもの。
textureID は一意ではない
パーツ内でのみ一意です。実ファイルでは "1"〜"5" が
眉・口・目に、"6"〜"14" が口・目に存在します。
参照は (partID, フレーム識別子) の組で持ってください。
名前から意味を推測しない
textureID は単なる番号のことが多く
("14", "24", "15_1")、
名前から母音や閉じ目を当てることはできません。推測が働かない場合は
無効に倒すべきで、誤った推測をしてはいけません。
z 順が反転しているファイルがある
ある時期より前に書き出されたファイルは textureZIndex が
前後逆です(体が最前面になっています)。原因は生成側が
ag-psd のレイヤー順を逆に仮定していたことで、既に修正されています。
古いファイルは再書き出しするか、z' = maxZ - z で正規化してください。
ファイル全体が 1 つのパーツになっているファイルがある
生成側の不具合で、入れ子のグループが 1 つのパーツに潰れたファイルが存在します
(36 レイヤーで 1 パーツ)。これは再書き出しで直すべきもので、
消費側で対処しようとすると static パーツ内のレイヤー単位フィルタが必要になり、
どの実装もそれを持っていません。
version は嘘をつくことがある
"0.2.2" と書かれているのに v0.2 以前の平坦な構造を持つファイルが実在します。
構造の判定はフィールドの有無で行ってください(規則 B1)。
完全なサンプル
最小構成の data.json。
{
"version": "0.5.2",
"requiredExtensions": ["EMG_switch_none"],
"baseCanvasWidth": 1024,
"baseCanvasHeight": 1024,
"textures": [
{ "textureFile": "texture.png", "width": 1024, "height": 1024 }
],
"parts": [
{ "partID": "Body", "type": "static",
"layers": [
{ "textureID": "body", "textureFile": "texture.png",
"x": 0, "y": 0, "width": 400, "height": 600,
"basePosition_x": 312, "basePosition_y": 300,
"textureZIndex": 0, "opacity": 1, "blendMode": "normal" }
] },
{ "partID": "Eyes", "type": "switch", "default": "open",
"control": "animated",
"layers": [
{ "textureID": "open", "textureFile": "texture.png",
"x": 400, "y": 0, "width": 120, "height": 40,
"basePosition_x": 452, "basePosition_y": 380,
"textureZIndex": 10 },
{ "textureID": "closed", "textureFile": "texture.png",
"x": 400, "y": 40, "width": 120, "height": 40,
"basePosition_x": 452, "basePosition_y": 380,
"textureZIndex": 10 }
] },
// 差分を持ちながら「どれも出さない」のが常態のパーツ
{ "partID": "Blush", "type": "switch",
"default": "cheek", "defaultVisible": false,
"layers": [
{ "textureID": "cheek", "textureFile": "texture.png",
"x": 400, "y": 80, "width": 160, "height": 30,
"basePosition_x": 432, "basePosition_y": 400,
"textureZIndex": 20 },
{ "textureID": "pale", "textureFile": "texture.png",
"x": 400, "y": 110, "width": 160, "height": 30,
"basePosition_x": 432, "basePosition_y": 400,
"textureZIndex": 20 }
] }
],
"sprites": [
{ "spriteID": "blink", "targetPartID": "Eyes",
"sequence": { "type": "ordered", "keys": [
{ "t": 0.00, "frame": "open" },
{ "t": 0.06, "frame": "closed" },
{ "t": 0.14, "frame": "open" }
] },
"trigger": { "type": "random_interval",
"intervalMin": 3.0, "intervalMax": 8.0 } }
],
"presets": [
{ "presetID": "shy", "label": "照れ",
"parts": { "Blush": "cheek" },
"toggles": { "Blush": true } }
]
}