15. 精灵图-让2D动图显示在3D场景中
本教程翻译自官方教程Sprites,各位也可以进入本站镜像站点查看
本文目录
精灵图Sprites
在本章我们将学习如果使用精灵图Sprites,精灵图其实就是一张2D的图片,可以有透明色背景(例如png,gif),也叫做雪碧图,大家比较熟悉的可能就是css sprite。在3D场景中,无论从什么角度看精灵图总是面对相机的,我们总是看到2D图片的全貌。
现在精灵图常常被用来展示一些动画角色、作为粒子发射器的粒子以及模拟一个复杂的3D对象,例如树木等。可以把精灵图认为是一个简化的“实体”,它在每一次的场景绘制调用中都被呈现。
在2d或2.5d游戏中,通常要求几千个精灵图同时作为动画被渲染,这个场景对精灵图的管理要求非常高,所以我们之后将会介绍一个特殊的系统,叫精灵贴图,来管理这些精灵图。在Babylon v4.1版本后可以使用。
单个精灵图都被整合在一张图片中,这个图片一般被称作精灵表spritesheet或者纹理图谱texture atlas。
-
一致性精灵表uniform spritesheet 是一张特殊的图片,在图片中精灵图有完全一样的大小,并顺序的进行排列。当你在阅读文档时,看到里面提到精灵表spritesheet这个术语,通常你可以假设这是一个 一致性精灵表。一致性精灵表由SpriteManager对象进行监听和管理。
-
压缩精灵表packed spritesheet 可以理解为精灵图被压缩了,因为精灵图在 压缩精灵表 中会拥有不同的大小,会以最优的方式进行排列,最终结果就是压缩了精灵表的总体积。可以会有各种各样的方法来压缩精灵表,但是原理都是一样的,所以我们统称为 压缩精灵表packed spritesheet,它由SpritePackedManager对象进行管理,在Babylon v4.1版本后可以使用。
如果要在Babylon中使用精灵图Sprite,必须使用SpriteManager和SpritePackedManager两个管理器来进行新建和管理,即使只有一个小小的精灵图,这个步骤也逃不掉。他们通过将一个精灵图Sprite的多个实例集中在一个位置,来优化GPU资源。
使用精灵图的最终效果
一致性精灵表管理Sprite Manager
对于同样体积的精灵图,可以使用如下代码创建:
// Create a sprite manager
var spriteManagerTrees = new BABYLON.SpriteManager("treesManager", "Assets/Palm-arecaceae.png", 2000, 800, scene);
SpriteManager的参数如下所示:
- 名称:管理器的名称
- 图片URL链接:大多数时候会使用背景透明的png图片。
- 管理器的负载:相当于在管理器中可允许的最大精灵图示例数量,在上例中,我们创建了一棵树的2000个实例。
- 单元格尺寸,就像图片大小一样,指定精灵图的大小,刚才我们也说到过, 一致性精灵表 的精灵图有完全一样的大小,这里就是指定这个大小,可以设置一个数字代表宽高一样,也可以设置一个对象单独指定宽高。(没有在这里指定也不要紧,可以通过 spriteManager.cellWidth 和 spriteManager.cellHeight 来单独指定)
- 场景Scene实例,决定管理器添加到哪里。
再举一个例子,请看下面的代码片段:
var spriteManagerPlayer = new BABYLON.SpriteManager("playerManager","Assets/Player.png", 2, {width: 64, height: 64}, scene);
上面的代码中我们只包含了2个实例,我们声明单个精灵图的大小是64x64。下面是一个精灵图的示例:
每个精灵图都必须包含在一个64x64像素的正方形中,不能多也不能少。
选中精灵图
在下面的Playground例子中,精灵图能够被选中:
要做到上例的效果,我们需要进行如下2步:
- 开启精灵图的选中属性:sprite.isPickable = true;
- 再启用spriteManager的选中属性:spriteManager.isPickable = true;
然后我们就能通过 scene.pickSprite 方法来选中他们了:
var pickResult = scene.pickSprite(this.pointerX, this.pointerY);
if (pickResult.hit) {
pickResult.pickedSprite.angle += 0.5;
}
也可以使用multiPickSprite方法获取鼠标下的所有精灵图:
var pickResult = scene.multiPickSprite(this.pointerX, this.pointerY);
for (var i = 0; i < pickResult.length; i++) {
pickResult[i].pickedSprite.angle += Math.PI / 4;
}
默认情况下,选中操作将使用精灵图的边界矩形 bounding rectangle(由于性能原因),也就是靠近精灵图的大概一个区域就能够选中,不够精确。时候我们需要精确的选中操作怎么办?那就是设置使用精灵图的alpha值,也就是透明度,来进行选中(值来自于精灵图的纹理)。但是要注意的是,这个方法只在alpha大于0.5的情况下才能选中精灵。
压缩精灵表管理Sprite Packed Manager
此功能仅在Babylonv4.1版本以上生效
就之前讨论的,压缩精灵表中的精灵图大小各不相同,所以只有一张图片还不够,我们还需要一个包含定义精灵图位置的Json文件。通常情况下,图片和Json文件应该有相同的名称,并且位于同一个文件目录下,例如:pack1.png 和 pack1.json。使用如下代码进行创建:
var spm = new BABYLON.SpritePackedManager("spm", "pack1.png", 4, scene);
参数包括:
- 名称:管理器的名称
- 图片URL链接:大多数时候会使用背景透明的png图片。
- 管理器的负载:相当于在管理器中可允许的最大精灵图示例数量,上例中最多包含4个实例
- 场景Scene实例,决定管理器添加到哪里。
初始化的时候,也可以直接使用现有Json对象,让它作为参数传递到管理器中。 例如:
var spm = new BABYLON.SpritePackedManager("spm", "pack1.png", 4, scene, JSONObject);
打包格式
对应上述图片的Json文件,是基于TexturePacker软件生成的,对于软件中的配置我们是这样设置的:输出格式为Json(软件支持多种格式,目前Babylon只认Json),trim设置为none(自动裁剪单个精灵图多余的边缘,none为不裁剪),rotation设置为关闭(是否字段旋转精灵图,以达到更大的压缩,这里选择不旋转)。对于以上关于 压缩精灵表 的配置, TexturePacker软件输出的Json文件内容如下:
{ "frames": {
"eye.png": {
"frame": {"x":0,"y":148,"w":400,"h":400},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":400,"h":400},
"sourceSize": {"w":400,"h":400}
},
"redman.png": {
"frame": {"x":0,"y":0,"w":55,"h":97},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":55,"h":97},
"sourceSize": {"w":55,"h":97}
},
"spot.png": {
"frame": {"x":199,"y":0,"w":148,"h":148},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":148,"h":148},
"sourceSize": {"w":148,"h":148}
},
"triangle.png": {
"frame": {"x":55,"y":0,"w":144,"h":72},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":144,"h":72},
"sourceSize": {"w":144,"h":72}
}
},
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "1.0",
"image": "pack1.png",
"format": "RGBA8888",
"size": {"w":400,"h":548},
"scale": "1",
"smartupdate": "$TexturePacker:SmartUpdate:c5944b8d86d99a167f95924d4a62d5c3:3ed0ae95f00621580b477fcf2f6edb75:5d0ff2351eb79b7bb8a91bc3358bcff4$"
}
}
上述Json文件看起很臃肿,但其实SpritePackedManager仅仅用到了frame属性对应的数据,所以以上的Json数据我们可以简化为:
{ "frames": {
"eye.png": {
"frame": {"x":0,"y":148,"w":400,"h":400}
},
"redman.png": {
"frame": {"x":0,"y":0,"w":55,"h":97}
},
"spot.png": {
"frame": {"x":199,"y":0,"w":148,"h":148}
},
"triangle.png": {
"frame": {"x":55,"y":0,"w":144,"h":72}
}
}
}
但是,如果我们打算在项目中使用SpriteMap精灵贴图,那么建议使用完整格式的Json,不要对其做人为的删减操作。
创建精灵图实例
只创建SpriteManager和SpritePackedManager两个管理器还不够,我们还需要创建一个精灵图的实例来链接到管理器。创建一个实例的代码如下所示:
var sprite = new BABYLON.Sprite("sprite", manager);
以上代码指定了精灵表上的第一个精灵图。
使用一致性精灵表spritesheet和SpriteManager实例的情况下,可以通过指定cellIndex索引来决定引用哪一个精灵图,cellIndex在精灵表中的计数是从左到右,自上而下的。代码示例如下:
var sprite = new BABYLON.Sprite("sprite", manager);
sprite.cellIndex = 2; //指定第二个精灵图
使用压缩精灵表packed spritesheet和SpritePackedManager的情况下,我们能使用上面提到的cellIndex或者精灵图sprite的名称cellRef,来指定使用哪个精灵图。代码示例如下:
var sprite = new BABYLON.Sprite("sprite", manager);
sprite.cellRef = "spot.png";
也可以更改精灵图的大小,方向或水平翻转:
sprite.size = 0.3;
sprite.angle = Math.PI/4;
sprite.invertU = -1;
自从Babylon.js v2.1版本开始,还可以对精灵图的宽和高进行设置:
sprite.width = 0.3;
sprite.height = 0.4;
还可以像操作其他3D物体一样操作精灵图:
sprite.position.y = -0.3;
精灵图动画
动画是精灵图的优点之一,最简单直接的方式是使用一致性精灵表和SpriteManager。只需载入一张大图片,里面包含了动画的所有帧,这些帧就是一张接一张的精灵图。请小心的指定精灵图的大小,精确到1个像素,避免动画失真。下面的例子展示了一个动态游戏角色的精灵表:
我们能够把这40个位置的精灵图串联起来播放,然后就会得到一个可以动的游戏角色,依据不同的播放起止位置,他可以实现走路、跳跃等动作。
player.playAnimation(0, 43, true, 100);
按照以上代码,代表游戏角色的精灵图,将从0到43帧的顺序播放动画。第三个参数是指动画是否循环播放,true为循环。最后一个参数表示播放速度,值越小,动画播放速度越快。
在压缩精灵表中也可以使用playAnimation方法来播放动画,前提是:精灵图在Json文件中存储的信息正确,并且在文件中是按照动画顺序放置的。
压缩精灵表的Playground实例
- Playground 直接使用Json对象(采用TexturePacker格式)展示压缩精灵表的演示
- Playground 直接使用优化过的Json对象来展示压缩精灵表的演示
- Playground 使用json文件的压缩精灵表演示
- Playground 使用json文件的压缩精灵表演示-多个精灵同时展示
- Playground 使用json文件的压缩精灵表演示-模仿幻灯片
- Playground 选中精灵图示例
精灵贴图Sprite Map
Babylon v4.1版本以后可用
在某些特定场景下,传统的精灵表很难解决所有问题,例如我们需要同时显示几千甚至几十万个精灵图在屏幕上时,精灵贴图在制作2d游戏时很常见,在Babylon的3D场景中,有时也会用得到。接下载我们将专注于2d和2.5的游戏关卡设计。这也存在一些限制:精灵贴图SpriteMap的初始化参数指定了特定的网格,所以精灵图位置将总是静态的。
精灵贴图SpriteMap是展示在3d平面Plane上的,能够在3d空间中进行装换。每个SpriteMap都执行一个绘制调用,并在内存中至少保留3个纹理缓冲区,具体取决于SpriteMap初始化时设置的层数。
SpriteMap使用的Json格式与压缩精灵表相同,但是支持旋转、挤出和填充,不就之后的版本也将支持删除空白区域的功能。
创建SpriteMap很简单:
var spriteMap = new BABYLON.SpriteMap(name, atlasJSON, spriteTexture, options, scene);
具体参数如下所示:
-
Name: 精灵贴图SpriteMap的名称。
-
atlasJSON: 精灵贴图使用的Json图集。
-
spriteTexture: 精灵贴图使用的纹理图集。
-
options: 初始化选项。
-
scene: 加入映射的场景scene。
上述options对象中可以传入多个参数,每一个参数都是帮助 精灵贴图SpriteMap 在内存中做好数据缓冲,然后进行更好的显示。这些选项如下所示: -
stageSize: 让精灵贴图进行平铺显示,二维向量表示横向平铺和纵向平铺,默认:Vector2(1,1)
-
outputSize: 在世界单元中控制精灵贴图的大小,二维向量表示宽和高。默认 : Vector2(1,1)
-
outputPosition: 在世界单元中控制精灵贴图的输出位置,相当于css中的background-position,决定显示纹理的哪一个部分。 默认 : Vector3.Zero
-
outputRotation: 在世界单元中控制精灵贴图的输出角度。默认 : Vector3.Zero
-
layerCount: 精灵贴图中的分层数量,使2d游戏有层次感. 默认 : 1
-
maxAnimationFrames: 在精灵表中,动画的最大帧数。默认 : 0
-
baseTile: 要添加到精灵贴图中的初始图块,所对应的帧ID。 默认 : 0
-
flipU: 用于在帧计算后,垂直翻转精灵图sprite的显示,布尔类型。默认 : false
-
colorMultiply: 一个三维向量Vector3,它将与精灵贴图的最终颜色值相乘,从而改变精灵图的颜色。. default : Vector3(1,1,1)
初始化后,我们能像普通的物体那样,通过引用spriteMaps.position 或 spriteMaps.rotation 来改变精灵贴图的位置和旋转。而其他选项的修改,例如stageSize、layerCount等,我们推荐先dispose释放精灵贴图SpriteMap之后,再重新创建一个新的图谱。
精灵贴图的图块
每个精灵贴图都由许多大小相等的图块tiles组成,图谱输出的平面会被分成一个网格。
要更改为某一个位置的图块,我们只需要使用以下方法:
spriteMap.changeTiles(layerID, tileID, frameID)
参数说明如下:
- layerID: 精灵贴图的目标分层索引,从0开始
- tileID: 一个分层下的目标图块位置,是一个二维向量组Vector2
- frameID: 更换到另一个精灵对应的帧ID
如果要一次进行多个更改,请将所有具有相似帧ID的tileID 二维向量Vector2都添加到数组中,并将其传递给tileID参数。 否则,必须为每个图块更新缓冲区,而不是分批更新(这不是最好的方法)。
如果需要做一个透明的单元格时,我们可以有两种方式做到:在纹理素材中找一个单像素的透明图片,或者在Json文件中创建一个空白的图块。
以下是一个创建透明单元格的实例:
{
"filename": "blank.png",
"frame": {"x":221,"y":221,"w":1,"h":1},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
"sourceSize": {"w":32,"h":32}
},
上述的Json对象,指向到了一个纹理文件的单个像素。在着色器shader中,这个像素被放大到了32x32的正确体积,然后作为一个透明数据,展示到了图块当中。
保存图块
当创建了一个具有正确图块tile位置的精灵贴图SpriteMap后,我们可以导出并保存这个配置,以便于将来的复用。加载“ .tilemaps”文件时,必须确保将其加载到的SpriteMap具有正确的layerCount,否则将抛出webGL错误。
保存的代码如下:
spriteMap.saveTileMaps()
载入的代码如下:
spriteMap.saveTileMaps(url)
- url: .tilemaps文件的url地址。
精灵贴图动画
是时候为精灵贴图总的图块添加动画了。这里有一个限制,每帧只能分配一个动画序列。不过这并没有限制我们的发挥,因为可以在Json文件中创建重复的帧引用,然后仅分配一个不同的动画给被添加的帧。与创建图块贴图相似,我们还需要“合成”这个动画。目前针对这些设置,还没有导出选项。
这些动画具有特殊的胶卷风格,这与其他精灵系统是一样的。可以分配任何的帧作为序列中的下一帧,并且每个动画帧都有它自己的独立计时。关于术语:帧和动画帧,请不要相混淆。帧是对精灵在图集上的位置的引用,而动画帧是此时显示的特定帧,由原始帧(为其分配了动画的帧)决定。尽管所有动画在全局和每个动画帧可能都具有不同的计时,但假定它们都是在循环播放。
要创建动画,请分配每个单元格。
spriteMap.addAnimationToTile(frameID, animationFrame, nextFrameID, animationFrameDisplayTiming, globalSpeed)
- frameID:为精灵图的哪一个帧分配动画,是一个整数id。
- animationFrame:分配到哪一个动画帧,为整数ID。
- nextFrameID:动画中的下一个精灵帧,为整数ID。
- animationFrameDisplayTiming:动画帧的播放计时,浮点数,值域在0-1。 这将在下面详细描述。
- globalSpeed:动画播放的速度,浮点数。
每一个精灵贴图都有一个计时器,它被输入到着色器shader中,然后显示出精灵贴图。在着色器中,它的调整时间值基本在0-1之间,在设置的帧率下循环。每个图块都会查找是否已分配了动画数据,然后检查该段时间是否在正确的动画帧上。这是通过检查动画帧显示时间值是否低于调整时间来确定的。这个时间值由一个0-1的浮点数控制,它能够加速或减慢所有动画帧的播放速度。
幸运的是,如果多个精灵贴图SpriteMaps使用相同的数据,那么则只需要构建一次动画。 我们只要把animationMap赋值到另一个精灵贴图spriteMap就可以了,它将自动复制所有的动画。
spriteMap1.animationMap = spriteMap0.animationMap
这听起来很复杂,所以接下来请看实例来加深一下理解。
精灵贴图的Playground实例
下一步
学习了精灵图之后,我们就要开始在经典的3d效果中使用它了,请阅读下一章16. 发射粒子-产生梦幻的效果。
延伸阅读
贡献者
参与贡献? 联系alexads@foxmail.com