Skip to content

Conversation

@johnnyshields
Copy link

@johnnyshields johnnyshields commented Jan 12, 2026

Ideas for future PR:

  • Add utility to normalize heightmap (set median pixel to zero, clamp or expand min/max)
  • Use mix-max of height map to adjust height depth

This PR implements height-based displacement for PhysicalMaterial using Parallax Occlusion Mapping with secant refinement. I'd be happy to add this as POMPhysicalMaterial (or ParallaxPhysicalMaterial) if you prefer, but I think there's no harm to add it just to PhysicalMaterial b/c without a heightmap the performance should be basically the same.

These pictures are flat wall meshes with textures that have a texture with a height map and normal map.

image

You can see how the bricks stick-out at different angles, versus standard normal+AO maps.

image image image image

Features:

  • First pass = linear layer computation, second pass = secant refinement to blend layers
  • Distance-based LOD with smooth fade to flat UVs at configurable range
  • Includes utilities to precompute/load normal map (fast) and AO map (slow, requires raytrace algo) from heightmap.

Parameters:

  • height_texture: Texture map for height (white=raised, 50% gray=surface, black=lowered)
  • height_scale: Depth of height texture
  • height_quality: (VeryLow to VeryHigh) which controls layer count, smoothing, and render distance

Performance optimizations:

  • #define USE_HEIGHT_TEXTURE -- if no heightmap is supplied, then USE_HEIGHT_TEXTURE will be false and the code/performance will be identical to the state before this PR.
  • Short-circuit to not apply POM to distant surfaces
  • Layer count scales with height_scale (depth) - pre-computated
  • Layer count and refinement scales with distance
  • #ifdef to elimate dead code paths
  • Use textureLod to avoid implicit LOD calculation overhead
  • Guards against division-by-zero

Re: height_quality, you tend not to see the quality difference except up close and in extreme cases, e.g. sharp metallic edges. The higher quality levels increase the number of layers (reduces "stacked pancake" effect) and the amount of secant smoothing (reduces artifacts). Bricks, concrete, and rough surfaces generally look very good on "Low" quality.

image

…calMaterial

Implement height-based displacement for PhysicalMaterial using Parallax Occlusion Mapping with secant refinement. Features include:

- First pass = linear layer computation, second pass = secant refinement to blend layers
- Distance-based LOD with smooth fade to flat UVs at configurable range
- Automatic normal derivation from height gradient when no normal map

Parameters:
- height_texture: Texture map for height (white=raised, 50% gray=surface, black=lowered)
- height_scale: Depth of height texture
- height_quality: (VeryLow to VeryHigh) which controls layer count, smoothing, and render distance

Performance optimizations:
- Short-circuit to not apply POM to distant surfaces
- Layer count scales with height_scale (depth) - pre-computated
- Layer count and refinement scales with distance
- #ifdef to elimate dead code paths
- Use textureLod to avoid implicit LOD calculation overhead
- Guards against division-by-zero
@johnnyshields johnnyshields changed the title Add Heightmap support using Parallax Occlusion Mapping (POM) to PhysicalMaterial PhysicalMaterial: Add Heightmap support using Parallax Occlusion Mapping (POM) Jan 13, 2026
PhysicalMaterialBase = 0x8020 had bit 5 already set in its value. When the height texture flag (also bit 5) was OR'd onto this
  base, it had no effect since the bit was already 1. This caused materials with and without height textures to get the same shader ID, leading to shader cache collisions where non-height materials received height-enabled shaders.
HeightQuality::Low => (8, 0, 8.0, 25.0),
HeightQuality::Medium => (8, 2, 10.0, 50.0),
HeightQuality::High => (12, 3, 15.0, 50.0),
HeightQuality::VeryHigh => (16, 4, 15.0, 50.0),
Copy link
Author

@johnnyshields johnnyshields Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These levels can be tuned further, this is just what made sense for me experimentally in basic manual testing.

(Rather than "Quality" we may wish to expose the underlying raw params to users, but many of the Tier-1 rendering engines like Unreal just use "Quality")

PrefilterMaterial = 0x8080,
PhysicalMaterialBase = 0x8040, // To 0x807F (6 bits: albedo, metallic_roughness, occlusion, normal, emissive, height)
DeferredPhysicalMaterialBase = 0x8080, // To 0x80BF (6 bits: albedo, metallic_roughness, occlusion, normal, emissive, alpha_cutout)
PrefilterMaterial = 0x80C0,
Copy link
Author

@johnnyshields johnnyshields Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shift of bits is needed to accommodate the heightmap. I'm not sure the actual performance impact of going from 5 to 6 bits on PhysicalMaterial, but if this is a regression then we could make my code here a new material (POMPhysicalMaterial) instead.

@johnnyshields
Copy link
Author

johnnyshields commented Jan 14, 2026

@asny any feedback would be appreciated! 🙇‍♂️ Please try examples/heightmap if you can spare a moment.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant