Animating thousands of fish with MultiMeshInstance
ABZU for rendering and animating thousands of fish using vertex animation and static mesh instancing. Shader and a MultiMeshInstance. Using the following technique you can render thousands of animated objects, even on low end hardware. We will start by animating one fish. Then, we will see how to extend that animation to thousands of fish.
Animating one Fish
MeshInstance and add a new ShaderMaterial. Here is the fish we will be using for the example images, you can use any fish model you like. Note QuaterniusDev and is shared with a creative commons license. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/ Skeleton to animate objects. However, bones are animated on the CPU and so you end having to calculate thousands of operations every frame and it becomes impossible to have thousands of objects. Using vertex animation in a vertex shader, you avoid using bones and can instead calculate the full animation in a few lines of code and completely on the GPU. The animation will be made of four key motions:
- A side to side motion
- A pivot motion around the center of the fish
- A panning wave motion
- A panning twist motion All the code for the animation will be in the vertex shader with uniforms controlling the amount of motion. We use uniforms to control the strength of the motion so that you can tweak the animation in editor and see the results in real time, without the shader having to recompile.
VERTEXin model space. We want the vertices to be in model space so that the motion is always relative to the orientation of the fish. For example, side-to-side will always move the fish back and forth in its left to right direction, instead of on thexaxis in the world orientation.TIME.
//time_scale is a uniform floatfloat time = TIME * time_scale;VERTEX.xbycosofTIME. Each time the mesh is rendered, all the vertices will move to the side by the amount ofcos(time).The resulting animation should look something like this:
//side_to_side is a uniform floatVERTEX.x += cos(time) * side_to_side;VERTEXby a rotation matrix for it to rotate around the center of the fish. We construct a rotation matrix like so:
//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)));xandzaxes by multiplying it byVERTEX.xz.With only the pivot applied you should see something like this:
VERTEX.xz = rotation_matrix * VERTEX.xz;body.bodyis a float that is0at the tail of the fish and1at its head.
float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2cosby the position along the spine, which is the variable we defined above,body.
//wave is a uniform floatVERTEX.x += cos(time + body) * wave;bodyto offsetcoseach vertex along the spine has a different position in the wave making it look like a wave is moving along the fish. The last motion is the twist, which is a panning roll along the spine. Similarly to the pivot, we first construct a rotation matrix.
//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)));xyaxes so that the fish appears to roll around its spine. For this to work, the fish’s spine needs to be centered on thezaxis.Here is the fish with twist applied: If we apply all these motions one after another, we get a fluid jelly-like motion.
VERTEX.xy = twist_matrix * VERTEX.xy;mask.maskis a float that goes from0at the front of the fish to1at the end usingsmoothstepto control the point at which the transition from0to1happens.
//mask_black and mask_white are uniformsfloat mask = smoothstep(mask_black, mask_white, 1.0 - body);maskused asCOLOR:maskwhich will limit it to the back half.
//wave motion with maskVERTEX.x += cos(time + body) * mask * wave;mix.mixallows us to mix the vertex position between a fully rotated vertex and one that is not rotated. We need to usemixinstead of multiplyingmaskby the rotatedVERTEXbecause we are not adding the motion to theVERTEXwe are replacing theVERTEXwith the rotated version. If we multiplied that bymask, we would shrink the fish.Putting the four motions together gives us the final animation. Go ahead and play with the uniforms in order to alter the swim cycle of the fish. You will find that you can create a wide variety of swim styles using these four motions.
//twist motion with maskVERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);Making a school of fish
Godot makes it easy to render thousands of the same object using a MultiMeshInstance node.School, because it will contain a school of fish. MultiMesh, and to that MultiMesh add your Mesh with the shader from above. Color.instance_countspecifies how many instances of the mesh you want to draw. For now, leaveinstance_countat0because you cannot change any of the other parameters whileinstance_countis larger than0. We will setinstance countin GDScript later.transform_formatspecifies whether the transforms used are 3D or 2D. For this tutorial, select 3D.color_formatandcustom_data_formatyou can choose betweenNone,Byte, andFloat.Nonemeans you won’t be passing in that data (either a per-instanceCOLORvariable, orINSTANCE_CUSTOM) to the shader.Bytemeans each number making up the color you pass in will be stored with 8 bits whileFloatmeans each number will be stored in a floating-point number (32 bits).Floatis slower but more precise,Bytewill take less memory and be faster, but you may see some visual artifacts.instance_countto the number of fish you want to have. Next we need to set the per-instance transforms. MultiMeshInstance tutorial. The second is to loop over all the instances and set their transforms in code. Below, we use GDScript to loop over all the instances and set their transform to a random position.Running this script will place the fish in random positions in a box around the position of the MultiMeshInstance. Note If performance is an issue for you, try running the scene with GLES2 or with fewer fish. Notice how all the fish are all in the same position in their swim cycle? It makes them look very robotic. The next step is to give each fish a different position in the swim cycle so the entire school looks more organic.
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)Animating a school of fish
cosfunctions is that they are animated with one parameter,time. In order to give each fish a unique position in the swim cycle, we only need to offsettime.INSTANCE_CUSTOMtotime.
float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);INSTANCE_CUSTOM. We do that by adding one line into theforloop from above. In theforloop we assign each instance a set of four random floats to use.
$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))INSTANCE_CUSTOMto make them swim faster or slower by multiplying byTIME.You can even experiment with changing the per-instance color the same way you changed the per-instance custom value. One problem that you will run into at this point is that the fish are animated, but they are not moving. You can move them by updating the per-instance transform for each fish every frame. Although doing so will be faster than moving thousands of MeshInstances per frame, it’ll still likely be slow. Particles to take advantage of the GPU and move each fish around individually while still receiving the benefits of instancing.
//set speed from 50% - 150% of regular speedfloat time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
