蒙版前处理方法(Java)
最終更新: 2022年12月8日
在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上正常显示。
但是,如果使用两个或多个蒙版纹理,则可以为一个蒙版纹理生成的蒙版上限数量为32个。
(仅使用一个蒙版时的蒙版上限数量为36个。相关详情会在之后说明。)
如果使用两个蒙版纹理,则可以使用的蒙版上限数量为32 * 2 = 64个。
使用多个蒙版纹理的设置如下。
| 1234 | int maskBufferCount = 2;CubismRenderer renderer = CubismRendererAndroid.create();renderer.initialize(cubismModel, maskBufferCount); |
如果没有传递给CubismRendererAndroid.initialize()的第二参数,则只会生成并使用一个蒙版纹理。
| 1 | renderer.initialize(cubismModel); |
按颜色信息分开
蒙版缓冲区是一个RGBA视频数组,与通常的纹理缓冲区等一样。
通常的蒙版处理仅使用此A通道来应用蒙版,而不使用RGB通道。
因此,通过在RGBA中具有单独的蒙版数据,一个蒙版缓冲区可以作为四个蒙版缓冲区处理。
分割分开
当4张蒙版图片不够用时,通过2等份、4等份和9等份处理蒙版缓冲区来增加蒙版数量。
由于还存在按颜色信息分割的情况,因此最多可以保存4 × 9、共36个不同蒙版。
此外,为防止蒙版图片被压扁,请使用应用蒙版的所有Drawable矩形绘制蒙版。
因此,需要生成范围、蒙版、使用蒙版生成矩阵。
使用多个蒙版纹理时,分割方法与仅使用一个蒙版纹理时相同。
但是,当使用多个蒙版纹理时,每个蒙版纹理的蒙版分配会尽可能平均分配,因此即使使用相同的模型也可以提高绘制质量。(如果增加蒙版纹理,则成本会相应增加)
例如,拥有32个蒙版的模型通常使用一个蒙版纹理绘制32个蒙版,但使用2个蒙版纹理时的蒙版分配为“每个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各通道置入一个蒙版,如果为5以上、6个以下,则将RGBA置入为2,2,1,1
// 按顺序使用RGBA
final int div = usingClipCount / COLOR_CHANNEL_COUNT; // 一个通道中要置入的基本蒙版数
final int mod = usingClipCount % COLOR_CHANNEL_COUNT; // 剩余,逐一分配到该编号的通道
// 准备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);
// 2等份并使用UV
}
} 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);
}
}
}
}
蒙版绘制,使用蒙版生成矩阵
根据绘制前检查的矩形范围及其所属位置,准备用于蒙版生成和蒙版使用的转变矩阵。
/*省略*/
// --- 实际绘制一个蒙版 ---
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中,可以将蒙版处理切换为高清方式。
要切换到高清方法,请将true传递给以下API。
CubismRenderer.isUsingHighPrecisionMask()