露骨的骨骼动画透明效果
骨骼动画是现代游戏中十分常用的技术。相比关键帧动画,可以节约大量的数据资源,调整起来也比较容易。而且它可以借由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()
将它绘制到缓冲区。该缓冲区与透明容器中的一个精灵绑定,将精灵设置透明,即可实现我们想要的效果了。效果参见演示右边的例子。
从演示中可以观察到缓冲区中的动画效果实际上并没有原图质量高,但确实解决了露骨的问题。另外,这种方法在性能上的消耗相对较高。因为一个骨骼动画,实际上只是同一个贴图的多个不同三角形片产生的组合而已,而缓冲区则要在每一帧绘制新的贴图。
是否将其应用到生产环境中,还需要仔细考量。