マスクの前処理方式(Java)
最終更新: 2023年1月26日
Live2D Cubism SDK for Javaでは、スマートフォンなどで描画速度を維持するために、
モデル描画処理の最初に一枚のマスクバッファに対してすべてのマスク形状を描画する『前処理方式』を採用しています。
原則的な描画方法では、マスクを必要とするDrawableを描画するタイミングで、その都度マスク形状を描画します(図参照)。
この方法では、Drawableがマスクを必要とするたびにレンダーターゲットの切り替え・バッファのクリアなど比較的高コストな処理が発生することになります。
そのため、スマートフォンなどで描画速度が低下する原因になる場合があります。
しかし、前もってマスクを用意するだけではマスクバッファが複数枚必要になり、メモリを圧迫することになります。
この点を解決するため、一枚のマスクバッファに対して以下の処理を行うことで、まるで複数枚のマスクバッファを利用しているかのように扱いつつ、メモリの圧迫を抑えることができます。
マスクの統合
前もってすべてのマスクを生成するため、同じマスク指定を受けているDrawableは同一のマスク画像を使うことで生成する枚数を抑えられます。
この処理はCubismRendererAndroid.initialize関数の呼び出しの中でCubismClippingManagerAndroid.initialize関数によって行われます。
void initialize(
int drawableCount,
final int[][] drawableMasks,
final int[] drawableMaskCounts
) {
// クリッピングマスクを使う描画オブジェクトを全て登録する
// クリッピングマスクは、通常数個程度に限定して使うものとする
for (int i = 0; i < drawableCount; i++) {
if (drawableMaskCounts[i] <= 0) {
// クリッピングマスクが使用されていないアートメッシュ(多くの場合使用しない)
_clippingContextListForDraw.add(null);
continue;
}
// 既にあるClipContextと同じかチェックする
CubismClippingContext cc = findSameClip(drawableMasks[i], drawableMaskCounts[i]);
if (cc == null) {
// 同一のマスクが存在していない場合は生成する
cc = new CubismClippingContext(this, drawableMasks[i], drawableMaskCounts[i]);
_clippingContextListForMask.add(cc);
}
cc.addClippedDrawable(i);
_clippingContextListForDraw.add(cc);
}
}
複数枚のマスク用テクスチャの利用
Cubism SDK for Java R1 beta1以降では、マスク用テクスチャを任意で複数枚使用することができます。
そのため、alpha1までに存在したマスクの使用上限数である36枚を超過したマスクをモデルに設定しても、SDK上で正常に表示させることができるようになります。
ただし、マスク用テクスチャを2枚以上使用した場合、マスク用テクスチャ1枚に対して生成されるマスクの上限数は32枚までとなります。
(1枚のみ使用する場合のマスクの上限数は36枚です。こちらの詳細は後述します)
仮にマスク用テクスチャを2枚使用した場合、使用できるマスクの上限数は32 * 2の64枚になります。
マスク用テクスチャを複数枚使用するための設定は以下のようになります。
int maskBufferCount = 2; CubismRenderer renderer = CubismRendererAndroid.create(); renderer.initialize(cubismModel, maskBufferCount);
CubismRendererAndroid.initialize() の第2引数に何も渡さない場合、マスク用テクスチャは1枚のみ生成、使用されます。
renderer.initialize(cubismModel);
色情報での分離
マスクバッファは実態として通常のテクスチャバッファなどと同じようにRGBAの映像用配列です。
通常のマスク処理ではこのAチャンネルのみを使用してマスクを適用しますがRGBのチャンネルは使用しません。
そこでRGBAで別々のマスクデータを持つことによって一枚のマスクバッファを4枚のマスクバッファとして取り扱えるようになります。
分割分離
マスク画像が4枚では足りなくなった時、マスクバッファを2分割、4分割、9分割で取り扱うことによってマスクの枚数を増やします。
色情報での分割もあるので4 x 9 の36枚まで違うマスクを保持できるようになっています。
またマスク画像がつぶれるのを防ぐため、マスクの適用を受けるすべてのDrawableの矩形でマスクを描画します。
このため範囲の生成とマスク生成、マスク使用でのマトリックスの生成が必要になります。
マスク用テクスチャを複数枚利用する場合も分割方式はマスク用テクスチャ1枚のみを利用した場合と同様となります。
ただし、マスク用テクスチャを複数枚利用する場合にはマスク用テクスチャ1枚あたりへのマスクの割り当てを可能な限り等分するようになるため、同じモデルでも描画の品質が上がります。(マスク用テクスチャを増やすと、その分コストはかかります)
例えば、マスクが32枚のモデルの場合には通常はマスク用テクスチャ1枚で32枚のマスクを描画しようとしますが、マスク用テクスチャを2枚使った場合のマスクの割り当ては「1枚あたり16枚」となります。
矩形の確認
マスク生成の初めのステップで、マスクごとにマスク適用先すべてが収まる矩形を確認します。
private void calcClippedDrawTotalBounds(CubismModel model, CubismClippingContext clippingContext) {
// 被クリッピングマスク(マスクされる描画オブジェクト)の全体の矩形
float clippedDrawTotalMinX = Float.MAX_VALUE;
float clippedDrawTotalMinY = Float.MAX_VALUE;
float clippedDrawTotalMaxX = -Float.MAX_VALUE;
float clippedDrawTotalMaxY = -Float.MAX_VALUE;
// このマスクが実際に必要か判定する
// このクリッピングを利用する「描画オブジェクト」がひとつでも使用可能であればマスクを生成する必要がある
final int clippedDrawCount = clippingContext.getClippedDrawableIndexList().size();
for (int clippedDrawableIndex = 0; clippedDrawableIndex < clippedDrawCount; clippedDrawableIndex++) {
// マスクを使用する描画オブジェクトの描画される矩形を求める
final int drawableIndex = clippingContext.getClippedDrawableIndexList().get(clippedDrawableIndex);
final int drawableVertexCount = model.getDrawableVertexCount(drawableIndex);
float[] drawableVertices = model.getDrawableVertices(drawableIndex);
float minX = Float.MAX_VALUE;
float minY = Float.MAX_VALUE;
float maxX = -Float.MAX_VALUE;
float maxY = -Float.MAX_VALUE;
int loop = drawableVertexCount * VERTEX_STEP;
for (int pi = VERTEX_OFFSET; pi < loop; pi += VERTEX_STEP) {
float x = drawableVertices[pi];
float y = drawableVertices[pi + 1];
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
// 有効な点がひとつも取れなかったのでスキップする
if (minX == Float.MAX_VALUE) {
continue;
}
// 全体の矩形に反映
if (minX < clippedDrawTotalMinX) clippedDrawTotalMinX = minX;
if (maxX > clippedDrawTotalMaxX) clippedDrawTotalMaxX = maxX;
if (minY < clippedDrawTotalMinY) clippedDrawTotalMinY = minY;
if (maxY > clippedDrawTotalMaxY) clippedDrawTotalMaxY = maxY;
}
if (clippedDrawTotalMinX == Float.MAX_VALUE) {
clippingContext.setUsing(false);
CubismRectangle clippedDrawRect = clippingContext.getAllClippedDrawRect();
clippedDrawRect.setX(0.0f);
clippedDrawRect.setY(0.0f);
clippedDrawRect.setWidth(0.0f);
clippedDrawRect.setHeight(0.0f);
} else {
clippingContext.setUsing(true);
float w = clippedDrawTotalMaxX - clippedDrawTotalMinX;
float h = clippedDrawTotalMaxY - clippedDrawTotalMinY;
CubismRectangle clippedDrawRect = clippingContext.getAllClippedDrawRect();
clippedDrawRect.setX(clippedDrawTotalMinX);
clippedDrawRect.setY(clippedDrawTotalMinY);
clippedDrawRect.setWidth(w);
clippedDrawRect.setHeight(h);
}
}
色分離、分割分離を受けたレイアウト決定
マスクごとに所属するマスクバッファの色チャンネル、分割位置を定めます。
private void setupLayoutBounds(int usingClipCount) {
// ひとつのRenderTextureを極力いっぱいに使ってマスクをレイアウトする
// マスクグループの数が4以下ならRGBA各チャンネルに1つずつマスクを配置し、5以上6以下ならRGBAを2,2,1,1と配置する
// RGBAを順番に使っていく
final int div = usingClipCount / COLOR_CHANNEL_COUNT; // 1チャンネルの配置する基本のマスク個数
final int mod = usingClipCount % COLOR_CHANNEL_COUNT; // 余り、この番号のチャンネルまでに1つずつ配分する
// RGBAそれぞれのチャンネルを用意していく(0:R, 1:G, 2:B, 3:A)
int curClipIndex = 0; // 順番に設定していく
for (int channelNo = 0; channelNo < COLOR_CHANNEL_COUNT; channelNo++) {
// このチャンネルにレイアウトする数
final int layoutCount = div + (channelNo < mod ? 1 : 0);
// 分割方法を決定する
if (layoutCount == 0) {
//何もしない
} else if (layoutCount == 1) {
// 全てをそのまま使う
CubismClippingContext cc = _clippingContextListForMask.get(curClipIndex++);
cc.setLayoutChannelNo(channelNo);
CubismRectangle bounds = cc.getLayoutBounds();
bounds.setX(0.0f);
bounds.setY(0.0f);
bounds.setWidth(1.0f);
bounds.setHeight(1.0f);
} else if (layoutCount == 2) {
for (int i = 0; i < layoutCount; i++) {
final int xpos = i % 2;
CubismClippingContext cc = _clippingContextListForMask.get(curClipIndex++);
cc.setLayoutChannelNo(channelNo);
CubismRectangle bounds = cc.getLayoutBounds();
bounds.setX(xpos * 0.5f);
bounds.setY(0.0f);
bounds.setWidth(0.5f);
bounds.setHeight(1.0f);
// UVを2つに分解して使う
}
} else if (layoutCount <= 4) {
// 4分割して使う
for (int i = 0; i < layoutCount; i++) {
final int xpos = i % 2;
final int ypos = i / 2;
CubismClippingContext cc = _clippingContextListForMask.get(curClipIndex++);
cc.setLayoutChannelNo(channelNo);
CubismRectangle bounds = cc.getLayoutBounds();
bounds.setX(xpos * 0.5f);
bounds.setY(ypos * 0.5f);
bounds.setWidth(0.5f);
bounds.setHeight(0.5f);
}
} else if (layoutCount <= 9) {
// 9分割して使う
for (int i = 0; i < layoutCount; i++) {
final int xpos = i % 3;
final int ypos = i / 3;
CubismClippingContext cc = _clippingContextListForMask.get(curClipIndex++);
cc.setLayoutChannelNo(channelNo);
CubismRectangle bounds = cc.getLayoutBounds();
bounds.setX(xpos / 3.0f);
bounds.setY(ypos / 3.0f);
bounds.setWidth(1.0f / 3.0f);
bounds.setHeight(1.0f / 3.0f);
}
} else {
cubismLogError("not supported mask count : " + layoutCount);
// デベロッパーモードならプログラムを停止させる。
assert false;
// もし実行を継続するなら、setupShaderProgram() メソッドはオーバーアクセスを引き起こす。そのため仕方なく適当に入れておく
// もちろん描画結果はろくなことにならない
for (int i = 0; i < layoutCount; i++) {
CubismClippingContext cc = _clippingContextListForMask.get(curClipIndex++);
cc.setLayoutChannelNo(0);
CubismRectangle bounds = cc.getLayoutBounds();
bounds.setX(0.0f);
bounds.setY(0.0f);
bounds.setWidth(1.0f);
bounds.setHeight(1.0f);
}
}
}
}
マスク描画、マスク使用のマトリックス生成
描画前に調べた矩形範囲と所属場所に基づいてマスク生成用、マスク使用用の変換マトリックスを用意します。
/*省略*/
// --- 実際に1つのマスクを描く ---
for (CubismClippingContext clipContext : _clippingContextListForMask) {
CubismRectangle allClippedDrawRect = clipContext.getAllClippedDrawRect();
CubismRectangle layoutBoundsOnTex01 = clipContext.getLayoutBounds();
// モデル座標上の矩形を、適宜マージンを付けて使う
CubismRectangle tmpBoundsOnModel = CubismRectangle.create(allClippedDrawRect);
final float margin = 0.05f;
tmpBoundsOnModel.expand(
allClippedDrawRect.getWidth() * margin,
allClippedDrawRect.getHeight() * margin
);
// ######## 本来は割り当てられた領域の全体を使わず必要最低限のサイズがよい
// シェーダー用の計算式を求める。回転を考慮しない場合は以下の通り
// movePeriod' = movePeriod * scaleX + offX [[movePeriod' = (movePeriod' - tmpBoundsOnModel.movePeriod) * scale + layoutBoundsOnT
final float scaleX = layoutBoundsOnTex01.getWidth() / tmpBoundsOnModel.getWidth();
final float scaleY = layoutBoundsOnTex01.getHeight() / tmpBoundsOnModel.getHeight();
// マスク生成時に使う行列を求める
CubismMatrix44 tmpMatrix = CubismMatrix44.create();
// シェーダに渡す行列を求める <<<<<<<<<< 要最適化(逆順に計算すればシンプルにできる)
// Layout0..1 を -1..1に変換
tmpMatrix.translateRelative(-1.0f, -1.0f);
tmpMatrix.scaleRelative(2.0f, 2.0f);
// view to Layout0..1
tmpMatrix.translateRelative(
layoutBoundsOnTex01.getX(),
layoutBoundsOnTex01.getY()
);
tmpMatrix.scaleRelative(scaleX, scaleY);
tmpMatrix.translateRelative(
-tmpBoundsOnModel.getX(),
-tmpBoundsOnModel.getY()
);
clipContext.getMatrixForDraw().setMatrix(tmpMatrix);
/*省略*/
}
マスクバッファの動的なサイズ変更

OpenGL ES 2.0レンダラーには、実行時にマスクバッファのサイズを変更するAPIを用意してあります。
現状、マスクバッファのサイズは初期値として256*256(ピクセル)を設定していますが、マスク生成領域を9枚に切るような場合、85*85(ピクセル)の矩形領域に描画したマスク形状を、更に拡大してクリッピング領域として使用します。
その結果、クリッピング結果のエッジがぼやけたり、滲みのような現象が見られます。
それを解決する方法として、マスクバッファのサイズをプログラム実行時に変更するAPIを用意しています。
例えば、マスクバッファのサイズを256*256 ⇒ 1024*1024とすることで、マスク生成領域を9枚に切るような場合、341*341の矩形領域にマスク形状を描画することができるので、拡大してクリッピング領域として使用しても、クリッピング結果のエッジのぼやけやにじみを解消することができます。
※マスクバッファのサイズを大きくする ⇒ 処理するピクセルが増えると速度は遅くなるが、描画結果は綺麗になる。
※マスクバッファのサイズを小さくする ⇒ 処理するピクセルが減るので速度は速くなるが、描画結果は汚くなる。
public void setClippingMaskBufferSize(final float width, final float height) {
// FrameBufferのサイズを変更するためにインスタンスを再生成する。
_clippingManager = new CubismClippingManagerAndroid();
_clippingManager.setClippingMaskBufferSize(width, height);
CubismModel model = getModel();
_clippingManager.initialize(
model.getDrawableCount(),
model.getDrawableMasks(),
model.getDrawableMaskCounts()
);
}
前処理方式によってパフォーマンスの向上が見込める理由
携帯端末特有の事情として、GPUに対するClear命令やレンダリングターゲット切り替え命令の処理コストが、他の命令よりも高い場合があります。
原則的方式で描画するときには、これら処理コストが高い命令を、マスクが必要になるDrawableの数だけ実行することになります。
しかし、前処理方式の場合では、これらの命令を実行する回数を削減することができるので、スマートフォンなどでのパフォーマンスの向上が見込めます。
実際にその効果を把握するための実験を行っております。
この実験の測定方法および結果については、「(Native)マスクの前処理方式」の「前処理方式によってパフォーマンスの向上が見込める理由」をご覧ください。
マスクの処理を高精細な方式に切り替える
描画の度にマスクを生成する方法だと、低スペック端末ではパフォーマンスに影響があります。
しかし、最終的に動画として出力する場合のように、ランタイムでのパフォーマンスよりも画面の品質の方を重視する場合にはこちらの方式が適しています。
Cubism SDK for Javaでは、マスクの処理を高精細な方式に切り替える事ができます。
高精細な方式に切り替えるには、以下のAPIにtrueを渡します。
CubismRenderer.isUsingHighPrecisionMask()