From 7ceafb2d67eb21373f017fa73995d4afe9403582 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Tue, 9 Jun 2026 13:40:18 +0200 Subject: [PATCH 1/5] Matrix3: Remove remaining usafe of `translate()` and `scale()`. (#33754) --- examples/jsm/loaders/SVGLoader.js | 6 +++--- examples/jsm/utils/GeometryCompressionUtils.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/jsm/loaders/SVGLoader.js b/examples/jsm/loaders/SVGLoader.js index f26c18ed601b0c..8aaaf909e09917 100644 --- a/examples/jsm/loaders/SVGLoader.js +++ b/examples/jsm/loaders/SVGLoader.js @@ -1657,7 +1657,7 @@ class SVGLoader extends Loader { const tx = parseFloatWithUnits( node.getAttribute( 'x' ) || 0 ); const ty = parseFloatWithUnits( node.getAttribute( 'y' ) || 0 ); - transform.translate( tx, ty ); + transform.makeTranslation( tx, ty ); } @@ -1709,7 +1709,7 @@ class SVGLoader extends Loader { } - currentTransform.translate( tx, ty ); + currentTransform.makeTranslation( tx, ty ); } @@ -1758,7 +1758,7 @@ class SVGLoader extends Loader { } - currentTransform.scale( scaleX, scaleY ); + currentTransform.makeScale( scaleX, scaleY ); } diff --git a/examples/jsm/utils/GeometryCompressionUtils.js b/examples/jsm/utils/GeometryCompressionUtils.js index 9d14bd0ac66b4d..a41f8b8d96b7d6 100644 --- a/examples/jsm/utils/GeometryCompressionUtils.js +++ b/examples/jsm/utils/GeometryCompressionUtils.js @@ -509,7 +509,7 @@ function quantizedEncodeUV( array, bytes ) { } - decodeMat.scale( + decodeMat.makeScale( ( max[ 0 ] - min[ 0 ] ) / segments, ( max[ 1 ] - min[ 1 ] ) / segments ); From 0883b62011cd44f0f6b9ab5d4df16d3c857b4c8d Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 9 Jun 2026 13:07:58 -0300 Subject: [PATCH 2/5] Inspector: Fix FPS counter freeze when `WebGLBackend` query is unavailable (#33755) --- examples/jsm/inspector/RendererInspector.js | 122 +++++++++++++------ examples/jsm/inspector/tabs/Performance.js | 2 +- src/renderers/common/Backend.js | 16 ++- src/renderers/common/TimestampQueryPool.js | 2 +- src/renderers/webgl-fallback/WebGLBackend.js | 12 ++ src/renderers/webgpu/WebGPUBackend.js | 12 ++ 6 files changed, 127 insertions(+), 39 deletions(-) diff --git a/examples/jsm/inspector/RendererInspector.js b/examples/jsm/inspector/RendererInspector.js index 82105754afc0c4..a9ec229a1ba1a9 100644 --- a/examples/jsm/inspector/RendererInspector.js +++ b/examples/jsm/inspector/RendererInspector.js @@ -191,88 +191,140 @@ export class RendererInspector extends InspectorBase { const renderer = this.getRenderer(); - await renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ); - await renderer.resolveTimestampsAsync( TimestampQuery.RENDER ); + if ( renderer.backend.hasTimestamp ) { - const computeFrames = renderer.backend.getTimestampFrames( TimestampQuery.COMPUTE ); - const renderFrames = renderer.backend.getTimestampFrames( TimestampQuery.RENDER ); + await renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ); + await renderer.resolveTimestampsAsync( TimestampQuery.RENDER ); - const frameIds = [ ...new Set( [ ...computeFrames, ...renderFrames ] ) ]; + const computeFrames = renderer.backend.getTimestampFrames( TimestampQuery.COMPUTE ); + const renderFrames = renderer.backend.getTimestampFrames( TimestampQuery.RENDER ); - for ( const frameId of frameIds ) { + const frameIds = [ ...new Set( [ ...computeFrames, ...renderFrames ] ) ]; - const frame = this.getFrameById( frameId ); + for ( const frameId of frameIds ) { - if ( frame !== null ) { + const frame = this.getFrameById( frameId ); - // resolve compute timestamps + if ( frame !== null ) { - if ( frame.resolvedCompute === false ) { + // resolve compute timestamps + + if ( frame.resolvedCompute === false ) { + + if ( frame.computes.length > 0 ) { - if ( frame.computes.length > 0 ) { + if ( computeFrames.includes( frameId ) ) { - if ( computeFrames.includes( frameId ) ) { + for ( const stats of frame.computes ) { - for ( const stats of frame.computes ) { + if ( renderer.backend.hasTimestampQuery( stats.uid ) ) { - if ( renderer.backend.hasTimestamp( stats.uid ) ) { + stats.gpu = renderer.backend.getTimestamp( stats.uid ); - stats.gpu = renderer.backend.getTimestamp( stats.uid ); + } else { - } else { + stats.gpu = 0; + stats.gpuNotAvailable = true; - stats.gpu = 0; - stats.gpuNotAvailable = true; + } } + frame.resolvedCompute = true; + } + } else { + frame.resolvedCompute = true; } - } else { - - frame.resolvedCompute = true; - } - } + // resolve render timestamps - // resolve render timestamps + if ( frame.resolvedRender === false ) { - if ( frame.resolvedRender === false ) { + if ( frame.renders.length > 0 ) { - if ( frame.renders.length > 0 ) { + if ( renderFrames.includes( frameId ) ) { - if ( renderFrames.includes( frameId ) ) { + for ( const stats of frame.renders ) { - for ( const stats of frame.renders ) { + if ( renderer.backend.hasTimestampQuery( stats.uid ) ) { - if ( renderer.backend.hasTimestamp( stats.uid ) ) { + stats.gpu = renderer.backend.getTimestamp( stats.uid ); - stats.gpu = renderer.backend.getTimestamp( stats.uid ); + } else { - } else { + stats.gpu = 0; + stats.gpuNotAvailable = true; - stats.gpu = 0; - stats.gpuNotAvailable = true; + } } + frame.resolvedRender = true; + } + } else { + frame.resolvedRender = true; } - } else { + } + + if ( frame.resolvedCompute === true && frame.resolvedRender === true ) { + + this.resolveFrame( frame ); + + } + + } + + } + + } else { - frame.resolvedRender = true; + for ( const frame of this.frames ) { + + if ( frame.resolvedCompute === true && frame.resolvedRender === true ) { + + continue; + + } + + const nextFrame = this.getFrameById( frame.frameId + 1 ); + + if ( nextFrame === null ) continue; + + if ( frame.resolvedCompute === false ) { + + for ( const stats of frame.computes ) { + + stats.gpu = 0; + stats.gpuNotAvailable = true; } + frame.resolvedCompute = true; + + } + + if ( frame.resolvedRender === false ) { + + for ( const stats of frame.renders ) { + + stats.gpu = 0; + stats.gpuNotAvailable = true; + + } + + frame.resolvedRender = true; + } if ( frame.resolvedCompute === true && frame.resolvedRender === true ) { diff --git a/examples/jsm/inspector/tabs/Performance.js b/examples/jsm/inspector/tabs/Performance.js index 8e3c8fea65e589..2d6258f8d6339b 100644 --- a/examples/jsm/inspector/tabs/Performance.js +++ b/examples/jsm/inspector/tabs/Performance.js @@ -249,7 +249,7 @@ class Performance extends Tab { // setText( this.frameStats.data[ 1 ], frame.cpu.toFixed( 2 ) ); - setText( this.frameStats.data[ 2 ], frame.gpu.toFixed( 2 ) ); + setText( this.frameStats.data[ 2 ], inspector.getRenderer().backend.hasTimestamp ? frame.gpu.toFixed( 2 ) : '-' ); setText( this.frameStats.data[ 3 ], frame.total.toFixed( 2 ) ); // diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index 27f7899be14fd0..1e0e661dcbe2e1 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -541,17 +541,29 @@ class Backend { } + /** + * Whether the backend supports query timestamps or not. + * + * @type {boolean} + * @readonly + */ + get hasTimestamp() { + + return false; + + } + /** * Returns `true` if a timestamp for the given uid is available. * * @param {string} uid - The unique identifier. * @return {boolean} Whether the timestamp is available or not. */ - hasTimestamp( uid ) { + hasTimestampQuery( uid ) { const queryPool = this._getQueryPool( uid ); - return queryPool.hasTimestamp( uid ); + return queryPool.hasTimestampQuery( uid ); } diff --git a/src/renderers/common/TimestampQueryPool.js b/src/renderers/common/TimestampQueryPool.js index 6d287512f61efd..3f09d8d694d7d6 100644 --- a/src/renderers/common/TimestampQueryPool.js +++ b/src/renderers/common/TimestampQueryPool.js @@ -126,7 +126,7 @@ class TimestampQueryPool { * @param {string} uid - A unique identifier for the render context. * @return {boolean} True if a timestamp is available, false otherwise. */ - hasTimestamp( uid ) { + hasTimestampQuery( uid ) { return this.timestamps.has( uid ); diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index eefa2b2e04a839..ae10dfa14e1e52 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -307,6 +307,18 @@ class WebGLBackend extends Backend { } + /** + * Whether the backend supports query timestamps or not. + * + * @type {boolean} + * @readonly + */ + get hasTimestamp() { + + return this.disjoint !== null; + + } + /** * This method performs a readback operation by moving buffer data from * a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 3bcc17f781bd5f..b4703cb3650a95 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -380,6 +380,18 @@ class WebGPUBackend extends Backend { } + /** + * Whether the backend supports query timestamps or not. + * + * @type {boolean} + * @readonly + */ + get hasTimestamp() { + + return true; + + } + /** * This method performs a readback operation by moving buffer data from * a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can From 943ef2c80ef2092ccb2384547cc218d753f95dcb Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 9 Jun 2026 13:23:56 -0300 Subject: [PATCH 3/5] TSL: Rename `directionToFaceDirection` -> `negateOnBackSide` (#33753) --- src/Three.TSL.js | 1 + src/materials/nodes/MeshBasicNodeMaterial.js | 4 +- src/nodes/accessors/Bitangent.js | 4 +- src/nodes/accessors/Normal.js | 4 +- src/nodes/accessors/Tangent.js | 4 +- src/nodes/display/FrontFacingNode.js | 47 ++++++++++++++++---- src/nodes/display/NormalMapNode.js | 4 +- 7 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/Three.TSL.js b/src/Three.TSL.js index d5d14c5cf3a470..a4a8b474aa0ff6 100644 --- a/src/Three.TSL.js +++ b/src/Three.TSL.js @@ -377,6 +377,7 @@ export const mx_worley_noise_float = TSL.mx_worley_noise_float; export const mx_worley_noise_vec2 = TSL.mx_worley_noise_vec2; export const mx_worley_noise_vec3 = TSL.mx_worley_noise_vec3; export const negate = TSL.negate; +export const negateOnBackSide = TSL.negateOnBackSide; export const neutralToneMapping = TSL.neutralToneMapping; export const nodeArray = TSL.nodeArray; export const nodeImmutable = TSL.nodeImmutable; diff --git a/src/materials/nodes/MeshBasicNodeMaterial.js b/src/materials/nodes/MeshBasicNodeMaterial.js index 270f2e9fb1c2a3..945107e6db5f1e 100644 --- a/src/materials/nodes/MeshBasicNodeMaterial.js +++ b/src/materials/nodes/MeshBasicNodeMaterial.js @@ -5,7 +5,7 @@ import BasicLightMapNode from '../../nodes/lighting/BasicLightMapNode.js'; import BasicLightingModel from '../../nodes/functions/BasicLightingModel.js'; import { normalViewGeometry } from '../../nodes/accessors/Normal.js'; import { diffuseColor } from '../../nodes/core/PropertyNode.js'; -import { directionToFaceDirection } from '../../nodes/display/FrontFacingNode.js'; +import { negateOnBackSide } from '../../nodes/display/FrontFacingNode.js'; import { MeshBasicMaterial } from '../MeshBasicMaterial.js'; @@ -66,7 +66,7 @@ class MeshBasicNodeMaterial extends NodeMaterial { */ setupNormal() { - return directionToFaceDirection( normalViewGeometry ); // see #28839 + return negateOnBackSide( normalViewGeometry ); // see #28839 } diff --git a/src/nodes/accessors/Bitangent.js b/src/nodes/accessors/Bitangent.js index c352db417640e2..949dffc64126aa 100644 --- a/src/nodes/accessors/Bitangent.js +++ b/src/nodes/accessors/Bitangent.js @@ -2,7 +2,7 @@ import { Fn } from '../tsl/TSLCore.js'; import { normalGeometry, normalLocal, normalView, normalWorld } from './Normal.js'; import { tangentGeometry, tangentLocal, tangentView, tangentWorld } from './Tangent.js'; import { bitangentViewFrame } from './TangentUtils.js'; -import { directionToFaceDirection } from '../display/FrontFacingNode.js'; +import { negateOnBackSide } from '../display/FrontFacingNode.js'; /** * Returns the bitangent node and assigns it to a varying if the material is not flat shaded. @@ -65,7 +65,7 @@ export const bitangentView = /*@__PURE__*/ ( Fn( ( builder ) => { if ( builder.isFlatShading() !== true ) { - node = directionToFaceDirection( node ); + node = negateOnBackSide( node ); } diff --git a/src/nodes/accessors/Normal.js b/src/nodes/accessors/Normal.js index 2026806f95d81b..5de2b10fb4579f 100644 --- a/src/nodes/accessors/Normal.js +++ b/src/nodes/accessors/Normal.js @@ -3,7 +3,7 @@ import { cameraViewMatrix } from './Camera.js'; import { modelNormalMatrix, modelWorldMatrix } from './ModelNode.js'; import { mat3, vec3, Fn, addMethodChaining } from '../tsl/TSLBase.js'; import { positionView } from './Position.js'; -import { directionToFaceDirection } from '../display/FrontFacingNode.js'; +import { negateOnBackSide } from '../display/FrontFacingNode.js'; import { warn } from '../../utils.js'; /** @@ -102,7 +102,7 @@ export const normalView = /*@__PURE__*/ ( Fn( ( builder ) => { if ( builder.isFlatShading() !== true ) { - node = directionToFaceDirection( node ); + node = negateOnBackSide( node ); } diff --git a/src/nodes/accessors/Tangent.js b/src/nodes/accessors/Tangent.js index f01f383eae8e2c..4e0ea57933c31a 100644 --- a/src/nodes/accessors/Tangent.js +++ b/src/nodes/accessors/Tangent.js @@ -3,7 +3,7 @@ import { cameraWorldMatrix } from './Camera.js'; import { modelViewMatrix } from './ModelNode.js'; import { Fn, vec4 } from '../tsl/TSLBase.js'; import { tangentViewFrame } from './TangentUtils.js'; -import { directionToFaceDirection } from '../display/FrontFacingNode.js'; +import { negateOnBackSide } from '../display/FrontFacingNode.js'; /** * TSL object that represents the tangent attribute of the current rendered object. @@ -43,7 +43,7 @@ export const tangentView = /*@__PURE__*/ ( Fn( ( builder ) => { if ( builder.isFlatShading() !== true ) { - node = directionToFaceDirection( node ); + node = negateOnBackSide( node ); } diff --git a/src/nodes/display/FrontFacingNode.js b/src/nodes/display/FrontFacingNode.js index 14bae2c0e17ecc..c411c530fba65b 100644 --- a/src/nodes/display/FrontFacingNode.js +++ b/src/nodes/display/FrontFacingNode.js @@ -1,5 +1,6 @@ import Node from '../core/Node.js'; import { nodeImmutable, float, Fn } from '../tsl/TSLBase.js'; +import { warnOnce } from '../../utils.js'; import { BackSide, DoubleSide } from '../../constants.js'; @@ -74,29 +75,57 @@ export const frontFacing = /*@__PURE__*/ nodeImmutable( FrontFacingNode ); export const faceDirection = /*@__PURE__*/ float( frontFacing ).mul( 2.0 ).sub( 1.0 ); /** - * Converts a direction vector to a face direction vector based on the material's side. + * Negates a vector if the rendering occurs on the back side of a face, + * based on the material's side configuration. * - * If the material is set to `BackSide`, the direction is inverted. - * If the material is set to `DoubleSide`, the direction is multiplied by `faceDirection`. + * - If the material's side is `BackSide`, the vector is inverted (negated). + * - If the material's side is `DoubleSide`, the vector is multiplied by `faceDirection` + * (negated only for back-facing fragments). + * - If the material's side is `FrontSide` (default), the vector remains unchanged. * * @tsl - * @param {Node} direction - The direction vector to convert. - * @returns {Node} The converted direction vector. + * @function + * @param {Node} vector - The vector to process. + * @returns {Node} The processed vector. */ -export const directionToFaceDirection = /*@__PURE__*/ Fn( ( [ direction ], { material } ) => { +export const negateOnBackSide = /*@__PURE__*/ Fn( ( [ vector ], { material } ) => { const side = material.side; if ( side === BackSide ) { - direction = direction.mul( - 1.0 ); + vector = vector.mul( - 1.0 ); } else if ( side === DoubleSide ) { - direction = direction.mul( faceDirection ); + vector = vector.mul( faceDirection ); } - return direction; + return vector; } ); + +/** + * Negates a vector if the rendering occurs on the back side of a face, + * based on the material's side configuration. + * + * - If the material's side is `BackSide`, the vector is inverted (negated). + * - If the material's side is `DoubleSide`, the vector is multiplied by `faceDirection` + * (negated only for back-facing fragments). + * - If the material's side is `FrontSide` (default), the vector remains unchanged. + * + * @tsl + * @function + * @deprecated since r185. Use {@link negateOnBackSide} instead. + * @param {Node} vector - The vector to convert. + * @returns {Node} The converted vector. + */ +export const directionToFaceDirection = ( vector ) => { + + warnOnce( 'TSL: "directionToFaceDirection()" has been renamed to "negateOnBackSide()".' ); // @deprecated r185 + + return negateOnBackSide( vector ); + +}; + diff --git a/src/nodes/display/NormalMapNode.js b/src/nodes/display/NormalMapNode.js index 9073408b3ad2eb..f66da71cb3bef1 100644 --- a/src/nodes/display/NormalMapNode.js +++ b/src/nodes/display/NormalMapNode.js @@ -5,7 +5,7 @@ import { TBNViewMatrix } from '../accessors/AccessorsUtils.js'; import { nodeProxy, vec3 } from '../tsl/TSLBase.js'; import { TangentSpaceNormalMap, ObjectSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking } from '../../constants.js'; -import { directionToFaceDirection } from './FrontFacingNode.js'; +import { negateOnBackSide } from './FrontFacingNode.js'; import { unpackNormal } from '../utils/Packing.js'; import { error } from '../../utils.js'; @@ -107,7 +107,7 @@ class NormalMapNode extends TempNode { if ( builder.isFlatShading() === true ) { - scale = directionToFaceDirection( scale ); + scale = negateOnBackSide( scale ); } From 0e20e6b48f1c73592670e2225ec074004f5af104 Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 9 Jun 2026 13:56:28 -0300 Subject: [PATCH 4/5] ShadowNode: Fix shadow viewer inspect and introduce `equirectDirection` (#33752) --- examples/webgpu_shadowmap_pointlight.html | 30 ++++++++++++------- src/Three.TSL.js | 1 + src/nodes/lighting/ShadowNode.js | 32 +++++++++++++++++---- src/nodes/utils/EquirectUV.js | 35 +++++++++++++++++++---- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/examples/webgpu_shadowmap_pointlight.html b/examples/webgpu_shadowmap_pointlight.html index 72a7ec7dfcf49b..5e367a9e815efb 100644 --- a/examples/webgpu_shadowmap_pointlight.html +++ b/examples/webgpu_shadowmap_pointlight.html @@ -1,24 +1,33 @@ - three.js webgpu - PointLight ShadowMap + three.js webgpu - point light shadow-map - + +
- three.js - WebGPU - PointLight ShadowMap + + +
+ three.jsPointLight ShadowMap +
+ + Animated point light shadows.
diff --git a/src/Three.TSL.js b/src/Three.TSL.js index a4a8b474aa0ff6..0199ee6fdc425f 100644 --- a/src/Three.TSL.js +++ b/src/Three.TSL.js @@ -177,6 +177,7 @@ export const dynamicBufferAttribute = TSL.dynamicBufferAttribute; export const element = TSL.element; export const emissive = TSL.emissive; export const equal = TSL.equal; +export const equirectDirection = TSL.equirectDirection; export const equirectUV = TSL.equirectUV; export const exp = TSL.exp; export const exp2 = TSL.exp2; diff --git a/src/nodes/lighting/ShadowNode.js b/src/nodes/lighting/ShadowNode.js index 19578e04d92700..8d4c76045911a8 100644 --- a/src/nodes/lighting/ShadowNode.js +++ b/src/nodes/lighting/ShadowNode.js @@ -13,7 +13,7 @@ import { Loop } from '../utils/LoopNode.js'; import { screenCoordinate } from '../display/ScreenNode.js'; import { Compatibility, GreaterEqualCompare, HalfFloatType, LessEqualCompare, LinearFilter, NearestFilter, PCFShadowMap, PCFSoftShadowMap, RGFormat, VSMShadowMap } from '../../constants.js'; import { renderGroup } from '../core/UniformGroupNode.js'; -import { viewZToLogarithmicDepth } from '../display/ViewportDepthNode.js'; +import { viewZToLogarithmicDepth, perspectiveDepthToViewZ, orthographicDepthToViewZ, viewZToOrthographicDepth } from '../display/ViewportDepthNode.js'; import { lightShadowMatrix } from '../accessors/Lights.js'; import { resetRendererAndSceneState, restoreRendererAndSceneState } from '../../renderers/common/RendererUtils.js'; import { getDataFromObject } from '../core/NodeUtils.js'; @@ -23,6 +23,7 @@ import { textureSize } from '../accessors/TextureSizeNode.js'; import { uv } from '../accessors/UV.js'; import { positionLocal } from '../accessors/Position.js'; import { uniform } from '../core/UniformNode.js'; +import { equirectDirection } from '../utils/EquirectUV.js'; // @@ -595,7 +596,7 @@ class ShadowNode extends ShadowBaseNode { if ( this.shadowMap.texture.isCubeTexture ) { - return cubeTexture( this.shadowMap.texture ); + return cubeTexture( this.shadowMap.texture, equirectDirection() ); } @@ -607,15 +608,36 @@ class ShadowNode extends ShadowBaseNode { return shadowOutput.toInspector( `${ inspectName } / Depth`, () => { - // TODO: Use linear depth + const shadowCameraNear = reference( 'near', 'float', this.shadow.camera ); + const shadowCameraFar = reference( 'far', 'float', this.shadow.camera ); + + let depthNode; if ( this.shadowMap.texture.isCubeTexture ) { - return cubeTexture( this.shadowMap.texture ).r.oneMinus(); + depthNode = cubeTexture( this.shadowMap.depthTexture, equirectDirection() ).r; + + } else { + + depthNode = textureLoad( this.shadowMap.depthTexture, uv().mul( textureSize( texture( this.shadowMap.depthTexture ) ) ) ).r; } - return textureLoad( this.shadowMap.depthTexture, uv().mul( textureSize( texture( this.shadowMap.depthTexture ) ) ) ).r.oneMinus(); + let linearDepth; + + if ( this.shadow.camera.isPerspectiveCamera ) { + + linearDepth = perspectiveDepthToViewZ( depthNode, shadowCameraNear, shadowCameraFar ); + + } else { + + linearDepth = orthographicDepthToViewZ( depthNode, shadowCameraNear, shadowCameraFar ); + + } + + linearDepth = viewZToOrthographicDepth( linearDepth, shadowCameraNear, shadowCameraFar ); + + return linearDepth.oneMinus(); } ); diff --git a/src/nodes/utils/EquirectUV.js b/src/nodes/utils/EquirectUV.js index 5ce3fe0195e0c1..266c952d930258 100644 --- a/src/nodes/utils/EquirectUV.js +++ b/src/nodes/utils/EquirectUV.js @@ -1,5 +1,6 @@ import { positionWorldDirection } from '../accessors/Position.js'; -import { Fn, vec2 } from '../tsl/TSLBase.js'; +import { uv as UV } from '../accessors/UV.js'; +import { Fn, vec2, vec3 } from '../tsl/TSLCore.js'; /** * TSL function for creating an equirect uv node. @@ -14,14 +15,38 @@ import { Fn, vec2 } from '../tsl/TSLBase.js'; * * @tsl * @function - * @param {?Node} [dirNode=positionWorldDirection] - A direction vector for sampling which is by default `positionWorldDirection`. + * @param {?Node} [direction=positionWorldDirection] - A direction vector for sampling which is by default `positionWorldDirection`. * @returns {Node} */ -export const equirectUV = /*@__PURE__*/ Fn( ( [ dir = positionWorldDirection ] ) => { +export const equirectUV = /*@__PURE__*/ Fn( ( [ direction = positionWorldDirection ] ) => { - const u = dir.z.atan( dir.x ).mul( 1 / ( Math.PI * 2 ) ).add( 0.5 ); - const v = dir.y.clamp( - 1.0, 1.0 ).asin().mul( 1 / Math.PI ).add( 0.5 ); + const u = direction.z.atan( direction.x ).mul( 1 / ( Math.PI * 2 ) ).add( 0.5 ); + const v = direction.y.clamp( - 1.0, 1.0 ).asin().mul( 1 / Math.PI ).add( 0.5 ); return vec2( u, v ); } ); + +/** + * TSL function for creating an equirect direction node. + * + * Can be used to compute a direction vector from the given equirectangular + * UV coordinates. + * + * @tsl + * @function + * @param {?Node} [uv=UV()] - The equirectangular UV coordinates. + * @returns {Node} The computed direction vector. + */ +export const equirectDirection = /*@__PURE__*/ Fn( ( [ uv = UV() ] ) => { + + const theta = uv.x.sub( 0.5 ).mul( Math.PI * 2 ); + const phi = uv.y.sub( 0.5 ).mul( Math.PI ); + const cosPhi = phi.cos(); + const x = cosPhi.mul( theta.cos() ); + const y = phi.sin(); + const z = cosPhi.mul( theta.sin() ); + + return vec3( x, y, z ); + +} ); From 2a766eca7e868b99a9419cecb47e5253179c5f48 Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 9 Jun 2026 14:02:35 -0300 Subject: [PATCH 5/5] Inspector: Migrate Graph to Canvas & add FPS graph to toggle button (#33756) --- examples/jsm/inspector/Inspector.js | 18 +++ examples/jsm/inspector/tabs/Timeline.js | 3 +- examples/jsm/inspector/ui/Graph.js | 187 ++++++++++++++++++++---- examples/jsm/inspector/ui/Profiler.js | 7 + examples/jsm/inspector/ui/Style.js | 25 +++- 5 files changed, 212 insertions(+), 28 deletions(-) diff --git a/examples/jsm/inspector/Inspector.js b/examples/jsm/inspector/Inspector.js index 49b32a7002bcb2..287b605eef4926 100644 --- a/examples/jsm/inspector/Inspector.js +++ b/examples/jsm/inspector/Inspector.js @@ -79,6 +79,11 @@ class Inspector extends RendererInspector { needsUpdate: false, duration: .02, time: 0 + }, + toggleGraph: { + needsUpdate: false, + duration: .02, + time: 0 } }; @@ -463,6 +468,7 @@ class Inspector extends RendererInspector { this.updateCycle( this.displayCycle.text ); this.updateCycle( this.displayCycle.graph ); + this.updateCycle( this.displayCycle.toggleGraph ); if ( this.displayCycle.text.needsUpdate ) { @@ -473,6 +479,17 @@ class Inspector extends RendererInspector { } + if ( this.displayCycle.toggleGraph.needsUpdate ) { + + if ( this.profiler.toggleGraph ) { + + this.profiler.toggleGraph.addPoint( 'fps', this.fps ); + this.profiler.toggleGraph.update(); + + } + + } + if ( this.displayCycle.graph.needsUpdate ) { this.performance.updateGraph( this, frame ); @@ -482,6 +499,7 @@ class Inspector extends RendererInspector { this.displayCycle.text.needsUpdate = false; this.displayCycle.graph.needsUpdate = false; + this.displayCycle.toggleGraph.needsUpdate = false; } diff --git a/examples/jsm/inspector/tabs/Timeline.js b/examples/jsm/inspector/tabs/Timeline.js index 25e20e50dbd451..832d8ad7c69620 100644 --- a/examples/jsm/inspector/tabs/Timeline.js +++ b/examples/jsm/inspector/tabs/Timeline.js @@ -208,7 +208,8 @@ class Timeline extends Tab { graphContainer.appendChild( this.graphSlider ); // Setup SVG from Graph - this.graph.domElement.style.width = '100%'; + this.graph.domElement.style.width = '0'; + this.graph.domElement.style.minWidth = '100%'; this.graph.domElement.style.height = '100%'; this.graphSlider.appendChild( this.graph.domElement ); diff --git a/examples/jsm/inspector/ui/Graph.js b/examples/jsm/inspector/ui/Graph.js index 10271d5d76b27c..5daa83f2aefdfb 100644 --- a/examples/jsm/inspector/ui/Graph.js +++ b/examples/jsm/inspector/ui/Graph.js @@ -1,4 +1,3 @@ - export class Graph { constructor( maxPoints = 512 ) { @@ -8,20 +7,37 @@ export class Graph { this.limit = 0; this.limitIndex = 0; - this.domElement = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); - this.domElement.setAttribute( 'class', 'graph-svg' ); + this.domElement = document.createElement( 'canvas' ); + this.domElement.setAttribute( 'class', 'graph-canvas' ); + + this.ctx = this.domElement.getContext( '2d' ); + + this.width = 0; + this.height = 0; + this.devicePixelRatio = window.devicePixelRatio || 1; } - addLine( id, color ) { + resize( width, height ) { + + this.width = width; + this.height = height; - const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); - path.setAttribute( 'class', 'graph-path' ); - path.style.stroke = color; - path.style.fill = color; - this.domElement.appendChild( path ); + this.devicePixelRatio = window.devicePixelRatio || 1; + this.domElement.width = width * this.devicePixelRatio; + this.domElement.height = height * this.devicePixelRatio; + + this.draw(); + + } + + addLine( id, color ) { - this.lines[ id ] = { path, color, points: [] }; + this.lines[ id ] = { + color: color, + resolved: null, + points: [] + }; } @@ -55,41 +71,162 @@ export class Graph { update() { - const svgWidth = this.domElement.clientWidth; - const svgHeight = this.domElement.clientHeight; - if ( svgWidth === 0 ) return; + const width = this.domElement.clientWidth; + const height = this.domElement.clientHeight; - const pointStep = svgWidth / ( this.maxPoints - 1 ); + if ( width === 0 || height === 0 ) return; + + if ( width !== this.width || height !== this.height ) { + + this.resize( width, height ); + + } else { + + this.draw(); + + } + + if ( this.limitIndex ++ > this.maxPoints ) { + + this.resetLimit(); + + } + + } + + draw() { + + const ctx = this.ctx; + const dpr = this.devicePixelRatio; + const width = this.width; + const height = this.height; + + ctx.clearRect( 0, 0, width * dpr, height * dpr ); + + if ( width === 0 || height === 0 ) return; + + ctx.save(); + ctx.scale( dpr, dpr ); + + const pointStep = width / ( this.maxPoints - 1 ); for ( const id in this.lines ) { const line = this.lines[ id ]; + if ( line.points.length === 0 ) continue; + + if ( ! line.resolved ) { + + line.resolved = this._resolveColor( line.color ); + + } + + const resolved = line.resolved; + const drawColor = resolved ? resolved.color : '#ffffff'; + const offset = width - ( ( line.points.length - 1 ) * pointStep ); + + // 1. Draw fill (with opacity) + let fillStyle = drawColor; + + if ( height > 0 ) { - let pathString = `M 0,${ svgHeight }`; + const gradient = ctx.createLinearGradient( 0, 0, 0, height ); + gradient.addColorStop( 0, drawColor ); + gradient.addColorStop( 1, ( resolved && resolved.transparent ) || 'rgba(0,0,0,0)' ); + fillStyle = gradient; + + } + + ctx.fillStyle = fillStyle; + ctx.globalAlpha = 0.4; + ctx.beginPath(); + ctx.moveTo( offset, height ); for ( let i = 0; i < line.points.length; i ++ ) { - const x = i * pointStep; - const y = svgHeight - ( line.points[ i ] / this.limit ) * svgHeight; - pathString += ` L ${ x },${ y }`; + const x = offset + i * pointStep; + const y = this.limit === 0 ? height : height - ( line.points[ i ] / this.limit ) * height; + ctx.lineTo( x, y ); } - pathString += ` L ${( line.points.length - 1 ) * pointStep},${ svgHeight } Z`; + ctx.lineTo( offset + ( line.points.length - 1 ) * pointStep, height ); + ctx.closePath(); + ctx.fill(); + + // 2. Draw stroke (full opacity) + ctx.strokeStyle = drawColor; + ctx.lineWidth = 2; + ctx.globalAlpha = 1.0; + ctx.beginPath(); + for ( let i = 0; i < line.points.length; i ++ ) { + + const x = offset + i * pointStep; + const y = this.limit === 0 ? height : height - ( line.points[ i ] / this.limit ) * height; + if ( i === 0 ) { + + ctx.moveTo( x, y ); + + } else { - const offset = svgWidth - ( ( line.points.length - 1 ) * pointStep ); - line.path.setAttribute( 'transform', `translate(${ offset }, 0)` ); - line.path.setAttribute( 'd', pathString ); + ctx.lineTo( x, y ); + + } + + } + + ctx.stroke(); } - // + ctx.restore(); - if ( this.limitIndex ++ > this.maxPoints ) { + } - this.resetLimit(); + _resolveColor( color ) { + + let resolved = color; + + if ( color.startsWith( 'var(' ) ) { + + const varName = color.slice( 4, - 1 ).trim(); + resolved = getComputedStyle( this.domElement ).getPropertyValue( varName ).trim(); + + if ( ! resolved ) { + + return null; + + } + + } + + let transparentColor = 'rgba(0,0,0,0)'; + + if ( resolved.startsWith( '#' ) ) { + + const hex = resolved.substring( 0, 7 ); + transparentColor = hex + '00'; + + } else if ( resolved.startsWith( 'rgb' ) ) { + + const match = resolved.match( /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)$/ ); + + if ( match ) { + + transparentColor = `rgba(${match[ 1 ]}, ${match[ 2 ]}, ${match[ 3 ]}, 0)`; + + } } + return { + color: resolved, + transparent: transparentColor + }; + + } + + dispose() { + } } diff --git a/examples/jsm/inspector/ui/Profiler.js b/examples/jsm/inspector/ui/Profiler.js index 38a7eac2e53fec..489cf11e9fd418 100644 --- a/examples/jsm/inspector/ui/Profiler.js +++ b/examples/jsm/inspector/ui/Profiler.js @@ -1,5 +1,6 @@ import { EventDispatcher } from 'three'; import { Style } from './Style.js'; +import { Graph } from './Graph.js'; import { getItem, setItem } from '../Inspector.js'; export class Profiler extends EventDispatcher { @@ -342,6 +343,12 @@ export class Profiler extends EventDispatcher { } + // Create a background performance graph for the toggle button + this.toggleGraph = new Graph( 80 ); + this.toggleGraph.addLine( 'fps', '#4c4c6bff' ); + this.toggleGraph.domElement.className = 'profiler-toggle-graph'; + this.toggleButton.appendChild( this.toggleGraph.domElement ); + } setupResizing() { diff --git a/examples/jsm/inspector/ui/Style.js b/examples/jsm/inspector/ui/Style.js index 3c0aca85227564..0005de0a68557f 100644 --- a/examples/jsm/inspector/ui/Style.js +++ b/examples/jsm/inspector/ui/Style.js @@ -52,6 +52,20 @@ export class Style { font-family: var(--font-family); } + .profiler-toggle-graph { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; + background: transparent; + border: none; + border-radius: inherit; + opacity: 0.5; + } + .profiler-toggle.position-right.panel-open { right: auto; left: 15px; @@ -75,6 +89,7 @@ export class Style { .toggle-icon { position: relative; + z-index: 1; display: flex; align-items: center; justify-content: center; @@ -143,6 +158,8 @@ export class Style { } .toggle-text { + position: relative; + z-index: 1; display: flex; align-items: baseline; padding: 8px 14px; @@ -157,6 +174,8 @@ export class Style { } .builtin-tabs-container { + position: relative; + z-index: 1; display: flex; align-items: stretch; gap: 0; @@ -1160,12 +1179,14 @@ export class Style { position: relative; } - .graph-svg { - width: 100%; + .graph-svg, .graph-canvas { + width: 0; + min-width: 100%; height: 80px; background-color: var(--profiler-header); border: 1px solid var(--profiler-border); border-radius: 4px; + display: block; } .graph-path {