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 );
}