A shader for simulating snow, moss or dust cover
Introduction
Back in 2007 I made an adaptive snow shader for MetaBall 2. It was made in an early, obsolete version of the Unreal 3 engine and had a few drawbacks, so I decided to redo it from scratch.
To make it more accessible, I ditched the Unreal engine and created the shader in MentalMill instead. However, the basic principles can be easily applied to any shader authoring tool, from ShaderFX to Unreal.
The images of the MentalMill node tree are heavily retouched to make easier to see what’s what. Each image shows only a part of the whole tree, with irrelevant links removed. For the whole network, check the DepositShader.xmsl in the DepositShaderAssets.zip (13.8 Mb).
The general idea
Deposits like dust or snow gather on areas facing more or less upwards. By transforming each texel’s normal to world, we can tell whether we want deposit there. Then we make a mask based on that information and use it to blend between textures.
Due to the transformation of normals, the deposit mask is independent from the object’s orientation: it stays on top.
Here is an overview of the shader parameters:
DiffuseMap: The diffuse texture of the base surface.
SpecularMap: The specular texture of the base surface. (Defines relative differences in specularity throughout the surface. The actual value of the brightest pixel on this texture is controlled by the SpecularStrength parameter.
NormalMap: The normal texture of the base surface.
CavityMap: The cavity texture of the base surface.
DiffuseDeposit: The diffuse texture of the deposit.
SpecularDeposit: The specular strength texture of the deposit.
SpecularStrength: The maximum value of the base specular texture.
SpecularDepositStrength: The maximum value of the deposit specular texture.
Glossiness: The size of the specular highlight on the base surface.
GlossinessDeposit: The size of the specular highlight on the deposit.
DepositTiling: The tiling of all deposit related textures.
NormalDepositStrength: The strength of the deposit normal texture.
DepositCoverage: How much of the surface is covered.
DepositTransition: How quickly the deposit fades at the edges.
DepositThickness: At low values the base normal texture shows through, indicating a thin layer of the deposit. Higher numbers make the deposit normal texture suppress the base.
RimLight: A fresnel like, view dependent effect lighting up the edges of the object. This is the color of the rim light on the base surface. Brighter colors are more visible.
RimLightDeposit: Rim light color on the deposit.
ShadowColor: The color of the shadow under the deposit. White makes it invisible.
ShadowWidth: How much the shadow hangs out from under the deposit cover.
And now lets see how all these parameters define the final look of the surface.
Input textures
Here is the first part of the node tree showing the relevant inputs and the related nodes. (The unlinked inputs will be discussed later.)
In this step we prepare the textures.
Let’s go through the nodes, from the top:
DiffuseMap: The base texture for the object. (In this example it’s a rock surface.)
DiffuseDetail: A grayscale image for high frequency surface details. It has nothing to do with the deposit, it’s just there to make the surface nicer. It’s tiling about 5 times.
The Compress node next to it remaps the texture values from the usual [0,1] domain to [0.25,1.75]. Then we multiply that by the diffuse texture, which operation will act similar to the Overlay blending mode in Photoshop: black pixels darken, white pixels lighten the diffuse texture, 50% grays do nothing.
DiffuseDeposit: The diffuse texture of the deposit, which is a snow texture in this case. Its tiling is controlled by the DepositTiling shader parameter.
SpecularMap: A grayscale texture defining the base strength of the specular highlight. This value will be adjusted later on: the base surface and the deposit cover each has a separate multiplier.
As for the color, the diffuse texture will be used. For more control you could use color constants or texture maps instead.
NormalMap: Tangent space normal texture.
NormalDetail: Same deal as with the DiffuseDetail node. The Make normal node is required before a tangent space normal map can be used on an shaded surface.
NormalDeposit: Tangent space normal texture for the deposit. Its tiling is controlled by the DepositTiling shader parameter, while it’s amount is set with NormalDepositStrength.
CavityMap: This map defines cracks and crevices: White means a location deep in a cranny, black means an easily accessible area. We’re going to use this map when creating the deposit mask, as stuff is more likely to accumulate in hard to reach areas.
The texture was generated by 3ds Max’s Render Surface Map feature, and mixed with a high contrast ambient occlusion map. I used the median filter on the result to make it smoother.
Diffuse, specular, normal
This second part of the node tree generates the final values for the diffuse, specular and normal channels.
The semi transparent nodes show how this section of the network connects to the first one. And just as before, only the important shader parameters are linked in.
At the bottom of the image is the DepositMask node which creates 3 masks. They are used in several linear interpolator nodes (Lerps) on the right hand side. (The mask’s data type is often converted.)
Several of its inputs are directly controlled by shader parameters. (The inner workings of the DepositMask node will be discussed later.)
These are the nodes, from the top:
ApplyShadow: It applies a “shadow” to the diffuse map, using the multiply operation. It generally darkens the base surface under and around the deposit.
Linked into it is the ShadowColor lerp, which blends between white (no deposit, no shadow) and the ShadowColor shader parameter, based on a mask.
FinalDiffuse: This lerp mixes the shadowed base diffuse with the deposit diffuse, using a mask. It is then used (amongst other things) to create the FinalSpecularColor.
FinalSpecularStr: Simply blends between the SpecularStrength and SpecularDepositStrength values.
FinalGlossiness: A lerp between the base and deposit glossiness values.
FinalNormal: Mixing the base and deposit normal maps, using a mask.
The deposit mask
Now let’s see how the DepositMask node works.
First the normals of the surface are converted to world space. We then extract the Z component of the result, which is basically a gradient showing how much a given point is facing upwards. Then it’s multiplied by the cavity map, so crevices have an effect. The amount they affect the mask depends on the DepositThickness parameter: lower values make it less significant.
So we have the base mask. The rest of the network modifies it and makes a mask for the diffuse/specular blends, one for mixing normals and one for the deposit shadow.
The DepositCoverage parameter is remapped to the [-1,1] domain, so it can make the base mask darker or lighter in the Coverage node.
Now let’s follow the branch which produces the Mask output: The result of Coverage is multiplied by the DepositTransition parameter, which effectively controls the contrast of the mask. After the numeric type conversion and the clamping to the [0,1] domain, the mask is ready.
The normal mask is the base mask multiplied by DepositThickness, so the higher that value goes, the brighter the mask gets, therefore the deposit normal becomes more visible.
The third mask is for the drop shadow of the deposit. It’s almost the same as the base mask, the only difference is that the coverage value is offset by the ShadowWidth parameter. Positive values make the shadow wider. The result is converted and clamped.
Illumination and emissive
All the data required for illumination was made during the previous steps, so now it’s just the matter of feeding it to the Phong node.
The result is then added to the rim light, which therefore acts as an emissive term.
The rim light is just a component falloff multiplied by both the diffuse color and the RimLightColor lerp.
Performance
Performance analysis by FXComposer 2.5, simulating a GeForce 8800 GT with driver version 174.74 . Values might vary on different hardware/software configurations.
Throughput is in MPix/s: The higher the number the faster the shader is.
[BasicPhong] [DepositShader] [Without dropshadow] [Merged textures]
Textures used: 3 8 8 4
Throughput : 1033 646 669 659
In the last case half of the textures were in alpha channels:
DiffuseMap alpha: SpecularMap
NormalMap alpha: CavityMap
NormalDetail alpha: DiffuseDetail
NormalDeposit alpha: DiffuseDeposit (This means that its a grayscale texture, so I added an extra parameter to colorize it.)
Examples and downloads
You can download the shader, the related textures, the rock mesh in .OBJ format and a Max 2010 scene from here: DepositShaderAssets.zip (13.8 Mb).
The RockLP.obj file can be imported and used as a preview object in MentalMill.
The Max file contains the animation showed at the beginning of the article. I encourage you to play around with it, mess with shader parameters, see the animation curves controlling them, change textures, etc.
It also contains the “Sandy” and “Mossy” examples.
The files are licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
FAQ
Q : It’s not a cheap shader, why would I want to use it?
A : This method has the following advantages:
- The orientation independence makes it way more flexible than any “baked” solution. Instead of “RockDry”, “RockMossy”, “RockSnowy1″ and “RockSnowy2″, its enough to have one “Rock” and use different shaders on it. It saves memory, simplifies asset management, instancing helps with speed. Level designers will use the flexibility to hide mesh repetition while populating outdoor environments.
- The deposit textures are shared between objects, so this method is more memory friendly than using new, deposit specific diffuse/normal textures. Also, this shader could look more realistic than the general modification of the base textures.
- The deposit will show up on any object, without any preparation (like additional UVs).
- Deposit coverage can be animated.
If you can leverage these features then this method is quite efficient.
Q : The shader uses 8 textures. Isn’t that too many?
A : You can optimize this aspect of the shader by merging textures. The grayscale textures (DiffuseDetail, SpecularMap and CavityMap) can be put into the alpha channels of other textures, or merged into a new RGB image.
Q : How can I make that cavity map?
A : In 3ds Max its very simple:
- Make a really highpoly object. The details must be present in the geometry, because texture maps won’t be processed. In the case of the rock formation above, I made a displaced SubDiv surface in Modo and transferred a frozen version to Max.
- Unwrap the object to a UV set. Doing it on a SDS or NURBS surface is easier than working on a million poly object.
- Select the mesh and go to the Rendering/Render surface map menu.
- Pick the texture resolution and target UV channel at the top.
- Seam bleed is the same as padding elsewhere in Max: it helps with unwanted pixels creeping in at the UV seams. I usually use 4-8 pixels.
- Click on the CavityMap button, save the image, and map it back to the surface, so it can be baked down to the lowpoly mesh. (On the rock it was linked to the self illumination channel.)
If you are not using Max then consider trying the excellent xNormal: www.xnormal.net</a>
It can bake cavity maps as explained in the second half of this tutorial by Donald Phan.
Q : I don’t want the snow falling straight down, but blown by the wind in a particular direction. How can I do that?
A : By using the following snippet one could rotate the mask to any direction:
Notes:
- By CTRL clicking in the transform node’s preview, you can pick a direction. Copy the normal vector from the tooltip to the DepositDirection node.
- The expand node remaps the [1.5,0] domain to [0,1]. Notice the fact of inversion.
- The smoothstep’s start = 0, end = 1.
- The last, power node has the exponent of 0.5. What you can see on the preview is not shading but the base mask gradient.
- The values above give a gradient profile close to the original method, but they are not the same. With this you have some room to adjust the profile of the falloff.
Q : In the provided .max file the right hand side viewport is blank.
A : No idea why it happens, but switching to “perspective” mode and then back to “Camera” fixes it.
Q : I think you’re wrong when you say that the… *insert objectionable statement here*
A : If you find something in this article which is confusing, misspelled, dubious or just plain wrong, please don’t hesitate to drop me a mail to zoltan at zspline dot net.
