用多网格实例制作数千条鱼的动画

本教程探讨了游戏中使用的技术 ABZU 用于渲染和动画数以千计的鱼使用顶点动画和静态网格实例。

在Godot,这可以通过一个习俗来完成。 Shader 和A MultiMeshInstance . 使用以下技术,您可以渲染数千个动画对象,即使是在低端硬件上。

我们将从制作一条鱼的动画开始。然后,我们将看到如何将动画扩展到数千条鱼。

为一条鱼制作动画

我们从一条鱼开始。将鱼模型加载到 MeshInstance 并添加新的 ShaderMaterial .

这是我们将用于示例图像的鱼,您可以使用任何您喜欢的鱼模型。

../../../_images/fish.png

注解

本教程中的鱼模型由 QuaterniusDev 并与创意共享许可证共享。CC0 1.0通用(CC0 1.0)公共领域奉献https://creativecommons.org/public domain/zero/1.0/

通常,您将使用骨骼和 Skeleton 动画对象。但是,骨骼在CPU上是动画的,因此你最终不得不计算每帧上千次的操作,因此不可能有上千个对象。在顶点明暗器中使用顶点动画,可以避免使用骨骼,而是在几行代码中计算完整动画,并完全在GPU上。

动画将由四个关键动作组成:

  1. 左右移动

  2. 围绕鱼的中心旋转

  3. 平移波运动

  4. 盘动扭转运动

动画的所有代码都将在统一控制运动量的顶点明暗器中。我们使用制服来控制运动的强度,这样您就可以在编辑器中调整动画并实时查看结果,而不必重新编译明暗器。

所有的运动都将通过应用于 VERTEX 在模型空间中。我们希望顶点在模型空间中,这样运动总是相对于鱼的方向。例如,边对边总是在鱼的左右方向来回移动,而不是在 x 世界方向的轴。

为了控制动画的速度,我们将从定义自己的时间变量开始,使用 TIME .

//time_scale is a uniform float
float time = TIME * time_scale;

我们要实现的第一个动作是左右移动。它可以通过抵消 VERTEX.x 通过 cos 属于 TIME . 每次渲染网格时,所有顶点都将以 cos(time) .

//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;

生成的动画应如下所示:

../../../_images/sidetoside.gif

接下来,我们添加轴。因为鱼的中心是(0,0),我们所要做的就是乘 VERTEX 通过旋转矩阵使其围绕鱼的中心旋转。

我们构造一个旋转矩阵,如下所示:

//angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around
//pivot is a uniform float
float pivot_angle = cos(time) * 0.1 * pivot;
mat2 rotation_matrix = mat2(vec2(cos(pivot_angle), -sin(pivot_angle)), vec2(sin(pivot_angle), cos(pivot_angle)));

然后我们把它应用到 xz 轴乘以它 VERTEX.xz .

VERTEX.xz = rotation_matrix * VERTEX.xz;

如果只应用了轴,您应该看到如下内容:

../../../_images/pivot.gif

接下来的两个动作需要将鱼的脊柱向下平移。为此,我们需要一个新的变量, body . body 是一个浮球 0 在鱼的尾部 1 在它的头上。

float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2

下一个运动是沿着鱼的长度向下移动的余弦波。为了使它沿着鱼的脊柱移动,我们将输入偏移到 cos 根据脊柱的位置,这是我们上面定义的变量, body .

//wave is a uniform float
VERTEX.x += cos(time + body) * wave;

这看起来非常类似于我们上面定义的侧向运动,但在本例中,通过使用 body 偏移 cos 脊椎上的每个顶点在波浪中都有不同的位置,使得波浪看起来像是在鱼身上移动。

../../../_images/wave.gif

最后一个动作是扭曲,这是沿着脊柱的平移。与轴类似,我们首先构造一个旋转矩阵。

//twist is a uniform float
float twist_angle = cos(time + body) * 0.3 * twist;
mat2 twist_matrix = mat2(vec2(cos(twist_angle), -sin(twist_angle)), vec2(sin(twist_angle), cos(twist_angle)));

我们将旋转应用于 xy 使鱼在脊柱周围滚动的轴。要使其起作用,鱼刺需要集中在 z 轴。

VERTEX.xy = twist_matrix * VERTEX.xy;

这是扭动的鱼:

../../../_images/twist.gif

如果我们一个接一个地应用所有这些动作,我们会得到一个流体胶状运动。

../../../_images/all_motions.gif

正常的鱼通常是用身体的后半部分来游泳的。因此,我们需要将平移运动限制在鱼的后半部分。为此,我们创建一个新变量, mask .

mask 是从 0 在鱼的前面 1 最后使用 smoothstep 控制过渡点 01 发生。

//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);

下面是鱼的图片 mask 用作 COLOR

../../../_images/mask.png

对于波,我们将运动乘以 mask 它将限制在后半部分。

//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;

为了在扭曲处使用遮罩,我们使用 mix . mix 允许我们在完全旋转的顶点和未旋转的顶点之间混合顶点位置。我们需要使用 mix 而不是乘法 mask 通过旋转 VERTEX 因为我们没有将运动添加到 VERTEX 我们正在替换 VERTEX 旋转版本。如果我们把它乘以 mask 我们会把鱼缩小。

//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);

把这四个动作放在一起,就得到了最后的动画。

../../../_images/all_motions_mask.gif

继续玩制服,以改变鱼的游泳周期。你会发现你可以用这四个动作创造出各种各样的游泳风格。

做鱼群

Godot使使用多网格实例节点呈现数千个相同的对象变得容易。

创建和使用多网格实例节点的方式与创建网格实例节点的方式相同。对于本教程,我们将命名多网格实例节点 School 因为里面会有一群鱼。

一旦你有了一个多网格实例,添加一个 MultiMesh 把你的 Mesh 使用上面的明暗器。

多重网格使用三个附加的每个实例属性绘制网格:变换(旋转、平移、缩放)、颜色和自定义。自定义用于通过 Color .

instance_count 指定要绘制的网格实例数。暂时离开 instance_count0 因为在 instance_count 大于 0 . 我们会出发的 instance count 在后面的gdscript中。

transform_format 指定使用的转换是三维还是二维。对于本教程,请选择三维。

对于两者 color_formatcustom_data_format 你可以选择 NoneByteFloat . None 意味着您不会传入该数据(每个实例一个 COLOR 变量,或 INSTANCE_CUSTOM )到材质球。 Byte 表示构成您输入的颜色的每个数字将存储8位,而 Float 意味着每个数字将存储在一个浮点数(32位)中。 Float 更慢但更精确, Byte 将占用更少的内存,速度更快,但您可能会看到一些视觉工件。

现在,设置 instance_count 到你想要的鱼的数量。

接下来,我们需要设置每个实例的转换。

有两种方法可以为多网格设置每个实例的变换。第一个完全在编辑器中,并在 MultiMeshInstance tutorial .

第二种方法是循环所有实例,并在代码中设置它们的转换。下面,我们使用gdscript循环所有实例,并将它们的转换设置为随机位置。

for i in range($School.multimesh.instance_count):
  var position = Transform()
  position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
  $School.multimesh.set_instance_transform(i, position)

运行此脚本将把fish放置在多网格实例位置周围的一个框中的随机位置。

注解

如果性能对你来说是个问题,那么试着用gles2或更少的鱼来运行场景。

注意到所有的鱼在它们的游泳周期中都处于相同的位置吗?它使它们看起来非常机器人化。下一步是在游泳周期中给每条鱼一个不同的位置,这样整个鱼群看起来更有机。

为一群鱼制作动画

其中一个好处是使用 cos 函数是用一个参数设置动画, time . 为了让每条鱼在游泳周期中有一个独特的位置,我们只需要抵消 time .

我们通过添加每个实例的自定义值来实现这一点 INSTANCE_CUSTOMtime .

float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

接下来,我们需要将一个值传递给 INSTANCE_CUSTOM . 我们通过在 for 从上面循环。在 for 循环我们为每个实例分配一组四个随机的浮点。

$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))

现在,鱼在游泳周期中都有独特的位置。你可以通过使用 INSTANCE_CUSTOM 使它们通过乘 TIME .

//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

甚至可以尝试以更改每个实例自定义值的方式更改每个实例的颜色。

在这一点上,你会遇到一个问题,那就是鱼是有动画的,但是它们不动。您可以通过更新每个帧的每个实例转换来移动它们。尽管这样做会比每帧移动数千个网格实例更快,但它仍然可能很慢。

在下一个教程中,我们将介绍如何使用 Particles 利用GPU,单独移动每一条鱼,同时仍能获得实例化的好处。