Welcome back to the stage of history! I recently spent a totally unnecessary amount of time putting together this absolutely ridiculous pixel art waterfall, so I figured I ought to do a technical breakdown of how I made it.
This thing is a combination of sprites, shaders, and no less than 13 particle systems! Dare I say it, the most advanced pixel art waterfall...ever? The whole process kind of reminded me of mixing a tune, where a bunch of mostly subtle moves add up to a more impressive overall effect.
Sorry in advance for the page load time, but I wanted to get a descent-enough frame rate on these GIFs so that you could see what's going on.
LightingSo the first thing I did was light the scene. This isn't exactly a part of the waterfall, but it makes a huge difference and it's sort of related.
I've discussed the tech for my lighting elsewhere, but most of it is accomplished via a fragment shader that draws a radial gradient and uses a grab pass to apply the gradient color with overlay blending. In this scene, I used various amounts of teal color in the radial gradients to simulate bounce light from the waterfall.
You probably know the overlay blend mode from photoshop or other apps. It's really an ingenious, simple algorithm for creating lighting effects without normal maps.
Of course, I couldn't resist adding some standard-issue god rays (also using overlay) with a bloom effect to emphasize direct sunlight from above.
Waterfall ShaderShaders aren't just for cool 3D people, they're amazing tools for pixel art games too! Even if you're not making a "hi-bit" pixel game, you can still use shaders to automate certain animation tasks, create patterned dithers, fake lighting, and more.
Sidenote: Devs are constantly yelling about how cool shaders are, so you should already be super hungry to learn more about them if you haven't yet. Unfortunately most of the tutorials I see are very hand-wave-y and tend to assume you know certain things about computer graphics and render pipelines. In particular, one of the most powerful tools for pixel games is the stencil buffer and in Unity I've found that this is really arcane and poorly explained in the docs.
If there's enough interest, I could be convinced to write a shader primer specifically for pixel artists + Unity, just let me know in the replies.
Anyways, there are two main effects that this shader accomplishes in a single pass, but I will discuss them separately. The first is a vertical "wiggle" effect that animates every column of pixels in the waterfall sprite, alternating between up and down for each column.
You may have noticed that the shader also applies a blue gradient from the bottom up.
This is literally just outputColor.b += input.uv.yThis wiggle effect is created by animating an offset value quantized to texels. The offset is applied to the vertical component of the uv and multiplied by direction. To get the value for direction (-1 or 1) which changes every other column, it's just floor(input.uv.x * _MainTex_TexelSize.z) % 2 * 2 - 1
If you're wondering where _TexelSize comes from, it's a very useful,
built-in Unity shader variable that will be magically populated with texel information for the specified texture.
You can clamp your offset UVs with saturate() so the texture doesn't wrap back around. It was also necessary to sample the texture using the original UV, and if that color had an alpha of 0, to not use the color sampled with the offset UV (in order to keep the shape of the sprite).
I came up with this wiggle effect because I wanted to create a blurry, distorted, rushing water look that would be appropriate for pixel art. As such, the typical methods you'd find in water shader tutorials that involve using noise/distortion textures, manipulating vertices, tessellation, etc. wouldn't be appropriate.
Part of my approach was to draw the rocks underneath the waterfall, and then lower the waterfall opacity in photoshop to something like 90% before I exported the flat image as a sprite. This is so a bit of those rocks behind the waterfall would be "baked" into the waterfall sprite. You can see this at the start of the GIF: the waterfall sprite is actually at 100% opacity, but you can still detect some of the rocks in there.
When I lower the opacity of the now-wiggling waterfall sprite at the end of the GIF, you can see the rock sprites underneath which are not moving. Since a low-opacity copy of those rock pixels are also wiggling in the waterfall sprite on top, it creates the illusion that the water is distorting/blurring the rocks behind it. It's kind of subtle, but I'm pleased with the results.
The next thing I did was add animated streams to the waterfall. This kind of thing is really the perfect candidate for shaders. It's the sort of animation that would be extremely tedious to do by hand, and inefficient because you'd have to create a bespoke animation of this sort for *every* other sprite like this in your game. Plus, if you aren't sure of the exact look you want, the shader lets you define and tweak parameters to very quickly try out different animations.
This kind of thing is super versatile and a huge time saver. The hours I've spent learning shaders are definitely paying huge dividends here. As a pixel artist, I absolutely recommend learning to write your own shaders for this reason...especially when you consider that visual shader-graph-style programs are deceptively complicated anyways and seem to be more 3D-centric (thus not catering to these kinds of pixel-perfect texture effects).
The basic trick here is to figure out a range of UV values that represents a stream. The lower bound of the range, or the starting position of the stream, will change over time. The upper bound is just the lower bound + the size of the stream. If this pixel's UVs falls between those bounds, then it's in the stream and we want to apply the stream color.
In the end, I took this basic idea and threw a bunch of params at it like speed, size, color, etc. You see me step through editing a number of these parameters in the GIF to arrive at the final settings. Mostly I would alter the effect a little bit, look at it, and then go "Man, it would be rad if I could like, make it fade out a bit too" and then go code up the new parameter for that.
The parameter that controls the curve of the streams is probably the most interesting bit and it's really just texelDistanceFromCenter^2 * curveAmount (and then of course, everything is quantized to the texel grid).
ParticlesAs I was working on this, I ended up with a ton of ideas for effects which would have been impractical to do with the shader. I implemented these as particles, and basically just started trying out ideas until I felt like I'd thrown everything against the wall and had found everything that stuck.
I'll just run through everything I ended up with as they appear in the GIF (from top to bottom).
1. At the very top, I added some very bright, nearly white particles that live for a short time and fade away to emulate sunlight catching the water at the top of the falls. These make use of Unity's lovely gravity modifier, and really add a lot of magic I think.
2. A bit lower down, all along the rocks on the cliff, I added 4 little particle systems that send out a small trickle of single-pixel particles to create the look of splashing water.
3. Along the left and right-most pixels of the waterfall, I added bright streams of pixels to simulate light catching the edge of the water. I'm sure this lighting isn't super realistic, but this is one of the most striking effects I added, and again I think it makes the waterfall look really magical.
4. Throughout the falls, I added 4 fixed-position streams of colored pixels similar to the bright particles on the edges. This just helps establish the flow of water, especially at the bottom where the shader-generated streams have faded out.
5. Similarly, I also added a single system at the top that creates some very long, and long-lived, streams across the falls that travel all the way to the bottom.
6. Finally, I created a nice bounce-y spray at the bottom, again making use of that convenient gravity modifier and some random x/y velocity.
I added some wobbly mist sprites at the bottom. These aren't particles of course, but I don't think we need a separate GIF for them! The sprites are actually created with another shader that draws a circle, quantizes to pixels, and then applies a parabolic fade out. I call the parameter for this fade out "haircut" because it originates from the top (it was originally conceived of for light-cookie sprites). In this case it's from the bottom up though, because I set scale.y = -1, a lovely property of Unity's transform component.
You might guess they're being moved with perlin noise or something, but it's really much simpler than that. They just move along some diagonal vector like ([-1, 1], [-1, 1]) out a number of pixels, and then back again in the opposite direction.
So that's everything! All told this took me a full weekend to finish. Happily, I can more or less copy-paste + tweak these effects for other types of streams moving forward (fountains, lava, etc.).