From 9aa1a2fc96a6b917be4deb33c596bfe8afae45b6 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Mon, 15 Jun 2026 15:09:22 +0200 Subject: [PATCH 1/2] WebGPURenderer: Fully support `ExternalTexture`. (#33816) --- .../webgl-fallback/utils/WebGLTextureUtils.js | 42 ++++++++++++------- .../webgpu/utils/WebGPUTextureUtils.js | 2 +- src/textures/ExternalTexture.js | 3 -- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js b/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js index c9f7cd9cfd8fac..c1800f6394e037 100644 --- a/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js +++ b/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js @@ -429,30 +429,42 @@ class WebGLTextureUtils { createTexture( texture, options ) { const { gl, backend } = this; - const { levels, width, height, depth } = options; - const glFormat = backend.utils.convert( texture.format, texture.colorSpace ); - const glType = backend.utils.convert( texture.type ); - const glInternalFormat = this.getInternalFormat( texture.internalFormat, glFormat, glType, texture.normalized, texture.colorSpace, texture.isVideoTexture ); + let textureGPU, glTextureType, glFormat, glType, glInternalFormat; - const textureGPU = gl.createTexture(); - const glTextureType = this.getGLTextureType( texture ); + if ( texture.isExternalTexture === true ) { - backend.state.bindTexture( glTextureType, textureGPU ); + textureGPU = texture.sourceTexture; + glTextureType = this.getGLTextureType( texture ); - this.setTextureParameters( glTextureType, texture ); + } else { - if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.isCompressedArrayTexture ) { + const { levels, width, height, depth } = options; - gl.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, width, height, depth ); + glFormat = backend.utils.convert( texture.format, texture.colorSpace ); + glType = backend.utils.convert( texture.type ); + glInternalFormat = this.getInternalFormat( texture.internalFormat, glFormat, glType, texture.normalized, texture.colorSpace, texture.isVideoTexture ); - } else if ( texture.isData3DTexture ) { + textureGPU = gl.createTexture(); + glTextureType = this.getGLTextureType( texture ); + + backend.state.bindTexture( glTextureType, textureGPU ); + + this.setTextureParameters( glTextureType, texture ); + + if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.isCompressedArrayTexture ) { + + gl.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, width, height, depth ); - gl.texStorage3D( gl.TEXTURE_3D, levels, glInternalFormat, width, height, depth ); + } else if ( texture.isData3DTexture ) { - } else if ( ! texture.isVideoTexture ) { + gl.texStorage3D( gl.TEXTURE_3D, levels, glInternalFormat, width, height, depth ); - gl.texStorage2D( glTextureType, levels, glInternalFormat, width, height ); + } else if ( ! texture.isVideoTexture ) { + + gl.texStorage2D( glTextureType, levels, glInternalFormat, width, height ); + + } } @@ -774,7 +786,7 @@ class WebGLTextureUtils { this.deallocateRenderBuffers( renderTarget ); - if ( isDefaultTexture === false ) { + if ( isDefaultTexture === false && texture.isExternalTexture !== true ) { gl.deleteTexture( textureGPU ); diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 99921a634c3e27..a0c470c50708ac 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -406,7 +406,7 @@ class WebGPUTextureUtils { const backend = this.backend; const textureData = backend.get( texture ); - if ( textureData.texture !== undefined && isDefaultTexture === false ) textureData.texture.destroy(); + if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true ) textureData.texture.destroy(); if ( textureData.msaaTexture !== undefined ) textureData.msaaTexture.destroy(); diff --git a/src/textures/ExternalTexture.js b/src/textures/ExternalTexture.js index ac46eb7232ccfd..7965e62be80f18 100644 --- a/src/textures/ExternalTexture.js +++ b/src/textures/ExternalTexture.js @@ -6,9 +6,6 @@ import { Texture } from './Texture.js'; * This may be a texture from a protected media stream, device camera feed, * or other data feeds like a depth sensor. * - * Note that this class is only supported in {@link WebGLRenderer}, and in - * the {@link WebGPURenderer} WebGPU backend. - * * @augments Texture */ class ExternalTexture extends Texture { From f3b7968c86d61364969df40c882a4646bda06f80 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Mon, 15 Jun 2026 18:02:56 +0200 Subject: [PATCH 2/2] Matrix4: Introduce faster version of `determinant()`. (#33814) --- src/math/Matrix4.js | 22 +++++++++++ src/renderers/WebGLRenderer.js | 2 +- src/renderers/webgl-fallback/WebGLBackend.js | 2 +- src/renderers/webgpu/WebGPUBackend.js | 4 +- .../webgpu/utils/WebGPUPipelineUtils.js | 2 +- test/unit/src/math/Matrix4.tests.js | 37 +++++++++++++++++++ 6 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/math/Matrix4.js b/src/math/Matrix4.js index efacbf44ec420d..260d3644528ae7 100644 --- a/src/math/Matrix4.js +++ b/src/math/Matrix4.js @@ -649,6 +649,28 @@ class Matrix4 { } + /** + * Computes and returns the determinant of the upper-left 3x3 submatrix. + * + * For affine matrices (like an object's world matrix), this value equals the + * full 4x4 {@link Matrix4#determinant} but is cheaper to compute. + * + * @return {number} The determinant of the upper-left 3x3 submatrix. + */ + determinant3x3() { + + const te = this.elements; + + const n11 = te[ 0 ], n12 = te[ 4 ], n13 = te[ 8 ]; + const n21 = te[ 1 ], n22 = te[ 5 ], n23 = te[ 9 ]; + const n31 = te[ 2 ], n32 = te[ 6 ], n33 = te[ 10 ]; + + return n11 * ( n22 * n33 - n23 * n32 ) - + n12 * ( n21 * n33 - n23 * n31 ) + + n13 * ( n21 * n32 - n22 * n31 ); + + } + /** * Transposes this matrix in place. * diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index c01542ea7fd0e5..8c70b3bc534e73 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -1185,7 +1185,7 @@ class WebGLRenderer { if ( scene === null ) scene = _emptyScene; // renderBufferDirect second parameter used to be fog (could be null) - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); const program = setProgram( camera, scene, geometry, material, object ); diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index 833b8108c240b9..53908d279e9bd5 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1080,7 +1080,7 @@ class WebGLBackend extends Backend { this._bindUniforms( renderObject.getBindings() ); - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); state.setMaterial( material, frontFaceCW, hardwareClippingPlanes ); diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 1c14545e4e3ead..6b4e767f47fbcb 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2058,7 +2058,7 @@ class WebGPUBackend extends Backend { const colorFormat = utils.getCurrentColorFormat( renderObject.context ); const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context ); const primitiveTopology = utils.getPrimitiveTopology( object, material ); - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); let needsUpdate = false; @@ -2120,7 +2120,7 @@ class WebGPUBackend extends Backend { // meshes with negative scale have a different frontFace render pipeline // descriptor value so the following must be honored in the cache key - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); return [ material.transparent, material.blending, material.premultipliedAlpha, diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js index 851059692556d1..f3551af6dd5b10 100644 --- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -847,7 +847,7 @@ class WebGPUPipelineUtils { let flipSided = ( material.side === BackSide ); - if ( object.isMesh && object.matrixWorld.determinant() < 0 ) flipSided = ! flipSided; + if ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ) flipSided = ! flipSided; descriptor.frontFace = ( flipSided === true ) ? GPUFrontFace.CW : GPUFrontFace.CCW; diff --git a/test/unit/src/math/Matrix4.tests.js b/test/unit/src/math/Matrix4.tests.js index 03b8ced4939411..f20cff559d67c5 100644 --- a/test/unit/src/math/Matrix4.tests.js +++ b/test/unit/src/math/Matrix4.tests.js @@ -467,6 +467,43 @@ export default QUnit.module( 'Maths', () => { } ); + QUnit.test( 'determinant3x3', ( assert ) => { + + // for affine matrices (the typical object world matrix), the 3x3 result + // equals the full 4x4 determinant since the bottom row is [ 0, 0, 0, 1 ] + + const a = new Matrix4(); + const position = new Vector3( 5, - 2, 3 ); + const quaternion = new Quaternion().setFromEuler( new Euler( 0.1, - 0.7, 1.3 ) ); + + // translation + rotation + non-uniform scale + + a.compose( position, quaternion, new Vector3( 2, 3, 0.5 ) ); + assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) <= eps, 'Affine matrix: Passed!' ); + + // reflection (negative scale on one axis flips the winding order) + + a.compose( position, quaternion, new Vector3( 2, 3, - 0.5 ) ); + assert.ok( a.determinant3x3() < 0, 'Reflection produces a negative determinant!' ); + assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) <= eps, 'Reflection matrix: Passed!' ); + + // shear + + a.multiply( new Matrix4().makeShear( 0.5, 0, 0.2, 0, 0.7, 0 ) ); + assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) <= eps, 'Shear matrix: Passed!' ); + + } ); + + QUnit.test( 'determinant3x3 (projective matrix)', ( assert ) => { + + // for non-affine (projective) matrices, the bottom row is not [ 0, 0, 0, 1 ] + // and so the 3x3 result generally differs from the full 4x4 determinant + + const a = new Matrix4().makePerspective( - 1, 1, 1, - 1, 1, 100 ); + assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) > eps, 'Passed!' ); + + } ); + QUnit.test( 'transpose', ( assert ) => { const a = new Matrix4();