Thursday, 8 September 2016

Pushing onward into Shaders!

Over the last few weeks I've been attempting to teach myself to do some basic stuff with Shaders in Unity, it's gone pretty well, I've found some cool resources and I've experimented a lot (and I've seen this color a lot as well)

Do not adjust your screens, this is intentional
But all that learning had a few positive outcomes. I wrote a couple really neat shaders, one of them I'm pretty happy with, an ocean shader for a top down scrolling shooter I'm working on. It looks something like this:



It's not the best thing under the sun, but I'm fairly happy with it. It's not going to blow Crysis' water out of the, uh, water? But for mobile it's quite nice. One major note, there are no reflections, I'd like to add them but since the shader is for mobile we'd need to basically draw the entire screen twice and our game is unfortunately not running fast enough to do that. Shame, but you can't win em all. Anyway, I figured the best way for me to solidify my knowledge is to try and share it with the world. Lets have a look at how I did it!

Shader "Empty/OceanTex"
{
    Properties
    {
         _Color ("Outer Color", Color) = (1,1,0,1)
         _Color2 ("Inner Color", Color) = (1,0,1,1)
         _DistortFactor("Distortion", Range(0.01,1)) = 0.1
         _MainTex("ColorMap", 2D) = "white" {}
         _DistortMap("DistortionMap", 2D) = "white" {}
    }

First up is the start of the file, and the properties section. Shader declares it as a, shader, and the string afterwards tells unity what to label the shader as in the material menu, a forward slash indicates to set up a subfolder in the list of shaders.

Within properties I define 5 variables, Two colors to form the gradient you see on the water above (the edges are ever so slightly darker than the centre)
Next comes the distort factor, which is simply how powerful the distortion of the waves is.
Finally the Main texture and the Distortion map, the main texture is the texture that is getting distorted, basically the ocean floor. You can try different approaches, I found it looked quite nice without any detail just some blurry blue, but having some objects drawn onto the texture like rocks or maybe fish can look quite nice too. The distortion map is simply a map for how to distort and ripple the surface of the water. Simple stuff.

Now for the subshader section.

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
          
            #include "UnityCG.cginc"


We open by declaring a subshader, and setting it's tags. For this one we render as opaque since it's the background. Everything else renders after this. LOD 100 declares the level of detail level, 100 is very low so even on low end devices, this shader will be used (which might actually be a bad idea since it's a little complex, maybe I should fix that...) 

Then we go to the Pass which is where the shader actually draw stuffs, CGPROGRAM indicates the beginning of our shader code, we're not in Unity any more... The two pragma lines indicate we're mapping the vertex and fragment functions to vert and frag respectively. (Vertex is the function used to calculate color at each vertex, while fragment is used to calculate the color of the pixels)
We also include UnityCG.cginc, it has some helper functions, honestly I can't remember if I use them. I'm new to this too so cut me some slack.
 
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv_MainTex: TEXCOORD0;
                float2 screenPos : TEXCOORD1;
            };

            struct v2f
            {
                float4 vertex : POSITION;
                float2 uv_MainTex : TEXCOORD0;
                float2 screenPos : TEXCOORD1;
            };


            sampler2D _DistortMap;
            sampler2D _MainTex;
            float4 _Color;
            float4 _Color2;

            float _DistortFactor;


Finally we start to get into the shader proper... Here we start to declare our variables. The struct appdata contains information sent to the shader by Unity, in our case we access the position of the vertex (this is in local space, as far as I recall), the UV coordinates on each vertex (basically pointing to where on the texture we draw) and the screen position, in this shader I use screen position since the shader covers the whole screen, but with some simple tweaks getting it to work on a mesh itself shouldn't prove too difficult. 

Next we declare v2f, this is the struct we'll passing to the frag function. Basically inside vert() we define our v2f object, fill in the data we want based on the vertex data, and send it off to the frag function. The graphics card automagically does some stuff to lerp the data between vertexes (so if vertex A is green and vertex B is red, the pixels in between those vertexes will know how to shade themselves between the two colors)

Next, we redeclare our variables from the properties section. Why? We're in CGPROGRAM now, which is running on the graphics card, not the CPU! This is a completely different program so it doesn't actually know about the stuff we declared in the properties field. Naming these variables the same as up in properties tells Unity to do some magic stuff and send the data we specify off to the graphics card. All you need to know is, if you redeclare the variables down here you can access them from the shader. Yay!

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv_MainTex = v.uv_MainTex;
                o.screenPos = ComputeScreenPos(o.vertex);
                return o;
            }


Finally we get to some actual code getting run, a lot of stuff has to be declared and redeclared but hopefully it's all worth it. This is the vert function, which as I said earlier is run on each vertex. We hand in Appdata (which we declared before, and has been magically populated by Unity, thanks!) and we're expected to return a v2f, for use in the frag function. So this function creates a new v2f called o.
Then, we set o's vertex to  mul(UNITY_MATRIX_MVP, v.vertex)
What the hell is that? You ask. Unity Matrix MVP is a helpful matrix which converts local coordinates into worldspace coordinates, MVP stands for Model View Projection. I don't have time to explain matrices or what all that stuff is (and to be honest, I don't actually know) but like I said before, multiplying it with the vertex position converts the vertex into a worldspace position.

Next, we set the v2f's uv coordinate to be the same as the one handed in by the appdata, we could modify it here if we wanted but we don't really care.
Finally, we calculate the screen position based on the output vertex (this distinction is important, remember our output is in worldspace, not local space) and return our v2f object. All done and dusted happy as larry. Great stuff.

           
            fixed4 frag (v2f i) : SV_Target
            {
                fixed xpos = i.screenPos.x;
                fixed val = xpos*2-1;
                val = abs(val);

                // sample the texture
                fixed3 distort = tex2D(_DistortMap,i.uv_MainTex);


                distort.y *= (2+_SinTime.x)*4;
                distort.x *= (2+_CosTime.x)*3;
                distort.xy *= _DistortFactor;
                distort.x *= _CosTime.z*_DistortFactor;
                distort.y *= _SinTime.z*_DistortFactor;


                fixed3 distort2 = tex2D(_DistortMap,i.uv_MainTex+distort);
                fixed spec = dot(distort2.xyz,fixed3(0.9,0.3,0));
                spec = pow(spec,8);
                fixed4 col =  lerp(_Color2,_Color,val)*tex2D(_MainTex,i.uv_MainTex+distort)*2+spec;
                return col;
            }
            ENDCG


And here is where the heavy lifting goes down, holy CRAP. This is the frag function, where we calculate the color of each pixel. The frag function retuns a fixed4, which is 4 fixed's stapled together (if you're unaware, a fixed is basically a float clamped in a small range, it's cheaper on memory requirements which can be important for older hardware but basically it's just a cheaper float, in our case we use it to represent RGBA values so the small size doesn't actually hurt us, fixed values are great for colors!

So we return the color as a fixed4, which gets rendered to screen, sounds good. Lets go through how we calculate the damn thing. 

First, we declare and calculate val, val is used to draw the gradient and we modify the x position in order to determing where on screen we are. Multiplying xpos by 2 and subtracting 1 changes the range from 0->1 to -1->+1 which is useful for the next bit. We then abs(val) to ensure it's always positive, so we get a nice up and down as we travel across the screen, the far left val is 1, at the centre it's 0, and on the right it's 1 again. 

Moving on,  we declare a fixed3 distort, this represents the RGB colors on our distortion map, for the ocean I used a normal map that came with the Unity standard assets, normally it's used for cliffs/rocky terrain but it looks fine as a water's surface. Anyway, we sample the distortion map at our current position and store the color as a fixed3. Reminder that a fixed3 is 3 fixed values stapled together.

 

Next, we offset distort's x and y coordinates by _SinTime and _CosTime, these two variables are supplied by Unity and are literally just sin(Time.time) if you're familiar with unity code, basically they're just sin and cos waves that animate over time, ideal for say, the swell of an ocean moving back and forth? I do some multiplications and additions here just to offset and tweak the numbers they give us so it feels a little more natural. Exposing these values might be wise, or unwise. Depends on your designers. I do a few other multiplications to distort the values a bit more based on time and the distortfactor (a variable exposed in the editor)

Next we declare distort2, we sample the distortion texture AGAIN, but this time offset by the distortion we just calculated, giving us an offset offset value, weird shit but hey it looks good (I made this through trial and error, remember, so it's a little odd) We use distort2 to calculate the specular highlights, so having it offset in a strange way makes the water shimmer like real water as it bobs up and down.

We then calculate a value called spec, this is a fake specular effect, I could have calculated this using actual light sources, but I figured it's probably a little cheaper to just fake it. To calculate the spec we perform a dot product between the distortion's xyz value, which while being an RGB can also represent a 3D vector, which is what we do with it here, so the distortion also functions as a direction for the surface of the water. The dot product compares it with another vector (0.9,0.3,0) and returns how similar it is. a value of 1 means the vectors are identical, 0 means perpendicular and -1 means opposite. We then raise this value to the power of 8 which creates a sharp falloff effect, which is what gives us the nice sparkles on the tips of the waves. Decreasing the power used will make the specular reflection softer, while increasing it makes it sharper. Since water is highly reflective, 8 is a good number as it results in very bright reflections in very small sections.

Finally, it all comes together and we calculate the final color. First, we lerp from color1 to color 2, by val, calculated earlier to give us the baseline background gradient. Next we sample the original background texture, but offset the UV position by distort, this results in the murky distortion effect expected of water. Finally we multiply by 2 (since we multiply the gradient and the background color together we basically halve the brightness of the two) and finally, we add the spec value, since spec is always white it gives us those nice bright reflections on the top of waves.

After all that, we return the color for the pixel, and ENDCG, tellilng Unity the shader is done, and that's the complete shader.

Holy carp that was a long explanation, the shader itself has a lot of flaws honestly, but it gets the job done so I'm happy. Take everything here with a grain of salt please, learn from my mistakes and make your shader 10x better, I just wanted to share my thought process. The shader also behaves quite strangely out of orthographic camera modes, so there's homework if you want to try and fix it.

I learned a lot of this stuff through hacking it and trawling the wiki, but I've also discovered a great series of tutorials on this stuff, check out this guy: https://www.youtube.com/channel/UCEklP9iLcpExB8vp_fWQseg

In particular, a lot of this shader is based off his Spelunky Ice shader: https://www.youtube.com/watch?v=7fMCTVhEzmU

Is it wise to link to a tutorial series significantly better than my own? Maybe not, but credit where credits due and his channel is kickass, besides if you've hammered through this much text you deserve a reward.

Tune in next week for hopefully a lot less text. Here's the final shader by the way:
https://my.mixtape.moe/vzjuxz.shader
Feel free to play around with it. You have my permission to use it however you see fit. Except for hurting people. Don't hurt people with my code.

Til next time.