用MultiMeshInstance动画化数以千计条鱼

ABZU 中使用的一种技术, 该技术使用顶点动画和静态网格实例, 来渲染和制作成千上万的鱼动画. Shader 和 MultiMeshInstance 实现。使用下面的技术,即使是在低端硬件上你也可以渲染成千上万的动画对象。 我们将从一条鱼的动画开始. 然后, 我们将看到如何将该动画扩展到数千条鱼.

动画化一条鱼

Plane Mesh 菜单并创建一个新的 ShaderMaterial. 这是我们用于示例图像的鱼, 您可以使用任何您喜欢的鱼模型. 注解 http://quaternius.com> 制作, 使用如下知识共享许可.CC0 1.0通用(CC0 1.0)公共领域贡献https://creativecommons.org/publicdomain/zero/1.0/ Skeleton 为对象做动画。然而,骨骼的动画是在 CPU 上做的,所以你必须为每一帧计算成千上万的操作,因此就不可能有成千上万的对象。在顶点着色器中使用顶点动画,你就可以避免使用骨骼,而是完全在 GPU 上,使用几行代码来计算完整的动画。 动画由四个关键帧动作组成:

  1. 从一边运动到另一边
  2. 绕着鱼的中心作旋转运动
  3. 平移波动运动
  4. 平移扭转运动 所有的动画代码都在顶点着色器中, 并由uniforms控制运动量. 我们使用uniforms来控制运动的强度, 这样你就可以在编辑器中调整动画, 并实时看到结果, 而不用重新编译着色器. VERTEX . 我们希望顶点在模型空间中, 使运动总是相对于鱼的方向. 例如,side-to-side将始终使鱼在其左至右的方向上来回移动, 而不是在世界方向的 x 轴上. 为了控制动画的速度, 我们将通过使用 “时间” 定义自己的时间变量开始.
    1. //time_scale is a uniform floatfloat time = TIME * time_scale;
    TIMEcos 抵消 VERTEX.x 来制作. 每次渲染网格时, 所有顶点都会移动到 “cos(时间)” 的数量.
    1. //side_to_side is a uniform floatVERTEX.x += cos(time) * side_to_side;
    生成的动画看起来是这样的: 接下来, 我们添加轴心点. 因为鱼以(0, 0)为中心, 我们所要做的只是将 “VERTEX” 乘以旋转矩阵, 使其围绕鱼的中心旋转. 我们构造一个旋转矩阵, 如下所示:
    1. //angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around//pivot is a uniform floatfloat 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”.
    1. VERTEX.xz = rotation_matrix * VERTEX.xz;
    在只应用枢轴(pivot )的情况下, 您会看到这个: body . body 是一个浮点数,在鱼的尾部是 0 ,在头部是 1 .
    1. float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2
    下一个运动是沿着鱼的长度向下移动的余弦波. 为了让它沿着鱼的脊柱移动, 我们用脊柱的位置来偏移输入到 “余弦” 的位置, 也就是我们在上面定义的变量 “身体”.
    1. //wave is a uniform floatVERTEX.x += cos(time + body) * wave;
    这看起来很像我们上面定义的左右运动, 但在这个例子中, 通过使用 “身体” 来偏移 “余弦”, 沿着脊柱的每个顶点在波浪中都有不同的位置, 使它看起来像是沿着鱼移动的波浪. 最后一个动作是扭转,也就是沿着脊柱滚动。类似轴心运动,我们首先构造一个旋转矩阵。
    1. //twist is a uniform floatfloat 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 轴为中心.
    1. VERTEX.xy = twist_matrix * VERTEX.xy;
    这是应用扭曲的鱼: 如果我们一个接一个地应用这些运动, 就得到一个类似液体凝胶似的运动. mask (遮罩)。 mask 是个浮点数,从鱼头的 0 过渡到鱼尾的 1 ,我们用 smoothstep 来控制在哪里进行由 01 的过渡。
    1. //mask_black and mask_white are uniformsfloat mask = smoothstep(mask_black, mask_white, 1.0 - body);
    COLOR 设置成 mask 后这条鱼的样子: mask 就可以把动作限制在后半部分。
    1. //wave motion with maskVERTEX.x += cos(time + body) * mask * wave;
    mix . mix 允许在完全旋转的顶点和未旋转的顶点之间混合顶点位置. 需要使用 mix 而不是将 mask 乘以旋转后的 VERTEX , 因为不是将运动加到 VERTEX 上, 而是用旋转后的版本替换 VERTEX . 如果把它乘以 mask , 就会把鱼缩小.
    1. //twist motion with maskVERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);
    将四个动作组合在一起, 就得到了最终的动画效果. 继续发挥uniforms的作用, 以改变鱼的游泳周期. 你会发现, 你可以用这四个动作创造出各种各样的游泳方式.

    制作一群鱼

    Godot可以使用MultiMeshInstance节点轻松渲染成千上万的相同对象. 创建和使用多重网格实例节点的方法, 与创建网格实例节点相同. 在本教程中, 我们将把多重网格实例节点命名为 “鱼群”, 因为里面会有一群鱼. MultiMesh,然后在 MultiMesh 上添加使用上述着色器的 Mesh。 Color 传入 4 个多用途变量。 “实例数量” 指定要绘制的网格的实例数量. 现在, 将 “实例数量” 保留为 “0”, 因为当” 实例数量 “大于 “0” 时, 您不能更改任何其他参数. 我们稍后将在GDScript中设置 “实例数量”. “变换格式” 指定使用的变换是3D还是2D. 对于本教程, 选择3D. color_formatcustom_data_format , 你可以在 None , ByteFloat 之间选择. None 意味着你不会将这些数据(无论是每个实例的 COLOR 变量, 还是 INSTANCE_CUSTOM )传递给着色器. Byte 意味着组成你传入的颜色的每一个数字将被存储为8位, 而 Float 意味着每一个数字将被存储为一个浮点数(32位). Float 速度较慢, 但更精确, Byte 占用内存较少, 速度较快, 但你可能会看到一些视觉上的伪像. 现在, 将 “实例_数量” 设置为您想要的鱼的数量. 接下来, 我们需要设置每个实例的变换. MultiMeshInstance tutorial 中进行了描述。 第二种方法是, 遍历所有实例, 并在代码中设置它们的变换. 下面, 我们使用GDScript遍历所有实例, 并将它们的变换设置为随机位置.
    1. 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运行场景或摆放更少的鱼. 你应该已经注意到所有鱼的游泳动作都是同步的了吧?这样看上去非常机械。下一步我们要做的就是让每一条鱼都处于游泳周期的不同位置,这样整个鱼群看起来就会更自然。

    动画鱼群

    使用 “余弦” 函数给鱼动画的一个好处是, 它们只需要一个参数,”时间 “. 为了让每条鱼在游泳周期中处于单独的位置, 我们只需要偏移” 时间”. 为此, 我们将每个实例的自定义值 “实例_自定义” 添加到 “时间” 中.
    1. float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
    接下来, 我们需要向 “实例_自定义” 传递一个值. 通过在上面的 “for” 循环中添加一行来实现这一点. 在 “for” 循环中, 为每个实例分配一组四个随机浮点数来使用.
    1. $School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))
    现在这些鱼在游泳周期中都有独特的位置. 你可以通过使用 “实例_自定义” 乘以 “时间” 让它们游泳更快或更慢, 从而让它们更个性化.
    1. //set speed from 50% - 150% of regular speedfloat time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
    甚至您还可以像更改每个实例的自定义值一样, 尝试更改每个实例的颜色. 这时你会遇到一个问题, 那就是鱼是有动画的, 但是它们没有移动. 你可以通过每一帧更新每个鱼的实例变换来移动它们. 虽然这样做会比每帧移动数千个MeshInstances要快, 但还是可能会很慢. 粒子来利用 GPU,分别移动每条鱼,同时还能获得实例化的好处。