EMG 0.5 リファレンス

.emg は、組み合わせ可能な2Dスプライトの配布形式です。ZIP の中に data.json とテクスチャアトラスの PNG が入っています。

0.3.0 基準となる版
0.4.0 互換性の規則を追加
0.5.0 表現力を追加
0.5.1 switch の「どれも出さない」
0.5.2 実装間で結果を一致させる規定
0.5.3 レイヤー単位のトランスフォーム
0.5.4 合成モードに加算を追加

本書は参考文書です。 規範は emg-json-spec.md(v0.3.0)、 emg-json-spec-0.4.0.mdemg-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[].controlUI 側での出し分けのヒント(描画には影響しない)
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[].defaultsequence.frames[]sequence.keys[].framepresets[].partsmapping.json の参照 — すべてがこの値で解決されます

frameName を持たないファイルでは textureID と同一になるため、 v0.3.0 / v0.4.0 のファイルの意味は一切変わりません。これが、既存ファイルを壊さずに 「複数レイヤーで 1 つの差分」を表現できる理由です。

{ "textureID": "上着",     "frameName": "制服" }   // ┐ 同時に表示される
{ "textureID": "スカート", "frameName": "制服" }   // ┘
{ "textureID": "シャツ",   "frameName": "私服" }
{ "textureID": "ズボン",   "frameName": "私服" }

1 フレームに 2 枚以上が属する場合、requiredExtensionsEMG_frame_name の宣言が必須です。 未対応の実装は defaulttextureID として探し、 一致するレイヤーが無いため何も描画できません。しかもそれを失敗として検知できません。

root

data.json の最上位オブジェクト。

フィールド必須不在時導入
versionstring必須0.3.0
baseCanvasWidthnumber必須0.3.0
baseCanvasHeightnumber必須0.3.0
texturesTexture[]必須0.3.0
partsPart[]必須0.3.0
spritesSprite[]必須0.3.0
requiredExtensionsstring[]任意[]0.4.0
presetsPreset[]任意[]0.5.0

version を解釈の分岐に使ってはいけません(規則 B1)。 構造の判定はフィールドの有無で行います。実データには version"0.2.2" と書かれているのに v0.2 以前の平坦な構造を持つファイルが存在します。

textures[]

アトラス PNG の宣言。1 枚に収まらない場合のみ複数。

フィールド必須説明導入
textureFilestring必須ZIP 内のエントリ名0.3.0
widthnumber必須アトラスの幅(px)0.3.0
heightnumber必須アトラスの高さ(px)0.3.0
{ "textures": [
  { "textureFile": "texture.png", "width": 4096, "height": 8192 }
] }

8 bit / sRGB。1 辺の上限は 8192px です 0.4.0

parts[]

描画の単位。type が全実装の分岐点です。

フィールド必須不在時導入
partIDstring必須0.3.0
type"static" | "switch"必須0.3.0
defaultstringswitch では必須先頭フレーム0.3.0
layersLayer[]必須0.3.0
control"animated" | "user"任意"animated"0.5.0
defaultVisibleboolean任意true0.5.0

type の意味

描画
staticlayersすべて描く体、背景、髪
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 は必須のままで、意味が変わります — 「表示状態にされたときに選ばれるフレーム」です。

switchdefaultVisible: false を使う場合、 EMG_switch_none の宣言が必須です。 未対応の実装は default のフレームを描いてしまい、 出ないはずのチークが出続けます。static のみでの使用では 宣言してはいけません(常に表示されるだけで絵は成立するため)。

control 0.5.0

意味
animated 主に sprites[]mapping.json時間軸に沿って切り替える 目、口
user 主に利用者が選ぶ 衣装、髪型

制約ではなくヒントです。 control: "user" のパーツを sprites[] が切り替えても、animated のパーツを 利用者が固定してもかまいません。制約にすると「演出で衣装を切り替える」といった 正当な用途を禁じることになるためです。

実装は無視してよく、描画には影響しません。用途は UI で、 「衣装」を表情の切り替え欄ではなく設定欄に出す、といった提示の分岐に使います。

parts[].layers[]

アトラスの矩形 1 つと、それをキャンバスのどこへ描くか。

フィールド必須不在時導入
textureIDstring必須0.3.0
textureFilestring必須0.3.0
x / ynumber必須0.3.0
width / heightnumber必須0.3.0
basePosition_x / _ynumber必須0.3.0
textureZIndexnumber必須0.3.0
opacitynumber任意1.00.3.0
blendModestring任意"normal"0.3.0 値は 0.4.0 加算は 0.5.4
anchor_x / _ynumber任意basePosition_*0.4.0
frameNamestring任意textureID0.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.4plus-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"MouthEyesEyebrows に同時に存在します。 レイヤーを一意に指すには (partID, フレーム識別子) の組が必要です。 裸の textureID を保存すると、列挙順で先に来たパーツが選ばれます (「眉がまばたきする」不具合の原因です)。

sprites[]

フレームの切り替え(sequence)と座標変換(tracks)。

フィールド必須不在時導入
spriteIDstring必須0.3.0
targetPartIDstring必須0.3.0
sequenceSequence任意0.3.0
fpsnumber任意120.4.0 で任意化
triggerTrigger任意自律再生しない0.3.0
targetLayerstring任意パーツ全体0.5.3
tracksTrack[]任意0.5.0
durationnumbertracks では必須0.5.0
loop"once" | "loop" | "pingpong"任意"loop"0.5.0
phaseOffsetnumber任意00.5.0

trigger が無ければ、プレイヤーは自律再生してはいけません。 外部から spriteID で明示的に再生させます。

loop 0.5.0

tracks の再生の仕方。duration を 1 周期として扱います。

挙動使いどころ
onceduration まで再生して末尾の値で止まる登場演出
loop末尾まで行ったら先頭へ飛ぶ(不連続になりうる)回転、点滅
pingpong末尾まで行ったら逆再生で戻る(つなぎ目が滑らか)揺れ、呼吸

phaseOffset は再生開始位置のずれ(秒)です。 同じ動きを複数パーツに付けるとき、値をずらすと揃って動く不自然さを避けられます。

targetPartID の制約

sprite の中身対象にできるパーツ
sequence を持つswitch のみ
tracks のみstatic / switch どちらでも 0.5.0

mapping.json が掌握するパーツは自律発火してはいけません。 blinkPartKey / blinkParts / lipSyncPartKey / lipSyncParts明示的に指定された partIDtargetPartID に持つ sprite が対象です。 キーワード一致で解決されただけのものは該当しません。

sequence

フレームの並びと再生順。frameskeys は排他です。

フィールド必須説明導入
type"ordered" | "random_hold"必須再生順の種別0.3.0
framesstring[]任意等間隔。fps の間隔で 1 つずつ進む0.3.0
keysKey[]任意不等間隔。{ t, frame } の配列0.5.0
type挙動
ordered先頭から順に再生する
random_hold1 つをランダムに選んで表示し続ける。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"必須発火の種別
intervalMinnumberrandom_interval では必須最小の間隔(秒)
intervalMaxnumberrandom_interval では必須最大の間隔(秒)
挙動
auto_loop再生が終わったらすぐ次を始める
random_intervalintervalMinintervalMax 秒のランダムな間隔で発火
external自律発火しない。spriteID で外部から再生させる

sprites[].tracks[] 0.5.0

移動・回転・拡縮・不透明度のアニメーション。

フィールド必須不在時
pathstring必須
keys{ t, v }[]必須
interpolation"step" | "linear" | "cubic"任意"linear"

path — 対象プロパティ

この 6 種のみです。 メッシュ変形・色調補正・textureZIndex の変更は範囲外です。

path単位既定値
translate_xpx(キャンバス座標)0
translate_ypx(キャンバス座標)0
rotation度。時計回りが正0
scale_x倍率1.0
scale_y倍率1.0
opacity0.0〜1.01.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": [ … ] } ]
}
規則内容
1sequence と併用してはいけません。sequence はパーツ内のフレームを切り替えるものなので、対象を 1 つに絞ることと両立しません
2値は対象パーツに実在するフレーム識別子であること
3同じフレーム識別子を持つレイヤーが複数ある場合、そのすべてが対象になります
4パーツのレイヤーが 1 枚しかない場合は書くべきではありません(効果が同じで宣言義務だけ増える)

targetLayer を使うファイルは EMG_layer_transform の宣言が必須です。 理解しない実装はこのフィールドを無視してパーツ全体を動かします。 髪だけが揺れるはずの絵で体ごと揺れるので、「動かない」ではなく「違う絵」になります。

同じことは動かしたい部分を独立した parts[] に切り出しても表現できます。 それでもこの機能があるのは、パーツが描画の単位であると同時に状態と意味づけの単位でもあるためです。 presets[]parts / togglesmapping.jsonblinkPartKey などはすべて partID で働くので、 動きの都合で房を独立させると、衣装の着せ替えプリセットが体を指しても房には及ばなくなります。

interpolation — キーとキーの間をどう埋めるか

挙動使いどころ
step 直前のキーの値を保持する。次のキーで不連続に飛ぶ 点滅、パラパラした動き
linear 2 つのキーを直線でつなぐ。既定値 ほとんどの用途
cubic Catmull-Rom で滑らかにつなぐ。キーの前後を見て曲線を作る 揺れ、加減速のある動き

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

複数パーツの状態をまとめて名前で呼ぶ。

フィールド必須説明
presetIDstring必須参照キー
labelstring任意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.jsonexpressions 無し

着せ替えは 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.jsonversion に従います 0.5.2

表情から目・口のパーツを直接指定することはできません。 解決は expressions[].parts を先に適用し、そのあと blink と lipSync が 自分のパーツを上書きします。partsEyes を 書いても黙って無効になります。目と口は 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.jsonversion に従います 0.5.2

表情から目・口のパーツを直接指定することはできません。 解決は expressions[].parts を先に適用し、 そのあと blink と lipSync が自分のパーツを上書きします。 partsEyes を書いても黙って無効になります。 目と口は 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 合成と一致すること

sequencetracks

1 つの sprite が両方を持てます。それぞれ独立に評価します。

時間の基準繰り返し
sequencefps または keys[].ttrigger が決める
tracksdurationloop が決める

loopsequence に効きません。 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未知のキー・未知のフィールドは無視して読み進める。 存在を理由にエラーとしてはならない
F2parts[].type が未知の値なら、default を持つなら switch、持たないなら static として扱う
F3sequence.type が未知の値なら ordered として扱う
F4trigger.type が未知の値なら自律発火してはならないexternal と同じ扱い)
F5requiredExtensions[] に未知の識別子があれば読み込みを拒否する

F1 と F5 は矛盾しません。 対象が違います。 知らなくても表示が成立する追加は無視して読み進め(F1)、 知らないと表示が成立しない追加は明示的に失敗する(F5)。 沈黙して誤った絵を出すより、検知できる状態を優先します。

後方互換 — 新しい実装が古いファイルを読む

#規則
B1version を解釈の分岐に使わない。構造の判定はフィールドの有無で行う
B2既存キーの意味を変更しない。異なる意味で再利用もしない
B3任意のフィールドを必須へ変更しない
B4新規フィールドには必ず「不在時の意味」を定義する
B5読み込みの対応は維持する。引退させるのは書き出し側のみ

requiredExtensions[] 0.4.0

理解できない実装に読ませてはならない機能の宣言。

宣言してよいのは「その機能を理解しない実装が、誤った絵を描いてしまう」場合だけです。 動かない・切り替えられない・初期状態のまま、といった劣化に対して宣言してはいけません。 不必要に古い実装を締め出すことになります。

識別子対象宣言が必要な条件
EMG_frame_name 0.5.0layers[].frameName 1 つのフレーム識別子に 2 枚以上のレイヤーが属する場合
EMG_switch_none 0.5.1parts[].defaultVisible type: "switch" のパーツに defaultVisible: false を指定した場合
EMG_layer_transform 0.5.3sprites[].targetLayer targetLayer を持つ sprite が 1 つでもある場合

宣言が不要なもの

controlpresets[]sequence.keys[]tracks[]anchor_*static での defaultVisible — いずれも無視されても絵は成立します。

実装対応表

2026-08-25 時点。

実装 frameName controlpresets 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 } }
  ]
}