Skip to content
HY Devlog
Go back

A Height Map Isn't Displacement, It's Parallax

I clipped this after tessellation came up in an interview. In the room I caught the word and moved on; afterwards, looking up what exactly it was, I landed on this post.

What I clipped is an explainer written for beginners. It opens with two cautions — the field is specialized, and it’s a technology that develops in real time, so some parts may be wrong. Given a post that states that up front, hunting for its errors isn’t the point.

The big picture is set up correctly. Objects are represented as triangles, they’re recognized by their vertices so a sphere looks faceted, and tessellation subdivides the faces. And it nails the single most important line.

Subdividing alone has little effect, so why is tessellation used?

So the technique used together with tessellation is the displacement map.

— 테셀레이션(tessellation) 란?

This pairing is right. As confirmed below, Microsoft’s Direct3D 11 docs write the same pairing explicitly. The beginner’s post didn’t miss the core.

What’s out of step is the two words next to it — what a normal map stores, and what a bump map is. And one more appears once you come over to Unity: the field labelled “Height Map” in the Standard Shader is not displacement.

Table of contents

Table of contents

What a normal map stores isn’t brightness, it’s direction

The post explains a normal map in one line.

normal map: a method that gives a sense of depth by outputting the X part of an object brighter and the Y part darker

— 테셀레이션(tessellation) 란?

It’s written as a method of outputting brightness. Unity’s docs write what gets stored differently.

Normal maps are a special kind of texture that allow you to add surface detail such as bumps, grooves, and scratches to a model.

each pixel, called a texel, represents a deviation in surface normal direction

each pixel represents the difference in direction the surface should face

— Normal map (Bump mapping) — Unity 6.6 manual

What sits in each texel is “the difference in direction”. Not brightness. A normal map is an RGB texture, and its three channels are read not as a color but as the components of a normal vector.

Then where does the brightness difference come from? It comes out as a result. The same page records the causality.

would only be apparent due to the way lighting appears on the surface, because these modified normals are used in the lighting calculations

— Normal map (Bump mapping) — Unity 6.6 manual

The modified normals go into the lighting calculation, and that’s why the brightness looks different. The post inverted that order, writing the result (brightness) as the stored content. Explain the sense of depth as “because it outputs brighter and darker” and there’s no account of why the shading turns along with the light when you move the light source.

What the post writesWhat Unity’s docs write
value in a texelbrighter / darker“a deviation in surface normal direction”
texture kindnot statedRGB texture (difference in direction)
status of the brightness differencethe causethe result — “these modified normals are used in the lighting calculations”
geometrynot stated“does not affect the actual polygonal nature of the mesh”

That last row is this whole post’s axis. The same page writes “does not affect the actual polygonal nature of the mesh, only how the lighting is calculated on the surfaces.” and adds that viewing at a glancing angle “will reveal the true nature of the flat surface”. That a normal map changes no shape on its own holds in other shaders too, and the range lilToon’s normal map setting affects is written up in lilToon Parameters: A Reference Cross-Checked Against the Official Docs.

A bump map isn’t a technique, it’s a category

The post lines up two techniques.

There are 2 methods for a sense of depth closely related to tessellation. They are the bump map and the normal map.

— 테셀레이션(tessellation) 란?

It stands the bump map and the normal map up as two. Unity’s docs don’t classify them that way.

Normal maps and height maps are both kinds of bump map

A height map is a simple black and white texture, where each pixel represents how much that point on the surface should appear to be raised

Unity can also accept Height Maps for bump mapping, but they must be converted to Normal Maps on import in order to use them.

— Normal map (Bump mapping) — Unity 6.6 manual

Bump map is the upper category, and normal maps and height maps are the two under it. The post put the category and one of its members on the same line. And the third sentence matters — Unity accepts height maps for bump mapping, but they have to be converted to normal maps on import to be usable. Even a map stored as brightness ends up turned into direction before it’s used.

So the post’s explanation of a normal map in terms of brightness is closer to calling a height map’s storage format by the normal map’s name. The two concepts sit under one umbrella called bump mapping, which is exactly where they get mixed up.

TermIts place in Unity’s docsStorage format
Bump mapthe upper category—
Normal mapone kind of bump mapRGB, difference in direction
Height mapone kind of bump map“simple black and white texture”, brighter is higher
import—a height map is converted to a normal map

A Height Map isn’t displacement, it’s parallax

Here’s the gap that appears when you come over to Unity. The post quotes displacement mapping like this.

displacement mapping is a technique that forms hard-to-model undulations based on the light and dark of the image applied as a displacement map. Bright areas are modelled to protrude and dark areas to recede.

— 테셀레이션(tessellation) 란?

“Bright areas protrude” — an explanation in which the shape actually changes. Take that explanation and open Unity’s Standard Shader, and there’s one field that accepts a greyscale map where brighter is higher. It’s named Height Map. Its documentation opens like this.

Height mapping (also known as parallax mapping) is a similar concept to normal mapping

actually shifts the areas of the visible surface texture around, to achieve a kind of surface-level occlusion effect

— Height map — Unity 6.6 manual

It’s parallax mapping. What changes isn’t geometry but the position of the visible texture. So the same page pins the limit.

the silhouette of the model will never be modified, because ultimately the effect is drawn onto the surface

— Height map — Unity 6.6 manual

The silhouette is never modified. Because the effect is drawn onto the surface. Which means: expect the displacement the post described — vertices actually pushed up — and plug a map into the Standard Shader’s Height Map, and the outline stays flat. The inner texture gets convincing while the model’s edge is level.

The docs record the relationship between the two as well. Normal mapping changes only the lighting, and height mapping goes one step further by shifting the visible texture. And it says they’re usually used together. There’s a line of caution about cost too — it’s more complex and more performance-intensive than normal mapping.

TechniqueWhat changesSilhouetteUnity’s field
Normal mapthe lighting calculationunchangedNormal Map
Height map (parallax)the visible texture’s position“never be modified”Height Map
Displacementvertex positionschangesnot in the Standard Shader

There is no field for the third row in the Standard Shader. The displacement the post talked about is a feature tied to tessellation, and that isn’t a checkbox on the Standard Shader — it requires picking a different shader. The routes are below.

Pairing tessellation with displacement is right

What the post writes as starting from DirectX 11 is right too.

From DirectX11 on, a module supporting tessellation came into use

— 테셀레이션(tessellation) 란?

Microsoft’s docs list three stages and call them “new”.

The Direct3D 11 pipeline implements tessellation using three new pipeline stages:

Hull-Shader Stage: A programmable shader stage that produces a geometry patch (and patch constants) that correspond to each input patch

Tessellator Stage: A fixed function pipeline stage that creates a sampling pattern of the domain that represents the geometry patch

Domain-Shader Stage: A programmable shader stage that calculates the vertex position that corresponds to each domain sample

— Tessellation Stages — Microsoft Learn

A fixed stage wedged between two programmable ones. Where these three sit in the pipeline is written out with stage numbers in The Graphics Pipeline in Order: From Vertex to Pixel. What matters here is why you use it, and the same page names displacement by name.

tessellation implemented by hardware can generate an incredible amount of visual detail (including support for displacement mapping) without adding the visual detail to the model sizes

The tessellation technique implemented in the Direct3D 11 pipeline also supports displacement mapping, which can produce stunning amounts of surface detail.

— Tessellation Stages — Microsoft Learn

The post’s pairing is the docs’ pairing. “Tessellation means nothing alone and only pays off combined with displacement” isn’t something the beginner’s post invented — it’s an item the people who introduced hardware tessellation wrote down as a benefit.

The same page records three more benefits — memory and bandwidth savings, continuous or view-dependent LOD, and “performing expensive computations at lower frequency”. That follows from a structure where only the low-detail model is sent and the detail is built on the GPU.

Where you turn it on in Unity differs per pipeline

So where do you turn it on in Unity? It differs per pipeline, and the best-known route is the narrowest.

In the Built-in pipeline it’s a surface shader directive.

Tessellation is indicated by tessellate:FunctionName modifier.

Surface shaders can optionally compute phong tessellation to smooth model surface even without any displacement mapping.

the shader is automatically compiled into the Shader Model 4.6 target

Only triangle domain - no quads, no isoline tessellation.

— Tessellation Surface Shader examples in the Built-In Render Pipeline — Unity 6.6 manual

One line, tessellate:. And three constraints come attached — it’s automatically compiled to Shader Model 4.6, so older targets drop out; only the triangle domain is supported; and being a surface shader at all is a constraint. The same page records it — “The Universal Render Pipeline (URP) doesn’t support surface shaders”, and the compatibility table lists HDRP and Custom SRP as unsupported too. How to read that table is written up in The Table on Unity’s Custom Shaders Page Is a Fork, Not an Index, and which built-in shader examples port to URP and which don’t is counted in Thirteen Built-In Shader Examples: The Further Down the List, the Less Ports to URP.

What catches you more is the notice on that same page — the Built-in Render Pipeline is deprecated and supported through the Unity 6.7 LTS lifecycle. Where I confirmed that notice is written up in What Puts It in the Inspector Isn’t Exposed, It’s Show In Inspector.

HDRP has dedicated shaders and options.

The Tessellation options control how Unity tessellates your Material’s surface and smooths geometry.

None: HDRP only uses the displacement map to tessellate the mesh.

Phong: HDRP applies Phong tessellation to the mesh.

Max Displacement: Maximun possible world displacement in meter for the Material.

— Tessellation — HDRP 14.0

The None mode’s description states this post’s point outright — “HDRP only uses the displacement map to tessellate the mesh.” The displacement map is the input that drives tessellation. Its role differs from the Standard Shader’s Height Map. The same page says the Tessellation Factor is only available in the Lit Tessellation and Layered Lit Tessellation shaders, and adds that in Master Stacks it appears as an input. And decal meshes don’t support tessellation.

RouteHow you turn it onConstraint
Built-in + surface shadertessellate:FunctionNameSM 4.6, triangle domain only, URP/HDRP unsupported
Built-in itself—deprecated (supported through 6.7 LTS)
HDRPLit Tessellation / Layered Lit Tessellationthose shaders only, decal meshes excluded
HDRP Master StackTessellation Factor exposed as an inputMax Displacement is Master Stack only
Standard Shadernowherethe Height Map is parallax

The cost is there just from turning it on

The post doesn’t cover cost. Fair enough for a beginner’s piece, but the first sentence that catches you when you actually turn it on is in HDRP’s docs.

Enabling the Tessellation option have an extra GPU cost, even if the tessellationFactor is 0.0 or 1.0.

To maintain good performance on the Xbox One or PlayStation 4, do not use values greater than 15.

— Tessellation — HDRP 14.0

There’s an extra cost even with the factor at 0 or 1. “Leave it on and set the value to 0 and it’s free” isn’t the case. And there’s a number written down — on Xbox One and PlayStation 4, don’t exceed 15. The same page recommends adding one more LOD using a shader without tessellation.

The Built-in side has its own cost material: a caution about distance-based tessellation, and a culling variant function.

Purely distance based tessellation is effective only when triangle sizes are quite similar.

For performance reasons, call the UnityEdgeLengthBasedTessCull function instead, which performs patch frustum culling.

— Tessellation Surface Shader examples in the Built-In Render Pipeline — Unity 6.6 manual

If triangle sizes aren’t uniform, distance-based goes wrong — small triangles get over-subdivided and large ones under-subdivided. And there’s a separate function doing per-patch frustum culling, which the same page says makes the shader a bit more expensive but saves GPU work on parts off screen.

Where and why you’d use it

Tessellation is for when you want to send a low-resolution mesh and build the detail on the GPU. Terrain, and surfaces like brickwork or rock that need to be bumpy down to the silhouette. If the silhouette can stay flat a normal map is far cheaper, and if only the inner texture needs to be more convincing, the Height Map (parallax) is in between. Don’t read the three as degrees of the same effect — what each changes is, respectively, the lighting, the texture’s position, and the vertices.

Thinking back to where I first heard the word, this distinction is what sets the depth of an answer. “A technique that subdivides faces” is what one search gets you, and the next question is “so why do that”. Displacement has to come out there, and following on with “how do you use it in Unity” takes you to per-pipeline routes and cost. All three layers are written in the docs.

A working example

Two things in this topic can be checked in code: that a mesh’s vertex count has nothing to do with tessellation, and which texture fields the current material actually has. Both are in one component.

The property names aren’t hardcoded. I could not confirm the Standard Shader’s property names in the current docs, so instead of guessing at names, it enumerates the properties the shader holds.

using System.Text;
using UnityEngine;
using UnityEngine.Rendering;

[RequireComponent(typeof(Renderer))]
public class SurfaceDetailProbe : MonoBehaviour
{
    [Header("Reporting")]
    [Tooltip("On lists every texture property. Off lists only assigned ones")]
    [SerializeField]
    private bool _listEmptySlots;

    [Tooltip("Properties whose name contains one of these get marked")]
    [SerializeField]
    private string[] _highlight = { "Normal", "Bump", "Parallax", "Height" };

    private void Start()
    {
        Probe();
    }

    [ContextMenu("Check now")]
    private void Probe()
    {
        if (!TryGetComponent(out Renderer meshRenderer))
        {
            Debug.LogError("There is no Renderer attached.", this);
            return;
        }

        ReportMesh();

        // This only inspects, so read sharedMaterial. Reading material
        // creates an instance.
        Material material = meshRenderer.sharedMaterial;
        if (material == null)
        {
            Debug.LogWarning("The material is empty.", this);
            return;
        }

        ReportTextureProperties(material);
    }

    private void ReportMesh()
    {
        if (!TryGetComponent(out MeshFilter filter)) { return; }

        Mesh mesh = filter.sharedMesh;
        if (mesh == null) { return; }

        // The docs say only "Returns the number of vertices in the Mesh".
        // Tessellation happens on the GPU, so it isn't reflected here.
        Debug.Log(
            $"mesh has {mesh.vertexCount} vertices — tessellation isn't here",
            this);
    }

    private void ReportTextureProperties(Material material)
    {
        Shader shader = material.shader;
        if (shader == null) { return; }

        StringBuilder builder = new StringBuilder();
        builder.AppendLine($"shader: {shader.name}");

        // Enumerate what the shader holds instead of guessing names.
        int count = shader.GetPropertyCount();

        for (int i = 0; i < count; i++)
        {
            if (shader.GetPropertyType(i) != ShaderPropertyType.Texture)
            {
                continue;
            }

            string name = shader.GetPropertyName(i);
            Texture assigned = material.GetTexture(name);

            if (assigned == null && !_listEmptySlots) { continue; }

            string mark = IsHighlighted(name) ? " <-" : string.Empty;
            string value = assigned != null ? assigned.name : "(empty)";

            builder.AppendLine($"  {name} = {value}{mark}");
        }

        Debug.Log(builder.ToString(), this);
    }

    private bool IsHighlighted(string propertyName)
    {
        foreach (string piece in _highlight)
        {
            if (string.IsNullOrEmpty(piece)) { continue; }

            if (propertyName.Contains(piece)) { return true; }
        }

        return false;
    }
}Scripts/Graphics/SurfaceDetailProbe.cs

Three things worth pointing out.

One — the vertex count doesn’t show tessellation. The docs write Mesh.vertexCount only as “Returns the number of vertices in the Mesh (Read Only).” Tessellation happens in the Tessellator stage, and the vertices that stage creates aren’t in the mesh asset. “I turned tessellation on and the vertex count didn’t change” is normal.

Two — it enumerates properties. The docs record all three — GetPropertyCount is “Returns the number of properties in this Shader.”, GetPropertyName(int) returns the name, and GetPropertyType(int) returns a ShaderPropertyType. Both are documented to throw ArgumentOutOfRangeException for a missing index, so the loop is bounded by GetPropertyCount. This way I can’t get wrong the names that differ per shader (Built-in, URP, HDRP and lilToon all differ).

Three — _highlight is a marker, not a verdict. Code can’t know whether a field with Height in its name is parallax or displacement. What that field does is in that shader’s documentation. So it only adds an arrow, and the judgement is the person’s.

How to choose

What you wantWhat to useThe sentence behind it
bumpy lighting onlyNormal map“only how the lighting is calculated on the surfaces”
inner texture with occlusionHeight map (parallax)“shifts the areas of the visible surface texture around”
change the silhouettetessellation + displacement“also supports displacement mapping”
tessellation in Built-insurface shader tessellate:“Tessellation is indicated by tessellate:FunctionName”
smoothing without displacementtessphong: (Phong)“even without any displacement mapping”
in HDRPLit Tessellation / Layered Lit Tessellation“only available in the Layered Lit Tessellation Shader and Lit Tessellation Shader”
picking a factor on console15 or lower“do not use values greater than 15”
saving work off screenUnityEdgeLengthBasedTessCull“performs patch frustum culling”
an LOD without tessellationadd one more LODthe docs recommend it

Here’s the order I’d decide in.

  1. Does the silhouette have to change? If not, stop at the normal map. It’s the cheapest.
  2. Do you only need inner occlusion? The Height Map (parallax) is that slot. The silhouette doesn’t change.
  3. If the silhouette must change, pick the shader first. The Standard Shader has no such field. Go to the tessellation shader for your pipeline.
  4. Accept the cost before turning it on. The docs say there’s extra cost even with the factor at 0.

Where not to use it

Summary


References

The clipping is jhnambi’s Tistory post 테셀레이션(tessellation) 란? (2022-11-20), in Korean; the quotes from it are my translations. It’s written for beginners, and it’s a post that attached its own caution that some parts may be wrong. Out of respect for that premise, instead of counting its errors this post swapped two terms for the docs’ definitions and added one more gap that appears on coming over to Unity. The big picture of pairing tessellation with displacement the post got right — and that’s the most important item this post confirmed.


Share this post:

Previous Post
The Page Says 2023, and What You Get Is the 2020 LTS Edition