木匣子

Web/Game/Programming/Life etc.

露骨的骨骼动画透明效果

技术背景:cocos2d-x 游戏开发,骨骼动画

骨骼动画是现代游戏中十分常用的技术。相比关键帧动画,可以节约大量的数据资源,调整起来也比较容易。而且它可以借由CPU计算力实现补间与混合,从一个动作平滑的切换到另一个动作。

然而,由于骨骼动画是由多个不同的关节分层构建而成,这导致了一个问题。当一个人物由不透明渐变到透明的过程中,每个骨骼独立变化至半透明,这时每个骨骼将一览无余。虽不至于毛骨悚然,但着实不太美观。为了更清楚说明这个效果,可以参看这个演示左边的例子。

也不是所有游戏都会涉及到整个骨骼的透明度变化,但在清理地面上的死亡角色时的,这通常是较简单做法。所以为了让骨骼比较“好”地从屏幕上消失,我试图寻找一种可用的方法。

早在 Flash 5 网络MV盛行的年代,许多MV中的美女主角从背景中透明淡去时,你常常会看到一个光凸凸的脑袋和分开的漂动的头发,……,非常煞风景。其实这也是相同的原因造成的。

直到今天,一些Flash页游团队在开发游戏的时候,依然会犯这样的错误。就在前不久,我路过一个团队的时候,看到他们的内测画面有一个人物,后面拖着几个半透明的…露骨的幻影。不过这太不应该了,因为 Flash Actionscript 3.0 已经很好地解决了这个问题,可以参考这篇文章

既然 Flash 已经有解决方法了,那么 Cocos2d-x 能不能做到类似 BlendMode.LAYER 的效果呢?很遗憾,我在源码里翻看了很久,没有发现类似的机制。原本我打算使用 setBlendFunc() 的方式尝试不同的叠加方案,但发现都无法完成想要的效果。后来我翻了一下 Adobe 的官方文档,了解了 BlendMode.LAYER 的工作机制:

强制为该显示对象创建一个透明度组。这意味着在对显示对象进行进一步处理之前,该对象已在临时缓冲区中预先构成。在以下情况下将会自动完成预先构成操作:显示对象通过位图缓存进行预缓存,或者显示对象是一个显示对象容器,该容器至少具有一个带有 blendMode 设置(而不是 “normal”)的子对象。

这显然与 Cocos2d-x 的渲染机制不同。cocos2d 直接从根场景开始渲染树,一直到叶子结点,从下层到上层完成渲染,没有构建组渲染的过程。但它却给了我启示——这个问题确实不能通过 setBlendFunc() 来完成,需要一些额外的工作。

实际上这个过程总结起来大意就是:将一个不透明的骨骼动画渲染到一个缓冲区(RenderTexture),然后使用这个缓冲区绘制精灵(Sprite),同时需要处理骨骼动画的变化(update)。

于是我根据这个思路做了以下尝试:

var ArmatureAlpha = cc.NodeRGBA.extend({
    _armature: null,
    initWithArmature: function (armature) {
        // bind armature
        this._armature = armature;
        // hack for jsb.
        this._armature.retain();

        // move _armature to the center of _texture
        var offset = new cc.Rect(this._armature.boundingBox());
        this._armature.setPosition(
            offset.width - (offset.width / 2 + offset.x),
            offset.height - (offset.height / 2 + offset.y)
        );

        // create a _texture, reserved space for animation
        this._texture = cc.RenderTexture.create(
            this._armature.boundingBox().width * 2,
            this._armature.boundingBox().height * 2
        );
        // hack for jsb.
        this._texture.retain();

        // _sprite to show the alpha animation of the whole armature
        this._sprite = cc.Sprite.createWithTexture(this._texture.getSprite().getTexture());
        this._sprite.setPosition(
            offset.width / 2 + offset.x,
            offset.height / 2 + offset.y
        );
        sprite = this._sprite;
        this._sprite.setScaleY(-1);
        this.addChild(this._sprite);

        this.setCascadeOpacityEnabled(true);
        this.setCascadeColorEnabled(true);
        this.scheduleUpdate();
        return true;
    },
    update: function (dt) {
        // render _armature to the _texture
        this._texture.beginWithClear(0, 0, 0, 0);
        this._armature.update(dt);
        this._armature.visit();
        this._texture.end();
    },
    onExit: function () {
        this._armature.release();
        this._texture.release();
        this._super();
    }
});

ArmatureAlpha.createWithArmature = function (armature) {
    var sg = new ArmatureAlpha();
    if (sg && sg.initWithArmature(armature)) {
        return sg;
    }
    return null;
};

以上代码创建了一个继承至 cc.NodeRGBA 的类,这是为了使用 setOpacity() 透明度,另外在 init() 中启用了 this.setCascadeOpacityEnabled(true) 这可以将透明度级联设置到子对象中。使用方法如下:

// load resources
ccs.ArmatureDataManager.getInstance().addArmatureFileInfo(b_nmw_png, b_nmw_plist, b_nmw_xml);

// create armature
var armature = ccs.Armature.create("nmw");

// bind with alpha
var alpha = ArmatureAlpha.createWithArmature(armature);

// set opacity and other properties
alpha.setOpacity(200);
alpha.setScale(0.5);

// add alpha to layer
this.addChild(alpha);

// control the armature normally
armature.getAnimation().playWithIndex(0);
armature.getAnimation().setSpeedScale(60 / 60);

在使用中,我们并未把创建的骨骼加入场景中,而是由透明容器托管它。这就将骨骼从渲染树中隔离了。而后我们在透明容器中更新骨骼 _armature.update() 并使用 _armature.visit() 将它绘制到缓冲区。该缓冲区与透明容器中的一个精灵绑定,将精灵设置透明,即可实现我们想要的效果了。效果参见演示右边的例子。

从演示中可以观察到缓冲区中的动画效果实际上并没有原图质量高,但确实解决了露骨的问题。另外,这种方法在性能上的消耗相对较高。因为一个骨骼动画,实际上只是同一个贴图的多个不同三角形片产生的组合而已,而缓冲区则要在每一帧绘制新的贴图。

是否将其应用到生产环境中,还需要仔细考量。