制作精灵动画-火苗

转译自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示例

原文链接

分享你的喜悦!