This past week was a lot of travel for me, so rather than rush or put out nothing for the imposter system I’ve been working on, here’s a quick breakdown of the fish swim shader I made for Mocean.
Early on in the design of Mocean, the team wanted to focus on making a small yet enjoyable experience. Our Art Lead had recently played Abzu and was inspired by their work. As a result, we worked on a swimming simulator, but to keep within our month deadline, we scaled the game down to be a fish in a pond. The goal was feasible, especially with the three of us working on different key aspects of the game. I chose to create the player character: a koi fish. After modeling, and texturing the character it came time to rig the fish. But I had different plans in mind.
A Rig-less fish
In a GDC Talk, Matt Nava of Giant Squid Studios sheds some light on how the team animated the fish in their game. Using vertex animation, the team was able to achieve “side to side translation, yaw rotation, and panning rotations along the spine”. This allowed the team to animate a wide range of fish with just one shader.
I immediately researched vert-frag shaders to learn about applying sine-wave displacement to the mesh vertices. During that time, I found a flag shader that used similar methods.
y = a sin(bx)
z+= _Amp * (sin(z + t*_SpeedX) *_Freq)
v.vertex.z += sin((v.vertex.z + _Time.y * _SpeedX) * _FrequencyX)* _AmplitudeX;
The problem I ran into was when the frequency would change at run-time. When the player sped up, increasing the frequency of the sine wave worked as expected and the fish appeared to swim faster. However, when the player slows down and the frequency decreases, the sine wave appears to “rewind” because the time variable is multiplied by a smaller number.
To remedy this, I needed to expand my sine function to include the phase of the wave. With this addition to the sine function, I can now calculate the next frequency the wave will have. Doing this lets me smoothly transition, avoiding the rewind effect.
y = a sin (bx + c)
y(t) = A * sin(ωt + θ)
y(t) = Amplitude * sin((Angular Frequency * time) + phase)
v.vertex.x += sin(((0.05 + _Time.y * _SpeedZ) * _FrequencyZ) + _Phase) * _AmplitudeZ * _HeadLimit;
In addition to this, I needed to calculate the next frequency, like I mentioned above. To do this, I had a function called every frame that would determine the next frequency from the phase and set it to the current. This gave me the smooth transition I needed
Next week I'll be back to the imposters. Until then enjoy the fish!
Play the game on itch.io