The Making of Turtle Quest

In this post I will talk about how my friend Jeffrey and I made TurtleQuest over the course of the weekend (Jan 25th-27th 2019) for the Global Game Jam. As the event began we tried to get into a group with an artist and failed (again as we do every year lol). So we ended up just doing a two man group and had to fend for ourselves on the art side. Not to worry, this wasn't are first rodeo lol.

We decided to take on the Assetless diversifier (no traditional art assets allowed), so the visuals of TurtleQuest were all procedurally generated or done with shader programs. The only exceptions were we used scaled Unity primitives for the turtle, shark and trash, and we used a particle system for the jellyfish (only with the unity default particle though so still counts??? hiyeahh).

Talkin' bout NOISE

Turtle Quest uses procedural noise for pretty much all of the art. From the textures on every object, to the shape and placement of the terrain/rocks/coral/kelp, to the rippling water refraction effect. Its all noise babeyyyy.

If you don't know what procedural noise is, I wrote a tutorial explaining it which also has links to a bunch of other better tutorials, so please take a gander!

Explaining Noise

Over the years I have accumulated and modified different noise functions I found and packaged them into two files. The first is a csharp file, Noise.cs, this is what I used for the terrain, rock, and coral generation so that way I could generate the meshes once and upload it to GPU. The second file is a shader include file, Noise.cginc. That's how I did all the texture effects, water refraction effect, and kelp rippling vertex displacement. So if you need your noise in a C# script use the first file, and if you need it in a shader, use the second file. Just make sure to #include "Assets/path/to/Noise.cginc" in your shaders if using the latter.

Additionally, if you are interested in Unity's upcoming Burst compiler and ECS stuff there is a new fast math library that includes its own noise functions as well. Open Package Manager -> enable experimental packages -> Unity.Mathematics if you want to try it out!

It's okay if you don't understand exactly what these functions do. The main way I learn is by just trying random stuff and seeing what happens. Eventually you kind of get a vague idea on what parameters to change first and you'll get better at it. Additionally when learning it's great to test noise in shaders because you don't even have to stop the game to see the changes as shaders can recompile while the games running!

Terrain Generation

I made code that generates a simple grid mesh in the xz plane and then I offset it in the y direction based on random noise generation. Here is the main noise calculations for the terrain:

Vector3 position = new Vector3(x, 0.0f, y);

float mountains = Noise.Ridged(position + offset1, 6, 0.0075f);
mountains *= .5f;
mountains += .5f;

float hills = Noise.Billow(position + offset1, 5, 0.005f);
hills *= .25f;
hills -= .25f;

float blendNoise = Noise.Fractal(position + offset2, 5, 0.003f);
float total = Noise.Blend(mountains, hills, blendNoise, -0.5f, 0.5f);

total = (total + 1) / 2.0f;
total = total * 0.8f;

return new Vector3(x, total * 60.0f, y);

So I make a position variable that stores the world position of each vertex that I then send into the noise functions. I take 2 noise samples, one more spikey type of noise (ridged) with lower frequency and higher applitude for big mountain shapes, then another more soft hilly noise (billow) with higher frequency but lower amplitude. I then take a 3rd simple noise sample (fractal) that I use to blend between the two types so theres two biomes kinda. Then I do some rescaling and return the same vertex positions but with a y based on all these noise functions!

Rock/Coral Generation

For the rock and corals I generate an icosphere, which is a type of sphere mesh where the vertices are very nicely spread out. And then I displace each vertex of the sphere with some random noise raising or lowering that vertex along its normal. Using lower frequency kinda slower noise I can make the rocks and using high frequency, very noisy noise I can make the corals. Both rocks and corals also use procedural textures like everything else, the rocks are some boring brown noise but the corals are a wacky shader function I wrote that makes a high frequency crazy bright pinkish colors!

Refraction Effect

For the underwater refraction effect, I just wrote an extra ripple function in Noise.cginc file (that all shaders were using) so at the end of their surf function they could all just add on this line to add the water refraction effect.

o.Albedo *= ripples(IN.worldPos);

It is using the built in shader parameter 'worldPos' so the effect is continous over all the objects that use it. If you used regular vertex position it would've been in object space so wouldn't be continuous over the whole scene. Now here is the ripples function.

float3 ripples(float3 wp) {
    return 0.5 + 0.5 * (worley(wp + float3(_Time.x * 7, _Time.x * 17, -_Time.x * 30), 3, .5, 0.5, 2.0, .5, 3.5));    
}

I will explain each piece of this glorious one liner ripples function. The first part '0.5 + 0.5 *' is to make the effect be lighter, because the worley noise function returns a value between 0 and 1, so this part rescales the total value to around 0.5-1.0, this is good because the ripple value is being multiplied with the albedo in all our objects and we dont want to want to make anything go full dark. 'wp' this part is providing our coordinate into the noise function which is the world position of the object. This world position is calculated in the vertex shader of all of our shaders and this is good because it makes the ripple effect look good on static and moving objects. Otherwise the ripples would still be going slow on fast moving objects and would look weird. The '+ float3(_Time...' part is basically making it so the ripples slowly scroll across all the objects, otherwise they wouldn't move at all on static objects. Each axis is just going at a different speed using the global _Time shader variable. The 3 is the number of octaves so basically how detailed the noise is, so thats not super detailed because they are just supposed to be blurry ripples, the rest of the function is pretty standard defaults for noise so its normal frequency and scales normally with higher octaves. The last two are cellType, which is when calculating the cellular noise (worley) distance we use just the first cell, and then distance function is using the max absolute distance in any dimension. Here is a good reference on worley noise that explains this better lol! Again though basically with noise you can just play around with values and see what happens, as long as you kinda understand what each parameter does you're golden.