Sunday, March 1, 2015

2.5D Sun In Depth Part 4: Heat Haze


Last part! The heat haze:

The heat haze is the green model in the image above.  It just sits on top of everything and ties the whole thing together.  It only uses 2 textures, one for distortion and one to mask out the glow and distortion.  Both of these textures are small and uncompressed.



The Shader

This is a 3 pass shader, The first pass draws an orange glow over everything based on a mask. The second takes a snapshot of the frame as a whole and saves it to a texture to use later.  The third uses that texture and distorts it with the distortion texture and draws it back onto the screen.

Since this isn't a surface shader we need to do some extra things that would normally be taken care of.

CGINCLUDE

Instead of CGPROGRAM like in the surface shaders we use CGINCLUDE which treats all of the programs (vertex and pixel) like includes that can be referenced from different passes later on.  These passes will have CGPROGRAM lines.

#include "UnityCG.cginc"

This is a general library of functions that will be used later on.  This particular library exists in the Unity program files directory, but you can write your own and put them in the same folder as your shaders.

Next is defining the vertex to pixel shader data structure.

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float4 screenPos : TEXCOORD1;
};

This is similar to the struct input from the last shader with the exception that you need to tell it which coordinates your variables are bound to.  Unity supports 10 coordinates (I think); POSITION which is a float4, COLOR which is a 8 bit int (0-255)  mapped to 0-1, and TEXCOORD0-7 which are all float4.  This limits the amount of data that you can pre-compute and send to the pixel shader, but in this shader we won't be getting anywhere near those limits. You can get 2 more interpolators with the tag "#pragma target 3.0" but this will limit the hardware your shader can be viewed on to video cards supporting shader model 3 and higher.

sampler2D _GrabTexture;// : register(s0);

You won't find this texture sampler up with the properties because it can't be defined by hand.  This is the texture that will be the screen after the grab pass.  The " : register(s0);" is apparently legacy but I'll leave it in there commented out just in case.

v2f vertMain( appdata_full v ) { 
v2f o;
o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
o.screenPos = ComputeScreenPos(o.pos);
o.uv =  v.texcoord.xy;
return o;
}

This is the vertex program for all of our shaders.  Unlike surface shaders, the entire struct needs to be filled out.  The vertex program also has to return the v2f (vertex to fragment) struct instead of void like a surface shader.  This shader is pretty simple.  The first line declares a new v2f variable.  The second computes the hardware screen position of the vertex.  The third computes and saves the interpolated screen position using a function from the UnityCG file.  The fourth passes through the uv coordinates.  And the fifth returns the data to be used in the pixel shader.

half4 fragMain ( v2f IN ) : COLOR {
half4 mainTex = tex2D (_MainTex, IN.uv);
mainTex *= _Color;
mainTex *= _Factor;
return mainTex;
}

This is going to be the first pass of the shader.  It returns a half4 to the COLOR buffer which is just the screen.  This is the orange glow shader and it just uses the mask and applies a _Color and _Factor parameter.

half4 fragDistort ( v2f IN ) : COLOR {

float2 screenPos = IN.screenPos.xy / IN.screenPos.w;
      
// FIXES UPSIDE DOWN
#if SHADER_API_D3D9
screenPos.y = 1 - screenPos.y;
#endif

float distFalloff = 1.0 / ( IN.screenPos.z * 0.1 );

half4 mainTex = tex2D( _MainTex, IN.uv );

half4 distort1 = tex2D( _DistortTex, IN.uv * _Tiling1.xy + _Time.y * _Tiling1.zw  );
half4 distort2 = tex2D( _DistortTex, IN.uv * _Tiling2.xy + _Time.y * _Tiling2.zw  );
half4 distort3 = tex2D( _DistortTex, IN.uv * _Tiling3.xy + _Time.y * _Tiling3.zw  );

half2 distort = ( ( distort1.xy + distort2.yz + distort2.zx ) - 1.5 ) * 0.01 * _Distortion * distFalloff * mainTex.x;

screenPos += distort;

half4 final = tex2D( _GrabTexture, screenPos );
final.w = mainTex.w;

return final;
}

This is the distort shader, it also returns a half 4 and applies it to the color buffer.

float2 screenPos = IN.screenPos.xy / IN.screenPos.w;
      
In this shader we use the screen position that was send out of the vertex shader earlier.  This line makes a new float2 variable, screenPos, the texture coordinate of the screen.

// FIXES UPSIDE DOWN
#if SHADER_API_D3D9
screenPos.y = 1 - screenPos.y;
#endif

On older hardware (shader model 2) the screen can be upside down.  This line checks for that case and flips the screenPos.  This should work in theory, I don't have old hardware to test it on though and it doesn't seem to work with Graphics Emulation. :/

float distFalloff = 1.0 / ( IN.screenPos.z * 0.1 );

Since distortion is a screen space effect it needs to be scaled down based on how far away it is from the camera.  This algorithm worked for the current situation but it is dependent on things like camera fov so you may have to change it to something else for your purposes.

half4 mainTex = tex2D( _MainTex, IN.uv );

We are using the main texture again as a mask.

half4 distort1 = tex2D( _DistortTex, IN.uv * _Tiling1.xy + _Time.y * _Tiling1.zw  );
half4 distort2 = tex2D( _DistortTex, IN.uv * _Tiling2.xy + _Time.y * _Tiling2.zw  );
half4 distort3 = tex2D( _DistortTex, IN.uv * _Tiling3.xy + _Time.y * _Tiling3.zw  );

Sample the distortion texture 3 times with different tiling and scrolling parameters to ensure that we don't see and repeating patterns in our distortion.

half2 distort = ( ( distort1.xy + distort2.yz + distort2.zx ) - 1.5 ) * 0.01 * _Distortion * distFalloff * mainTex.x;

Add the three distortion textures together.  Swizzle the channels to further ensure no repeating patterns.  Subtract 1.5 to normalize the distortion range, multiply it by a super low number unless you want crazy distortion, multiply it by the _Distortion parameter, the distFalloff variable, and the mask.

screenPos += distort;

Now just add the distortion to the screenPos;

half4 final = tex2D( _GrabTexture, screenPos );

Sample the _GrabTexture, which is the screen texture, with the screenPos as texture coordinates.

final.w = mainTex.w;

Set the alpha to the masks alpha.

return final;

And done!  Throw an ENDCG in there and the CGINCLUDE is finished and ready to be used in the passes

Subshader {

Tags {"Queue" = "Transparent" }
   
Now we are going to start using these programs in passes.  First set up the subshader and set the queue to transparent.

Pass {
ZTest LEqual
ZWrite Off
Blend SrcAlpha One

CGPROGRAM
#pragma fragmentoption ARB_precision_hint_fastest
#pragma vertex vertMain
#pragma fragment fragMain
ENDCG
}

The first pass is the orange glow.  Most of this stuff has been covered.  The new stuff is after CGPROGRAM.

#pragma fragmentoption ARB_precision_hint_fastest

Makes the shader faster but less precise.  Good for a full screen effect like distortion and this simple glow.

#pragma vertex vertMain

Specifies the vertex program to use for this pass.

#pragma fragment fragMain

Specifies the pixel shader to use for this pass...  And that's it!  All the vertex shaders, pixel shaders, and other stuff were defined in the CGINCLUDE above.

GrabPass {
Name "BASE"
Tags { "LightMode" = "Always" }
}

The second pass grabs the screen and saves it to that _GrabTexture parameter.  It must be all implicit because that's all there is to it.

Pass {
ZTest LEqual
ZWrite Off
Fog { Mode off }  
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM
#pragma fragmentoption ARB_precision_hint_fastest
#pragma vertex vertMain
#pragma fragment fragDistort
ENDCG
}

The last pass is the distortion.  The only new tag is Fog { Mode off } which turns off fog for this pass.  Everything will already have been fogged appropriately and having fog on your distortion pass would essentially double the amount of fog on everything behind it.

#pragma vertex vertMain

The vertex program vertMain is shared between this pass and the first pass.

#pragma fragment fragDistort

Set this pass to use the distortion program as its pixel shader.  Throw in some closing brackets and we're done.

Final sun without and with heat haze shader

And so we have reached the end of the Sun Shader Epic.  I hope you enjoyed it and maybe learned something, but if not that's ok too.

No comments:

Post a Comment