I downloaded something from the fab store the other day and ran into a water shader that had some interesting thing going on. It had leaves underwater water that were properly displaced when the camera moved. There is no transparency involved here. It is an opaque shader on a simple plane.
I looked into it and saw that they used a BumpOffset node. Coming from Unity and Blender, I didn’t know what that was and it was not enough for me to just use that node. So I thought for a bit and discovered this should be a fairly easy effect to reproduce. I started out with wanting to do a similar water shader, but somewhere along the road it changed into an ice shader.
Texture
I wanted to use one texture for multiple purposes. The result is this:

While it looks messy and ugly, here’s how it works. In the red and green channel are two noises. Both of these are supposed to be applied at two different depths so they kind of move indepently from each other and overlap each other in interesting ways. In the blue channel i have added a cracks texture that will become the surface of the ice shader. All of these channels are tileable. The kind of quick and dirty setup for this in Substance Designer looks like this:

Basic Setup
The heart of the Shader I made was a custom function that was supposed to work similar to the BumpOffset node. The idea is that we can move a texture when the camera moves. We simulate a depth by taking the camera view vector and checking what vector it would need to be if the surface was lower. From this simulated view vector we could calculate the vector needed for the texture to move to accomodate for that depth.

With some fancy and extensive math we can calculate all of that. However, with some boring and non-extensive math, we can cheat. I have done something much simpler. The same logic, but much simpler. Take the actual view vector in the image. I removed the Z component, essentially “projecting” it on to the surface. By inverting it, we get the same direction as the “Texture Offset” vector in the image, but with a different length. Because the depth should be adjustable anyway, this wasn’t too much of a big deal.
Here is this material function. The whole shader does not use the UV map, it takes the XY components of the world position as the basis for the texture coordinates.

Ice Material
The basic texture coordinates (for calculating surface values like roughness and normal) uses the same approach but without any depth calculation.

The roughness is essentially a mix of the R (noise) and B (cracks) channels. The noise is the basis for it, that is why it has a power node with an intensity parameter attached to it.

Then we have the moving textures. It’s the same texture (although the preview would suggest otherwise, but that’s because of the UVs), but with different values plugged into our custom function. One has the base depth and one has the depth plus an offset. Additionally, both of them use a scale parameter different from the texture sampled for surface details. That way, the scale of what’s underneath the surface and what’s on the surface can be controlled separately.
The way I combined the red and green channel is by multiplying one and make that the exponent for the power of the other.

The normals were a tiny bit tricky as well, because I wanted to combine the red and the blue channel. I’m not going to go into too much detail because this blog post is going on for long enough as it is, but you can see the setup below.

At the end, we of course color all that. There’s a separate color for the cracks, and two colors for the simulated depth textures simply lerping the combined grayscale.

Result
The final parameters editable on a material instance look like this:

And this is the shader in action:
Conclusion
I first worked with actually projecting the view vector on to the normal, and that might have some advantages for walls, but since most of the normals point close to up, simply removing the Z component works well enough. That was a nice learning.
One disadvantage is that currently the same texture is sampled three times with different texture coordinates. Maybe that can be reduced somehow, though I don’t know how. On the other hand, that would, of course, also allow for using three different textures.

One response to “Ice Shader”
The result looks awesome! Really interesting what shaders can do