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).