cocos creator DragonBones 源码阅读

关注点:

  • 定制引擎
  • 龙骨渲染流程
  • 合批问题
  • 龙骨格式
  • 如何提交渲染

unity

github.com/DragonBones...

运行替换插槽的测试例

可以看到切换插槽是可以参与渲染排序的,unity在这块的确做的非常完善,包括9.ReplaceSkin也是经常用的功能。

应该是dragonBones官方对unity的支持非常到位,毕竟这个仓库是DragonBones官方维护的。

cocos

挂点

先查挂点数据是如何同步的,挂点都是在子节点上

查renderFlow意义不大

从Assembler入手,主体逻辑

js 复制代码
let ArmatureDisplay = cc.Class({
    name: 'dragonBones.ArmatureDisplay',
    extends: RenderComponent,
})
  • extensions\dragonbones\webgl-assembler.js 绑定Assembler
js 复制代码
export default class ArmatureAssembler extends Assembler {
    updateRenderData (comp, batchData) {}
    fillBuffers (comp, renderer) {
        // 组装顶点的核心逻辑
        
        // renderer._meshBuffer == cc.renderer._handle._meshBuffer 是等价的
       _buffer = renderer._meshBuffer;
       
        // Traverse all armature.
        renderer.worldMatDirty++; // 这个worldMatDirty的作用参考扩展
        // 这个函数会填充_buffer
        this.realTimeTraverse(armature, worldMat, 1.0);

        // sync attached node matrix, 同步挂点的逻辑,需要关注下attachUtil的逻辑
        // 主要逻辑就是更新了attachNode的矩阵,并且标记对应的flag
        // 这样当再次渲染到这些node的时候,render_flow会自动更新相关的属性
        comp.attachUtil._syncAttachedNode();
    }
}
Assembler.register(Armature, ArmatureAssembler);
js 复制代码
    _syncAttachedNode () {
        let bone = isCached 
            ? boneInfos[boneNode._boneIndex] 
            : boneNode._bone; // 多了一个_bone属性,在组件初始化的时候会赋值好
    }

dragonBones_ArmatureDisplay._armature 来源

组件初始化流程,在脚本初始化的时候,就会调用_buildArmature函数,是初始化逻辑的一部分

js 复制代码
ctor () {
    this.attachUtil = new AttachUtil();
    this._factory = dragonBones.CCFactory.getInstance(); // 工厂单例
},
_buildArmature(){
    // 解析ske.json,armatureKey采用的是 ske_json_uuid # png_uuid
    // 调用dblib.parseDragonBonesData(ske_json_data, name) 将原始数据解析为 DragonBonesData 实例,并缓存到工厂中。
    // 牵扯到dblib的就暂时不关心了,dblib提供了类似回调的机制,相关的适配逻辑代码在CCFactory.js
    this._armatureKey = this.dragonAsset.init(this._factory, atlasUUID);

    if (this.isAnimationCached()) {
        this._armature = this._armatureCache.getArmatureCache(this.armatureName, this._armatureKey, atlasUUID);
    } 

    if (CC_EDITOR || this._cacheMode === AnimationCacheMode.REALTIME) {
        // 落在这个逻辑里面,需要看下_factory的来源
        this._displayProxy = this._factory.buildArmatureDisplay(this.armatureName, this._armatureKey, "", atlasUUID);
        // 来源就在这里,需要往上看
        this._armature = this._displayProxy._armature; 
    }

    // 将挂点和bones关联起来,其中node._bone就是在这里面赋值的
    this.attachUtil._associateAttachedNode();
}

最核心的逻辑: realTimeTraverse填充渲染数据

既然我们知道最终数据是填充到_buffer里面了,直接倒推逻辑即可

js 复制代码
export default class ArmatureAssembler extends Assembler {
    realTimeTraverse (armature, parentMat, parentOpacity){
     // armature 来自 dragonBones_ArmatureDisplay._armature,它是db的数据类型
      let slots = armature._slots;
        let vbuf, ibuf, uintbuf;
        let material;
        let vertices, indices;
        let slotColor;
        let slot;
        let slotMat;
        let slotMatm;
        let offsetInfo;

        for (let i = 0, l = slots.length; i < l; i++) {
            slot = slots[i]; // 需要看下这个来源
            slotColor = slot._color;

            if (!slot._visible || !slot._displayData) continue;

            if (parentMat) {
                slot._mulMat(slot._worldMatrix, parentMat, slot._matrix);
            } else {
                Mat4.copy(slot._worldMatrix, slot._matrix);
            }

            if (slot.childArmature) {
                this.realTimeTraverse(slot.childArmature, slot._worldMatrix, parentOpacity * slotColor.a / 255);
                continue;
            }

            material = _getSlotMaterial(slot.getTexture(), slot._blendMode);
            if (!material) {
                continue;
            }

            if (_mustFlush || material.getHash() !== _renderer.material.getHash()) {
                _mustFlush = false;
                _renderer._flush();
                _renderer.node = _node;
                _renderer.material = material;
            }

            _handleColor(slotColor, parentOpacity);
            slotMat = slot._worldMatrix;
            slotMatm = slotMat.m;

            vertices = slot._localVertices; // 顶点数据来源
            _vertexCount = vertices.length >> 2;

            indices = slot._indices; // 顶点索引数据来源
            _indexCount = indices.length;
            
            offsetInfo = _buffer.request(_vertexCount, _indexCount); // 申请缓冲区
            _indexOffset = offsetInfo.indiceOffset;  // 申请的缓冲区偏移
            _vfOffset = offsetInfo.byteOffset >> 2;
            _vertexOffset = offsetInfo.vertexOffset; // 申请的缓冲区偏移
            vbuf = _buffer._vData;
            ibuf = _buffer._iData;
            uintbuf = _buffer._uintVData;

            _m00 = slotMatm[0];
            _m04 = slotMatm[4];
            _m12 = slotMatm[12];
            _m01 = slotMatm[1];
            _m05 = slotMatm[5];
            _m13 = slotMatm[13];
            
            // 填充顶点缓冲区
            for (let vi = 0, vl = vertices.length; vi < vl;) {
                // 运行时动态计算bones的xy、uv
                _x = vertices[vi++]; 
                _y = vertices[vi++];

                vbuf[_vfOffset++] = _x * _m00 + _y * _m04 + _m12; // x
                vbuf[_vfOffset++] = _x * _m01 + _y * _m05 + _m13; // y

                vbuf[_vfOffset++] = vertices[vi++]; // u
                vbuf[_vfOffset++] = vertices[vi++]; // v
                uintbuf[_vfOffset++] = _c; // color
            }

            // 填充顶点索引缓冲区
            for (let ii = 0, il = indices.length; ii < il; ii ++) {
                ibuf[_indexOffset++] = _vertexOffset + indices[ii];
            }
        }
    }
}

CCFactory

js 复制代码
var BaseFactory = dragonBones.BaseFactory;
var CCFactory = dragonBones.CCFactory = cc.Class({
    // 继承自dragonBones.Factory,所以它有db_lib.buildArmature等函数
    extends: BaseFactory,
    ctor () {
        let eventManager = new dragonBones.CCArmatureDisplay();
        this._dragonBones = new dragonBones.DragonBones(eventManager); // 这个就是DragonBones官方的lib库
    },
    update (dt) {
        this._dragonBones.advanceTime(dt); // 让时间往前走,驱动龙骨的更新
    },
    buildArmatureDisplay (armatureName, dragonBonesName, skinName, textureAtlasName) {
        // 通过缓存的 DragonBonesData 实例和 TextureAtlasData 实例创建一个骨架。
        // buildArmature也会回调到CCFactory的_buildArmature
        // buildArmature内部也会处理slot
        let armature = this.buildArmature(
            armatureName, 
            dragonBonesName, // 这个就是_armatureKey
            skinName, 
            textureAtlasName
        );
        // _display其实就是CCArmatureDisplay,类似显示代理,从命名上也能看出来
        return armature && armature._display; 
    },
    _buildSlot (dataPackage, slotData, displays) {
        let slot = BaseObject.borrowObject(dragonBones.CCSlot);
        let display = slot;
        slot.init(slotData, displays, display, display);
        return slot;
    },
    _buildArmature (dataPackage) {
        // db_lib的类型
        let armature = BaseObject.borrowObject(dragonBones.Armature);

        armature._skinData = dataPackage.skin;
        armature._animation = BaseObject.borrowObject(dragonBones.Animation);
        armature._animation._armature = armature;
        armature._animation.animations = dataPackage.armature.animations;

        armature._isChildArmature = false;

        // fixed dragonbones sort issue
        // armature._sortSlots = this._sortSlots;


        var display = new dragonBones.CCArmatureDisplay(); // display真正的来源

        // 会设置display,同时会设置display.dbInit,也就是CCArmatureDisplay.dbInit
        armature.init(dataPackage.armature,
            display, display, this._dragonBones
        );
        
        return armature;
    },
}    
js 复制代码
dragonBones.CCArmatureDisplay = cc.Class({
    name: 'dragonBones.CCArmatureDisplay',
    // db api
    // armature: 龙骨的类型Armature
    dbInit (armature) {
        this._armature = armature;
    },
}

CCSlot

ini 复制代码
vertices = slot._localVertices; // 顶点数据来源
indices = slot._indices; // 顶点索引数据来源

都是初始化进行了填充,indices是固定的[0, 1, 2, 1, 3, 2]

js 复制代码
_updateFrame () {
    // 这里面进行了初始化
}

换装

大致的关系如下图:

js 复制代码
var armatureDisplay = this.node.getComponent(dragonBones.ArmatureDisplay);
const factory = dragonBones.CCFactory.getInstance();
const armatureKey = armatureDisplay.getArmatureKey();
const slot = armatureDisplay.armature().getSlot("2");
const b = factory.replaceSlotDisplay(
  armatureKey, // 实例的缓存名称
  armatureDisplay.armatureName, // 骨架数据名称
  "1", // 插槽数据名称
  "1", // 显示对象数据名称
  slot // 要把这个插槽的内容替换为 1,1
);

replaceSlotDisplay的注释

js 复制代码
 /**
 * - 用特定的显示对象数据替换特定插槽当前的显示对象数据。
 * 用 "dragonBonesName/armatureName/slotName/displayName" 指定显示对象数据。
 * @param dragonBonesName - DragonBonesData 实例的缓存名称。
 * @param armatureName - 骨架数据名称。
 * @param slotName - 插槽数据名称。
 * @param displayName - 显示对象数据名称。
 * @param slot - 插槽。
 * @param displayIndex - 被替换的显示对象数据的索引。 (如果未设置,则替换当前的显示对象数据)
 * @example
 * <pre>
 *     let slot = armature.getSlot("weapon");
 *     factory.replaceSlotDisplay("dragonBonesName", "armatureName", "slotName", "displayName", slot);
 * </pre>
 * @version DragonBones 4.5
 * @language zh_CN
 */
BaseFactory.prototype.replaceSlotDisplay = function (dragonBonesName, armatureName, slotName, displayName, slot, displayIndex) {

总结

DragonBones组件会一次性将龙骨全部顶点数据提交到buffer,所以导致无法穿插节点,因为cocos是按照节点顺序进行渲染的。

扩展

worldMatDirty

js 复制代码
_proto._children = function (node) {
    let cullingMask = _cullingMask;
    let batcher = _batcher;

    let parentOpacity = batcher.parentOpacity;
    let opacity = (batcher.parentOpacity *= (node._opacity / 255));

    let worldTransformFlag = batcher.worldMatDirty ? WORLD_TRANSFORM : 0; // 这里
    let worldOpacityFlag = batcher.parentOpacityDirty ? OPACITY_COLOR : 0;
    let worldDirtyFlag = worldTransformFlag | worldOpacityFlag;

    let children = node._children;
    for (let i = 0, l = children.length; i < l; i++) {
        let c = children[i];

        // Advance the modification of the flag to avoid node attribute modification is invalid when opacity === 0.
        c._renderFlag |= worldDirtyFlag;
        if (!c._activeInHierarchy || c._opacity === 0) continue;

        _cullingMask = c._cullingMask = c.groupIndex === 0 ? cullingMask : 1 << c.groupIndex;

        // TODO: Maybe has better way to implement cascade opacity
        let colorVal = c._color._val;
        c._color._fastSetA(c._opacity * opacity);
        flows[c._renderFlag]._func(c);
        c._color._val = colorVal;
    }

    batcher.parentOpacity = parentOpacity;

    this._next._func(node);
};

_armatureKey

相关推荐
LuciferHuang38 分钟前
震惊!三万star开源项目竟有致命Bug?
前端·javascript·debug
GISer_Jing39 分钟前
前端实习总结——案例与大纲
前端·javascript
天天进步201543 分钟前
前端工程化:Webpack从入门到精通
前端·webpack·node.js
姑苏洛言2 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
知识分享小能手2 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
姑苏洛言2 小时前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
你的人类朋友3 小时前
🤔什么时候用BFF架构?
前端·javascript·后端
知识分享小能手4 小时前
Bootstrap 5学习教程,从入门到精通,Bootstrap 5 表单验证语法知识点及案例代码(34)
前端·javascript·学习·typescript·bootstrap·html·css3
一只小灿灿4 小时前
前端计算机视觉:使用 OpenCV.js 在浏览器中实现图像处理
前端·opencv·计算机视觉