大量のSD制作を支える「IDOLY PRIDE」におけるSpine活用事例② 〜アイテム付与/モーション合成編〜

はじめに
株式会社QualiArtsでUnityエンジニアをしている吉成です。2021年6月リリースの「IDOLY PRIDE」(以降、アイプラ)の開発に携わり、主にUnityでのゲーム開発やSD周りの設計を担当しています。
本記事は、3回に渡ってアイプラにおけるSpineの活用事例を紹介する記事の第2回目の記事です。
第1回目の記事はこちらをご覧になってください。
第2回目となる本記事では『アイテム付与/モーション合成編』として、キャラクターにアイテムを持たせるために行った工夫や、モーションを効率的に制作するために行ったモーション合成の方法の紹介を行います。
アイテム(小物)
アイプラには様々なアイテム(小物)が存在します。

これらのアイテム情報をキャラクターのSpineに入れるのは次の理由から厳しいです。
- 全アイテムを入れると、キャラクターのAtlasテクスチャを圧迫してしまう
- 常に全アイテムの情報をロードすることになるのでデータ量も増加してしまう
- 全キャラクターに同じアイテム情報を入れるのは冗長
- アイテムが増えるたびに全キャラクターのSpineが作り直しになる

そこでアイテムはキャラクターとは別のSpineとして作成します。

それにより先ほどの課題を解決することができます。
- キャラクターのAtlasテクスチャが圧迫されない
- 必要なアイテムしか生成しないので、データ量も圧迫しない
- アイテム情報はアイテムのSpineに切り出されて重複しない
- アイテムが増えてもキャラクターのSpineに変更は入らない
そして、アイテムとキャラクターのSpineをそれぞれ生成し、重ねて表示をすることでアイテムを持っている状態を実現します。 この時、アイテム側にもキャラクターと同じ名前、同じ尺のモーションを用意し、同時に再生することで一緒に動いているように見せます。


しかし単純に重ねるだけだと限界があります。 キャラクターのSpineの中にアイテムを挟み込むことができないため、キャラクターとアイテムが互い違いに重なる時に破綻します。

SkeletonRenderSeparatorによる指定スロット分割
この課題はSkeletonRenderSeparatorを用いることで対応が可能です。 SkeletonRenderSeparatorを用いると、Spineで制作を行った際のスロット名で分割を行うことができます。
コードは次のようになります。
void SetSeparator()
{
// 指定スロットを境にキャラクターのSpineを分割
_characterSkeletonAnimation.FindAndApplySeparatorSlots((slotName) => slotName == "split_have");
_characterSkeletonAnimation.FindAndApplySeparatorSlots((slotName) => slotName == "split_back", false);
SkeletonRenderSeparator.AddToSkeletonRenderer(_characterSkeletonAnimation);
// 指定スロットを境にキャラクターのSpineを分割
_itemSkeletonAnimation.FindAndApplySeparatorSlots((slotName) => slotName == "split_have");
_itemSkeletonAnimation.FindAndApplySeparatorSlots((slotName) => slotName == "split_back", false);
SkeletonRenderSeparator.AddToSkeletonRenderer(_itemSkeletonAnimation, baseSortingOrder: 1);
}
}
Spineで制作を行う際に、分割を行いたい箇所のスロットを用意しておきます。
たとえば今回のアイテムでは、split_have
のスロットより上の部分は前髪などよりも前に突き出して表示をする部分、
split_have
とsplit_back
の間の部分は抱えこむようにして表示をする部分、
split_back
より下の部分は身体の後ろに表示をする部分、などのように分けられています。
この処理により互い違いに重ねられるように現状のSpineが分割されます。





また、分割前のHierarchy構造は次のようになっており、

分割後のHierarchy構造は次のようになります。

Hierarchyを見ても、元々キャラクターとアイテムで1つずつだったSpineのオブジェクトが複数に分割されていることが分かると思います。
SkeletonRenderSeparatorで分割を行った際には、分割されたそれぞれのオブジェクトにOrderInLayerの値が設定されます。
OrderInLayerの値はデフォルトでは0から順番に5ずつ振られていきます。
今回キャラクターではsplit_have
よりも上の前髪などの部分には10、split_have
とsplit_back
の間の身体などの部分には5、split_back
より下の影の部分には0が設定されます。
また、アイテムは引数でbaseSortingOrderで1を渡しているので、OrderInLayerの値はデフォルトでは0からではなく1から順番に5ずつ振られていきます。
そのため、split_have
とsplit_back
の間の浮き輪の手前の部分には6、split_back
より下の浮き輪の奥の部分には1が設定されます。
これにより、split_have
とsplit_back
の間の身体と浮き輪では浮き輪が手前に表示され、split_back
より下の影と浮き輪でも浮き輪が手前に表示されるようになります。
また今回は無いですが、前髪などよりも前に突き出して表示をしたいアイテムはsplit_have
よりも上に作成すれば、前髪よりも手前に表示されるようになります。
そして分割されたSpineを重ねて表示をすれば完成となります。

アイテムまとめ
アイテムとキャラクターのSpineを分けました。それによりデータ量が削減され、運用的にも楽になりました。
また、Spineの間にSpineを挟み込めるようにしました。具体的にはキャラクターのSpineにアイテムのSpineを挟み込めるようにしました。これはSkeletonRenderSeparatorを用いることで実現可能です。
モーション合成
口パク
突然ですが、任意のタイミングでキャラクターに口パクをさせたいとします。 口パクもモーションで実現します。ただし任意のタイミングで口パクをさせようと思うと、通常モーションの中に口パクをそのまま入れることはできません。
この課題はモーションを合成することによって解決することができます。 通常モーションと口パクのモーションを別で用意し、合成することで実現ができます。
モーションはトラックという単位ごとに再生を行うことが可能です。 そして異なるトラックで流したモーションは合成されます。
コードは次のようになります。
// 指定トラックに指定モーションが定義されていたら、指定トラックでモーションを再生
TrackEntry PlaySafeTrackMotion(SkeletonAnimation skeletonAnimation, string motionName, int trackIndex, bool isLoop)
{
var anim = skeletonAnimation.AnimationState.Data.SkeletonData.FindAnimation(motionName);
// モーションがなければ何もしない
if (anim == null) return null;
return skeleton.AnimationState.SetAnimation(trackIndex, anim, isLoop);
}
たとえば、待機モーションと口パクモーションを合成することで、


待機モーション中に口パクをさせることができます。

身長補正
アイプラには様々な身長のキャラクターがいます。 身長ごとにモーションを制作するのは大変なので、共通で使えるモーションはキャラクターを跨いで使いまわしたいです。
参考に一番身長の低いすずと、一番身長の高い瑠依と、基準キャラクターとしている中間の身長の優を並べてみます。



微妙に身長が異なることが分かるでしょうか。
身長補正は身長補正用のモーションを合成することによって実現します。 このモーションはボーン構造に補正を掛けるための1フレームのモーションとなります。 身長補正用のモーションは全てのキャラクターで同じモーションを利用し、重み付けとしてAlphaの値を設定することで補正量を指定します。 基準キャラクターを0とし、一番身長の低いキャラクターを-1としてモーションを制作します。

その際のコードは次のようになります。
// 身長差補正値を設定
void SetHeightOffset(float heightOffset)
{
// 補正用トラック(configトラック)を取得し、Alphaに補正値を設定
var track = skeletonAnimation.AnimationState.GetCurrent(configTrackIndex);
if (track == null) return;
track.Alpha = heightOffset;
}
また、この処理はキャラクターとアイテムの両方に行います。
そして、待機モーションと身長補正用のモーション(Alpha:-1)を合成することで、


身長補正が適用された待機モーションを再生することができます。

左が身長補正の入っていないすずで、右が身長補正の入っているすずです。右のすずの方が少しだけ身長が低いことが分かりますでしょうか。


また身長の異なるキャラクター同士でハイタッチなどを行えるように、身長補正を入れても手の位置は変わらないようにしています。

キャラクターのモーショントラック
現在は次の表で示す6つのモーショントラックを定義しています。
トラック | モーション名 | 内容 |
---|---|---|
1 | config | 身長補正に利用 |
2 | loop_long | (オプション)特殊な衣装用のオーバーライドモーション(長尺) |
3 | loop-short | (オプション)特殊な衣装用のオーバーライドモーション(短尺) |
4 | 指定モーション名 | キャラ固有モーション、衣装固有モーションなど |
5 | 口パクモーション名 | loop_mouth_positive、loop_mouth_normal、loop_mouth_negative |
6 | 姿勢固定モーション名 | 現状の仕様ではまだ必要なさそう |
モーションを合成する際は、値の小さいトラックから順番に合成されます。
モーション合成まとめ
モーションは複数に分けて合成することが可能です。 それにより口パク、身長補正などを実現しました。またAlphaではモーションの影響度を指定できます。 そして身長補正を別で用意することでモーションの共通化を行いました。
おわりに
キャラクターにアイテムを持たせるために行った工夫や、モーションを効率的に制作するために行ったモーション合成の方法の紹介を行いました。
最終回となる次回では『バリエーションの実現/データ分割編』として、様々なバリエーションを実現するために行った工夫と、効率的な制作や運用を行うために行ったデータ分割の工夫の紹介を行います。