The last note owned the UV ellipse a pixel covers. This one does not pick a new sampler. A height field offsets UVs along the tangent-space view ray so a 2-triangle quad reads as carved brick. Interior parallax and (optional) height-field self-shadow can look like depth. The outline of the mesh does not. The silhouette is still the quad.

Same wall, same light, three shaders. Left FLAT: geometric , UV — a painted card. Middle BUMP: height-derived normals at the geometric UV; grooves shade, bricks do not walk with view. Right POM: roof-hull march; interior bricks slide, mortar hides in the wells it should hide in. Do not hang a UV-error number on this photograph.

Same sentence as a poster: bump vs POM, raking graze, not real displacement.

That is the lie, measured. Proud bricks extend the outline on the left; the POM quad is a straight card. Red is . That fraction is . AABB span is the extrusion proof: the grid actually left the plane. Interior pixels can still be a good POM hit. Coverage cannot grow.
Hero: scale , bias , fixed, height R32F, Mesa 25.0.7 llvmpipe. Face-on MAE(bump, POM) . Analytic UV error on a spherical indent interior at : . Graze silhouette XOR (floor displaced vs roof quad) . Assertions on this run: 28 pass / 0 fail.
How it presents
Stimulus is a CPU-authored running-bond brick, POT, REPEAT, roof convention ( flush with the mesh, carved). Science instruments use a spherical indent (CLAMP_TO_EDGE) and a 1-D cosine ridge. The wall is procedural clay — per-brick families, grog, stria — so UV slide has frequency to show. It is not a photo scan and not a PBR brick.

Interior parallax is visible before the graze hero. Still photo-only.
Two regimes, never mixed:
- Face-on (, ): offset is . Bump, offset, steep, POM must match (same normals, same UV). Science MAE(bump, POM) . If “POM” looks more 3-D here, the TBN sign or the residual offset is wrong.
- Graze (): this is the article. Interior parallax is the wow. Silhouette disagreement with the displaced mesh is the lie. Too few layers is the swim.
Software raster does not have to animate. Swim at a locked pose is banding / missed wells. Silhouette failure at a locked pose is a straight edge next to a brick profile.
Height maps cannot represent overhangs (they are functions ). Self-occlusion here is “this ridge hides that well from this ray,” not a cave.
Why: TBN, , POM lerp
TBN and tangent-space view
Orthonormal tangent frame at the fragment, columns in world space. Locked handedness: , right-handed.
Science TBN is CPU, from the quad (or the displaced-grid vertex) — the same matrix the shader is given as attributes. Do not trust llvmpipe dFdx of world position as the science TBN.
Front face: . If , skip the march.
Height convention (roof default)
The geometric quad is the outer hull at normalized height . Texture is elevation of the real surface: flush, carved in.
is bias (locked on this run). Scale is the physical height range in units of the quad’s -edge world length. Locked hero . Lateral UV travel for a unit drop in normalized height:
Proud-brick silhouette control: a second authorship with the mesh at the floor (height ) and bricks extruding to . Same . That is the outline that should grow and does not under POM.
Misses: if the ray never meets the height field inside the volume, sample the floor .
Ray in the height volume
Origin at the geometric hit, roof , UV . Parameter is normalized depth into the volume (drop in height):
Hit is the smallest with .
Offset mapping — named knife, not hero
True parallax (Kaneko-style), one sample:
Offset limiting (Welsh) drops the so . At graze, true offset explodes; limited offset under-walks. Both belong on the family strip. Neither is the hero.
Steep parallax — even layers, first crossing
Steep hit: smallest with . Staircase in . Hero fixed; compile-time cap .
POM — linear search + secant lerp (hero)
After the first crossing between layers and , interpolate the zero of assuming both are linear in :
This is a secant step on the samples, not an analytic ray–height intersection unless is linear between those UVs. Relief ( bisections after the crossing) is implemented and omitted from the family strip; the ladder already shows a linear search skipping a thin well.
Sampling the height inside the march
Do not texture() inside the loop. Non-uniform iteration count implicit derivatives are illegal.
Two locked values:
- : sharp wells, aliases when the wall minifies.
- from the geometric UV Jacobian (CPU , same formula as mipmaps / anisotropic): relief melts, self-occlusion fades.
Do not claim an along-ray cone LOD. After , albedo at ; normal from central differences of at the hit.
Self-shadow (second march, not a shadow map)
From toward the light in tangent space, same roof/floor volume. If any sample along that ray lies below the height field before exiting, the point is in height-field shadow. Binary (any hit) is the default. This is in-family. It is not shadow-map bias, not PCF, not glPolygonOffset.
Silhouette predicate (the lie, as a formula)
Let be coverage of the 2-triangle wall. POM shades only where . Let be coverage of the displaced grid of the same height.
gl_FragDepth from the hit can correct interior depth for later compositing; it cannot grow coverage. Default: do not write gl_FragDepth. This run does not.
Unique artifacts: the slice, then the XOR

This is the thing the article exists to draw. CPU science, not a GL photo. 1-D cosine ridge, view ray, layer planes.
- Primary: . Steep first-hit, POM secant, and dense-1D true hit agree on the same first ridge (nested rings). Dense-1D , POM , steep .
- Inset: skipped well — intentional. True hit still on ridge 1; steep/POM skip to ridge 2. Too few layers is not “POM is broken”; it is the swim hero on the -ladder.



Spherical indent, , center , wrap clamp, indent interior only. Heat is . Outside the rim the height field is the roof (); a raw sphere quadratic “miss” is not the height-field hit and is not averaged in.
Quote the numbers. Do not quote the brick photographs as UV error.
Scale , bias , Mesa llvmpipe:
| row | angle | metric | value | |
|---|---|---|---|---|
| POM indent interior | 4 | mean UV err | ||
| POM indent interior | 32 | mean UV err | ||
| same | 32 | p95 UV err | ||
| sil-xor floor vs roof | 32 | profile | fraction | 0.575 |
| face-on | 32 | MAE(bump, POM) | 0 | |
| face-on | 32 | sil XOR (not the lie) | 0.029 | |
| POM field | 32 | mean first-hit layer | 9.06 | |
| shadow | 32 | agree vs , same field | 0.993 |
Hero rounding: ; ; AABB span . (asserted). Analytic error is the sphere indent, not the brick. Do not invent a GL UV error for the graze / brick-hero photographs. Those frames are photo-only.
Do not invent a “POM quality score.”
Family, then the ladders



Controls
Face-on: POM must not invent depth

, same roof hull. Science path is an ortho UV-grid MAE, not the perspective photograph: MAE(bump, POM) . The photograph still has a few degrees of edge parallax; it is a picture of the control, not a second metric. If POM looked more 3-D here, TBN handedness would be the first suspect.
Scale / bias
is bump (no UV walk). Locked hero is readable interior parallax on this wall. spans too many tiles per unit : first crossing is the wrong brick, rays hit the volume floor, the wall swims. Bias locked .
Too few steps
misses wells and staircases — spatial proxy for swim. cleans the same pose. The march-slice inset is the same failure in 1-D, labeled so it cannot be read as a broken hero.
Self-occlusion is a second height march

Left: view-march only; hidden wells can still shade if the view hit is right and the light is ignored. Middle: light-march darkens contact. Right: displaced-mesh truth (real triangles, same height). Caption is the claim: not a shadow map. Shadow agree is vs on the same height field, not vs ray-traced triangles. Do not quote it as a geometry match.
Height LOD — one sentence of Jacobian

Minified wall, CPU geometric UV Jacobian, , . Left : sharp wells, aliases. Right : relief melts. Continuity with mipmaps / anisotropic: isotropic LOD of the geometric UV, not an along-ray cone. Not a new theorem. No AF quality table.
Offset leaves the tile

Periodic brick should walk to the next tile; clamp flattens a rim. Photo only.

What this box actually measured
Host: OSMesa, Mesa 25.0.7-2+deb13u1, llvmpipe (LLVM 19.1.7, 256 bits). FBO RGBA32F, height upload R32F, 8-bit fallback not hit, no sRGB, no MSAA. gl_FragDepth not written. Brick wrap REPEAT; sphere indent CLAMP_TO_EDGE. Assertions: 28 pass / 0 fail, including TBN orthonormal / right-handed, , displaced AABB span (0.1415), analytic UV err , face-on MAE , and profile (0.575).
Can claim: on this OSMesa / llvmpipe build, a CPU tangent-space height march on an authored field offsets UV along a known and reduces UV error vs a sphere as grows (until the linear-search / lerp model saturates). A GLSL 330 fragment march with a fixed cap and textureLod is a photograph of this software rasterizer, not of a discrete GPU’s POM unit (there isn’t one). Face-on, bump and POM match on the science path (MAE ). At graze, the displaced-grid coverage disagrees with the 2-triangle POM quad; the XOR is the silhouette lie. Height-field light-march darkens wells the view ray does not hide. That is not a hardware shadow map.
Cannot claim: NVIDIA / AMD / Intel hardware POM, tessellation quality, or “games do this many taps.” That llvmpipe loop performance matches a GPU. No milliseconds. That dFdx/dFdy TBN or implicit LOD inside the march equal hardware. That POM lerp is the ray–height intersection. That writing gl_FragDepth “fixes” displacement — coverage is still the quad. That a height field is geometry.
Honesty, short:
- Metrics are CPU on float height. Graze / brick-hero / self-shadow wall panels are photographs of this software rasterizer. Do not quote them as UV error or .
- Face-on MAE is the UV-grid control. The perspective face-on photograph is a picture of that control, not a second metric.
- Analytic UV error is scored on the spherical indent interior only. POM lerp is not the ray–height intersection; residual vs analytic is in scope.
- March-slice “analytic” is a dense 1-D first-crossing of the authored cosine ridge, not a closed-form sphere.
- Silhouette XOR uses a profile camera. Face-on XOR is not the lie. AABB span is the extrusion proof.
- Self-shadow agree is vs on the same field, not vs the displaced mesh and not a hardware shadow map.
- Height LOD. , . Relief softening is visible but modest. Geometric Jacobian only.
- Scale is locked for readable interior parallax on this wall. is the swim/miss rung.
- No
gl_FragDepth, no cone-step, no tessellation. Coverage of the POM path is still the 2-triangle quad. - Brick is CPU procedural, not a photo scan. Analytic error is on the sphere, not the brick.
Bump perturbs at the geometric UV. Offset is one push. POM marches. Displacement moves vertices. Only the last one grows the silhouette.
Pin the graze 3-up as the presentation. Pin bump-vs-POM as the lighting hero. Pin the silhouette XOR as the lie. Pin the march slice plus the analytic UV-error heatmap as the science theorem. The formula is the caption. The XOR is why the outline stayed a quad.