Skip to content

Add a parallax terrain example, and tighten the occlusion march limit - #9223

Merged
mvaligursky merged 1 commit into
mainfrom
mv-parallax-terrain
Aug 25, 2026
Merged

Add a parallax terrain example, and tighten the occlusion march limit#9223
mvaligursky merged 1 commit into
mainfrom
mv-parallax-terrain

Conversation

@mvaligursky

@mvaligursky mvaligursky commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

New example: materials/parallax-terrain

Screenshot 2026-08-25 at 14 08 37

A cpu generated displaced grid with a single ground material over all of it, so the marched parallax mode is seen across a wide range of distances and viewing angles at once - the case it earns its cost in over the single tap one. A directional light with cascaded shadows and 40 trees give the relief something to be shadowed by, and the camera is the hybrid CameraControls (orbit + fly) so the whole terrain can be visited.

Controls: parallax Mode / Samples / Height, Shadow Cascades (filtering, count) and Soft Shadows (falloff, samples, blocker samples).

Textures are ambientCG Ground092C, CC0, 1K converted to webp - 0.7 MB against 3.3 MB for the equivalent jpg maps. The tree model is the existing low-poly-tree.glb, credited in the example header.

Behaviour change: parallaxMaxSlope 10 → 5

⚠️ This is a visible change for any material using PARALLAX_OCCLUSION.

The constant caps the march at that multiple of the relief depth, which bounds how far the lookup slides along the surface at a grazing angle. That slide is what makes the relief appear to swim as the camera moves: parallax fakes depth by displacing the texture lookup by depth × tan(θ) while the pixel itself never moves, and d(tan θ)/dθ = sec²θ is ~13 at 74° off the normal, so a small camera move slides the texture a long way.

Measured on this terrain, on a slope at ~74°, with the relief only 0.25 world units deep:

parallaxMaxSlope lateral slide (world) variation over a 1.8 unit camera move
10 (before) 0.81 → 0.55 0.26
3 0.65 → 0.52 0.17
1.5 0.44 → 0.43 0.03

5 was picked by eye as the point where grazing relief still reads without the visible swim. The trade is less apparent depth at extreme grazing angles in exchange for a stable image. Ruled out as causes by measurement, for the record: the distance fade (forcing it to 1 changed the result by 0.0001), the explicit mip level (lod 0 vs +2 moves the depth ~7%), and mesh tessellation (nothing in the march depends on triangle density).

Verified on WebGL2 and WebGPU, all five shadow filtering modes and cascade counts 1-4.

New `materials/parallax-terrain` example: a cpu generated displaced grid
with one ground material over all of it, so the marched mode is seen
across a wide range of distances and viewing angles at once - which is
where it earns its cost over the single tap one. A directional light with
cascaded shadows and 40 trees give the relief something to be shadowed
by, and the panel exposes the parallax mode, sample budget and height
alongside the cascade and soft shadow settings.

Also lowers `parallaxMaxSlope` from 10 to 5. It caps the ray at that
multiple of the relief depth, which is what bounds how far the lookup
slides along the surface at a grazing angle - and that slide is what
makes the relief appear to swim as the camera moves, because the geometry
never moves, only the texture lookup does. Measured on this terrain, the
slide varied by 0.26 world units over a small camera move at 10, 0.17 at
3 and 0.03 at 1.5, against a relief only 0.25 deep. 5 keeps grazing
relief without the visible swim. This is a visible change for any
material using PARALLAX_OCCLUSION: less relief at extreme grazing
angles, in exchange for a stable image.

Textures are ambientCG Ground092C, CC0, 1K converted to webp (0.7MB
against 3.3MB for the equivalent jpg maps).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2380.8 KB (−0.0 KB, −0.00%) 611.8 KB (−0.0 KB, −0.00%) 474.9 KB (−0.2 KB, −0.03%)
playcanvas.min.mjs 2378.1 KB (−0.0 KB, −0.00%) 610.5 KB (−0.0 KB, −0.00%) 474.4 KB (+0.0 KB, +0.01%)

@mvaligursky
mvaligursky merged commit da9042c into main Aug 25, 2026
10 checks passed
@mvaligursky
mvaligursky deleted the mv-parallax-terrain branch August 25, 2026 13:09
@Maksims

Maksims commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Feels like there is a height fade out based on fragment distance is required, to fade into no-parallax based on proximity.

@mvaligursky

mvaligursky commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Feels like there is a height fade out based on fragment distance is required, to fade into no-parallax based on proximity.

Both number of taps for parallax and number of taps for shadows fade out based on pixel size .. so in the distance the parallax is cheaper all the way to disabled.

See this https://x.com/ValigurskyM/status/2091916601293496351

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants