Skip to content

feat: Add planar reflections to StandardMaterial - #9902

Merged
mvaligursky merged 3 commits into
mainfrom
mv-reflection-planar
Nov 2, 2026
Merged

mvaligursky merged 3 commits into
mainfrom
mv-reflection-planar

Conversation

@mvaligursky

@mvaligursky mvaligursky commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Adds planar reflection support to StandardMaterial. A material can reflect a texture rendered by a camera mirrored by the plane of its surface, for example by the PlanarRenderer script, giving exact reflections on floors, mirrors and water while keeping its full lighting.

Changes:

  • StandardMaterial takes a planar reflection texture and the view projection matrix of the camera that rendered it. The texture replaces only the environment reflections: lights, shadows and fresnel are unchanged, and the environment of the material or the scene keeps providing the ambient lighting
  • The surface is projected into the texture along the reflected ray, so the reflection works for planes of any orientation, and normal maps and bevels distort it
  • Rougher surfaces sample smaller mipmaps of the texture, blurring the reflection. Clear coat and sheen reflect the planar texture too
  • A material with a planar reflection refracts the scene color (useDynamicRefraction), as the texture holds no environment to refract
  • The PlanarRenderer script provides the matrix for the material, and can render a high dynamic range texture, so bright light sources keep their brightness in the reflection

API Changes:

  • New StandardMaterial#planarReflectionMap (Texture|null, default null) and StandardMaterial#planarReflectionMatrix (Mat4, copied into the material)
  • New PlanarRenderer#viewProjectionMatrix getter (scripts/esm/planar-renderer.mjs), updated by frameUpdate()
  • New PlanarRenderer attribute hdr, false by default. When enabled and the device can render to half-float textures, the texture is PIXELFORMAT_RGBA16F, and the camera renders linear colors with TONEMAP_NONE and GAMMA_NONE, leaving tone mapping and gamma correction to the camera rendering the material
const planarRenderer = reflectionCamera.script.create(PlanarRenderer, {
    properties: {
        sceneCameraEntity: camera,
        mipmaps: true,
        hdr: true
    }
});

// each frame, after the main camera was updated
const texture = planarRenderer.frameUpdate();
material.planarReflectionMap = texture;
material.planarReflectionMatrix = planarRenderer.viewProjectionMatrix;
material.update();

Examples:

  • graphics/reflection-planar is rebuilt to use the new material properties: a lit, polished floor in the area lights room, with a Cubemap mode for comparison and controls for the resolution and roughness

The polished floor of the area lights room keeps its lit material and
replaces only its environment reflection chunk with the planar
reflection, blurred by roughness using the mipmaps of the texture. Adds
an opt-in hdr attribute to the PlanarRenderer script, rendering linear
colors into a half-float texture.
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2792.2 KB (+2.2 KB, +0.08%) 724.0 KB (+399 B, +0.05%) 561.7 KB (+664 B, +0.12%)
playcanvas.min.mjs 2788.9 KB (+2.2 KB, +0.08%) 723.3 KB (+428 B, +0.06%) 560.7 KB (−18 B, −0.00%)

- StandardMaterial#planarReflectionMap and #planarReflectionMatrix replace the environment
  reflections of the material with a planar reflection texture, while the environment keeps
  providing the ambient lighting
- PlanarRenderer#viewProjectionMatrix returns the matrix projecting world positions into the
  texture
- The planar reflection example uses them instead of overriding the reflection shader chunk
@mvaligursky mvaligursky changed the title feat: Rebuild the planar reflection example with a lit, reflective floor feat: Add planar reflections to StandardMaterial Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Public API report

This PR changes the public API surface (+3 / −0), per the docs' rules (@ignore / @Private / undocumented are excluded).

Show API diff
+StandardMaterial.get planarReflectionMatrix(): Mat4
+StandardMaterial.planarReflectionMap: Texture | null
+StandardMaterial.set planarReflectionMatrix(value: Mat4)

Informational only — this never fails the build.

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated PR review by Codex (OpenAI AI agent), posted through the account holder's authorized automated review process.

Reviewed 855234c against b2a7693. Checked material defaults/copying/uniform updates and shader invalidation, environment versus planar reflection selection, ambient/refraction interactions, both shader implementations, and the PlanarRenderer matrix/HDR integration. One actionable P2 is reported inline: disabling HDR does not restore the reflection camera's LDR color-processing state.

Validation: 4,572 unit tests passing (43 pending), 4,570 native WebGPU/Metal tests passing (40 pending), and 81 bundle tests passing. Builds and consumer type tests pass. Eight additional native tests cover seven planar shader variants plus the script's HDR target and view-projection matrix. Chrome/WebGL2 also compiled and drew all seven variants without GL or shader errors: plain, clear coat, sheen, normal mapping, dynamic refraction with a scene-color grab, material ambient, and scene ambient. A browser probe reproduced the HDR toggle issue.

Changed source/script/build-helper ESLint and diff whitespace checks pass. The material test file's lint is not clean because the existing configuration rejects its Mocha/Chai patterns; the same categories of errors occur on the base revision. The Engine conventions skill guided temporary probes; repository source was not modified.

Limitations: shader execution and camera state were validated, but the production reflection example was not visually compared and reflection quality/performance across scenes was not benchmarked.

Comment thread scripts/esm/planar-renderer.mjs Outdated
Comment on lines +426 to +429
if (this._texture.format === PIXELFORMAT_RGBA16F) {
planarCamera.toneMapping = TONEMAP_NONE;
planarCamera.gammaCorrection = GAMMA_NONE;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Restore the LDR camera settings when HDR is disabled

This override is one-way, although updateRenderTarget() supports changing hdr at runtime. With a planar camera configured for TONEMAP_ACES / GAMMA_SRGB, I reproduced hdr = false -> true -> false: the texture returns to PIXELFORMAT_SRGBA8, but the camera remains TONEMAP_NONE / GAMMA_NONE rather than its original settings. The LDR pass therefore no longer uses its configured tone mapper (or tone-mapping exposure), and high-intensity colors can clip instead of following the original LDR rendering path. Please preserve and restore the non-HDR camera settings when leaving the HDR format, including a fallback to LDR, and add a toggle regression test.

Automated review by Codex (OpenAI AI agent).

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated PR review by Codex (OpenAI AI agent), posted through the account holder's authorized automated review process.

Re-reviewed f1af31e and the follow-up to the HDR transition finding. The previous P2 is fixed; no new actionable findings.

The original Chrome/WebGL2 reproduction now returns to TONEMAP_ACES / GAMMA_SRGB after hdr = false -> true -> false, with the expected LDR/HDR/LDR texture formats. Expanded native WebGPU probes also pass for resizing while HDR is active, restoring settings on half-float capability fallback, entering HDR again after changing the LDR settings, switching back while the camera is disabled, and restoring settings / releasing the target when the script is destroyed. The seven planar shader variants still compile and draw without GL or WebGPU validation errors.

Validation: 4,572 unit tests passing (43 pending), 4,570 native WebGPU/Metal tests passing (40 pending), 81 bundle tests passing, and 9 additional focused native tests passing. Engine builds (cache hits for unchanged engine outputs), consumer type tests, updated-script ESLint, and diff whitespace checks pass. The Engine conventions skill guided the temporary probes; repository source was not modified.

There are no outstanding actionable findings from my reviews. Limitations remain: no visual comparison of the production example or cross-scene reflection quality/performance benchmark.

@mvaligursky
mvaligursky merged commit b2f213f into main Nov 2, 2026
11 checks passed
@mvaligursky
mvaligursky deleted the mv-reflection-planar branch November 2, 2026 11:04

This branch was successfully deployed

2 active deployments
Preview – engine — f1af31eb Deployed Nov 2, 2026 by vercel[bot]
Preview – engine-api-docs — f1af31eb Deployed Nov 2, 2026 by vercel[bot]
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