Animating thousands of fish with MultiMeshInstance

本教程探索了游戏“ABZU <https://www.gdcvault.com/play/1024409/Creating-the-Art-of-ABZ>”中使用的一种技术,该技术使用顶点动画和静态网格实例,来渲染和制作成千上万的鱼动画。

在Godot中,这可以通过自定义:参考:`着色器<类_着色器>`和:参考:`多重网格实例<类_多重网格实例>`实现。使用下面的技术,你可以渲染成千上万的动画对象,即使是在低端硬件上。

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

Animating one Fish

We will start with a single fish. Load your fish model into a MeshInstance and add a new ShaderMaterial.

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

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

注解

本教程中的鱼模型由“QuaterniusDev<http://quaternius.com>”制作,使用如下知识共享许可。CC0 1.0通用(CC0 1.0)公共领域贡献https://creativecommons.org/publicdomain/zero/1.0/

通常情况下,您将使用单根骨和:参考:`骨骼<类_骨骼>`为对象做动画。然而,单根骨的动画在CPU上进行计算,所以你必须为每一帧计算成千上万的操作,那么就不可能有成千上万的对象。在顶点着色器中使用顶点动画,你就可以避免使用单根骨,而是完全在GPU上,使用几行代码来计算完整的动画。

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

  1. 从一边运动到另一边
  2. 绕着鱼的中心作旋转运动
  3. 平移波动运动
  4. 平移扭转运动

有关动画的所有代码都将在顶点着色器中,并有一致的控制运动量。 我们统一来控制运动的强度,以便您可以在编辑器中调整动画并实时查看结果,而不需要着色器重新编译。

所有动作都将使用模型空间中应用于``VERTEX``的余弦波进行。 我们希望顶点位于模型空间中,以便运动始终相对于鱼的方向。 例如,从一侧到另一侧将始终在左右方向上来回移动鱼,而不是在世界方向上的``x``轴上。

为了控制动画的速度,我们将通过使用“时间”定义自己的时间变量开始。

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

我们将实施的第一项议案是左右运动。 它可以通过``TIME``的``cos``抵消``VERTEX.x``来制作。 每次渲染网格时,所有顶点都会移动到“cos(时间)”的数量。

//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)));

然后我们把它应用到“x”轴和“z”轴上乘以“顶点.xz”。

VERTEX.xz = rotation_matrix * VERTEX.xz;

With only the pivot applied you should see something like this:

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

接下来的两个动作需要顺着鱼的脊柱向下平移。为此,我们需要一个新的变量,“身体”。“身体”是一个浮点数,在鱼的尾巴上是“0”,在它的头部是“1”。

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

下一个运动是沿着鱼的长度向下移动的余弦波。为了让它沿着鱼的脊柱移动,我们用脊柱的位置来偏移输入到“余弦”的位置,也就是我们在上面定义的变量“身体”。

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

这看起来很像我们上面定义的左右运动,但在这个例子中,通过使用“身体”来偏移“余弦”,沿着脊柱的每个顶点在波浪中都有不同的位置,使它看起来像是沿着鱼移动的波浪。

../../../_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;

Here is the fish with twist applied:

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

如果我们一个接一个地应用这些运动,就得到一个类似液体凝胶似的运动。

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

通常鱼主要使用身体的后半部分游泳。因此,我们需要将平移运动限制在鱼的后半部分。为此,我们创建一个新变量“遮罩”。

“遮罩”是一个浮点数,它使用“平滑步进”控制从“0”到“1”之间发生转换的点,从鱼最前面的“0”到最后的“1”。

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

以下是这条鱼的图片,使用“颜色”作为“遮罩”:

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

对于波浪,我们将运动乘以“遮罩”,这就把它限制在后半部分。

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

为了将遮罩应用于扭曲,我们使用“混合”。“混合”允许我们混合一个完全旋转的顶点和一个没有旋转的顶点之间的顶点位置。我们需要使用“混合”而不是“遮罩”乘以旋转的“顶点”,因为我们没有将运动添加到“顶点”,而是用旋转的版本替换了“顶点”。如果我们把它乘以“遮罩”,鱼就会缩小。

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

将四个动作组合在一起,就得到了最终的动画效果。

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

为了改变鱼的游泳周期,继续统一播放吧。你会发现可以用这四个动作创造出多种多样的游泳风格。

制作一群鱼

Godot makes it easy to render thousands of the same object using a MultiMeshInstance node.

创建和使用多重网格实例节点的方法,与创建网格实例节点相同。在本教程中,我们将把多重网格实例节点命名为“鱼群”,因为里面会有一群鱼。

一旦你有了一个多重网格实例,添加:参考:多重网格<类_多重网格>,然后添加:参考:`网格<类_网格>`和上面的着色器。

多重网格使用三个额外的实例属性来绘制网格:变换(旋转、平移、缩放)、颜色和自定义。自定义用于使用:参考:`颜色<类_颜色>`传入4个多用途变量。

“实例_数量”指定要绘制的网格的实例数量。现在,将“实例_数量”保留为“0”,因为当“实例_数量”大于“0”时,您不能更改任何其他参数。我们稍后将在GDScript中设置“实例数量”。

“变换_格式”指定使用的变换是3D还是2D。对于本教程,选择3D。

对于“颜色_格式”和“自定义_数据_格式”,您可以在“无”,“字节”,和“浮点数”之间进行选择,“无”表示您不会传递该数据(可以是每个实例的“颜色”变量,也可以是“实例_自定义”变量)到着色器中。“字节”表示每个组成颜色的数字将被存储为8位,“浮点数”表示每个数字将被存储为一个浮点数(32位)。“浮点数”更慢但更精确,“字节”将占用更少的内存和更快,但您可能会看到一些视觉瑕疵。

现在,将“实例_数量”设置为您想要的鱼的数量。

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

有两种方法可以为多个时间轴设置每个实例的变换。第一个完全在编辑器中,在:参考:`多重网格实例教程<文档_使用_多重_网格_实例>`中进行了描述。

第二种方法是,遍历所有实例,并在代码中设置它们的变换。下面,我们使用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)

运行此脚本,会在多重网格实例位置周围的框中随机放置鱼。

注解

如果你关注性能问题,试着使用GLES2运行场景或摆放更少的鱼。

你注意到所有的鱼在它们的游泳周期中都处于相同的位置吗?这让他们看起来很机械刻板。下一步是让每条鱼在游泳周期中处于不同的位置,这样整个鱼群看起来就更加生动自然了。

Animating a school of fish

使用“余弦”函数给鱼动画的一个好处是,它们只需要一个参数,“时间”。为了让每条鱼在游泳周期中处于单独的位置,我们只需要偏移“时间”。

为此,我们将每个实例的自定义值“实例_自定义”添加到“时间”中。

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

接下来,我们需要向“实例_自定义”传递一个值。通过在上面的“for”循环中添加一行来实现这一点。在“for”循环中,为每个实例分配一组四个随机浮点数来使用。

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

现在这些鱼在游泳周期中都有独特的位置。你可以通过使用“实例_自定义”乘以“时间”让它们游泳更快或更慢,从而让它们更个性化。

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

甚至您还可以像更改每个实例的自定义值一样,尝试更改每个实例的颜色。

您会遇到一个问题,鱼虽然是有动画的,但它们并没有移动。您可以通过更新每帧鱼的每个实例转换来移动它们。尽管这样做比每帧移动数千个网格实例要快,它仍然可能是缓慢的。

下一个教程,我们将介绍如何使用:参考:`粒子<类_粒子>`来利用GPU,分别移动每条鱼,同时还能获得实例化的好处。