diff --git a/examples/jsm/helpers/LightProbeHelper.js b/examples/jsm/helpers/LightProbeHelper.js
index e6da80faa45945..1f9e375a11d370 100644
--- a/examples/jsm/helpers/LightProbeHelper.js
+++ b/examples/jsm/helpers/LightProbeHelper.js
@@ -58,11 +58,12 @@ class LightProbeHelper extends Mesh {
#define RECIPROCAL_PI 0.318309886
- vec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {
- // matrix is assumed to be orthogonal
+ vec3 transformNormalByInverseViewMatrix( in vec3 normal, in mat4 viewMatrix ) {
- return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );
+ // upper-left 3x3 of view matrix is assumed to be orthogonal
+
+ return normalize( ( vec4( normal, 0.0 ) * viewMatrix ).xyz );
}
@@ -101,7 +102,7 @@ class LightProbeHelper extends Mesh {
vec3 normal = normalize( vNormal );
- vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
+ vec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );
vec3 irradiance = shGetIrradianceAt( worldNormal, sh );
diff --git a/examples/jsm/loaders/SVGLoader.js b/examples/jsm/loaders/SVGLoader.js
index 4bae8dee0d0b14..756efffb1fd162 100644
--- a/examples/jsm/loaders/SVGLoader.js
+++ b/examples/jsm/loaders/SVGLoader.js
@@ -2008,442 +2008,206 @@ class SVGLoader extends Loader {
*/
static createShapes( shapePath ) {
- const BIGNUMBER = 999999999;
-
- const IntersectionLocationType = {
- ORIGIN: 0,
- DESTINATION: 1,
- BETWEEN: 2,
- LEFT: 3,
- RIGHT: 4,
- BEHIND: 5,
- BEYOND: 6
- };
-
- const classifyResult = {
- loc: IntersectionLocationType.ORIGIN,
- t: 0
- };
-
- function findEdgeIntersection( a0, a1, b0, b1 ) {
-
- const x1 = a0.x;
- const x2 = a1.x;
- const x3 = b0.x;
- const x4 = b1.x;
- const y1 = a0.y;
- const y2 = a1.y;
- const y3 = b0.y;
- const y4 = b1.y;
- const nom1 = ( x4 - x3 ) * ( y1 - y3 ) - ( y4 - y3 ) * ( x1 - x3 );
- const nom2 = ( x2 - x1 ) * ( y1 - y3 ) - ( y2 - y1 ) * ( x1 - x3 );
- const denom = ( y4 - y3 ) * ( x2 - x1 ) - ( x4 - x3 ) * ( y2 - y1 );
- const t1 = nom1 / denom;
- const t2 = nom2 / denom;
-
- if ( ( ( denom === 0 ) && ( nom1 !== 0 ) ) || ( t1 <= 0 ) || ( t1 >= 1 ) || ( t2 < 0 ) || ( t2 > 1 ) ) {
-
- //1. lines are parallel or edges don't intersect
-
- return null;
-
- } else if ( ( nom1 === 0 ) && ( denom === 0 ) ) {
-
- //2. lines are colinear
-
- //check if endpoints of edge2 (b0-b1) lies on edge1 (a0-a1)
- for ( let i = 0; i < 2; i ++ ) {
-
- classifyPoint( i === 0 ? b0 : b1, a0, a1 );
- //find position of this endpoints relatively to edge1
- if ( classifyResult.loc == IntersectionLocationType.ORIGIN ) {
-
- const point = ( i === 0 ? b0 : b1 );
- return { x: point.x, y: point.y, t: classifyResult.t };
-
- } else if ( classifyResult.loc == IntersectionLocationType.BETWEEN ) {
-
- const x = + ( ( x1 + classifyResult.t * ( x2 - x1 ) ).toPrecision( 10 ) );
- const y = + ( ( y1 + classifyResult.t * ( y2 - y1 ) ).toPrecision( 10 ) );
- return { x: x, y: y, t: classifyResult.t, };
-
- }
-
- }
-
- return null;
+ // Point-in-polygon test using the even-odd ray-casting rule. Valid for
+ // simple (non self-intersecting) polygons.
+ function pointInPolygon( p, polygon ) {
- } else {
-
- //3. edges intersect
-
- for ( let i = 0; i < 2; i ++ ) {
+ let inside = false;
+ const n = polygon.length;
- classifyPoint( i === 0 ? b0 : b1, a0, a1 );
+ for ( let i = 0, j = n - 1; i < n; j = i ++ ) {
- if ( classifyResult.loc == IntersectionLocationType.ORIGIN ) {
+ const a = polygon[ i ];
+ const b = polygon[ j ];
- const point = ( i === 0 ? b0 : b1 );
- return { x: point.x, y: point.y, t: classifyResult.t };
+ if ( ( a.y > p.y ) !== ( b.y > p.y ) &&
+ p.x < ( b.x - a.x ) * ( p.y - a.y ) / ( b.y - a.y ) + a.x ) {
- }
+ inside = ! inside;
}
- const x = + ( ( x1 + t1 * ( x2 - x1 ) ).toPrecision( 10 ) );
- const y = + ( ( y1 + t1 * ( y2 - y1 ) ).toPrecision( 10 ) );
- return { x: x, y: y, t: t1 };
-
- }
-
- }
-
- function classifyPoint( p, edgeStart, edgeEnd ) {
-
- const ax = edgeEnd.x - edgeStart.x;
- const ay = edgeEnd.y - edgeStart.y;
- const bx = p.x - edgeStart.x;
- const by = p.y - edgeStart.y;
- const sa = ax * by - bx * ay;
-
- if ( ( p.x === edgeStart.x ) && ( p.y === edgeStart.y ) ) {
-
- classifyResult.loc = IntersectionLocationType.ORIGIN;
- classifyResult.t = 0;
- return;
-
}
- if ( ( p.x === edgeEnd.x ) && ( p.y === edgeEnd.y ) ) {
+ return inside;
- classifyResult.loc = IntersectionLocationType.DESTINATION;
- classifyResult.t = 1;
- return;
-
- }
-
- if ( sa < - Number.EPSILON ) {
-
- classifyResult.loc = IntersectionLocationType.LEFT;
- return;
-
- }
+ }
- if ( sa > Number.EPSILON ) {
+ // Returns a point guaranteed to be strictly inside the given simple
+ // polygon. First tries the bounding-box center; if that falls outside
+ // the polygon, casts a horizontal ray at the center's y and picks the
+ // midpoint between the first two sorted intercepts.
+ //
+ // Port of paper.js' Path#getInteriorPoint()
+ // https://github.com/paperjs/paper.js/blob/develop/src/path/PathItem.Boolean.js
+ function getInteriorPoint( polygon, boundingBox ) {
- classifyResult.loc = IntersectionLocationType.RIGHT;
- return;
+ const point = boundingBox.getCenter( new Vector2() );
+ if ( pointInPolygon( point, polygon ) ) return point;
- }
+ const y = point.y;
+ const intercepts = [];
+ const n = polygon.length;
- if ( ( ( ax * bx ) < 0 ) || ( ( ay * by ) < 0 ) ) {
+ for ( let i = 0; i < n; i ++ ) {
- classifyResult.loc = IntersectionLocationType.BEHIND;
- return;
+ const a = polygon[ i ];
+ const b = polygon[ ( i + 1 ) % n ];
- }
+ // Half-open crossing rule — counts each vertex exactly once and
+ // skips horizontal edges.
+ if ( ( a.y > y ) !== ( b.y > y ) ) {
- if ( ( Math.sqrt( ax * ax + ay * ay ) ) < ( Math.sqrt( bx * bx + by * by ) ) ) {
+ const x = a.x + ( y - a.y ) * ( b.x - a.x ) / ( b.y - a.y );
+ intercepts.push( x );
- classifyResult.loc = IntersectionLocationType.BEYOND;
- return;
+ }
}
- let t;
-
- if ( ax !== 0 ) {
-
- t = bx / ax;
-
- } else {
+ if ( intercepts.length > 1 ) {
- t = by / ay;
+ intercepts.sort( ( a, b ) => a - b );
+ point.x = ( intercepts[ 0 ] + intercepts[ 1 ] ) / 2;
}
- classifyResult.loc = IntersectionLocationType.BETWEEN;
- classifyResult.t = t;
+ return point;
}
- function getIntersections( path1, path2 ) {
-
- const intersectionsRaw = [];
- const intersections = [];
-
- for ( let index = 1; index < path1.length; index ++ ) {
-
- const path1EdgeStart = path1[ index - 1 ];
- const path1EdgeEnd = path1[ index ];
-
- for ( let index2 = 1; index2 < path2.length; index2 ++ ) {
-
- const path2EdgeStart = path2[ index2 - 1 ];
- const path2EdgeEnd = path2[ index2 ];
+ // Resolve fill-rule. SVG defaults to 'nonzero'.
+ let fillRule = ( shapePath.userData && shapePath.userData.style && shapePath.userData.style.fillRule ) || 'nonzero';
- const intersection = findEdgeIntersection( path1EdgeStart, path1EdgeEnd, path2EdgeStart, path2EdgeEnd );
+ if ( fillRule !== 'nonzero' && fillRule !== 'evenodd' ) {
- if ( intersection !== null && intersectionsRaw.find( i => i.t <= intersection.t + Number.EPSILON && i.t >= intersection.t - Number.EPSILON ) === undefined ) {
-
- intersectionsRaw.push( intersection );
- intersections.push( new Vector2( intersection.x, intersection.y ) );
-
- }
-
- }
-
- }
-
- return intersections;
+ console.warn( 'THREE.SVGLoader: fill-rule "' + fillRule + '" is not supported, falling back to "nonzero".' );
+ fillRule = 'nonzero';
}
- function getScanlineIntersections( scanline, boundingBox, paths ) {
-
- const center = new Vector2();
- boundingBox.getCenter( center );
-
- const allIntersections = [];
-
- paths.forEach( path => {
-
- // check if the center of the bounding box is in the bounding box of the paths.
- // this is a pruning method to limit the search of intersections in paths that can't envelop of the current path.
- // if a path envelops another path. The center of that other path, has to be inside the bounding box of the enveloping path.
- if ( path.boundingBox.containsPoint( center ) ) {
-
- const intersections = getIntersections( scanline, path.points );
-
- intersections.forEach( p => {
-
- allIntersections.push( { identifier: path.identifier, isCW: path.isCW, point: p } );
-
- } );
-
- }
-
- } );
-
- allIntersections.sort( ( i1, i2 ) => {
-
- return i1.point.x - i2.point.x;
-
+ // Predicate that decides whether a winding number falls inside the fill
+ // region, per the SVG fill-rule spec. Works for negative windings too,
+ // because JavaScript's bitwise AND preserves odd/even under two's
+ // complement.
+ const isInside = fillRule === 'nonzero'
+ ? ( w => w !== 0 )
+ : ( w => ( w & 1 ) !== 0 );
+
+ // Build an entry per usable subpath. Self-winding follows the standard
+ // convention used by ShapeUtils: counter-clockwise (signed area > 0)
+ // contributes +1 to the winding number at an interior point,
+ // clockwise contributes -1.
+ const entries = [];
+
+ for ( const subPath of shapePath.subPaths ) {
+
+ const points = subPath.getPoints();
+ if ( points.length < 3 ) continue;
+
+ const area = ShapeUtils.area( points );
+ if ( area === 0 ) continue;
+
+ const boundingBox = new Box2();
+ for ( let i = 0; i < points.length; i ++ ) boundingBox.expandByPoint( points[ i ] );
+
+ entries.push( {
+ subPath: subPath,
+ points: points,
+ boundingBox: boundingBox,
+ interiorPoint: getInteriorPoint( points, boundingBox ),
+ absArea: Math.abs( area ),
+ winding: area < 0 ? - 1 : 1,
+ container: null,
+ exclude: false,
+ role: null
} );
- return allIntersections;
-
}
- function isHoleTo( simplePath, allPaths, scanlineMinX, scanlineMaxX, _fillRule ) {
-
- if ( _fillRule === null || _fillRule === undefined || _fillRule === '' ) {
-
- _fillRule = 'nonzero';
+ // Sort by area descending. This guarantees that any subpath that could
+ // contain `entries[i]` is located at a smaller index and has already
+ // been processed when it's entries[i]'s turn. Port of paper.js'
+ // reorientPaths() algorithm.
+ entries.sort( ( a, b ) => b.absArea - a.absArea );
- }
-
- const centerBoundingBox = new Vector2();
- simplePath.boundingBox.getCenter( centerBoundingBox );
-
- const scanline = [ new Vector2( scanlineMinX, centerBoundingBox.y ), new Vector2( scanlineMaxX, centerBoundingBox.y ) ];
-
- const scanlineIntersections = getScanlineIntersections( scanline, simplePath.boundingBox, allPaths );
-
- scanlineIntersections.sort( ( i1, i2 ) => {
-
- return i1.point.x - i2.point.x;
-
- } );
-
- const baseIntersections = [];
- const otherIntersections = [];
-
- scanlineIntersections.forEach( i => {
-
- if ( i.identifier === simplePath.identifier ) {
-
- baseIntersections.push( i );
-
- } else {
-
- otherIntersections.push( i );
-
- }
-
- } );
-
- const firstXOfPath = baseIntersections[ 0 ].point.x;
-
- // build up the path hierarchy
- const stack = [];
- let i = 0;
-
- while ( i < otherIntersections.length && otherIntersections[ i ].point.x < firstXOfPath ) {
-
- if ( stack.length > 0 && stack[ stack.length - 1 ] === otherIntersections[ i ].identifier ) {
-
- stack.pop();
+ // Walk already-processed entries from closest-in-size to largest,
+ // stopping at the innermost container. Accumulate the container's
+ // cumulative winding into this entry's winding so that the final value
+ // equals the winding number at this entry's interior point.
+ //
+ // A subpath only contributes to the fill boundary when crossing it
+ // actually flips the "insideness" per the fill rule; otherwise it's a
+ // redundant overlap and gets excluded to avoid double-counting.
+ for ( let i = 0; i < entries.length; i ++ ) {
- } else {
+ const entry = entries[ i ];
+ let containerWinding = 0;
- stack.push( otherIntersections[ i ].identifier );
+ for ( let j = i - 1; j >= 0; j -- ) {
- }
+ const candidate = entries[ j ];
+ if ( ! candidate.boundingBox.containsPoint( entry.interiorPoint ) ) continue;
+ if ( ! pointInPolygon( entry.interiorPoint, candidate.points ) ) continue;
- i ++;
+ entry.container = candidate.exclude ? candidate.container : candidate;
+ containerWinding = candidate.winding;
+ entry.winding += containerWinding;
+ break;
}
- stack.push( simplePath.identifier );
-
- if ( _fillRule === 'evenodd' ) {
-
- const isHole = stack.length % 2 === 0 ? true : false;
- const isHoleFor = stack[ stack.length - 2 ];
-
- return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor };
-
- } else if ( _fillRule === 'nonzero' ) {
-
- // check if path is a hole by counting the amount of paths with alternating rotations it has to cross.
- let isHole = true;
- let isHoleFor = null;
- let lastCWValue = null;
-
- for ( let i = 0; i < stack.length; i ++ ) {
-
- const identifier = stack[ i ];
- if ( isHole ) {
-
- lastCWValue = allPaths[ identifier ].isCW;
- isHole = false;
- isHoleFor = identifier;
-
- } else if ( lastCWValue !== allPaths[ identifier ].isCW ) {
-
- lastCWValue = allPaths[ identifier ].isCW;
- isHole = true;
-
- }
-
- }
-
- return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor };
-
- } else {
+ if ( isInside( entry.winding ) === isInside( containerWinding ) ) {
- console.warn( 'fill-rule: "' + _fillRule + '" is currently not implemented.' );
+ entry.exclude = true;
}
}
- // check for self intersecting paths
- // TODO
-
- // check intersecting paths
- // TODO
-
- // prepare paths for hole detection
- let scanlineMinX = BIGNUMBER;
- let scanlineMaxX = - BIGNUMBER;
-
- let simplePaths = shapePath.subPaths.map( p => {
-
- const points = p.getPoints();
- let maxY = - BIGNUMBER;
- let minY = BIGNUMBER;
- let maxX = - BIGNUMBER;
- let minX = BIGNUMBER;
-
- //points.forEach(p => p.y *= -1);
-
- for ( let i = 0; i < points.length; i ++ ) {
-
- const p = points[ i ];
-
- if ( p.y > maxY ) {
-
- maxY = p.y;
-
- }
-
- if ( p.y < minY ) {
-
- minY = p.y;
-
- }
-
- if ( p.x > maxX ) {
+ // Classify retained entries. An entry is an outer shape if it has no
+ // container or if its container is itself a hole (a solid nested inside
+ // a hole becomes a new top-level shape); otherwise it's a hole in its
+ // container. Entries were already sorted outermost-first, so each
+ // container's role is known by the time we look at it.
+ for ( const entry of entries ) {
- maxX = p.x;
-
- }
-
- if ( p.x < minX ) {
-
- minX = p.x;
-
- }
-
- }
-
- //
- if ( scanlineMaxX <= maxX ) {
-
- scanlineMaxX = maxX + 1;
-
- }
-
- if ( scanlineMinX >= minX ) {
-
- scanlineMinX = minX - 1;
-
- }
-
- return { curves: p.curves, points: points, isCW: ShapeUtils.isClockWise( points ), identifier: - 1, boundingBox: new Box2( new Vector2( minX, minY ), new Vector2( maxX, maxY ) ) };
-
- } );
-
- simplePaths = simplePaths.filter( sp => sp.points.length > 1 );
-
- for ( let identifier = 0; identifier < simplePaths.length; identifier ++ ) {
-
- simplePaths[ identifier ].identifier = identifier;
+ if ( entry.exclude ) continue;
+ entry.role = ( entry.container === null || entry.container.role === 'hole' ) ? 'outer' : 'hole';
}
- // check if path is solid or a hole
- const isAHole = simplePaths.map( p => isHoleTo( p, simplePaths, scanlineMinX, scanlineMaxX, ( shapePath.userData ? shapePath.userData.style.fillRule : undefined ) ) );
+ // Build Shapes for outers first, then attach holes to their container's
+ // Shape.
+ const shapes = [];
+ const shapeByEntry = new Map();
+ for ( const entry of entries ) {
- const shapesToReturn = [];
- simplePaths.forEach( p => {
+ if ( entry.exclude || entry.role !== 'outer' ) continue;
- const amIAHole = isAHole[ p.identifier ];
+ const shape = new Shape();
+ shape.curves = entry.subPath.curves;
+ shapes.push( shape );
+ shapeByEntry.set( entry, shape );
- if ( ! amIAHole.isHole ) {
+ }
- const shape = new Shape();
- shape.curves = p.curves;
- const holes = isAHole.filter( h => h.isHole && h.for === p.identifier );
- holes.forEach( h => {
+ for ( const entry of entries ) {
- const hole = simplePaths[ h.identifier ];
- const path = new Path();
- path.curves = hole.curves;
- shape.holes.push( path );
+ if ( entry.exclude || entry.role !== 'hole' ) continue;
- } );
- shapesToReturn.push( shape );
+ const shape = shapeByEntry.get( entry.container );
+ if ( ! shape ) continue;
- }
+ const hole = new Path();
+ hole.curves = entry.subPath.curves;
+ shape.holes.push( hole );
- } );
+ }
- return shapesToReturn;
+ return shapes;
}
diff --git a/examples/models/svg/emoji.svg b/examples/models/svg/emoji.svg
new file mode 100644
index 00000000000000..5dd464e1b87a24
--- /dev/null
+++ b/examples/models/svg/emoji.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/webgl_loader_svg.html b/examples/webgl_loader_svg.html
index 187ae68689274c..34d96963246c93 100644
--- a/examples/webgl_loader_svg.html
+++ b/examples/webgl_loader_svg.html
@@ -120,6 +120,7 @@
'singlePointTest2': 'models/svg/singlePointTest2.svg',
'singlePointTest3': 'models/svg/singlePointTest3.svg',
'emptyPath': 'models/svg/emptyPath.svg',
+ 'emoji': 'models/svg/emoji.svg',
} ).name( 'SVG File' ).onChange( update );
diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js
index a75f1fd4f9f0cb..8f43867fec0d29 100644
--- a/src/nodes/display/PassNode.js
+++ b/src/nodes/display/PassNode.js
@@ -251,14 +251,21 @@ class PassNode extends TempNode {
*/
this._height = 1;
- const depthTexture = new DepthTexture();
- depthTexture.isRenderTargetTexture = true;
- //depthTexture.type = FloatType;
- depthTexture.name = 'depth';
-
const renderTarget = new RenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType, ...options, } );
renderTarget.texture.name = 'output';
- renderTarget.depthTexture = depthTexture;
+
+ let depthTexture = null;
+
+ if ( this.scope === PassNode.DEPTH || options.depthBuffer !== false ) {
+
+ depthTexture = new DepthTexture();
+ depthTexture.isRenderTargetTexture = true;
+ //depthTexture.type = FloatType;
+ depthTexture.name = 'depth';
+
+ renderTarget.depthTexture = depthTexture;
+
+ }
/**
* The pass's render target.
@@ -310,13 +317,18 @@ class PassNode extends TempNode {
* A dictionary holding the internal result textures.
*
* @private
- * @type {Object}
+ * @type {{ output: Texture, depth?: DepthTexture }}
*/
this._textures = {
- output: renderTarget.texture,
- depth: depthTexture
+ output: renderTarget.texture
};
+ if ( depthTexture !== null ) {
+
+ this._textures.depth = depthTexture;
+
+ }
+
/**
* A dictionary holding the internal texture nodes.
*
@@ -567,6 +579,12 @@ class PassNode extends TempNode {
if ( texture === undefined ) {
+ if ( name === 'depth' ) {
+
+ throw new Error( 'THREE.PassNode: Depth texture is not available for this pass.' );
+
+ }
+
const refTexture = this.renderTarget.texture;
texture = refTexture.clone();
@@ -757,7 +775,7 @@ class PassNode extends TempNode {
this.renderTarget.texture.type = renderer.getOutputBufferType();
- if ( renderer.reversedDepthBuffer === true ) {
+ if ( renderer.reversedDepthBuffer === true && this.renderTarget.depthTexture !== null ) {
this.renderTarget.depthTexture.type = FloatType;
diff --git a/src/renderers/shaders/ShaderChunk/common.glsl.js b/src/renderers/shaders/ShaderChunk/common.glsl.js
index 8c452c97bb87c9..771d504a8ef5e3 100644
--- a/src/renderers/shaders/ShaderChunk/common.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/common.glsl.js
@@ -64,12 +64,21 @@ vec3 transformDirection( in vec3 dir, in mat4 matrix ) {
}
-vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {
+#define inverseTransformDirection transformNormalByInverseViewMatrix // keeping this for backwards compatibility
- // dir can be either a direction vector or a normal vector
- // upper-left 3x3 of matrix is assumed to be orthogonal
+vec3 transformNormalByInverseViewMatrix( in vec3 normal, in mat4 viewMatrix ) {
- return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );
+ // upper-left 3x3 of view matrix is assumed to be orthogonal
+
+ return normalize( ( vec4( normal, 0.0 ) * viewMatrix ).xyz );
+
+}
+
+vec3 transformDirectionByInverseViewMatrix( in vec3 dir, in mat4 viewMatrix ) {
+
+ // upper-left 3x3 of view matrix is assumed to be orthogonal
+
+ return normalize( ( vec4( dir, 0.0 ) * viewMatrix ).xyz );
}
diff --git a/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js
index fd753723ac337f..d06111180167fe 100644
--- a/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js
@@ -16,7 +16,7 @@ export default /* glsl */`
}
// Transforming Normal Vectors with the Inverse Transformation
- vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
+ vec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );
#ifdef ENVMAP_MODE_REFLECTION
diff --git a/src/renderers/shaders/ShaderChunk/envmap_physical_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/envmap_physical_pars_fragment.glsl.js
index 1fb80e4d41c98a..39d62f0f50bfeb 100644
--- a/src/renderers/shaders/ShaderChunk/envmap_physical_pars_fragment.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/envmap_physical_pars_fragment.glsl.js
@@ -5,7 +5,7 @@ export default /* glsl */`
#ifdef ENVMAP_TYPE_CUBE_UV
- vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
+ vec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );
vec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 );
@@ -28,7 +28,7 @@ export default /* glsl */`
// Mixing the reflection with the normal is more accurate and keeps rough objects from gathering light from behind their tangent plane.
reflectVec = normalize( mix( reflectVec, normal, pow4( roughness ) ) );
- reflectVec = inverseTransformDirection( reflectVec, viewMatrix );
+ reflectVec = transformDirectionByInverseViewMatrix( reflectVec, viewMatrix );
vec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness );
diff --git a/src/renderers/shaders/ShaderChunk/envmap_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/envmap_vertex.glsl.js
index 537c94f1bbde6e..14a9df4d8b2f45 100644
--- a/src/renderers/shaders/ShaderChunk/envmap_vertex.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/envmap_vertex.glsl.js
@@ -19,7 +19,7 @@ export default /* glsl */`
}
- vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );
+ vec3 worldNormal = transformNormalByInverseViewMatrix( transformedNormal, viewMatrix );
#ifdef ENVMAP_MODE_REFLECTION
diff --git a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
index e691f32387d381..a335fc2809df13 100644
--- a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
@@ -37,7 +37,7 @@ vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {
vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {
- vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
+ vec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );
vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
index 081e4812d80807..b0d1a1556d103d 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
@@ -6,7 +6,7 @@ export default /* glsl */`
// Offsetting the position used for querying occlusion along the world normal can be used to reduce shadow acne.
- vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );
+ vec3 shadowWorldNormal = transformNormalByInverseViewMatrix( transformedNormal, viewMatrix );
#else
diff --git a/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js
index 1bbf86c20a3adb..e77c9f8c757743 100644
--- a/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js
@@ -21,7 +21,7 @@ export default /* glsl */`
vec3 pos = vWorldPosition;
vec3 v = normalize( cameraPosition - pos );
- vec3 n = inverseTransformDirection( normal, viewMatrix );
+ vec3 n = transformNormalByInverseViewMatrix( normal, viewMatrix );
vec4 transmitted = getIBLVolumeRefraction(
n, v, material.roughness, material.diffuseContribution, material.specularColorBlended, material.specularF90,
diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js
index 5523b4f040d849..1c0388fc3250ac 100644
--- a/test/e2e/puppeteer.js
+++ b/test/e2e/puppeteer.js
@@ -39,6 +39,7 @@ const exceptionList = [
'webgpu_storage_buffer',
'webgpu_tsl_editor',
'webxr_vr_video',
+ 'webgpu_tsl_transpiler',
// Need more time to render
'css3d_mixed',