Shaders For People Who Don't Know How To Shader: Smooth blending with Sine

Last time we learned about bitmasks! Now we're going to learn about using sine functions to smoothly blend between colors!

Setup

Go to https://opengameart.org/content/forest-background and download the files into your project, then unzip the files.

Go into the layers folder and select all four images at once. In the inspector, change the texture type to Sprite, change the Mesh Type to Full Rect, change the wrap mode to Repeat, and change the filter mode to Point, then click Apply.

You might notice there are three filter modes available: bilinear, trilinear, and point. These determine the amount of blending between pixels on a texture. When you bilinear or trilinear filtering on a low-res photo, they blur them a bit which helps the image look less pixelated, but when we're dealing with pixel art we don't want any blurring, we want to see those beautiful pixels!

Add four sprite renderers to your scene, keeping their transform positions at origin (0, 0, 0) and put the sprites into them. Set their Order In Layer so that parallax-forest-back-trees is in the back, then parallax-forest-lights, parallax-forest-middle-trees, and then parallax-forest-front-trees.

While doing this, you will probably want to hit the 2d button at the top of the scene window so you can look at everything head on.

When you're done, you should have something like this:

Making the colormasks

Just as we did for the last sprite-focused tutorial, we're going to make colormasks for these layers. Pull each image up in your favorite image editing program and change each color to pure red, green, or blue. Because these are pixel art, it's really easy to use a paint bucket tool set to not follow adjacent to change all the colors at once.

I am also attaching the masks I made in case you don't feel like doing it yourself right now.

Because there were more than three colors in the foreground image, I actually cut the grass highlight color out and put it in its own image.

Be sure to import these masks into your project with the same settings as the other sprites!

Okay now go through your scene and replace each sprite with the mask instead! If, like me, you separated the foreground into an additional layer, you'll also need to add one more sprite renderer for it (select one and press ctrl + D to duplicate it!)

You should now have this. 

Majestic, isn't it?

Making the shader

In your project, go to or create a folder in your Shaders folder called Unlit, then right click and select Create > Shader > Unlit Shader.

Name the new shader ColorMaskBlend. Double click it to open in Visual Studio.

Making a sprite shader

Right now this shader is set up assuming we're throwing it on a quad, so we just need to tweak it a bit to work with a sprite renderer instead. We've made a sprite shader before so this shouldn't be too hard. 

Change the name up top to "Xibanya/Unlit/ColorMaskBlend" then in the properties add the attribute [PerRendererData] in front of _MainTex. (That means we don't put the main texture into the material ourselves, instead it takes the main texture from something else -- in this case, the sprite renderer!)

You'll want to change your tags to the ones shown here, then put Blend SrcAlpha OneMinusSrcAlpha under your tags.

(You might remember from past lessons that Blend SrcAlpha OneMinusSrcAlpha is the default kind of transparency!)

Inside the pass, we'll get rid of the junk related to fog and update our structs to include color, since we'll be getting a color from the sprite renderer instead of the material properties like usual.

In the last few months I've also gotten in the habit of having the v2f SV_POSITION interpolator be called "pos" instead of "vertex" to further distinguish it visually from "vertex" coming in from appdata, but that is a matter of taste.

Then some quick modifications of the vertex and fragment functions to pass along the color and get rid of the fog stuff and we'll have this.

Save and go back to Unity.

In your project files, make a new material for every mask sprite you have.

Select all of them at once. Using the Shader drop down in the inspector, put them all on ColorMaskBlend.

In your scene, select each sprite one at a time and drag the corresponding new material into the Material slot.

When you're done, everything should look exactly the same as before! Hooray!

Color Mask

Now we'll adjust our shader so that we can replace the red, green, and blue channels with a color we set in the material inspector. We've done this before in the last sprite tutorial, so you're already an expert!

Add three new color properties like this

Then declare them as half4's in your subshader. I'm going to put them all on the same line, but you can put them each on their own line if you like. Do whatever makes it easier to read your own code!

Now the frag shader is actually where we're going to do things differently from before. In the past we made colormasks assuming a base image and a mask image, but in this shader we're just going off of a mask image alone. So we'll start with this:

We won't want to multiply the color we're getting from the sprite renderer until the very end, because we need the unaltered colors from the mask to know how to blend our other colors.

Since there's no base texture to blend these with, we can just add our color properties to our final color directly. Easy peasy!

Save and head back to Unity to have a look. If you did it right, everything should be white right now!

Now the fun part. Select each sprite, expand the material section of the inspector, and put in some nice colors!

Blending over time

The hardest part about that last step was just picking colors. So many neat color schemes! It's too hard to settle on just one. Fortunately we don't have to! We can set up our shader to blend between different sets of colors!

Add these new properties

And declare 'em in the subshader.

Now's not a bad time to save and go back to Unity and pick out colors you want to use to blend. If you can't decide, picking the color on the opposite side of the color wheel will work nicely.

We'll use the x, y, and z values of the _Speed property to control the speed of the blend between r, g, and b, so we can also set that to something higher than 0 now too. Then back to Visual studio!

We'll want to multiply the speed by _Time.y to figure out how to lerp between the color. We've used _Time.y before when we were scrolling UVs, so this oughtta work the same way, right?

Well...

that's not the gif cutting off, it really is that janky-looking. Remember that frac discards the parts of a number that are higher than one, which makes it handy when working with _Time.y, since it allows the number to grow infinitely big while giving us a result between 0 and 1 (and remember, we like values between 0 and 1 because they're easy to work with!) So it turns something like 5.15 into 0.15, 1024.324 to 0.324, 0.75 stays 0.75, etc. But that just means that when the value loops back around to 0 there's a sudden jump back to the first color. What we want is to bounce back and and forth between 0 and 1. But how can we tell the shader "when you get to one, start going backwards until you get to 0, then start going forwards again?"

(btw you may have noticed in the code snippet above I put _Speed.r, _Speed.g, _Speed.b, instead of _Speed.x, _Speed.y, _Speed.z -- they do the exact same thing, retrieve the first value of the four part vector, and I decided to put .r because that value is associated with the red channel of the mask, but if you find that confusing, you can use .x instead! also, cool fact, if you're using a color, you can get red by putting col.x too if you want! .r and .x are just options for readability, and as I always say, use whatever you find easiest to read!) 

Give me a sine

As ya'll know I'm really bad at math and any time math chat starts up my eyes glaze over and my brain shuts off, but there's a cool math thing we want to use, and it's actually really easy.

If you go here  https://www.desmos.com/calculator you can mess with a graphing calculator. In the left pane you can type in equations to see graphed, like here's y=x

If you put in y=sin(x) you get this nice squiggle!

Check out how it goes evenly up and down, forever. This is just what we need to have our smoothly looping blend! We just need to tweak it a little. If you zoom in (there are + - zoom buttons on the right side of the screen) it'll be clear that the squiggle bounces between -1 and 1.

And we don't want -1 to 1, we want 0 to 1. The range is twice as big as we want it to be, so we should multiply it by 0.5.

Okay that makes it go from -0.5 to 0.5, which is the right range, but we want to nudge that up a bit.

Yaaa perfect! Does that * 0.5 + 0.5 look familiar? It should, because when we do toon shading we always adjust our dot product like that!

When I was in school I think I heard the teachers say some fancy things like "amplitude" and "frequency" but as I mentioned before, that was math chat so my brain had shut down by that point. What I do understand is that if I add something to the sine wave the whole shape scoots up and if I subtract it goes down, and like a color, if I multiply it by a number less than one, the range gets smaller, and if I multiply it by a number bigger than one, the range gets bigger. Don't ask me about any more math things than that 'cuz that's about all I know.

With that in mind, let's tweak what we've got in our frag function!

We can take out the frac because we're always going to get something between 0 and 1 thanks to how we're using that * 0.5 + 0.5 pattern. 

There we go! Shader serenity.

Poor frac, thought we were gonna take it out for a spin but we ended up using something else instead. But don't we have a spare float we aren't using? I'm talking about _Speed.w. Let's use that to scroll the UVs so we can make this thing parallax the way the original creator intended!

We'll use _Time.x here because it's regular _Time.y divided by 20, which makes it much slower and I think our parallax shouldn't be too fast. If you want to have faster values though, you can use _Time.y instead. (if you don't understand how the UV scrolling works, you can brush up on it in the lesson about it here!)

Save, go back to Unity, and put in some speeds for the different layers by putting values into the W field in the Speed property in the material inspector. 

Like we learned in the lesson on parallaxing, stuff far away should seem to change position the slowest, so the absolute value of the speed of the layers should be smaller the farther back we go. I say absolute value because whether the values are positive or negative will depend on if you want it to seem the viewer is walking left or right, but it's the actual distance from 0 that determines the speed. Also if you split the foreground layer into two different sprites, you'll want to make sure both layers are using the same speed.

(If the colors of your sprites are looping but they seem to be getting cut off at the edges of the original sprite, make sure you imported the sprites with the Mesh Type Full Rect.)

That's nice, isn't it? If you want to use the same speeds as me, this is with the background at 0.05, sunbeams at 0.1, middle trees 0.2, and foreground layers at 0.35.

Oh yeah, don't forget, if you want you can still use the color from the sprite renderer itself!

And you can also blend opacity too

This would probably be a good base for a number of interesting sprite effects. I bet you can think of a few! Let me know what you come up with!

The shader we made in this tutorial is attached to this post as ColorMaskBlend.shader. If you have any questions or want to share what you come up with, let me know in the comments here, on Twitter, or in Discord. And if this tutorial helped you out, please consider becoming a patron!  

Next: Masked UI Images 

This tutorial is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike4.0 International License. The code shared with this tutorial is licensed under a CreativeCommonsAttribution 4.0 International License. The images used in this tutorial are by Luis Zuno, shared under a CC0 license. You can support his work here:  https://www.patreon.com/ansimuz 

Become a patron to

36
Unlock 36 exclusive posts
Be part of the community
Connect via private message