Accueil / npm / three@0.186.1 / examples / jsm / inspector / RendererInspector.js

RendererInspector.js


import { InspectorBase, TimestampQuery, warnOnce, RendererUtils, MeshBasicNodeMaterial, AdditiveBlending, NoToneMapping, LinearSRGBColorSpace } from 'three/webgpu';
import { vec3 } from 'three/tsl';

class ObjectStats {

	constructor( uid, name ) {

		this.uid = uid;
		this.cid = uid.match( /^(.*):f(\d+)$/ )[ 1 ]; // call id
		this.name = name;
		this.timestamp = 0;
		this.cpu = 0;
		this.gpu = 0;
		this.fps = 0;

		this.children = [];
		this.parent = null;

	}

}

class RenderStats extends ObjectStats {

	constructor( uid, scene, camera, renderTarget ) {

		let name = scene.name;

		if ( name === '' ) {

			if ( scene.isScene ) {

				name = 'Scene';

			} else if ( scene.isQuadMesh ) {

				name = 'QuadMesh';

			}

		}

		super( uid, name );

		this.scene = scene;
		this.camera = camera;
		this.renderTarget = renderTarget;

		this.isRenderStats = true;

	}

}

class ComputeStats extends ObjectStats {

	constructor( uid, computeNode ) {

		const name = computeNode.name || ( computeNode.isComputeNode ? 'Compute' : 'Compute Group' );

		super( uid, name );

		this.computeNode = computeNode;

		this.isComputeStats = true;

	}

}

export class RendererInspector extends InspectorBase {

	constructor() {

		super();

		this.currentFrame = null;
		this.currentContext = null;
		this.currentRender = null;
		this.currentCompute = null;
		this.currentNodes = null;
		this.lastFrame = null;

		this.frames = [];
		this.framesLib = {};
		this.maxFrames = 512;

		this._lastFinishTime = 0;
		this._resolveTimestampPromise = null;
		this._rAFId = null;

		this.overdraw = false;
		this._overdrawMaterial = null;

		this.isRendererInspector = true;

	}

	getParent() {

		return this.currentContext || this.getFrame();

	}

	begin() {

		super.begin();

		this.currentFrame = this._createFrame();
		this.currentContext = this.currentFrame;
		this.currentRender = null;
		this.currentCompute = null;
		this.currentNodes = [];

	}

	finish() {

		super.finish();

		const now = performance.now();

		const frame = this.currentFrame;
		frame.finishTime = now;
		frame.deltaTime = now - ( this._lastFinishTime > 0 ? this._lastFinishTime : now );

		this.addFrame( frame );

		this.fps = this._getFPS();

		this.lastFrame = frame;

		this.currentFrame = null;
		this.currentContext = null;
		this.currentRender = null;
		this.currentCompute = null;
		this.currentNodes = null;

		this._lastFinishTime = now;

		if ( this.overdraw === true ) {

			this._renderOverdraw();

		}

	}

	_renderOverdraw() {

		const primaryPass = this.getPrimaryPass();

		if ( primaryPass === null ) return;

		const renderer = this.getRenderer();

		if ( this._overdrawMaterial === null ) {

			// additive constant, so each pixel sums the depth-passing fragments it shaded

			this._overdrawMaterial = new MeshBasicNodeMaterial( {
				colorNode: vec3( 0.25 ),
				blending: AdditiveBlending,
				depthTest: true,
				depthWrite: true
			} );

		}

		const { scene, camera } = primaryPass;

		// raw render against black with no tone mapping, so the count stays linear

		const state = RendererUtils.resetRendererAndSceneState( renderer, scene );

		renderer.toneMapping = NoToneMapping;
		renderer.outputColorSpace = LinearSRGBColorSpace;

		scene.overrideMaterial = this._overdrawMaterial;

		renderer.render( scene, camera );

		RendererUtils.restoreRendererAndSceneState( renderer, scene, state );

	}

	_getFPS() {

		let frameSum = 0;
		let timeSum = 0;

		for ( let i = this.frames.length - 1; i >= 0; i -- ) {

			const frame = this.frames[ i ];

			frameSum ++;
			timeSum += frame.deltaTime;

			if ( timeSum >= 1000 ) break;

		}

		return ( frameSum * 1000 ) / timeSum;

	}

	_createFrame() {

		return {
			frameId: this.nodeFrame.frameId,
			resolvedCompute: false,
			resolvedRender: false,
			deltaTime: 0,
			startTime: performance.now(),
			finishTime: 0,
			miscellaneous: 0,
			children: [],
			renders: [],
			computes: []
		};

	}

	getPrimaryPass() {

		const frame = this.getFrame();

		// first scene render of the frame; nested shadow / RTT passes come after

		let primary = null;

		for ( const render of frame.renders ) {

			if ( render.scene.isScene === true ) {

				primary = render;
				break;

			}

		}

		return primary;

	}

	getFrame() {

		return this.currentFrame || this.lastFrame;

	}

	getFrameById( frameId ) {

		return this.framesLib[ frameId ] || null;

	}

	updateTabs() { }

	resolveFrame( /*frame*/ ) { }

	async resolveTimestamp() {

		if ( this._resolveTimestampPromise !== null ) {

			return this._resolveTimestampPromise;

		}

		this._resolveTimestampPromise = new Promise( ( resolve ) => {

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

					const frameIds = [ ...new Set( [ ...computeFrames, ...renderFrames ] ) ];

					for ( const frameId of frameIds ) {

						const frame = this.getFrameById( frameId );

						if ( frame !== null ) {

							// resolve compute timestamps

							if ( frame.resolvedCompute === false ) {

								if ( frame.computes.length > 0 ) {

									if ( computeFrames.includes( frameId ) ) {

										for ( const stats of frame.computes ) {

											if ( renderer.backend.hasTimestampQuery( stats.uid ) ) {

												stats.gpu = renderer.backend.getTimestamp( stats.uid );

											} else {

												stats.gpu = 0;
												stats.gpuNotAvailable = true;

											}

										}

										frame.resolvedCompute = true;

									}

								} else {

									frame.resolvedCompute = true;

								}

							}

							// resolve render timestamps

							if ( frame.resolvedRender === false ) {

								if ( frame.renders.length > 0 ) {

									if ( renderFrames.includes( frameId ) ) {

										for ( const stats of frame.renders ) {

											if ( renderer.backend.hasTimestampQuery( stats.uid ) ) {

												stats.gpu = renderer.backend.getTimestamp( stats.uid );

											} else {

												stats.gpu = 0;
												stats.gpuNotAvailable = true;

											}

										}

										frame.resolvedRender = true;

									}

								} else {

									frame.resolvedRender = true;

								}

							}

							if ( frame.resolvedCompute === true && frame.resolvedRender === true ) {

								this.resolveFrame( frame );

							}

						}

					}

				} else {

					for ( const frame of this.frames ) {

						if ( frame.resolvedCompute === true && frame.resolvedRender === true ) {

							continue;

						}

						const previousFrame = this.getFrameById( frame.frameId - 1 );

						if ( previousFrame === 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 ) {

							this.resolveFrame( frame );

						}

					}

				}

				this._resolveTimestampPromise = null;

				resolve();

			} );

		} );

		return this._resolveTimestampPromise;

	}

	get isAvailable() {

		const renderer = this.getRenderer();

		return renderer !== null;

	}

	addFrame( frame ) {

		// Limit to max frames.

		if ( this.frames.length >= this.maxFrames ) {

			const removedFrame = this.frames.shift();
			delete this.framesLib[ removedFrame.frameId ];

		}

		this.frames.push( frame );
		this.framesLib[ frame.frameId ] = frame;

		if ( this.isAvailable ) {

			this.updateTabs();
			this.resolveTimestamp();

		}

	}

	inspect( node ) {

		if ( this.enabled === false ) return;

		const currentNodes = this.currentNodes;

		if ( currentNodes !== null ) {

			currentNodes.push( node );

		} else {

			warnOnce( 'RendererInspector: Unable to inspect node outside of frame scope. Use "renderer.setAnimationLoop()".' );

		}

	}

	beginCompute( uid, computeNode ) {

		const frame = this.getFrame();

		if ( ! frame ) return;

		const currentCompute = new ComputeStats( uid, computeNode );
		currentCompute.timestamp = performance.now();
		currentCompute.parent = this.getParent();

		frame.computes.push( currentCompute );
		currentCompute.parent.children.push( currentCompute );

		this.currentCompute = currentCompute;
		this.currentContext = currentCompute;

	}

	finishCompute() {

		const frame = this.getFrame();

		if ( ! frame ) return;

		const currentCompute = this.currentCompute;
		currentCompute.cpu = performance.now() - currentCompute.timestamp;

		this.currentContext = currentCompute.parent;
		this.currentCompute = currentCompute.parent && currentCompute.parent.isComputeStats ? currentCompute.parent : null;

	}

	beginRender( uid, scene, camera, renderTarget ) {

		const frame = this.getFrame();

		if ( ! frame ) return;

		const currentRender = new RenderStats( uid, scene, camera, renderTarget );
		currentRender.timestamp = performance.now();
		currentRender.parent = this.getParent();

		frame.renders.push( currentRender );
		currentRender.parent.children.push( currentRender );

		this.currentRender = currentRender;
		this.currentContext = currentRender;

	}

	finishRender() {

		const frame = this.getFrame();

		if ( ! frame ) return;

		const currentRender = this.currentRender;
		currentRender.cpu = performance.now() - currentRender.timestamp;

		this.currentContext = currentRender.parent;
		this.currentRender = currentRender.parent && currentRender.parent.isRenderStats ? currentRender.parent : null;

	}

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

	}

}