diff --git a/examples/jsm/inspector/Extension.js b/examples/jsm/inspector/Extension.js new file mode 100644 index 00000000000000..18e3507fc5ecf1 --- /dev/null +++ b/examples/jsm/inspector/Extension.js @@ -0,0 +1,13 @@ +import { Tab } from 'three/addons/inspector/ui/Tab.js'; + +export class Extension extends Tab { + + constructor( name, options = {} ) { + + super( name, options ); + + this.isExtension = true; + + } + +} diff --git a/examples/jsm/inspector/Inspector.js b/examples/jsm/inspector/Inspector.js index 1d6fd9697ae614..97534de1851c34 100644 --- a/examples/jsm/inspector/Inspector.js +++ b/examples/jsm/inspector/Inspector.js @@ -8,32 +8,9 @@ import { Parameters } from './tabs/Parameters.js'; import { Settings } from './tabs/Settings.js'; import { Viewer } from './tabs/Viewer.js'; import { Timeline } from './tabs/Timeline.js'; -import { setText, splitPath, splitCamelCase } from './ui/utils.js'; +import { setText } from './ui/utils.js'; -import { QuadMesh, NodeMaterial, CanvasTarget, setConsoleFunction, REVISION, NoToneMapping } from 'three/webgpu'; -import { renderOutput, vec2, vec3, vec4, Fn, screenUV, step, OnMaterialUpdate, uniform } from 'three/tsl'; - -const aspectRatioUV = /*@__PURE__*/ Fn( ( [ uv, textureNode ] ) => { - - const aspect = uniform( 0 ); - - OnMaterialUpdate( () => { - - const { width, height } = textureNode.value; - - aspect.value = width / height; - - } ); - - const centered = uv.sub( 0.5 ); - const corrected = vec2( centered.x.div( aspect ), centered.y ); - const finalUV = corrected.add( 0.5 ); - - const inBounds = step( 0.0, finalUV.x ).mul( step( finalUV.x, 1.0 ) ).mul( step( 0.0, finalUV.y ) ).mul( step( finalUV.y, 1.0 ) ); - - return vec3( finalUV, inBounds ); - -} ); +import { setConsoleFunction, REVISION } from 'three/webgpu'; class Inspector extends RendererInspector { @@ -43,7 +20,7 @@ class Inspector extends RendererInspector { // init profiler - const profiler = new Profiler(); + const profiler = new Profiler( this ); profiler.addEventListener( 'resize', ( e ) => this.dispatchEvent( e ) ); const parameters = new Parameters( { @@ -81,7 +58,6 @@ class Inspector extends RendererInspector { } this.statsData = new Map(); - this.canvasNodes = new Map(); this.profiler = profiler; this.performance = performance; this.memory = memory; @@ -89,7 +65,9 @@ class Inspector extends RendererInspector { this.parameters = parameters; this.viewer = viewer; this.timeline = timeline; + this.settings = settings; this.once = {}; + this.extensionsData = new WeakMap(); this.displayCycle = { text: { @@ -112,6 +90,34 @@ class Inspector extends RendererInspector { } + onExtension( name, callback ) { + + const extensionAdded = ( e ) => { + + if ( e.name === name ) { + + callback( e.tab ); + + this.settings.removeEventListener( 'extensionadded', extensionAdded ); + + } + + }; + + if ( this.settings.extensions[ name ] && this.settings.extensions[ name ].loaded ) { + + callback( this.settings.extensions[ name ] ); + + } else { + + this.settings.addEventListener( 'extensionadded', extensionAdded ); + + } + + return this; + + } + hide() { this.profiler.hide(); @@ -154,6 +160,14 @@ class Inspector extends RendererInspector { } + setActiveExtension( name, value ) { + + this.settings.setActiveExtension( name, value ); + + return this; + + } + resolveConsoleOnce( type, message ) { const key = type + message; @@ -351,115 +365,62 @@ class Inspector extends RendererInspector { } - getCanvasDataByNode( node ) { - - let canvasData = this.canvasNodes.get( node ); - - if ( canvasData === undefined ) { - - const renderer = this.getRenderer(); - - const canvas = document.createElement( 'canvas' ); - - const canvasTarget = new CanvasTarget( canvas ); - canvasTarget.setPixelRatio( window.devicePixelRatio ); - canvasTarget.setSize( 140, 140 ); - - const id = node.id; - - const { path, name } = splitPath( splitCamelCase( node.getName() || '(unnamed)' ) ); + getNodes() { - const target = node.context( { getUV: ( textureNode ) => { - - const uvData = aspectRatioUV( screenUV, textureNode ); - const correctedUV = uvData.xy; - const mask = uvData.z; - - return correctedUV.mul( mask ); - - } } ); - - let output = vec4( vec3( target ), 1 ); - output = renderOutput( output, NoToneMapping, renderer.outputColorSpace ); - output = output.context( { inspector: true } ); - - const material = new NodeMaterial(); - material.outputNode = output; - - const quad = new QuadMesh( material ); - quad.name = 'Viewer - ' + name; - - canvasData = { - id, - name, - path, - node, - quad, - canvasTarget, - material - }; - - this.canvasNodes.set( node, canvasData ); - - } - - return canvasData; + return this.currentNodes; } - resolveViewer() { - - const nodes = this.currentNodes; - const renderer = this.getRenderer(); + getAverageDeltaTime( statsData, property, frames = this.fps ) { - if ( nodes.length === 0 ) return; + const statsArray = statsData.stats; - if ( ! renderer.backend.isWebGPUBackend ) { + let sum = 0; + let count = 0; - this.resolveConsoleOnce( 'warn', 'Inspector: Viewer is only available with WebGPU.' ); + for ( let i = statsArray.length - 1; i >= 0 && count < frames; i -- ) { - return; + const stats = statsArray[ i ]; + const value = stats[ property ]; - } + if ( value > 0 ) { - // + // ignore invalid values - if ( ! this.viewer.isVisible ) { + sum += value; + count ++; - this.viewer.show(); + } } - const canvasDataList = nodes.map( node => this.getCanvasDataByNode( node ) ); - - this.viewer.update( renderer, canvasDataList ); + return count > 0 ? sum / count : 0; } - getAverageDeltaTime( statsData, property, frames = this.fps ) { + updateTabs() { - const statsArray = statsData.stats; + // tabs - let sum = 0; - let count = 0; + const tabs = Object.values( this.profiler.tabs ); - for ( let i = statsArray.length - 1; i >= 0 && count < frames; i -- ) { + for ( const tab of tabs ) { - const stats = statsArray[ i ]; - const value = stats[ property ]; + let tabData = this.extensionsData.get( tab ); - if ( value > 0 ) { + if ( tabData === undefined ) { - // ignore invalid values + tab.init( this ); - sum += value; - count ++; + tabData = {}; + + this.extensionsData.set( tab, tabData ); } - } + tab.update( this ); - return count > 0 ? sum / count : 0; + } } @@ -537,6 +498,45 @@ class Inspector extends RendererInspector { } + static getItem( id ) { + + console.warn( 'Inspector.getItem is deprecated. Use getItem directly instead.' ); + return getItem( id ); + + } + + static setItem( id, state ) { + + console.warn( 'Inspector.setItem is deprecated. Use setItem directly instead.' ); + setItem( id, state ); + + } + +} + +function getItem( id ) { + + const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' ); + return data[ id ] || {}; + +} + +function setItem( id, state ) { + + const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' ); + + if ( state === null ) { + + delete data[ id ]; + + } else { + + data[ id ] = state; + + } + + localStorage.setItem( 'threejs-inspector', JSON.stringify( data ) ); + } -export { Inspector }; +export { Inspector, getItem, setItem }; diff --git a/examples/jsm/inspector/RendererInspector.js b/examples/jsm/inspector/RendererInspector.js index 3770fccd39713e..82105754afc0c4 100644 --- a/examples/jsm/inspector/RendererInspector.js +++ b/examples/jsm/inspector/RendererInspector.js @@ -173,7 +173,7 @@ export class RendererInspector extends InspectorBase { } - resolveViewer() { } + updateTabs() { } resolveFrame( /*frame*/ ) { } @@ -321,7 +321,7 @@ export class RendererInspector extends InspectorBase { if ( this.isAvailable ) { - this.resolveViewer(); + this.updateTabs(); this.resolveTimestamp(); } diff --git a/examples/jsm/inspector/extensions/extensions.json b/examples/jsm/inspector/extensions/extensions.json new file mode 100644 index 00000000000000..796793bb871e7f --- /dev/null +++ b/examples/jsm/inspector/extensions/extensions.json @@ -0,0 +1,6 @@ +[ + { + "name": "TSL Graph", + "url": "./tsl-graph/TSLGraphEditor.js" + } +] diff --git a/examples/jsm/inspector/addons/tsl-graph/TSLGraphEditor.js b/examples/jsm/inspector/extensions/tsl-graph/TSLGraphEditor.js similarity index 82% rename from examples/jsm/inspector/addons/tsl-graph/TSLGraphEditor.js rename to examples/jsm/inspector/extensions/tsl-graph/TSLGraphEditor.js index e806c17687d955..de32102f00496c 100644 --- a/examples/jsm/inspector/addons/tsl-graph/TSLGraphEditor.js +++ b/examples/jsm/inspector/extensions/tsl-graph/TSLGraphEditor.js @@ -1,5 +1,5 @@ -import { error } from 'three/webgpu'; -import { Tab } from '../../ui/Tab.js'; +import { Raycaster, Vector2, BoxHelper, error, warn } from 'three/webgpu'; +import { Extension } from 'three/addons/inspector/Extension.js'; import { TSLGraphLoader } from './TSLGraphLoader.js'; const HOST_SOURCE = 'tsl-graph-host'; @@ -15,7 +15,7 @@ const _resposeByCommand = { const _refMaterials = new WeakMap(); -export class TSLGraphEditor extends Tab { +class TSLGraphEditor extends Extension { constructor( options = {} ) { @@ -37,6 +37,7 @@ export class TSLGraphEditor extends Tab { headerDiv.style.display = 'flex'; headerDiv.style.justifyContent = 'center'; headerDiv.style.gap = '4px'; + headerDiv.style.position = 'relative'; const importBtn = document.createElement( 'button' ); importBtn.innerHTML = ''; @@ -59,17 +60,46 @@ export class TSLGraphEditor extends Tab { manageBtn.style.padding = '5px 8px'; manageBtn.onclick = () => this._showManagerModal(); + const autoIdBtn = document.createElement( 'button' ); + autoIdBtn.innerHTML = ''; + autoIdBtn.className = 'panel-action-btn'; + autoIdBtn.title = 'Auto-Generate Graph ID'; + autoIdBtn.style.padding = '5px 8px'; + autoIdBtn.style.position = 'absolute'; + autoIdBtn.style.right = '4px'; + autoIdBtn.style.top = '4px'; + + this.autoGraphId = false; + + autoIdBtn.onclick = () => { + + this.autoGraphId = ! this.autoGraphId; + + if ( this.autoGraphId ) { + + autoIdBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; + autoIdBtn.style.color = '#fff'; + + } else { + + autoIdBtn.style.backgroundColor = ''; + autoIdBtn.style.color = ''; + + } + + }; + headerDiv.appendChild( importBtn ); headerDiv.appendChild( exportBtn ); headerDiv.appendChild( manageBtn ); + headerDiv.appendChild( autoIdBtn ); this.content.appendChild( headerDiv ); this.iframe = document.createElement( 'iframe' ); this.iframe.style.width = '100%'; - this.iframe.style.minHeight = '600px'; + this.iframe.style.height = '100%'; this.iframe.style.border = 'none'; - this.iframe.style.flex = '1'; this.iframe.src = editorUrl.toString(); this.editorOrigin = new URL( this.iframe.src ).origin; @@ -102,6 +132,126 @@ export class TSLGraphEditor extends Tab { } + _initPicker( inspector ) { + + const renderer = inspector.getRenderer(); + + let boundingBox = null; + + const raycaster = new Raycaster(); + const pointer = new Vector2(); + + const removeBoundingBox = () => { + + if ( boundingBox ) { + + boundingBox.removeFromParent(); + boundingBox.dispose(); + boundingBox = null; + + } + + }; + + this.addEventListener( 'change', ( { material } ) => { + + if ( material === null ) { + + removeBoundingBox(); + + } + + } ); + + this.addEventListener( 'remove', ( { graphId } ) => { + + const frame = inspector.getFrame(); + const scene = frame && frame.renders.length > 0 ? frame.renders[ 0 ].scene : null; + + if ( scene ) { + + scene.traverse( ( object ) => { + + if ( object.material && object.material.userData && object.material.userData.graphId === graphId ) { + + this.restoreMaterial( object.material ); + + } + + } ); + + } + + } ); + + const pointerDownPosition = new Vector2(); + + renderer.domElement.addEventListener( 'pointerdown', ( e ) => { + + pointerDownPosition.set( e.clientX, e.clientY ); + + } ); + + renderer.domElement.addEventListener( 'pointerup', ( e ) => { + + const frame = inspector.getFrame(); + + for ( const render of frame.renders ) { + + const scene = render.scene; + + if ( scene.isScene !== true ) continue; + + const camera = render.camera; + + if ( pointerDownPosition.distanceTo( pointer.set( e.clientX, e.clientY ) ) > 2 ) return; + + const rect = renderer.domElement.getBoundingClientRect(); + pointer.x = ( ( e.clientX - rect.left ) / rect.width ) * 2 - 1; + pointer.y = - ( ( e.clientY - rect.top ) / rect.height ) * 2 + 1; + + raycaster.setFromCamera( pointer, camera ); + + const intersects = raycaster.intersectObjects( scene.children, true ); + + let graphMaterial = null; + + if ( intersects.length > 0 ) { + + for ( const intersect of intersects ) { + + const object = intersect.object; + const material = object.material; + + if ( material && material.isNodeMaterial ) { + + removeBoundingBox(); + + boundingBox = new BoxHelper( object, 0xffff00 ); + scene.add( boundingBox ); + + graphMaterial = material; + + } + + if ( object.isMesh || object.isSprite ) { + + break; + + } + + } + + } + + this.setMaterial( graphMaterial ); + + } + + } ); + + } + apply( scene ) { const loader = new TSLGraphLoader(); @@ -119,6 +269,12 @@ export class TSLGraphEditor extends Tab { } + init( inspector ) { + + this._initPicker( inspector ); + + } + async setMaterial( material ) { if ( this.material === material ) return; @@ -269,7 +425,7 @@ export class TSLGraphEditor extends Tab { if ( material.isNodeMaterial !== true ) { - error( 'Inspector: "Material" needs be a "NodeMaterial".' ); + error( 'TSLGraphEditor: "Material" needs be a "NodeMaterial".' ); return; @@ -277,9 +433,17 @@ export class TSLGraphEditor extends Tab { if ( material.userData.graphId === undefined ) { - error( 'Inspector: "NodeMaterial" has no graphId. Set a "graphId" for the material in "material.userData.graphId".' ); + if ( this.autoGraphId ) { - return; + material.userData.graphId = material.name || 'id:' + material.id; + + } else { + + warn( 'TSLGraphEditor: "NodeMaterial" has no graphId. Set a "graphId" for the material in "material.userData.graphId".' ); + + return; + + } } @@ -625,7 +789,7 @@ export class TSLGraphEditor extends Tab { } catch ( err ) { - error( 'TSLGraph: Failed to parse or load imported JSON.', err ); + error( 'TSLGraphEditor: Failed to parse or load imported JSON.', err ); } @@ -748,3 +912,5 @@ export class TSLGraphEditor extends Tab { } } + +export default TSLGraphEditor; diff --git a/examples/jsm/inspector/addons/tsl-graph/TSLGraphLoader.js b/examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js similarity index 100% rename from examples/jsm/inspector/addons/tsl-graph/TSLGraphLoader.js rename to examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js diff --git a/examples/jsm/inspector/tabs/Memory.js b/examples/jsm/inspector/tabs/Memory.js index 333c3751b7d56a..440bbba6b05c11 100644 --- a/examples/jsm/inspector/tabs/Memory.js +++ b/examples/jsm/inspector/tabs/Memory.js @@ -79,7 +79,7 @@ class Memory extends Tab { const memory = renderer.info.memory; this.graph.addPoint( 'total', memory.total ); - + if ( this.graph.limit === 0 ) this.graph.limit = 1; this.graph.update(); @@ -98,17 +98,17 @@ class Memory extends Tab { setText( this.attributes.data[ 1 ], memory.attributes.toString() ); setText( this.attributes.data[ 2 ], formatBytes( memory.attributesSize ) ); setText( this.geometries.data[ 1 ], memory.geometries.toString() ); - + setText( this.indexAttributes.data[ 1 ], memory.indexAttributes.toString() ); setText( this.indexAttributes.data[ 2 ], formatBytes( memory.indexAttributesSize ) ); - + setText( this.indirectStorageAttributes.data[ 1 ], memory.indirectStorageAttributes.toString() ); setText( this.indirectStorageAttributes.data[ 2 ], formatBytes( memory.indirectStorageAttributesSize ) ); setText( this.programs.data[ 1 ], memory.programs.toString() ); - + setText( this.renderTargets.data[ 1 ], memory.renderTargets.toString() ); - + setText( this.storageAttributes.data[ 1 ], memory.storageAttributes.toString() ); setText( this.storageAttributes.data[ 2 ], formatBytes( memory.storageAttributesSize ) ); setText( this.textures.data[ 1 ], memory.textures.toString() ); diff --git a/examples/jsm/inspector/tabs/Settings.js b/examples/jsm/inspector/tabs/Settings.js index 4bf887e9f41c71..c7e4c3794bdc73 100644 --- a/examples/jsm/inspector/tabs/Settings.js +++ b/examples/jsm/inspector/tabs/Settings.js @@ -1,5 +1,8 @@ import { Parameters } from './Parameters.js'; import { WebGPURenderer, WebGLBackend, Node } from 'three/webgpu'; +import { getItem, setItem } from '../Inspector.js'; + +const _EXTENSIONS_PATH = '../extensions/extensions.json'; const _init = WebGPURenderer.prototype.init; @@ -29,63 +32,48 @@ function forceWebGL( enable ) { } -function loadState() { - - let settings = {}; +let _state = null; - try { +function _loadState() { - const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' ); - settings = data.settings || {}; + if ( _state !== null ) return _state; - } catch ( e ) { + const settings = getItem( 'settings' ); - console.error( 'Failed to load settings:', e ); - - } - - const state = { - forceWebGL: settings.forceWebGL || false, - captureStackTrace: settings.captureStackTrace || false + _state = { + forceWebGL: settings.forceWebGL !== undefined ? settings.forceWebGL : false, + captureStackTrace: settings.captureStackTrace !== undefined ? settings.captureStackTrace : false, + activeExtensions: settings.activeExtensions !== undefined ? settings.activeExtensions : {} }; - return state; - -} + if ( _state.forceWebGL ) { -function saveState( state ) { - - try { - - const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' ); - data.settings = state; - - localStorage.setItem( 'threejs-inspector', JSON.stringify( data ) ); - - } catch ( e ) { - - console.error( 'Failed to save settings:', e ); + forceWebGL( true ); } -} + if ( _state.captureStackTrace ) { -// - -const state = loadState(); + Node.captureStackTrace = true; -if ( state.forceWebGL ) { + } - forceWebGL( true ); + return _state; } -if ( state.captureStackTrace ) { +function _saveState() { - Node.captureStackTrace = true; + setItem( 'settings', { + forceWebGL: _state.forceWebGL, + captureStackTrace: _state.captureStackTrace, + activeExtensions: _state.activeExtensions + } ); } +_loadState(); + // class Settings extends Parameters { @@ -94,23 +82,27 @@ class Settings extends Parameters { super( { name: 'Settings' } ); + this.extensions = {}; + + const currentState = _loadState(); + // UI const rendererGroup = this.createGroup( 'Renderer' ); - rendererGroup.add( state, 'forceWebGL' ).name( 'Force WebGL' ).onChange( ( enable ) => { + rendererGroup.add( currentState, 'forceWebGL' ).name( 'Force WebGL' ).onChange( ( enable ) => { forceWebGL( enable ); - saveState( state ); + _saveState(); location.reload(); } ); - rendererGroup.add( state, 'captureStackTrace' ).name( 'Capture Stack Trace' ).onChange( ( enable ) => { + rendererGroup.add( currentState, 'captureStackTrace' ).name( 'Capture Stack Trace' ).onChange( ( enable ) => { Node.captureStackTrace = enable; - saveState( state ); + _saveState(); location.reload(); @@ -118,6 +110,155 @@ class Settings extends Parameters { } + init() { + + const extensionsGroup = this.createGroup( 'Extensions' ); + + this._getExtensions().then( extensions => { + + for ( const extension of extensions ) { + + extension.active = false; + extension.loaded = false; + extension.tab = null; + + this.extensions[ extension.name ] = extension; + + extension.ui = extensionsGroup.add( { [ extension.name ]: false }, extension.name ).onChange( async ( value ) => { + + this.setActiveExtension( extension.name, value ); + + // User preference + + if ( value ) { + + _state.activeExtensions[ extension.name ] = { + name: extension.name, + url: extension.url + }; + + } else { + + delete _state.activeExtensions[ extension.name ]; + + + } + + // + + this._updateExtensionUI( extension ); + + _saveState(); + + } ); + + // Set user-defined state + + if ( _state.activeExtensions[ extension.name ] !== undefined ) { + + extension.ui.setValue( true ); + + } + + } + + } ); + + } + + async setActiveExtension( name, value ) { + + const extension = this.extensions[ name ]; + const inspector = this.inspector; + + if ( extension ) { + + if ( value ) { + + await this._loadExtension( inspector, extension ); + + } else { + + await this._unloadExtension( inspector, extension ); + + } + + } + + } + + _updateExtensionUI( extension ) { + + const forceActive = extension.active && _state.activeExtensions[ extension.name ] === undefined; + + if ( forceActive ) { + + extension.ui.checkbox.checked = true; + extension.ui.domElement.style.setProperty( '--accent-color', 'var(--color-green)' ); + + } else { + + extension.ui.domElement.style.removeProperty( '--accent-color' ); + + } + + } + + async _unloadExtension( inspector, extension ) { + + if ( extension.active === false ) return; + + // + + inspector.removeTab( extension.tab ); + + extension.active = false; + extension.loaded = false; + extension.tab = null; + + this._updateExtensionUI( extension ); + + this.dispatchEvent( { type: 'extensionremoved', name: extension.name } ); + + } + + async _loadExtension( inspector, extension ) { + + if ( extension.active === true ) return; + + // + + extension.active = true; + + const extUrl = new URL( extension.url, new URL( _EXTENSIONS_PATH, import.meta.url ) ).href; + + const module = await import( extUrl ); + + const keys = Object.keys( module ); + const ExtensionClass = module[ keys[ 0 ] ]; + const extensionTab = new ExtensionClass(); + + inspector.addTab( extensionTab ); + + extension.loaded = true; + extension.tab = extensionTab; + + this._updateExtensionUI( extension ); + + this.dispatchEvent( { type: 'extensionadded', name: extension.name, tab: extensionTab } ); + + } + + async _getExtensions() { + + const url = new URL( _EXTENSIONS_PATH, import.meta.url ); + + const extensions = await fetch( url ).then( res => res.json() ); + + return extensions; + + } + } export { Settings }; diff --git a/examples/jsm/inspector/tabs/Timeline.js b/examples/jsm/inspector/tabs/Timeline.js index f0d9a02b224f0f..1be327bdaecb37 100644 --- a/examples/jsm/inspector/tabs/Timeline.js +++ b/examples/jsm/inspector/tabs/Timeline.js @@ -1,5 +1,6 @@ import { Tab } from '../ui/Tab.js'; import { Graph } from '../ui/Graph.js'; +import { getItem, setItem } from '../Inspector.js'; const LIMIT = 500; @@ -93,10 +94,9 @@ class Timeline extends Tab { this.recordRefreshButton.style.alignItems = 'center'; this.recordRefreshButton.addEventListener( 'click', () => { - const storage = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' ); - storage.timeline = storage.timeline || {}; - storage.timeline.recording = true; - localStorage.setItem( 'threejs-inspector', JSON.stringify( storage ) ); + const timelineSettings = getItem( 'timeline' ); + timelineSettings.recording = true; + setItem( 'timeline', timelineSettings ); window.location.reload(); @@ -442,12 +442,12 @@ class Timeline extends Tab { this.renderer = renderer; - const storage = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' ); + const timelineSettings = getItem( 'timeline' ); - if ( storage.timeline && storage.timeline.recording ) { + if ( timelineSettings.recording ) { - storage.timeline.recording = false; - localStorage.setItem( 'threejs-inspector', JSON.stringify( storage ) ); + timelineSettings.recording = false; + setItem( 'timeline', timelineSettings ); this.toggleRecording(); diff --git a/examples/jsm/inspector/tabs/Viewer.js b/examples/jsm/inspector/tabs/Viewer.js index 93e0ff1e65dcad..a9879e9a48cd04 100644 --- a/examples/jsm/inspector/tabs/Viewer.js +++ b/examples/jsm/inspector/tabs/Viewer.js @@ -1,8 +1,32 @@ import { Tab } from '../ui/Tab.js'; import { List } from '../ui/List.js'; import { Item } from '../ui/Item.js'; +import { splitPath, splitCamelCase } from '../ui/utils.js'; -import { RendererUtils, NoToneMapping, LinearSRGBColorSpace } from 'three/webgpu'; +import { RendererUtils, NoToneMapping, LinearSRGBColorSpace, QuadMesh, NodeMaterial, CanvasTarget } from 'three/webgpu'; +import { renderOutput, vec2, vec3, vec4, Fn, screenUV, step, OnMaterialUpdate, uniform } from 'three/tsl'; + +const aspectRatioUV = /*@__PURE__*/ Fn( ( [ uv, textureNode ] ) => { + + const aspect = uniform( 0 ); + + OnMaterialUpdate( () => { + + const { width, height } = textureNode.value; + + aspect.value = width / height; + + } ); + + const centered = uv.sub( 0.5 ); + const corrected = vec2( centered.x.div( aspect ), centered.y ); + const finalUV = corrected.add( 0.5 ); + + const inBounds = step( 0.0, finalUV.x ).mul( step( finalUV.x, 1.0 ) ).mul( step( 0.0, finalUV.y ) ).mul( step( finalUV.y, 1.0 ) ); + + return vec3( finalUV, inBounds ); + +} ); class Viewer extends Tab { @@ -26,6 +50,7 @@ class Viewer extends Tab { this.itemLibrary = new Map(); this.folderLibrary = new Map(); + this.canvasNodes = new Map(); this.currentDataList = []; this.nodeList = nodeList; this.nodes = nodes; @@ -68,9 +93,84 @@ class Viewer extends Tab { } - update( renderer, canvasDataList ) { + getCanvasDataByNode( renderer, node ) { + + let canvasData = this.canvasNodes.get( node ); + + if ( canvasData === undefined ) { + + const canvas = document.createElement( 'canvas' ); + + const canvasTarget = new CanvasTarget( canvas ); + canvasTarget.setPixelRatio( window.devicePixelRatio ); + canvasTarget.setSize( 140, 140 ); + + const id = node.id; + + const { path, name } = splitPath( splitCamelCase( node.getName() || '(unnamed)' ) ); + + const target = node.context( { getUV: ( textureNode ) => { + + const uvData = aspectRatioUV( screenUV, textureNode ); + const correctedUV = uvData.xy; + const mask = uvData.z; + + return correctedUV.mul( mask ); + + } } ); + + let output = vec4( vec3( target ), 1 ); + output = renderOutput( output, NoToneMapping, renderer.outputColorSpace ); + output = output.context( { inspector: true } ); + + const material = new NodeMaterial(); + material.outputNode = output; + + const quad = new QuadMesh( material ); + quad.name = 'Viewer - ' + name; + + canvasData = { + id, + name, + path, + node, + quad, + canvasTarget, + material + }; + + this.canvasNodes.set( node, canvasData ); + + } + + return canvasData; + + } + + update( inspector ) { + + const renderer = inspector.getRenderer(); + const nodes = inspector.getNodes(); + + if ( nodes.length > 0 ) { + + if ( ! renderer.backend.isWebGPUBackend ) { + + inspector.resolveConsoleOnce( 'warn', 'Inspector: Viewer is only available with WebGPU.' ); + + return; + + } + + if ( ! this.isVisible ) { + + this.show(); + + } + + } - if ( ! this.isActive && ! this.isDetached ) return; + const canvasDataList = nodes.map( node => this.getCanvasDataByNode( renderer, node ) ); // diff --git a/examples/jsm/inspector/ui/Profiler.js b/examples/jsm/inspector/ui/Profiler.js index 66b24f69342cef..9aef787373377e 100644 --- a/examples/jsm/inspector/ui/Profiler.js +++ b/examples/jsm/inspector/ui/Profiler.js @@ -1,12 +1,14 @@ import { EventDispatcher } from 'three'; import { Style } from './Style.js'; +import { getItem, setItem } from '../Inspector.js'; export class Profiler extends EventDispatcher { - constructor() { + constructor( inspector ) { super(); + this.inspector = inspector; this.tabs = {}; this.activeTabId = null; this.isResizing = false; @@ -517,6 +519,9 @@ export class Profiler extends EventDispatcher { // Update panel size when tabs change this.updatePanelSize(); + // Set profiler reference + tab.profiler = this; + } addBuiltinTab( tab ) { @@ -573,7 +578,6 @@ export class Profiler extends EventDispatcher { // Store references tab.builtinButton = builtinButton; tab.miniContent = miniContent; - tab.profiler = this; // If the tab was hidden before being added, hide the builtin button if ( ! tab.isVisible ) { @@ -595,6 +599,98 @@ export class Profiler extends EventDispatcher { } + removeTab( tab ) { + + if ( ! tab || this.tabs[ tab.id ] === undefined ) return; + + delete this.tabs[ tab.id ]; + + if ( tab.isDetached && tab.detachedWindow ) { + + if ( tab.detachedWindow.panel && tab.detachedWindow.panel.parentNode ) { + + tab.detachedWindow.panel.parentNode.removeChild( tab.detachedWindow.panel ); + + } + + const index = this.detachedWindows.indexOf( tab.detachedWindow ); + + if ( index !== - 1 ) { + + this.detachedWindows.splice( index, 1 ); + + } + + } + + if ( ! tab.builtin ) { + + if ( tab.button && tab.button.parentNode ) { + + tab.button.parentNode.removeChild( tab.button ); + + } + + } else { + + if ( tab.builtinButton && tab.builtinButton.parentNode ) { + + tab.builtinButton.parentNode.removeChild( tab.builtinButton ); + + } + + if ( tab.miniContent && tab.miniContent.parentNode ) { + + tab.miniContent.parentNode.removeChild( tab.miniContent ); + + } + + // Clean up builtin container if empty + const hasVisibleBuiltinButtons = Array.from( this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) ) + .some( btn => btn.style.display !== 'none' ); + + if ( ! hasVisibleBuiltinButtons ) { + + this.builtinTabsContainer.style.display = 'none'; + + } + + } + + if ( tab.content && tab.content.parentNode ) { + + tab.content.parentNode.removeChild( tab.content ); + + } + + if ( this.activeTabId === tab.id ) { + + this.activeTabId = null; + + // Try to activate another tab + const remainingTabs = Object.values( this.tabs ).filter( t => ! t.isDetached && t.isVisible ); + + if ( remainingTabs.length > 0 ) { + + this.setActiveTab( remainingTabs[ 0 ].id ); + + } else { + + this.updatePanelSize(); + + } + + } else { + + this.updatePanelSize(); + + } + + tab.onVisibilityChange = null; + tab.profiler = null; + + } + updatePanelSize() { // Check if there are any visible tabs in the panel @@ -1643,11 +1739,7 @@ export class Profiler extends EventDispatcher { try { - const savedData = localStorage.getItem( 'threejs-inspector' ); - const data = JSON.parse( savedData || '{}' ); - - data.layout = layout; - localStorage.setItem( 'threejs-inspector', JSON.stringify( data ) ); + setItem( 'layout', layout ); } catch ( e ) { @@ -1663,14 +1755,9 @@ export class Profiler extends EventDispatcher { try { - const savedData = localStorage.getItem( 'threejs-inspector' ); - - if ( ! savedData ) return; - - const parsedData = JSON.parse( savedData ); - const layout = parsedData.layout; + const layout = getItem( 'layout' ); - if ( ! layout ) return; + if ( Object.keys( layout ).length === 0 ) return; // Constrain detached tabs positions to current screen bounds if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) { diff --git a/examples/jsm/inspector/ui/Tab.js b/examples/jsm/inspector/ui/Tab.js index eebad749d32b15..fe0c3b9fcf7331 100644 --- a/examples/jsm/inspector/ui/Tab.js +++ b/examples/jsm/inspector/ui/Tab.js @@ -54,6 +54,16 @@ export class Tab extends EventDispatcher { } + get inspector() { + + return this.profiler.inspector; + + } + + init( /*inspector*/ ) { } + + update( /*inspector*/ ) { } + setActive( isActive ) { this.button.classList.toggle( 'active', isActive ); diff --git a/examples/jsm/inspector/ui/Values.js b/examples/jsm/inspector/ui/Values.js index 61d74b43e62f8d..db255e3d5c7deb 100644 --- a/examples/jsm/inspector/ui/Values.js +++ b/examples/jsm/inspector/ui/Values.js @@ -204,7 +204,7 @@ class ValueCheckbox extends Value { setValue( val ) { - this.checkbox.value = val; + this.checkbox.checked = val; return super.setValue( val ); diff --git a/examples/jsm/loaders/EXRLoader.js b/examples/jsm/loaders/EXRLoader.js index 462fd43b510477..5b691e189d40f5 100644 --- a/examples/jsm/loaders/EXRLoader.js +++ b/examples/jsm/loaders/EXRLoader.js @@ -1826,11 +1826,14 @@ class EXRLoader extends DataTextureLoader { const cd = channelData[ offset ]; + const dotIndex = cd.name.lastIndexOf( '.' ); + const suffix = dotIndex >= 0 ? cd.name.substring( dotIndex + 1 ) : cd.name; + for ( let i = 0; i < channelRules.length; ++ i ) { const rule = channelRules[ i ]; - if ( cd.name == rule.name ) { + if ( suffix === rule.name && cd.type === rule.type ) { cd.compression = rule.compression; diff --git a/examples/webgpu_tsl_graph.html b/examples/webgpu_tsl_graph.html index ef3497b61dfa3e..8db95bfb24ad23 100644 --- a/examples/webgpu_tsl_graph.html +++ b/examples/webgpu_tsl_graph.html @@ -49,8 +49,7 @@ import { Inspector } from 'three/addons/inspector/Inspector.js'; - import { TSLGraphLoader } from 'three/addons/inspector/addons/tsl-graph/TSLGraphLoader.js'; - import { TSLGraphEditor } from 'three/addons/inspector/addons/tsl-graph/TSLGraphEditor.js'; + import { TSLGraphLoader } from 'three/addons/inspector/extensions/tsl-graph/TSLGraphLoader.js'; let camera, scene, renderer; let controls; @@ -60,13 +59,6 @@ async function initTSLGraph() { - // TSL Graph Editor - - const tslGraph = new TSLGraphEditor(); - - renderer.inspector.addTab( tslGraph ); - renderer.inspector.setActiveTab( tslGraph ); - // Create Materials const m1 = new THREE.MeshPhysicalNodeMaterial(); @@ -89,123 +81,37 @@ } - // Initialize - - // Load and apply TSL Graph from a file or from Local Storage if exists - // Every time a TSL Graph is changed, it will be stored in the local storage - - if ( tslGraph.hasGraphs ) { - - tslGraph.apply( scene ); - - } else { - - // Load a TSL Graph from a file - - const tslLoader = new TSLGraphLoader(); - const applier = await tslLoader.setPath( './shaders/' ).loadAsync( 'tsl-graphs.json' ); - - applier.apply( scene ); - - } - - // Picker a Material + // TSL Graph Editor - let boundingBox = null; + renderer.inspector.onExtension( 'TSL Graph', async ( tslGraph ) => { - const raycaster = new THREE.Raycaster(); - const pointer = new THREE.Vector2(); + renderer.inspector.setActiveTab( tslGraph ); - function removeBoundingBox() { + // Apply TSL Graph from Local Storage if exists + // Every time a TSL Graph is changed, it will be stored in the local storage - scene.remove( boundingBox ); - boundingBox.dispose(); + if ( tslGraph.hasGraphs ) { - } + tslGraph.apply( scene ); - tslGraph.addEventListener( 'change', ( { material } ) => { + } else { - if ( material === null && boundingBox ) { + // Load a TSL Graph from a file + // Use it for production - removeBoundingBox(); + const tslLoader = new TSLGraphLoader(); + const applier = await tslLoader.setPath( './shaders/' ).loadAsync( 'tsl-graphs.json' ); - boundingBox = null; + applier.apply( scene ); } } ); - tslGraph.addEventListener( 'remove', ( { graphId } ) => { - - scene.traverse( ( object ) => { - - if ( object.material && object.material.userData && object.material.userData.graphId === graphId ) { - - tslGraph.restoreMaterial( object.material ); - - } - - } ); - - } ); - - const pointerDownPosition = new THREE.Vector2(); - - renderer.domElement.addEventListener( 'pointerdown', ( e ) => { - - pointerDownPosition.set( e.clientX, e.clientY ); + // Active TSL Graph Editor + // Only is needed if you don't activate it from the GUI - } ); - - renderer.domElement.addEventListener( 'pointerup', ( e ) => { - - if ( pointerDownPosition.distanceTo( pointer.set( e.clientX, e.clientY ) ) > 2 ) return; - - const rect = renderer.domElement.getBoundingClientRect(); - pointer.x = ( ( e.clientX - rect.left ) / rect.width ) * 2 - 1; - pointer.y = - ( ( e.clientY - rect.top ) / rect.height ) * 2 + 1; - - raycaster.setFromCamera( pointer, camera ); - - const intersects = raycaster.intersectObjects( scene.children, true ); - - let graphMaterial = null; - - if ( intersects.length > 0 ) { - - for ( const intersect of intersects ) { - - const object = intersect.object; - const material = object.material; - - if ( material.userData && material.userData.graphId ) { - - if ( boundingBox ) { - - removeBoundingBox(); - - } - - boundingBox = new THREE.BoxHelper( object, 0xffff00 ); - scene.add( boundingBox ); - - graphMaterial = material; - - } - - if ( object.isMesh || object.isSprite ) { - - break; - - } - - } - - } - - tslGraph.setMaterial( graphMaterial ); - - } ); + renderer.inspector.setActiveExtension( 'TSL Graph', true ); }