蒙版前处理方法(Web)

最終更新: 2024年1月18日

在Cubism 5 SDK及更高版本中,部分类名等已变更。

有关详细信息,请参考CHANGELOG

在Live2D Cubism SDK for Web中,为了保持智能手机等的绘制速度,在模型绘制处理开始时,采用了为一个蒙版缓冲区绘制所有蒙版形状的“预处理方法”。

在原理绘制方法中,每次绘制需要蒙版的Drawable时,都会绘制蒙版形状(见图)。
使用这种方法,可以表达一个高清的蒙版,每次Drawable需要蒙版时,都会发生切换渲染目标和清除缓冲区等相对高成本的处理。
因此,它可能会导致智能手机等上的绘图速度降低。

但是,仅仅提前准备好蒙版需要多个蒙版缓冲区,这会给内存记忆带来压力。
为了解决这个问题,可以对一个蒙版缓冲区进行以下处理,在将其视为正在使用多个蒙版缓冲区的同时,减少内存记忆压力。

蒙版集成

由于所有的蒙版都是预先生成的,因此接受相同蒙版指定的Drawable可以通过使用相同蒙版图片以减少生成的张数。

这个处理在CubismRenderer_WebGL.initialize函数调用中通过CubismClippingManager_WebGL.initialize函数完成。

public initialize(model: CubismModel, drawableCount: number, drawableMasks: Int32Array[], drawableMaskCounts: Int32Array): void
{
    // 注册所有使用剪贴蒙版的绘制物件
    // 剪贴蒙版通常仅限于几个
    for(let i: number = 0; i < drawableCount; i++)
    {
        if(drawableMaskCounts[i] <= 0)
        {
            // 没有使用剪贴蒙版的图形网格(大多数情况下不使用)
            this._clippingContextListForDraw.pushBack(null);
            continue;
        }

        // 检查是否与已有的ClipContext相同
        let clippingContext: CubismClippingContext =
            this.findSameClip(drawableMasks[i], drawableMaskCounts[i]);
        if(clippingContext == null)
        {
            // 如果不存在相同的蒙版则生成
            clippingContext = new CubismClippingContext(this, drawableMasks[i],
                drawableMaskCounts[i]);
            this._clippingContextListForMask.pushBack(clippingContext);
        }

        clippingContext.addClippedDrawable(i);

        this._clippingContextListForDraw.pushBack(clippingContext);
    }
}

使用多个蒙版纹理

Cubism SDK for Web R6或更高版本中,您可以任意使用多个蒙版纹理。
因此,即使给模型设置了超过R5之前存在的36个蒙版上限的蒙版,也能够在SDK上正常显示。

但是,如果使用两个或多个蒙版纹理,则可以为一个蒙版纹理生成的蒙版上限数量为32个。
(仅使用一个蒙版时的蒙版上限数量为36个。相关详情会在之后说明。)
如果使用两个蒙版纹理,则可以使用的蒙版上限数量为32 * 2 = 64个。

使用多个蒙版纹理的设置如下。

let maskBufferCount = 2;

this._renderer = new CubismRenderer_WebGL();
this._renderer.initialize(this._model, maskBufferCount);

如果没有传递给CubismRenderer_WebGL.initialize()的第二参数,则只会生成并使用一个蒙版纹理。

this._renderer.initialize(this._model);

按颜色信息分开

蒙版缓冲区是一个RGBA视频数组,与通常的纹理缓冲区等一样。
通常的蒙版处理仅使用此A通道来应用蒙版,而不使用RGB通道。
因此,通过在RGBA中具有单独的蒙版数据,一个蒙版缓冲区可以作为四个蒙版缓冲区处理。

分割分开

当4张蒙版图片不够用时,通过2等份、4等份和9等份处理蒙版缓冲区来增加蒙版数量。
由于还存在按颜色信息分割的情况,因此最多可以保存4 × 9、共36个不同蒙版。

此外,为防止蒙版图片被压扁,请使用应用蒙版的所有Drawable矩形绘制蒙版。
因此,需要生成范围、蒙版、使用蒙版生成矩阵。

使用多个蒙版纹理时,分割方法与仅使用一个蒙版纹理时相同。
但是,当使用多个蒙版纹理时,每个蒙版纹理的蒙版分配会尽可能平均分配,因此即使使用相同的模型也可以提高绘制质量。(如果增加蒙版纹理,处理成本会相应增加。)
例如,拥有32个蒙版的模型通常使用一个蒙版纹理绘制32个蒙版,但使用2个蒙版纹理时的蒙版分配为“每个16个”。

检查矩形

在蒙版生成的第一步中,对于每个蒙版,检查完全覆盖蒙版的矩形。

public calcClippedDrawTotalBounds(model: CubismModel, clippingContext: CubismClippingContext): void
{
    // 被剪贴蒙版(要添加蒙版的绘制物件)的全体矩形
    let clippedDrawTotalMinX: number = Number.MAX_VALUE;
    let clippedDrawTotalMinY: number = Number.MAX_VALUE;
    let clippedDrawTotalMaxX: number = Number.MIN_VALUE;
    let clippedDrawTotalMaxY: number = Number.MIN_VALUE;

    // 判断该蒙版是否真的需要
    // 如果至少可以使用一个使用该剪贴的“绘制物件”,则需要生成蒙版
    const clippedDrawCount: number = clippingContext._clippedDrawableIndexList.length;

    for(let clippedDrawableIndex: number = 0; clippedDrawableIndex < clippedDrawCount;
        clippedDrawableIndex++)
    {
        // 查找使用蒙版的绘制物件的绘制矩形
        const drawableIndex: number =
            clippingContext._clippedDrawableIndexList[clippedDrawableIndex];

        const drawableVertexCount: number = model.getDrawableVertexCount(drawableIndex);
        let drawableVertexes: Float32Array = model.getDrawableVertices(drawableIndex);

        let minX: number = Number.MAX_VALUE;
        let minY: number = Number.MAX_VALUE;
        let maxX: number = Number.MIN_VALUE;
        let maxY: number = Number.MIN_VALUE;

        let loop: number = drawableVertexCount * Constant.vertexStep;
        for(let pi: number = Constant.vertexOffset; pi < loop; pi += Constant.vertexStep)
        {
            let x: number = drawableVertexes[pi];
            let y: number = drawableVertexes[pi + 1];

            if(x < minX)
            {
                minX = x;
            }
            if(x > maxX)
            {
                maxX = x;
            }
            if(y < minY)
            {
                minY = y;
            }
            if(y > maxY)
            {
                maxY = y;
            }
        }

        // 由于无法获得任何有效点,因此跳过
        if(minX == Number.MAX_VALUE)
        {
            continue;
        }

        // 应用到全体矩形
        if(minX < clippedDrawTotalMinX)
        {
            clippedDrawTotalMinX = minX;
        }
        if(minY < clippedDrawTotalMinY)
        {
            clippedDrawTotalMinY = minY;
        }
        if(maxX > clippedDrawTotalMaxX)
        {
            clippedDrawTotalMaxX = maxX;
        }
        if(maxY > clippedDrawTotalMaxY)
        {
            clippedDrawTotalMaxY = maxY;
        }

        if(clippedDrawTotalMinX == Number.MAX_VALUE)
        {
            clippingContext._allClippedDrawRect.x = 0.0;
            clippingContext._allClippedDrawRect.y = 0.0;
            clippingContext._allClippedDrawRect.width = 0.0;
            clippingContext._allClippedDrawRect.height = 0.0;
            clippingContext._isUsing = false;
        }
        else
        {
            clippingContext._isUsing = true;
            let w: number = clippedDrawTotalMaxX - clippedDrawTotalMinX;
            let h: number = clippedDrawTotalMaxY - clippedDrawTotalMinY;
            clippingContext._allClippedDrawRect.x = clippedDrawTotalMinX;
            clippingContext._allClippedDrawRect.y = clippedDrawTotalMinY;
            clippingContext._allClippedDrawRect.width = w;
            clippingContext._allClippedDrawRect.height = h;
        }
    }
}

颜色分开、分割分开的编排决定

确定每个蒙版所属的蒙版缓冲区的颜色通道和分割位置。

public setupLayoutBounds(usingClipCount: number): void
{
    // 尽可能使用一个RenderTexture来编排蒙版
    // 如果蒙版组数为4个或更少,则为RGBA各通道置入一个蒙版,如果为5以上、6个以下,则将RGBA置入为2,2,1,1

    // 按顺序使用RGBA
    let div: number = usingClipCount / ColorChannelCount; // 一个通道中要置入的基本蒙版
    let mod: number = usingClipCount % ColorChannelCount; // 剩余,逐一分配到该编号的通道

    // 小数点后舍去
    div = ~~div;
    mod = ~~mod;

    // 准备RGBA各通道(0:R, 1:G, 2:B, 3:A)
    let curClipIndex: number = 0; // 按顺序设置

    for(let channelIndex: number = 0; channelIndex < ColorChannelCount; channelIndex++)
    {
        // 在该通道上编排的数量
        let layoutCount: number = div + (channelIndex < mod ? 1 : 0);

        // 决定如何分割
        if(layoutCount == 0)
        {
            // 什么都不做
        }
        else if(layoutCount == 1)
        {
            // 直接使用所有内容
            let clipContext: CubismClippingContext =
                this._clippingContextListForMask.at(curClipIndex++);
            clipContext._layoutChannelIndex = channelIndex;
            clipContext._layoutBounds.x = 0.0;
            clipContext._layoutBounds.y = 0.0;
            clipContext._layoutBounds.width = 1.0;
            clipContext._layoutBounds.height = 1.0;
        }
        else if(layoutCount == 2)
        {
            for(let i: number = 0; i < layoutCount; i++)
            {
                let xpos: number = i % 2;

                // 小数点后舍去
                xpos = ~~xpos;

                let cc: CubismClippingContext =
                    this._clippingContextListForMask.at(curClipIndex++);
                cc._layoutChannelIndex = channelIndex;

                cc._layoutBounds.x = xpos * 0.5;
                cc._layoutBounds.y = 0.0;
                cc._layoutBounds.width = 0.5;
                cc._layoutBounds.height = 1.0;
                // 2等份并使用UV
            }
        }
        else if(layoutCount <= 4)
        {
            // 4等份并使用
            for(let i: number = 0; i < layoutCount; i++)
            {
                let xpos: number = i % 2;
                let ypos: number = i / 2;

                // 小数点后舍去
                xpos = ~~xpos;
                ypos = ~~ypos;

                let cc = this._clippingContextListForMask.at(curClipIndex++);
                cc._layoutChannelIndex = channelIndex;

                cc._layoutBounds.x = xpos * 0.5;
                cc._layoutBounds.y = ypos * 0.5;
                cc._layoutBounds.width = 0.5;
                cc._layoutBounds.height = 0.5;
            }
        }
        else if(layoutCount <= 9)
        {
            // 9等份并使用
            for(let i: number = 0; i < layoutCount; i++)
            {
                let xpos = i % 3;
                let ypos = i / 3;

                // 小数点后舍去
                xpos = ~~xpos;
                ypos = ~~ypos;

                let cc: CubismClippingContext =
                    this._clippingContextListForMask.at(curClipIndex++);
                cc._layoutChannelIndex = channelIndex;

                cc._layoutBounds.x = xpos / 3.0;
                cc._layoutBounds.y = ypos / 3.0;
                cc._layoutBounds.width = 1.0 / 3.0;
                cc._layoutBounds.height = 1.0 / 3.0;
            }
        }
        else
        {
            CubismLogError("not supported mask count : {0}", layoutCount);
        }
    }
}

蒙版绘制,使用蒙版生成矩阵

根据绘制前检查的矩形范围及其所属位置,准备用于蒙版生成和蒙版使用的转变矩阵。

    // --- 实际绘制一个蒙版 ---
    let clipContext: CubismClippingContext = this._clippingContextListForMask.at(clipIndex);
    let allClipedDrawRect: csmRect = clipContext._allClippedDrawRect;   // 使用该蒙版的所有绘制物件逻辑座标上的矩形边界
    let layoutBoundsOnTex01: csmRect = clipContext._layoutBounds; // 将蒙版放入其中

    // 对模型座标上的矩形添加适当的余量并使用
    const MARGIN: number = 0.05;
    this._tmpBoundsOnModel.setRect(allClipedDrawRect);
    this._tmpBoundsOnModel.expand(allClipedDrawRect.width * MARGIN,
                                  allClipedDrawRect.height * MARGIN);
    // ########## 本来,不使用全体分配区域的情况下,所需最小尺寸为宜

    // 求用于着色器的公式。不考虑旋转时,如下所示
    // movePeriod' = movePeriod * scaleX + offX                  [[ movePeriod' = (movePeriod - tmpBoundsOnModel.movePeriod)*scale + layoutBoundsOnTex01.movePeriod ]]
    const scaleX: number = layoutBoundsOnTex01.width / this._tmpBoundsOnModel.width;
    const scaleY: number = layoutBoundsOnTex01.height / this._tmpBoundsOnModel.height;

    // 找到生成蒙版时要使用的矩阵
    {
        // 求传递给着色器的矩阵 <<<<<<<<<<<<<<<<<<<<<<<< 需要优化(逆序计算可以很简单)
        this._tmpMatrix.loadIdentity();
        {
            // 将layout0 .. 1转变为−1 .. 1
            this._tmpMatrix.translateRelative(-1.0, -1.0);
            this._tmpMatrix.scaleRelative(2.0, 2.0);
        }
        {
            // view to layout0..1
            this._tmpMatrix.translateRelative(layoutBoundsOnTex01.x, layoutBoundsOnTex01.y);
            this._tmpMatrix.scaleRelative(scaleX, scaleY);  // new = [translate][scale]
            this._tmpMatrix.translateRelative(-this._tmpBoundsOnModel.x,
                                              -this._tmpBoundsOnModel.y);
            // new = [translate][scale][translate]
        }
        // tmpMatrixForMask为计算结果
        this._tmpMatrixForMask.setMatrix(this._tmpMatrix.getArray());
    }

    // --------- 计算draw时的mask参考矩阵
    {
        // 求传递给着色器的矩阵 <<<<<<<<<<<<<<<<<<<<<<<< 需要优化(逆序计算可以很简单)
        this._tmpMatrix.loadIdentity();
        {
            this._tmpMatrix.translateRelative(layoutBoundsOnTex01.x, layoutBoundsOnTex01.y);
            this._tmpMatrix.scaleRelative(scaleX, scaleY);  // new = [translate][scale]
            this._tmpMatrix.translateRelative(-this._tmpBoundsOnModel.x,
                                              -this._tmpBoundsOnModel.y);
            // new = [translate][scale][translate]
        }
        this._tmpMatrixForDraw.setMatrix(this._tmpMatrix.getArray());
    }
    clipContext._matrixForMask.setMatrix(this._tmpMatrixForMask.getArray());
    clipContext._matrixForDraw.setMatrix(this._tmpMatrixForDraw.getArray());

蒙版缓冲区的动态大小变更

CubismRenderer_WebGL提供了一个API,以在运行时变更蒙版缓冲区的大小。
目前,蒙版缓冲区的大小设置为256 * 256(像素)作为初始值,但如果要将蒙版生成区域切割成9张,则将在85 * 85(像素)的矩形区域内绘制的蒙版形状进一步放大,作为剪贴区域使用。

因此,剪贴结果的边缘会模糊或渗色。
作为解决方案,我们提供了一个 API,可以在程序执行时变更蒙版缓冲区的大小。

例如,通过将蒙版缓冲区的大小从256 * 256变更为1024 * 1024,如果将蒙版生成区域切成9张,则可以在341 * 341的矩形区域中绘制蒙版形状,因此,您还可以放大并将其用作剪贴区域,以消除剪贴结果边缘的模糊和渗色。

* 扩大蒙版缓冲区的大小 ⇒ 如果要处理的像素增加,速度会变慢,但绘制结果会很漂亮。
* 缩小蒙版缓冲区的大小 ⇒ 由于要处理的像素减少了,所以速度会更快,但是绘制结果会很脏。

public setClippingMaskBufferSize(size: number)
{
    // 放弃/重新创建副本以变更FrameBuffer的大小
    this._clippingManager.release();
    this._clippingManager = void 0;
    this._clippingManager = null;

    this._clippingManager = new CubismClippingManager_WebGL();

    this._clippingManager.setClippingMaskBufferSize(size);

    this._clippingManager.initialize(
        this.getModel(),
        this.getModel().getDrawableCount(),
        this.getModel().getDrawableMasks(),
        this.getModel().getDrawableMaskCounts()
    );
}

将蒙版处理切换为高清方法

每次绘制时生成蒙版的方法会影响低规格终端的性能。

但是,当画面质量比运行时的性能更重要时(例如最终输出为视频),此方法更适合。

使用Cubism SDK for Web R6或更高版本,可以将蒙版处理切换为高清方法。
要切换到高清方法,请将true传递给以下API。

CubismRenderer.useHighPrecisionMask()
请问这篇文章对您有帮助吗?
关于本报道,敬请提出您的意见及要求。