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