Height Maps & Parallaxing

Last time we learned about making specmaps. Now we're gonna learn about height maps, also called parallax maps.

### What is a height map?

Like usual, when I write "map" what I mean is a texture that has information about how to make a 3d object look. A height map or parallax map is a map that we can use to make parts of a 3d object seem to stick out more than other parts, ie, to have greater height

Without height map.

With height map.

This might sound a lot like a normal map, which makes a 3d thing seem to have greater bumpiness, but they do something a little different. The normal map uses lighting to make something look bumpier than it really is. The height map uses parallaxing to make something look taller than it really is.

### Parallaxing

When you were a kid, did you ever notice that if you went for a walk at night, the moon followed you? The things you were passing by -- or moving parallel to -- would get closer or farther, but the moon was always in the same place in the sky. If, like me, you grew up near hills, you might have also noticed that the hills didn't seem to move much as you crossed town, although if you traveled far enough you could eventually leave them behind too. The farther things are from you, the less they seem to move when the angle you look at them changes (like when you pass by in a car) and the closer things are, the more they seem to move when your own position and view angle changes.

In a side-scrolling 2D game like Mario or Rayman, the creators take advantage of the fact that you feel like you pass by things closer to you faster than things that are far away by having multiple layers of backgrounds that scroll at different speeds depending on how close they should feel. It's just an illusion -- the sprites are all flat of course, but it really helps give a scene a feeling of depth!

Well 3D graphics can use this phenomenon too. But instead of scrolling, we can bunch the texture coordinates up or stretch them out based on the view direction to trick ourselves into seeing depth! That's what makes this different from a normal map. A normal map keeps the texture coordinates the same but changes the direction we say the object is pointing in. A height map keeps the direction the same but changes the texture coordinates we use for all the other maps.

The difference between the two is pretty clear if you look at them each on their own. You can follow along with the next few images in your own project or just read along with me.

If you want to follow along in your own project, go to  https://www.textures.com/download/pbr0419/137925 and download the files into a folder in your project, somewhere like Assets/Textures/Acoustic Foam. (Don't worry, the textures are free!)   Then make a material and put this checker pattern into the main texture slot and put the material on a plane. Keep the Standard shader on the material.

The material on this plane just has this checker pattern as the main albedo texture and no other maps.

Now it has the checker pattern as the main albedo texture and the acoustic foam normal map. Note how the checker pattern is still perfectly straight.

Here, the checker pattern is being used as the main albedo texture and the acoustic foam height map is being used.

If you look closely you can see how the effect isn't uniform! That's cuz the view direction to these two parts isn't the same.

The effect is pretty neat, but it works best when your view is nearly perpendicular (ironically?) and it doesn't hold up well at all if you look at the plane from the side.

So, while very cool, the illusion can only take you so far. Seems best used in small doses, on things that the player won't be able to look at from an angle like this.

And here's the plane with the checker pattern as the albedo texture, and the acoustic foam normal map and acoustic foam height map. Team work makes the dream work!

Blah blah blah enough talk, let's make a shader that does this already!

### Setup

Open up a scene in Unity. Add a plane by right clicking inside the hierarchy and selecting 3D Object > Plane.

Put the plane's transform at 0, 0, 0 and keep the default material on it, it'll just be here to give us a sense of scale. Also add one or two other shapes the same way -- whatever you like. I will be using a sphere and a cube. Place them wherever you want on top of or hovering over the plane.

Either in this folder or in a subfolder of your Materials folder, make a new material called Crystal Ore. Put the new material on your non-plane shapes.

Name the new shader file BasicParallax, then double click to open it in Visual Studio. Change the first line to Shader "Xibanya/Standard/BasicParallax" then save and go back to Unity.

Assign the BasicParallax shader to the Crystal Ore material. You can now drag the albedo map into the texture slot.

We've already covered how to use a normal map, a metallic map, and so on, so I'm going to assume you already know how to do that. (If you don't, follow those links then come back when you're ready!)  Go to this basic shader I've made here, click the button that says Raw, and copy and paste the shader into BasicParallax (but be sure to go back and change the name up top back to  Xibanya/Standard/BasicParallax before you hit save!)

You can now drag in the normal and AO maps.

I made this shader to do the bare minimum to write to all the properties used by the Standard lighting model and nothing more than that, so we'll need to make an additional tweak so that this shader can use a roughness map.

[Toggle(ROUGHNESS)]_Roughness("Roughness?", float) = 0

Then change the part after the glossmap is unpacked to this

(If you're not familiar with roughness maps or keywords, you can read up on them here!)

Once that's done, save and go back to Unity. Now drag in the roughness map and set the Roughness toggle to true.

Whew! Finally we have the basics set up, time to get to that parallaxing!

[Space]
[Toggle(_PARALLAXMAP)]_Parallaxmap("Parallax?", float) = 0
_ParallaxMap("Parallax Map", 2D) = "white" {}
_Parallax("Parallax Strength", Range(0, 0.1)) = 0.005

We'll make parallaxing a feature we activate/deactivate with the keyword _PARALLAXMAP. We need to use tangent view direction to correctly apply a parallax map. If we're writing a surface shader, as long as this keyword is defined (aka we've toggled it on) this is the view direction we'll get from our input struct. If the _PARALLAXMAP keyword isn't defined, we will still get tangent view direction if we write to o.Normal, but we might not always do that and it's easy to forget and then get confused, so better to just get in the habit of using it every time we want to unpack a parallax map!

Under our Roughness keyword pragma, add this line:

and add float3 viewDir to our Input struct.

Lastly, declare the parallax map texture and parallax strength properties in the subshader.

sampler2D _ParallaxMap;
half _Parallax;

Save and head back to Unity to make sure we haven't  made any typos or anything. If all went well, you can now drag the CrystalOre_1k_height texture into the parallax map slot on the material. Be sure to toggle parallaxing on while you're at it.

Of course, this won't do anything yet because we haven't made any changes to the surface function!

We use a parallax map to determine how to scrunch up or stretch out the texture coordinates, so we'll want to handle all our parallaxing stuff before we do anything else.

Add this at the very top of the surface function:

We'll unpack the texture and take the green channel (most height maps are black and white, making every channel the same, so we just need to use one!) then we'll feed that, our parallax strength, and the tangent view direction (helpfully calculated for us behind the scenes by Unity surface shader magic!) into a built-in function called ParallaxOffset

ParallaxOffset is a function that determines how much the uv coordinates should get bunched up or stretched out based on view direction. It can be found in UnityCG.cginc, a library that's included automatically in every surface shader. ParallaxOffset returns a float2, which we can then add to our texture coordinates IN.uv_MainTex (which is also a float2!) so that every texture we unpack after that will be unpacked with the offset.

If you're curious, it looks like this, but you don't need to know how it works to be able to use it to add parallaxing to any of your surface shaders!

Anyway, our work here is done! Save and go back to Unity. Play with the Parallax Strength slider to see the effect in action.

Strength at 0

Strength at 0.1

In the properties I had you limit parallax strength between 0 and 0.1 with the range slider -- that's because any higher than 0.1 things start to look a little crazy even at the most ideal viewing angles, but you're welcome to change the property from a range to a float to play around with different parallax strengths!

trippy!

The shader we made in this tutorial is attached to this post as BasicParallax.shader.  If you have any questions or want to share what you come up with, let me know in the comments here, on Twitter, or in Discord. And if this tutorial helped you out, please consider becoming a patron!