Wednesday, November 20, 2013

Moss Shader

We wanted some soft "texture" in our scene to make it more inviting and organic, so in envisioning the scene we decided we ought to add some cushy moss to the fallen log.

To reiterate - we're using Unity on a mac - so we don't have access to DirectX 11's tessellation support to grow isoline fur or anything like that.  The tried and true method in games prior to that technology has been to implement a shell/fin method - but we wanted to take that idea a step further.

A simple 3 pass shell extrusion shader.

In order to have decent-quality fur using the method mentioned above, you need a significant number of shells, each serving as a cross section of the fur volume (which in Unity would each require a separate draw call per sub shader, since we'd prefer to do all the extrusion using the vertex program).  This can bring down performance, requires a lot of redundant code, and a long compilation cycle.

I want to keep the draw call count low per model, so instead I chose to integrate the effects of multiple cross-section layers in only one layer.  This essentially turns into a ray casting problem.

Rather than spend a bunch of shader instructions writing the ray casting itself, I hijacked another popular shading concept called parallax mapping.

Essentially, naive parallax mapping allows for a texture to seem "deeper" inset into the surface being shaded, as a function of a height map.  If you replace the height map value with a constant, you can set the entire texture a certain "depth" into the material.  Do this enough times with linear varying depths, and integrate things such as opacity, surface normal, root-tip color, etc in a loop, and you get a holographic ray-marched-esque shader of the fur volume.

In pseudocode:
for each iteration
{
shift uv by ParallaxOffset(0, depth*iteration/numIters, viewDirection);
look up cross section tex2d(tex, shiftedUV);
integrate tinted texture over length with root&tip color;
integrate alpha over length with root&tip opacity;
integrate normal from tex2D(NormalMap, shiftedUV);
}
normalize(Normal);

With 30 iterations and 1 draw call you can achieve the look of 30 shells.

One shortcoming of naive parallax mapping is that you don't have true silhouette edges.  This could be solved by implementing relief mapping.  Instead I opted to AlphaTest anything lower than a certain fixed value, allowing for blades of moss and grass to be clipped.

Clumping was also achieved by additionally offsetting the uv value at the tip by some smoothly varying vectors, encoded in a normal map generated from tiling FBM.  Same goes for keep alive/wind.

Gross directionality changes and density are attenuated via vertex colors = (tangentShift, bitangentShift, 0, density).


Resulting hologram moss.

3 comments:

Anonymous said...

Hi,

I came across your post and liked the results you got here, so i tried to see if I could achieve the same result, but I can't seem to get the colors. Seems everything is in grayscale. Any tips?:)

Anonymous said...

ops, this was meant to be a question for your "SSGI in Unity" post:)

Lou said...

Hi - thanks for the comment! A few things are worth noting that I gloss over in the post. First off, it'd be good to set yourself up with a clean starting point - duplicating the SSAOShader.shader , frag_ao.cginc, and SSAOEffect.cs files, and make SSGI versions of all of them (the .cs requires no further edits beyond search/replaces, but the shader and cginc are where the magic lives).

In the post I only show code for the composite pass of the shader file, if things are grayscale for you, you probably haven't set up the fragment shader (starting line 209) to work with the color buffer. To do this, sum should be turned into a half4 instead of a half, and anywhere you see tex2d().r, remove the .r so that you get the full .rgb value.

You also need to alter your frag_gi.cginc to feed this data. At the top, change the function to return a float4. Under the float occ definition, add a float3 gi definition. Instead of reading just the depth+normal buffer, pull on the color buffer as well, around line 34, with a float3 that stores .rgb coming off of the shifted _MainTex (similar to sampleND). Finally, change the return statement at the end to actually pass out the half4(gi, 1-occ).

It sounds like a lot, but it's only about 10 lines of altered/added code in total. Hope this helps!