Last time we learned about the deferred rendering path. Now we're gonna learn about bitmasks. Today's tutorial is going to be a little different than usual since we won't be making a shader together, but hopefully you find it enlightening! And if not, at the very least you can lol at my bad drawings.
Throughout this lesson, you'll see a few words with asterisks or a sort of cross symbol after them (the symbol is actually called a dagger!) If you already understand those words, then you can just keep reading as usual. If you don't know what those words mean or want to read more about them, highlight the symbol with your mouse, press ctrl + c to copy it, press ctrl + f to open up the Find bar in your browser, then press ctrl + v to paste the symbol in.
Press the down arrow in the find box to go to the explanation, and when you're done reading, press the up arrow to go back to where you were.
Sometimes you don't need anything too fancy, but you need something with slightly more QoL features than the Unity built-ins. This was the situation I found myself in when I just needed an additive particle shader without a bunch of overly complicated special effects crammed into it (as tends to accumulate in the shaders I make for personal use haha...) I put this together using the built-in "Legacy Shaders/Particles/Additive" shader as a base, keeping an eye towards performance, since I'm using this for a texture sheet animated particle effect.
This shader uses a Color Mask, which is something we've only looked at in passing. A shader color mask lets you specify which color channels the shader will actually write to, which is handy if you want to combine different color channels from different passes. In the shader itself, you can hard code the color mask you want by going to the section of the subshader right above the pass you want to mask and putting ColorMask then the channels you want right after, so, ColorMask A if you want that pass to only write to the alpha channel, ColorMask RB if you just wanted to write to the Red and Blue channels, and so on.
This is quite easy to do when hard coding the values, but if you want to be able to select them on an individual basis in the material editor, you're going to have to either write a custom editor (tedious!) or learn a little bit about how bitmasking works.
You may have known that in your shader, you can have a dropdown for any enum without writing a custom material editor by putting [Enum(Namespace.Enumtype)]_Property("Label", int) = 0 (or whatever) You can certainly do that for the ColorWriteMask enum with [Enum(UnityEngine.Rendering.ColorWriteMask)], but without a custom editor, you'll pretty quickly run into a problem!
This is the ColorWriteMask enum. Note that RGB is not an option! If you use a default material inspector enum popup to access these values, you won't be able to select RGB, which happens to be one of the most common ColorWrite masks you'll want to use!
Fortunately we can still just manually set the values of the enum popup, like with [Enum(Off,0,Front,1,Back,2)] _Cull("Cull", int) = 0, which you've probably seen in these shaders a few times now. But to do that we have to know the numbers we want to be able to pick from ahead of time. So how do we figure that one out? (also it doesn't have to be an enum dropdown, you can just have a field for typing in a number, but that still leaves you stuck at the same problem, just what number do you put in?)
The key to getting the value needed into a convenient enum popup is to understand where the numbers assigned to the enums are coming from. You've probably noticed they don't go up linearly, and these enum values go up in numeric order, but they don't go up in the actual order of the channels red, then green, then blue, then alpha.
Alpha = 1,
Blue = 2,
Green = 4,
Red = 8,
All = 15
Instead, not only are they listed in reverse order, they're incrementing in powers of two* -- except All, assigned the value 15, which is not even a power of two! What gives?
The reason why has to do with binary, a way of writing numbers that only uses 0 and 1. I know every time I start reading about binary my eyes start to glaze over, but this part isn't too hard, as long as you pretend it isn't binary and instead think of it like playing with one of those toys you had as a really little kid that had differently shaped holes for pushing blocks through.
so we got these here plastic blocks ...I am not an artist, OK??
and we have the playset workshop table where you fiddle around with the blocks. You can see pretty clearly where each of these shapes goes. So you could think of the block tabletop with the hole in it like the mask, 'cuz it only lets a specific shape through.
So if you have something like this, it's gonna let the cylinder and and the triangular prism through, but not the cube.
In this case, if you see a combination of a triangle and a circle, that pretty obviously means that the hole will let both shapes in. And if you had a tool for punching triangle-shaped and circle-shaped holes in things, you wouldn't need a new tool to make the triangle-circle-shaped hole, you could just punch the same spot in the table with both tools. 'Cause you know they operate on tight margins at the table-with-shape-holes-for-toddlers factory.
Pop quiz! Each of these holes will allow two of our toy blocks to pass through! Can you tell which blocks can go through which holes?
The answers probably won't surprise you!
We can pretty easily tell which blocks these holes will allow to pass through. We know that the toy block shapes we have are a cylinder (circle shape - round sides, no corners), a triangular prism (triangle shape - flat sides, three corners), and a cube (square shape, flat sides, 4 corners) so if we have a slot that isn't any of those exactly, like round sides AND three corners, then that means it's a combo. That seems obvious now, but that's only because of all that hard work you put in as a two year old at the toy-block-slot-pushing-table-thing. And you thought that would never pay off!
When I was a little kid I also used to play a lot with rubber stamps, and you can see a similar idea at work there. Let's say we have 3 rubber stamps that stamp images like this:
By stamping the same place with two different stamps, you can combine the stamps' patterns.
The process by which these twice stamped patterns are made is just like the concept of squishing the two different shapes together to make the combined holes.
In this case, we can look at that end result and know that it's a combination of two different stamps. How? 'cuz there are two circles colored in. The only stamps we have are ones that have one colored circle in each, so if the patterns are lined up and two circles are colored in, then that must be the result of stamping twice.
so back to binary, it just so happens that when you write powers of two in binary, you'll see something interesting.
- 0 in binary is 0000
- 1 in binary is 0001
- 2 in binary is 0010
- 4 in binary is 0100
- 8 in binary is 1000
In binary, these powers of two just have one, uh, 1! It makes them look really distinctive † (so distinctive that you can spot 'em without knowing binary!) Imagine if we thought of each number with just one 1 as the pattern of an individual stamp that we have, and if we spot any number that's got more than one 1, think of it as the combined pattern made by stamping over the same spot with two or more different stamps.
Circling (ha) back to the ColorWriteMask enum,
We can visualize the individual channels of RGBA getting squished together like this. (Color in a circle means it’s filled in, no color means it’s empty.) In the enum, Red is 8, which is 1000 in binary, so we can imagine it like the top row here. Green is assigned a value of 4, or 0100 in binary, and we can imagine it like the second row here, and so on.
It just so happens that 1 + 2 + 4 + 8 = 15, which matches the All enum value. And 15 in binary is 1111 - just like how after we squished everything together, all four circles are colored in.
So if we want RGB, with no alpha, we want something that looks like this:
That's 1110 in binary. (cuz we have the three circles colored in, going left to right, then the last one's empty.) Fortunately we don't even have to know how to convert to and from binary cuz we already have this enum telling us that All (RGBA) is 15, and Alpha is 1, so All without Alpha (RGB) is 15 - 1. I'm terrible at math but even I know that 15 - 1 = 14! Convenient how that works out!
Apropos of nothing, this'll be a good tidbit to file away in your brainpan for later:
Now hold on, you might say, that's not special! Regular numbers in decimal‡ work like that too if we put zeros in the places we haven't used yet! And I would say, you are exactly right! But we don't put zeroes in the places we haven't used yet when we're using numbers in everyday life, now do we? When you use a number for counting how much of something there is, it's just a waste of space to put zeros in front of places we haven't used yet because they don't tell us anything we wouldn't know otherwise. ("This house was built 35 years ago", vs "This house was built 0035 years ago,"...it's still 35 years either way!) The difference here is that we are not using these numbers for counting.
We're not saying Green is 4 because there's 4 of anything in there. We're just saying green is 4 to use the pattern made by how it's written in binary, 0100, like a rubber stamp. And since we're using these numbers for their pattern, the 0s in front are important, cuz they're not really numbers at all, they're more like pictures that can tell us information.
With that in mind we can now see why the folks at Unity put the values of the ColorWriteMask enum in the order that they did. While the numbers seem to be going backwards when written in decimal (R-G-B-A -> 8-4-2-1), if we're going to make rubber stamps to represent red, green, blue, and alpha that could be combined like in the image with the colored circles -- if we're going to be using them in a bitmask -- then they're in the exact right order!
Anyhow all that just to say, yeah to get RBG from RGBA you subtract one and stuff that number into the property enum. Tadaa!
Okay so that was a bit of a lengthy explanation for one line of code, but this is a really really useful trick. Just with a single integer (a whole number with no fractions or anything) you can set a ton of combinations of stuff! There's a lot more that can be done with it that we didn't even go over today, but imo even thinking about binary is scary enough, so if you made it this far, you're definitely somebody I'd want in the foxhole with me in the Robot Wars.
So, continuing on with the shader, that _ColorMask value is used like this
Which, if you may recall, is a lot like how we looked at ColorMasking in the glass shader we made not too long ago! And we'll be returning to the idea soon when we start diving into stencil masks.
So that's what I got for ya today, you can find my improved additive particle shader attached to this post. Give it a whirl and let me know if you have any questions or need any help!
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.
* A power of two is a number you can get by multiplying 2 by itself a certain number of times. 1 counts because any number raised to the 0th power is 1. So the numbers we see here are
- 2⁰ = 1
- 2 (any number raised to the power of one is just itself!)
- 2² (2x2 or "two squared") = 4
- 2³ (2x2x2 or "two cubed") = 8
† The reason why powers of two in binary have just one 1 in them isn't a coincidence, it's because binary is a base two number system. Base two means you only go up two times before adding another digit. In contrast, decimal, the number system we use in everyday life, is a base ten system, which means you count ten times before adding another digit. And here are what the first few powers of ten look like:
- 10⁰ = 1
- 10² (10x10, or "ten squared") = 100
- 10³ (10x10x10, or "ten cubed") = 1000
Look familiar? It's more obvious if we add the zeroes in front of the numbers in decimal too.
- 1, or 2⁰ = 0001 in binary, 10⁰ = 0001 in decimal
- 2 = 0010 in binary, 10 = 0010 in decimal
- 4, or 2² = 0100 in binary, 10² = 0100 in decimal
- 8, or 2³ = 1000 in binary, 10³ = 1000 in decimal
Yep, they all just have just one 1 in them. That's because whatever base number system you use, the base multiplied by itself any number of times is always going to be written with just one 1 and the rest of the digits will be written with 0.
‡ The normal way we write numbers (in base ten, using 0-9) is called decimal. I know this is confusing because most of the time when you see the word "decimal" it's for talking about an amount that's less than one but bigger than 0, so you'll need to tell the difference from context. If someone says "in decimal" or "to decimal" (like you might say "in English" or "to English") then they're talking about the number system that goes up to 9 before adding another digit. If someone says "a decimal" or "the decimal" then they mean a fractional value.