转译自DCL官方博客

嗨,大家好。我是一个持续进行3D实验且合格的产品设计师,专注于3D的全栈web开发人员, 同时还是一名WebXR和VR应用方面的平面设计师,360虚拟和立体摄影师和3D建模师。
因此,正如你所能想象的,保持对Decentraland的全职承诺相当有挑战。不过,我的梦想是,有一天把我们小组的Cyber Junk概念带到整个虚拟世界中去。
我提到的这个小组叫做Design Quarter,我们于2017年成立,目的是激发并呈现一个vibey hub/hive/lab/gallery,在3D、设计、艺术和其他相关学科领域开展合作。我们正在发展成为一个提供原创设计的一站式商店。
从最初的概念到最终的产品,无论项目大小,我们的目标都是在Decentraland中有偿地为他人和我们自己创造、建造和增长收益。
Design Quarter迎来黑客马拉松
在6月份的Decentraland黑客马拉松中,Design Quarter的一个作品是一个2D精灵火场景。

由于在Decentraland SDK中,不支持纹理的动画gif,所以我们决定使用sprite animation(精灵火动画模块)。我很高兴与大家分享我对这项技术的基本理解——这是自20世纪70年代末以来电子游戏的一个特性。
这里显示的代码当然可以得到改进,但是我选择按照当时使用的方式来表达它。
可以通过https://brent-ooaissvdra.now.sh查看输入的场景
我们的原始源文件也可以通过下面的链接来下载。https://drive.google.com/file/d/10r6vnytLCgc_r7Vb-9p2eWDFx3W9VL36/view
sprite animation(精灵火动画模块)
Spritesheet添加纹理到一个平坦的平面上,同时在上面循环地呈现各种静止图像。然后,我们就能看到动画运动的幻觉,或者三维的外观,就像我们的火焰在天空中燃烧一样。

为了节约资源,spritesheet是一个单一的图像文件,它将多个稍微不同的图像(称为帧)按顺序排列在有序的行和列中。多个图像可以避免为每个动画帧加载一个全新的纹理文件;相反,spritesheet被重新定位在每一个新的渲染表面。
复杂的spritesheet可能包含来自不同视图的一系列字符操作,每个操作从特定的帧开始,并在一定数量的帧上执行。
胶片卷可以看作是spritesheet概念的一个很好的例子,它有一个包含许多行的单列。当电影通过投影机“滚动”这些行时,在一个平面大屏幕上就会产生平滑运动的错觉。
如果.png图像用于sprites,它可能包含透明度级别,以便更好地显示和融入背景。
好了,现在我们知道精灵是什么了。我们如何将这个spritesheet映射到一个表面呢?
UV mapping
当您将UV映射应用到一个表面时,您将使用一个特定于该表面的新3D空间为对象的XYZ坐标分配一个纹理图像。这是由三个顶点组成的表面三角形,我们也称它为多边形。
这个UV空间的相对坐标值被命名为UVW,以区别于对象世界的XYZ。由于纹理只需要平面上的两个维度,我们使用UV坐标轴而忽略了W。
提示:通过简单地反转多边形的相同UV点集的顺序,我们可以反转面的方向。这就是所谓的多边形“绕线”,面朝法线是顺时针或逆时针绕线的直接结果。
正方形或矩形表面,即平面形状,实际上由两个三角形组成,它们位于同一个平面上,共享一条边,因此每个三角形也有两个顶点。在描述一个平面时,我们可以用四个不同的点来表示,而不是用一个三角形的三个UVs和另一个三角形的另外三个UVs——这两个uv将包含两个重复项。
侦察工作
在综合的,不断增加完善的一整套Decentraland发展文件中,有一节是关于材料的,其中载有下列UV映射的例子:

尽管上面给出的示例对于最初使用的spritemap是正确的,但是我很快就搞不清楚它实际上是如何工作的。毕竟,我的fire spritesheet大小不同,有不同的行数和列数,它所投射的表面的比例也不同。
无法简单地用另一个图像替换atlas.png图像。
所以我开始修改这些数字,观察它们对我的纹理图像的影响。经过倾斜,缩放,重复,甚至完全失去,然后必须重新找到纹理,一个模式开始显现出来。
一个一致的模式,连同上面的概念,开始变得有用了。以便让你对我的发现有一个基本的了解,下面对我的粗略发现进行了注释。
plane.uvs = [
// ONE FACE
0.33, // (B) Horizontal width right end position
0, // (C) Vertical height bottom start position
0, // (A) Horizontal width left start position
0, // (C) Vertical height bottom start position
0, // (A) Horizontal width left start position
0.33, // (D) Vertical top height end position
0.33, // (B) Horizontal width right end position
0.33, // (D) Vertical top height end position
// OTHER FACE
0.66, // (B) Horizontal width right end position
0.0, // (C) Vertical height bottom start position
0.33, // (A) Horizontal width left start position
0.0, // (C) Vertical height bottom start position
0.33, // (A) Horizontal width left start position
0.33, // (D) Vertical top height end position
0.66, // (B) Horizontal width right end position
0.33 // (D) Vertical top height end position
]
我注意到,为了使图像不错位或倾斜,我必须在某些地方使用相同的一对值。请注意,A、B、C和D只是我的注释标记,仅用于表示匹配的对。无论是左边、宽度、顶部、高度、开始还是结束,每个值本身,在0到1之间,表示一个维度在,与我的纹理相关的0到100%之间。
在进行了更多的修改之后,我确切地发现了这些工具需要做些什么才能使上述方法变得实用。
遍历注释代码将使你对结果有一个清晰而有用的概念。
注意我是如何保持spriteRow和spriteCol变量的灵活性的,因为它们对于所使用的图像是惟一的。
另外,不要被spritePlane.uvs的动态计算搞混。它们就是这样的,以便于任何用户定义的变量都可以自动应用,你无需担心。
注释代码如下:
// User defined variables
let spriteCols = 10 // number of columns
let spriteRows = 6 // number of rows
let timer = 0.1 // timer speed
let currSpriteCel = 1 // starting position
// Calculated variables
let spriteCels = spriteCols * spriteRows
let colFactor = 1/spriteCols
let rowFactor = 1/spriteRows
// Create material
const spriteMaterial = new BasicMaterial()
spriteMaterial.texture = new Texture("materials/fire.png")
// Create shape component
const spritePlane = new PlaneShape()
// Set the starting UV's
let currRowStart = spriteRows - Math.floor((currSpriteCel-1)/spriteCols)
let currColStart = ((currSpriteCel-1)%spriteCols)
spritePlane.uvs = [
(currColStart+1)*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, currRowStart*rowFactor,
(currColStart+1)*colFactor, currRowStart*rowFactor,
(currColStart+1)*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, currRowStart*rowFactor,
(currColStart+1)*colFactor, currRowStart*rowFactor
]
// Create sprite entity and assign shape and initially mapped sprite material
const spriteFire = new Entity()
spriteFire.addComponent(spritePlane)
spriteFire.addComponent(new Transform({
position: new Vector3(16, 7.3, 16),
rotation: Quaternion.Euler(0, 0, 0),
scale: new Vector3(2,2,2)
}))
spriteFire.addComponent(spriteMaterial)
engine.addEntity(spriteFire)
// Define system to update sprite on every frame
export class spriteAnimate {
update(dt: number) {
if (timer > 0) {
timer -= dt
} else {
timer = 0.1
currSpriteCel += 1
if (currSpriteCel == spriteCels) { currSpriteCel = 1 }
else { currSpriteCel = currSpriteCel + 1 }
let currRowStart = spriteRows - Math.floor((currSpriteCel-1)/spriteCols)
let currColStart = ((currSpriteCel-1)%spriteCols)
spritePlane.uvs = [
(currColStart+1)*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, currRowStart*rowFactor,
(currColStart+1)*colFactor, currRowStart*rowFactor,
(currColStart+1)*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, (currRowStart-1)*rowFactor,
currColStart*colFactor, currRowStart*rowFactor,
(currColStart+1)*colFactor, currRowStart*rowFactor
]
}
}
}
// Add instance of the system to the scene
let animationSystem = engine.addSystem(new spriteAnimate())
添加广告牌功能
我们平稳过渡的下一步是让平面始终面向你。
值得庆幸的是,Decentraland使这部分变得简单,它提供了一个Billboard(广告牌)组件,可以自动处理这部分。我们只需要将这个组件添加到保存Sprite(精灵)的实体中即可。
spriteFire.addComponent(new Billboard(false, true, false))
你将注意到,我们在这里传递三个参数,每个参数在x、y和z轴上启用或禁用Billboard(广告牌)模式。在这里,我们的广告牌只在Y轴上旋转,这意味着它将跟随玩家在地面的运动,但会保持固定的向上方向。
你可以在文档网站上阅读更多关于Decentraland Billboard(广告牌)的信息。另外,虽然超出了本文的范围,但要更好地理解广告牌背后的数学原理的话,欢迎联系我。
总结
现在我们已经介绍了一些基本知识,并希望在平面上把纹理制作成动画。我们使用了一种通用的方法来实现这一点,这种方法应该能够根据包含的行和列的数量,来正确处理不同个体的纹理图像。
请随意联系我,我经常在Design Quarter或Discord中闲逛,你会发现我的。
玩得开心,一定要报名参加即将到来的9月16日的Game Jam !Design Quarter也参加了。与此同时,在我继续学习和理解的过程中,我会分享更多我的发现。
[原始的fire.png附在后面]
可以在https://opengameart.org/content/animated-fire找到更多的fire示例
