Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion examples/jsm/objects/Water.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Color,
FrontSide,
FloatType,
HalfFloatType,
Matrix4,
Mesh,
Expand Down Expand Up @@ -85,7 +86,7 @@ class Water extends Mesh {

const mirrorCamera = new PerspectiveCamera();

const renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, { type: HalfFloatType } );
let renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, { type: HalfFloatType } );

const mirrorShader = {

Expand Down Expand Up @@ -237,6 +238,15 @@ class Water extends Mesh {

scope.onBeforeRender = function ( renderer, scene, camera ) {

// Lazily fall back to FloatType if EXT_color_buffer_half_float is unsupported (e.g. some mobile GPUs)
Copy link
Collaborator

@Mugen87 Mugen87 Mar 4, 2026

Choose a reason for hiding this comment

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

I would expect a fallback to UnsignedByteType if half float isn't available. The render target setup for transmission works like that:

const hasHalfFloatSupport = extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' );
currentRenderState.state.transmissionRenderTarget[ camera.id ] = new WebGLRenderTarget( 1, 1, {
generateMipmaps: true,
type: hasHalfFloatSupport ? HalfFloatType : UnsignedByteType,
minFilter: LinearMipmapLinearFilter,
samples: Math.max( 4, capabilities.samples ), // to avoid feedback loops, the transmission render target requires a resolve, see #26177
stencilBuffer: stencil,
resolveDepthBuffer: false,
resolveStencilBuffer: false,
colorSpace: ColorManagement.workingColorSpace,
} );

Copy link
Collaborator

@Mugen87 Mugen87 Mar 4, 2026

Choose a reason for hiding this comment

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

To clarify: There is no need to use FloatType. We can keep using HalfFloatType but just request EXT_color_buffer_float instead. This extension includes formats like gl.RGBA16F.

Reference: https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float/

The thing is, the renderer already requests EXT_color_buffer_float here:

getExtension( 'EXT_color_buffer_float' );
getExtension( 'WEBGL_clip_cull_distance' );
getExtension( 'OES_texture_float_linear' );
getExtension( 'EXT_color_buffer_half_float' );
getExtension( 'WEBGL_multisampled_render_to_texture' );
getExtension( 'WEBGL_render_shared_exponent' );

I'm now confused why the changes in the PR are required in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My phone has the following capabilities:

❌ EXT_color_buffer_half_float
✅ EXT_color_buffer_float
✅ OES_texture_float_linear

If I replace FloatType with UnsignedByteType as fallback, I get darker brights

With PR as it is

Image

With UnsignedByteType fallback

Image

What would you like me to do?

Copy link
Collaborator

@Mugen87 Mugen87 Mar 4, 2026

Choose a reason for hiding this comment

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

If EXT_color_buffer_float is supported on your device, HalfFloatType should work. If it doesn't, we must find out the root cause.

Do you see any console warnings when HalfFloatType is used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No console warnings, silently passes. Driver issue?

Btw, I moved the fallback to WebGLTextures as it seems like the right place instead as suggested by @mrdoob .

Copy link
Collaborator

@Mugen87 Mugen87 Mar 9, 2026

Choose a reason for hiding this comment

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

It seems the issue was introduced with #32680 which has added bloom to the demo.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The pixels in webgl_shaders_ocean can get very bright due to how the sky is implemented now (it does not use a built-in gamma correction since r183). If you then generate a PMREM from the scene and use it as an environment map, the objects in your scene end up bright as well. If you then add bloom to the scene, it further increases the luminance. I think this gets to a point where certain devices can't handle the values anymore.

Given that there is no issues on Desktop and the demo uses RGBAF16 on all devices, the root cause must be related to something device specific.

Copy link
Contributor Author

@RodrigoHamuy RodrigoHamuy Mar 10, 2026

Choose a reason for hiding this comment

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

I think the issue in r183 happens even if I disable bloom (which I did while debugging this).
r182 does work fine, although, damn, that was a nice upgrade in the last version 😄 . The new sky looks so much better!!

Copy link
Collaborator

@Mugen87 Mugen87 Mar 10, 2026

Choose a reason for hiding this comment

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

Indeed, without bloom the rendering breaks but more subtle. Here is a test link right before the bloom addition:

https://rawcdn.githack.com/mrdoob/three.js/98a7dd547f988ad0cfaf697ec41a0ff5bf254d0b/examples/webgl_shaders_ocean.html

With a high elevation, the box mesh becomes black (it should turn white though). Nevertheless, bloom seems to amplify the issue. The breakage on mobile starts with #32677 though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we must somehow isolate this issue into a smaller test case. Like the one from #33114 (comment). In this way, it will be easier to get feedback from the browser dev side.

if ( renderTarget.texture.type === HalfFloatType && ! renderer.extensions.has( 'EXT_color_buffer_half_float' ) ) {

renderTarget.dispose();
renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, { type: FloatType } );
material.uniforms[ 'mirrorSampler' ].value = renderTarget.texture;

}

mirrorWorldPosition.setFromMatrixPosition( scope.matrixWorld );
cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );

Expand Down
6 changes: 5 additions & 1 deletion src/extras/PMREMGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
NoBlending,
RGBAFormat,
HalfFloatType,
FloatType,
BackSide,
LinearSRGBColorSpace
} from '../constants.js';
Expand Down Expand Up @@ -288,11 +289,14 @@ class PMREMGenerator {
const width = 3 * Math.max( this._cubeSize, 16 * 7 );
const height = 4 * this._cubeSize;

const extensions = this._renderer.extensions;
const type = extensions.has( 'EXT_color_buffer_half_float' ) ? HalfFloatType : FloatType;

const params = {
magFilter: LinearFilter,
minFilter: LinearFilter,
generateMipmaps: false,
type: HalfFloatType,
type: type,
format: RGBAFormat,
colorSpace: LinearSRGBColorSpace,
depthBuffer: false
Expand Down