diff --git a/examples/jsm/inspector/Inspector.js b/examples/jsm/inspector/Inspector.js index 7206722701b1f6..b99752c5b3f89c 100644 --- a/examples/jsm/inspector/Inspector.js +++ b/examples/jsm/inspector/Inspector.js @@ -10,7 +10,7 @@ import { Viewer } from './tabs/Viewer.js'; import { Timeline } from './tabs/Timeline.js'; import { setText } from './ui/utils.js'; -import { setConsoleFunction, REVISION } from 'three/webgpu'; +import { setConsoleFunction, getConsoleFunction, REVISION } from 'three/webgpu'; class Inspector extends RendererInspector { @@ -70,6 +70,8 @@ class Inspector extends RendererInspector { this.settings = settings; this.once = {}; this.extensionsData = new WeakMap(); + this.previousConsoleFunction = null; + this._domObserver = null; this.displayCycle = { text: { @@ -274,81 +276,129 @@ class Inspector extends RendererInspector { } - init() { + setRenderer( renderer ) { - const renderer = this.getRenderer(); + super.setRenderer( renderer ); - let sign = `THREE.WebGPURenderer: ${ REVISION } [ "`; + if ( renderer !== null ) { - if ( renderer.backend.isWebGPUBackend ) { + const previousConsoleFunction = getConsoleFunction(); - sign += 'WebGPU'; + this.previousConsoleFunction = previousConsoleFunction; - } else if ( renderer.backend.isWebGLBackend ) { + setConsoleFunction( ( type, message, ...params ) => { - sign += 'WebGL2'; + if ( previousConsoleFunction ) { - } + previousConsoleFunction( type, message, ...params ); - sign += '" ]'; + } - this.console.addMessage( 'info', sign ); + this.resolveConsole( type, message, ...params ); - // + } ); + + if ( this.isAvailable ) { + + const init = async () => { + + if ( renderer.hasInitialized() === false ) await renderer.init(); + + renderer.backend.trackTimestamp = true; + + if ( renderer.hasFeature( 'timestamp-query' ) !== true ) { - if ( renderer.inspector.domElement.parentElement === null ) { + this.console.addMessage( 'error', 'THREE.Inspector: GPU Timestamp Queries not available.' ); - if ( renderer.domElement.parentElement !== null ) { + } - renderer.domElement.parentElement.appendChild( renderer.inspector.domElement ); + let sign = `THREE.WebGPURenderer: ${ REVISION } [ "`; - } else { + if ( renderer.backend.isWebGPUBackend ) { - const observer = new MutationObserver( () => { + sign += 'WebGPU'; - if ( renderer.domElement.parentElement !== null ) { + } else if ( renderer.backend.isWebGLBackend ) { - renderer.domElement.parentElement.appendChild( renderer.inspector.domElement ); - observer.disconnect(); + sign += 'WebGL2'; } - } ); + sign += '" ]'; - observer.observe( document.body || document.documentElement, { childList: true, subtree: true } ); + this.console.addMessage( 'info', sign ); - } + // - } + const domElement = this.domElement; - } + if ( domElement.parentElement === null ) { - setRenderer( renderer ) { + if ( renderer.domElement.parentElement !== null ) { - super.setRenderer( renderer ); + renderer.domElement.parentElement.appendChild( domElement ); - if ( renderer !== null ) { + } else { - setConsoleFunction( this.resolveConsole.bind( this ) ); + if ( this._domObserver !== null ) { - if ( this.isAvailable ) { + this._domObserver.disconnect(); - renderer.init().then( () => { + } - renderer.backend.trackTimestamp = true; + this._domObserver = new MutationObserver( () => { - if ( renderer.hasFeature( 'timestamp-query' ) !== true ) { + if ( renderer.domElement && renderer.domElement.parentElement !== null ) { - this.console.addMessage( 'error', 'THREE.Inspector: GPU Timestamp Queries not available.' ); + renderer.domElement.parentElement.appendChild( domElement ); + + if ( this._domObserver !== null ) { + + this._domObserver.disconnect(); + this._domObserver = null; + + } + + } + + } ); + + this._domObserver.observe( document.body || document.documentElement, { childList: true, subtree: true } ); + + } } - } ); + }; + + init(); this.timeline.setRenderer( renderer ); } + } else { + + if ( this.previousConsoleFunction ) { + + setConsoleFunction( this.previousConsoleFunction ); + this.previousConsoleFunction = undefined; + + } + + if ( this._domObserver !== null ) { + + this._domObserver.disconnect(); + this._domObserver = null; + + } + + this.profiler.dispose(); + + this.statsData.clear(); + + super.dispose(); + } return this; @@ -575,6 +625,14 @@ class Inspector extends RendererInspector { } + dispose() { + + super.dispose(); + + this.setRenderer( null ); + + } + } function getItem( id ) { diff --git a/examples/jsm/inspector/RendererInspector.js b/examples/jsm/inspector/RendererInspector.js index 0f4bf34c384e1a..172351ca0fb7a0 100644 --- a/examples/jsm/inspector/RendererInspector.js +++ b/examples/jsm/inspector/RendererInspector.js @@ -84,6 +84,7 @@ export class RendererInspector extends InspectorBase { this._lastFinishTime = 0; this._resolveTimestampPromise = null; + this._rAFId = null; this.overdraw = false; this._overdrawMaterial = null; @@ -262,15 +263,33 @@ export class RendererInspector extends InspectorBase { this._resolveTimestampPromise = new Promise( ( resolve ) => { - requestAnimationFrame( async () => { + this._rAFId = requestAnimationFrame( async () => { + + this._rAFId = null; const renderer = this.getRenderer(); + if ( renderer === null ) { + + this._resolveTimestampPromise = null; + resolve(); + return; + + } + if ( renderer.backend.hasTimestamp ) { await renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ); await renderer.resolveTimestampsAsync( TimestampQuery.RENDER ); + if ( renderer !== this.getRenderer() ) { + + this._resolveTimestampPromise = null; + resolve(); + return; + + } + const computeFrames = renderer.backend.getTimestampFrames( TimestampQuery.COMPUTE ); const renderFrames = renderer.backend.getTimestampFrames( TimestampQuery.RENDER ); @@ -551,4 +570,26 @@ export class RendererInspector extends InspectorBase { } + dispose() { + + if ( this._rAFId !== null ) { + + cancelAnimationFrame( this._rAFId ); + this._rAFId = null; + + } + + this._resolveTimestampPromise = null; + + if ( this._overdrawMaterial !== null ) { + + this._overdrawMaterial.dispose(); + this._overdrawMaterial = null; + + } + + super.dispose(); + + } + } diff --git a/examples/jsm/inspector/tabs/Viewer.js b/examples/jsm/inspector/tabs/Viewer.js index ca1b6a5bb31cbb..9f2d3afdb4b6ec 100644 --- a/examples/jsm/inspector/tabs/Viewer.js +++ b/examples/jsm/inspector/tabs/Viewer.js @@ -675,6 +675,7 @@ class Viewer extends Tab { const onPointerDown = ( e ) => { isDragging = true; + line.classList.add( 'active' ); e.preventDefault(); }; @@ -700,6 +701,7 @@ class Viewer extends Tab { const onPointerUp = () => { isDragging = false; + line.classList.remove( 'active' ); }; @@ -946,6 +948,13 @@ class Viewer extends Tab { const mainCanvas = renderer.domElement; const rect = mainCanvas.getBoundingClientRect(); + + if ( rect.width <= 0 || rect.height <= 0 ) { + + return; + + } + const targetParent = document.fullscreenElement || this.profiler.domElement; const parentRect = targetParent.getBoundingClientRect(); const localLeft = rect.left - parentRect.left; @@ -1019,7 +1028,7 @@ class Viewer extends Tab { if ( ! renderer.backend.isWebGPUBackend ) { - inspector.resolveConsoleOnce( 'warn', 'Inspector: Viewer is only available with WebGPU.' ); + // Viewer is only available with WebGPU. return; @@ -1313,6 +1322,53 @@ class Viewer extends Tab { } + dispose() { + + if ( this.splitActive ) { + + this.stopSplitMode(); + + } + + if ( this.splitCanvas ) { + + if ( this.splitCanvas.parentElement ) { + + this.splitCanvas.parentElement.removeChild( this.splitCanvas ); + + } + + this.splitCanvas = null; + + } + + if ( this.splitCanvasTarget ) { + + this.splitCanvasTarget.dispose(); + this.splitCanvasTarget = null; + + } + + if ( this.splitMaterial ) { + + this.splitMaterial.dispose(); + this.splitMaterial = null; + + } + + for ( const canvasData of this.canvasNodes.values() ) { + + canvasData.canvasTarget.dispose(); + canvasData.material.dispose(); + + } + + this.canvasNodes.clear(); + + super.dispose(); + + } + } export { Viewer }; diff --git a/examples/jsm/inspector/ui/Profiler.js b/examples/jsm/inspector/ui/Profiler.js index a1d9675a0194d1..8b9dbc7aad0ab7 100644 --- a/examples/jsm/inspector/ui/Profiler.js +++ b/examples/jsm/inspector/ui/Profiler.js @@ -2243,4 +2243,24 @@ export class Profiler extends EventDispatcher { } + dispose() { + + for ( const tab of Object.values( this.tabs ) ) { + + tab.dispose(); + + } + + this.domElement.remove(); + + for ( const detachedWindow of this.detachedWindows ) { + + detachedWindow.panel.remove(); + + } + + this.toggleGraph.dispose(); + + } + } diff --git a/examples/jsm/inspector/ui/Style.js b/examples/jsm/inspector/ui/Style.js index f0da31cf05f275..4158895fba7e2a 100644 --- a/examples/jsm/inspector/ui/Style.js +++ b/examples/jsm/inspector/ui/Style.js @@ -2112,14 +2112,20 @@ export class Style { position: absolute; top: 0; bottom: 0; - width: 0; + width: 1px; left: 50%; - background: transparent; + background-color: transparent; cursor: ew-resize; pointer-events: auto !important; z-index: 10; touch-action: none; - transform: translateX(-50%); + transition: background-color 0.15s ease-out; + } + + .split-screen-line:hover, + .split-screen-line:active, + .split-screen-line.active { + background-color: var(--color-accent); } .split-screen-line::before { @@ -2128,7 +2134,7 @@ export class Style { top: 0; bottom: 0; left: -12px; - width: 24px; + width: 25px; background: transparent; cursor: ew-resize; } @@ -2136,27 +2142,27 @@ export class Style { .split-screen-line::after { content: ''; position: absolute; - top: 0; - bottom: 0; - left: -7px; - width: 14px; + top: -1px; + bottom: -1px; + left: -5px; + width: 11px; pointer-events: none; background-image: - url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8'%3E%3Cpath d='M0 0 L14 0 L7 8 Z' fill='%239a9aab'/%3E%3C/svg%3E"), - url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8'%3E%3Cpath d='M0 8 L14 8 L7 0 Z' fill='%239a9aab'/%3E%3C/svg%3E"); + url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M-0.5 -1 L5.5 7 L11.5 -1 Z' fill='rgba(30,30,36,0.85)' stroke='%234a4a5a' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E"), + url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M-0.5 8 L5.5 0 L11.5 8 Z' fill='rgba(30,30,36,0.85)' stroke='%234a4a5a' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E"); background-position: top center, bottom center; background-repeat: no-repeat; - opacity: 0.8; - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); - transition: opacity 0.2s, filter 0.2s; + opacity: 1; + transition: opacity 0.15s ease-out; } - .split-screen-line:hover::after { + .split-screen-line:hover::after, + .split-screen-line:active::after, + .split-screen-line.active::after { opacity: 1; background-image: - url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8'%3E%3Cpath d='M0 0 L14 0 L7 8 Z' fill='%2300aaff'/%3E%3C/svg%3E"), - url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8'%3E%3Cpath d='M0 8 L14 8 L7 0 Z' fill='%2300aaff'/%3E%3C/svg%3E"); - filter: drop-shadow(0 0 4px rgba(0, 170, 255, 0.8)); + url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M-0.5 -1 L5.5 7 L11.5 -1 Z' fill='%2300aaff' stroke='%2300aaff' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E"), + url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M-0.5 8 L5.5 0 L11.5 8 Z' fill='%2300aaff' stroke='%2300aaff' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E"); } /* Grid Mode styles for List component */ diff --git a/examples/jsm/objects/GaussianSplat.js b/examples/jsm/objects/GaussianSplat.js index 21cd11d84951e0..5820a3da7ffe0c 100644 --- a/examples/jsm/objects/GaussianSplat.js +++ b/examples/jsm/objects/GaussianSplat.js @@ -17,6 +17,7 @@ import { If, atan, cameraProjectionMatrix, + cameraViewport, cos, dot, exp, @@ -27,7 +28,6 @@ import { min, normalize, positionGeometry, - screenSize, sin, sqrt, storage, @@ -753,7 +753,7 @@ function createMaterialNodes( buffers, sort, localCameraPosition ) { const z = min( viewCenter.z, - 0.01 ).toVar( 'z' ); const invZ = float( 1 ).div( z ).toVar( 'invZ' ); const invZ2 = invZ.mul( invZ ).toVar( 'invZ2' ); - const focal = screenSize.mul( 0.5 ).mul( vec2( cameraProjectionMatrix[ 0 ].x, cameraProjectionMatrix[ 1 ].y ) ).toVar( 'focal' ); + const focal = cameraViewport.zw.mul( 0.5 ).mul( vec2( cameraProjectionMatrix[ 0 ].x, cameraProjectionMatrix[ 1 ].y ) ).toVar( 'focal' ); const j00 = focal.x.negate().mul( invZ ).toVar( 'j00' ); const j11 = focal.y.negate().mul( invZ ).toVar( 'j11' ); @@ -799,7 +799,7 @@ function createMaterialNodes( buffers, sort, localCameraPosition ) { const scale1 = min( sqrt( lambda1 ), MAX_SCREEN_SPACE_SPLAT_SIZE ).toVar( 'scale1' ); const scale2 = min( sqrt( lambda2 ), MAX_SCREEN_SPACE_SPLAT_SIZE ).toVar( 'scale2' ); const offsetPixels = axis1.mul( positionGeometry.x ).mul( scale1 ).add( axis2.mul( positionGeometry.y ).mul( scale2 ) ).toVar( 'offsetPixels' ); - const offsetNdc = offsetPixels.mul( 2 ).div( screenSize ).toVar( 'offsetNdc' ); + const offsetNdc = offsetPixels.mul( 2 ).div( cameraViewport.zw ).toVar( 'offsetNdc' ); const clip = centerClip.add( vec4( offsetNdc.mul( centerClip.w ), 0, 0 ) ).toVar( 'clip' ); const clipLimit = centerClip.w.mul( CLIP_XY ).toVar( 'clipLimit' ); diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index 196c29d9f09ce1..4f27863ea80bb1 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -3,9 +3,13 @@ import { Fn, nodeImmutable } from '../tsl/TSLBase.js'; import { uniform } from '../core/UniformNode.js'; import { Matrix4 } from '../../math/Matrix4.js'; -import { cameraViewMatrix } from './Camera.js'; +import { builtin } from './BuiltinNode.js'; +import { cameraIndex, cameraViewMatrix } from './Camera.js'; +import { uniformArray } from './UniformArrayNode.js'; import { Matrix3 } from '../../math/Matrix3.js'; +const _modelViewMatrix = /*@__PURE__*/ new Matrix4(); + /** * This type of node is a specialized version of `Object3DNode` * with larger set of model related metrics. Unlike `Object3DNode`, @@ -150,11 +154,46 @@ export const highpModelViewMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { builder.context.isHighPrecisionModelViewMatrix = true; - return uniform( 'mat4' ).onObjectUpdate( ( { object, camera } ) => { + const camera = builder.camera; + + let highpModelViewMatrix; + + if ( camera.isArrayCamera && camera.cameras.length > 0 ) { + + const matrices = []; + + for ( let i = 0; i < camera.cameras.length; i ++ ) { + + matrices.push( new Matrix4() ); + + } + + const modelViewMatrices = uniformArray( matrices ).onObjectUpdate( ( { object, camera }, self ) => { + + const subCameras = camera.cameras; + const array = self.array; - return object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld ); + for ( let i = 0, l = subCameras.length; i < l; i ++ ) { + + array[ i ].multiplyMatrices( subCameras[ i ].matrixWorldInverse, object.matrixWorld ); + + } + + } ); + + highpModelViewMatrix = modelViewMatrices.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex ); + + } else { + + highpModelViewMatrix = uniform( 'mat4' ).onObjectUpdate( ( { object, camera } ) => { + + return object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld ); + + } ); + + } - } ); + return highpModelViewMatrix; } ).once() )().toVar( 'highpModelViewMatrix' ); @@ -169,16 +208,53 @@ export const highpModelNormalViewMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { const isHighPrecisionModelViewMatrix = builder.context.isHighPrecisionModelViewMatrix; - return uniform( 'mat3' ).onObjectUpdate( ( { object, camera } ) => { + const camera = builder.camera; - if ( isHighPrecisionModelViewMatrix !== true ) { + let highpModelNormalViewMatrix; - object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld ); + if ( camera.isArrayCamera && camera.cameras.length > 0 ) { + + const matrices = []; + + for ( let i = 0; i < camera.cameras.length; i ++ ) { + + matrices.push( new Matrix3() ); } - return object.normalMatrix.getNormalMatrix( object.modelViewMatrix ); + const normalViewMatrices = uniformArray( matrices ).onObjectUpdate( ( { object, camera }, self ) => { + + const subCameras = camera.cameras; + const array = self.array; + + for ( let i = 0, l = subCameras.length; i < l; i ++ ) { + + _modelViewMatrix.multiplyMatrices( subCameras[ i ].matrixWorldInverse, object.matrixWorld ); + + array[ i ].getNormalMatrix( _modelViewMatrix ); + + } + + } ); + + highpModelNormalViewMatrix = normalViewMatrices.element( camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex ); + + } else { + + highpModelNormalViewMatrix = uniform( 'mat3' ).onObjectUpdate( ( { object, camera } ) => { + + if ( isHighPrecisionModelViewMatrix !== true ) { + + object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld ); + + } + + return object.normalMatrix.getNormalMatrix( object.modelViewMatrix ); + + } ); + + } - } ); + return highpModelNormalViewMatrix; } ).once() )().toVar( 'highpModelNormalViewMatrix' ); diff --git a/src/renderers/common/InspectorBase.js b/src/renderers/common/InspectorBase.js index 9ee396163c5195..fd7740213bb6af 100644 --- a/src/renderers/common/InspectorBase.js +++ b/src/renderers/common/InspectorBase.js @@ -83,11 +83,6 @@ class InspectorBase extends EventDispatcher { } - /** - * Initializes the inspector. - */ - init() { } - /** * Called when a frame begins. */ @@ -169,6 +164,17 @@ class InspectorBase extends EventDispatcher { */ copyFramebufferToTexture( /*framebufferTexture*/ ) { } + /** + * Frees all internal resources of the inspector. + */ + dispose() { + + if ( this.isRunning === true ) this.finish(); + + this.dispatchEvent( { type: 'dispose' } ); + + } + } export default InspectorBase; diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index f6ad3db4eeb1da..398f653252ed73 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -842,12 +842,6 @@ class Renderer { this._animation.start(); this._initialized = true; - // - - this._inspector.init(); - - // - resolve( this ); } ); @@ -2702,6 +2696,7 @@ class Renderer { this.info.dispose(); + this._inspector.dispose(); this._animation.dispose(); this._objects.dispose(); this._geometries.dispose();