In this post, I’m going to go over one of the shaders I wrote for One Drop Bot, how it works, and how to use it. This shader doesn’t use any lights, and runs using textures. All “dynamic” lighting is actually rim lighting. What that means is that the edges of an object are lit based on its orientation to the camera. This makes it perfect for running on systems with low graphical capabilities such as smart phones and tablets.

One Drop Bot is a PC game, but its shaders were originally written to run on mobile devices.

The first shader I’ll go over is the one that has the most realistic lighting which I’ve called the Environment shader for its use in the environments. Today I’m going to go over how the shader works and how to write one yourself. This shader uses a combination of prerendered textures to give a realistic look.

As you can see, the shader has 3 variables. The first is a texture that’s been baked in Blender Cycles, but any baked texture will do. The second texture is the reflection texture. It’s a cube map also prerendered in Blender Cycles. The last variable indicates rim power. That is the power the reflection has over the material, whether it’s only on the edge or appears on the front as well. The lower the rim power, the stronger the reflection.

Rim Power is set to 0 here.

Let’s get into the meat of how this works shall we?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Shader "OMT/OMTEnvironment" { Properties { _MainTex ( "Texture" , 2 D) = "white" {} _Cube ( "Reflection" , CUBE) = "" { } _Power ( "Rim Power" , Float) = 3 } SubShader { Tags { "RenderType" = "Opaque" } LOD 100 Pass { CGPROGRAM # pragma vertex vert # pragma fragment frag // make fog work # pragma multi_compile_fog # include "UnityCG.cginc"

First, create the shader, here its named after the project name I used. And I used it primarily for the environment, so it is called Environment. Then the variables called Properties. These are the three variables that control how the material will look. Then in the subshader, we grab all the basic things and start the Pass.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; fixed4 rim : COLOR; float3 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float3 normal : NORMAL; UNITY_FOG_COORDS( 1 ) float4 pos : SV_POSITION; float4 posWorld : TEXCOORD1; float3 I : TEXCOORD2; float4 rim : COLOR; };

Then we initialize the appdata and the v2f variables. I’ll explain these more in a bit.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 sampler2D _MainTex; float4 _MainTex_ST; uniform samplerCUBE _Cube; float _Power; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.posWorld = mul(unity_ObjectToWorld, v.vertex); o.normal = normalize( mul ( float4(v.normal, 0.0 ), unity_WorldToObject).xyz); o.rim = v.rim; o.uv = TRANSFORM_TEX(v.uv, _MainTex); float3 viewDir = WorldSpaceViewDir( v.vertex ); float3 worldN = UnityObjectToWorldNormal( v.normal ); o.I = reflect( -viewDir, worldN ); UNITY_TRANSFER_FOG(o,o.vertex); return o; }

Next comes vert, the vertex shader. This is where much of the magic happens (but not all of it). Here we need to get the objects position and normals. Mul here multiplies the unity functions with our object’s info to give us these two things. We also need the view direction and normals to calculate reflection.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fixed4 frag (v2f i) : SV_Target { //rim lighting float3 normalDir = i.normal; float3 viewDir = normalize( _WorldSpaceCameraPos.xyz - i.posWorld.xyz); float rim = 1 - saturate ( dot(viewDir, normalDir) ); fixed rimLight = pow(rim, _Power); // sample the textures fixed4 col = tex2D(_MainTex, i.uv); fixed4 reflcol = texCUBE( _Cube, i.I ); col = lerp(col, reflcol, (rimLight * reflcol + reflcol* 0.5f )/ 1.5f ); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; }

The fragment shader is where the rest of the magic happens. After getting the view direction by subtracting the objects pos from the camera, taking the dot product of the view direction and the normals of the object give us the rim. Using the power formula gives it the clean edge look.

The last formula to calculate col is where everything comes together. The reflection intensity is based on the angle of the edge and the intensity of the reflection. The reason for this is that when a strong light hits a non-metalic object, it tends to shine through.

Here’s the complete shader.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 Shader "OMT/OMTEnvironment" { Properties { _MainTex ( "Texture" , 2 D) = "white" {} _Cube ( "Reflection" , CUBE) = "" { } _Power ( "Rim Power" , Float) = 3 } SubShader { Tags { "RenderType" = "Opaque" } LOD 100 Pass { CGPROGRAM # pragma vertex vert # pragma fragment frag // make fog work # pragma multi_compile_fog # include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; fixed4 rim : COLOR; float3 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float3 normal : NORMAL; UNITY_FOG_COORDS( 1 ) float4 pos : SV_POSITION; float4 posWorld : TEXCOORD1; float3 I : TEXCOORD2; float4 rim : COLOR; }; sampler2D _MainTex; float4 _MainTex_ST; uniform samplerCUBE _Cube; float _Power; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.posWorld = mul(unity_ObjectToWorld, v.vertex); o.normal = normalize( mul ( float4(v.normal, 0.0 ), unity_WorldToObject).xyz); o.rim = v.rim; o.uv = TRANSFORM_TEX(v.uv, _MainTex); float3 viewDir = WorldSpaceViewDir( v.vertex ); float3 worldN = UnityObjectToWorldNormal( v.normal ); o.I = reflect( -viewDir, worldN ); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { //rim lighting float3 normalDir = i.normal; float3 viewDir = normalize( _WorldSpaceCameraPos.xyz - i.posWorld.xyz); float rim = 1 - saturate ( dot(viewDir, normalDir) ); fixed rimLight = pow(rim, _Power); // sample the textures fixed4 col = tex2D(_MainTex, i.uv); fixed4 reflcol = texCUBE( _Cube, i.I ); col = lerp(col, reflcol, (rimLight * reflcol + reflcol* 0.5f )/ 1.5f ); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }

Thanks for reading, and I hope this helps anyone trying to write shaders for their games!