diff --git a/examples/jsm/misc/RollerCoaster.js b/examples/jsm/misc/RollerCoaster.js index a1c82cf4d9205f..228f8a1e58685a 100644 --- a/examples/jsm/misc/RollerCoaster.js +++ b/examples/jsm/misc/RollerCoaster.js @@ -195,6 +195,10 @@ class RollerCoasterGeometry extends BufferGeometry { const offset = new Vector3(); + const sample1 = new Vector3(); + const sample2 = new Vector3(); + const rollQuaternion = new Quaternion(); + for ( let i = 1; i <= divisions; i ++ ) { point.copy( curve.getPointAt( i / divisions ) ); @@ -209,6 +213,20 @@ class RollerCoasterGeometry extends BufferGeometry { quaternion.setFromAxisAngle( up, angle ); + // banking + + const bankDelta = 0.01; + const t = i / divisions; + + sample1.copy( curve.getTangentAt( ( ( t - bankDelta ) % 1 + 1 ) % 1 ) ); + sample2.copy( curve.getTangentAt( ( t + bankDelta ) % 1 ) ); + + let headingChange = Math.atan2( sample2.x, sample2.z ) - Math.atan2( sample1.x, sample1.z ); + if ( headingChange > Math.PI ) headingChange -= Math.PI * 2; + if ( headingChange < -Math.PI ) headingChange += Math.PI * 2; + + quaternion.premultiply( rollQuaternion.setFromAxisAngle( forward, - Math.atan( headingChange * 8 ) * 0.5 ) ); + if ( i % 2 === 0 ) { drawShape( step, color2 ); @@ -356,6 +374,11 @@ class RollerCoasterLiftersGeometry extends BufferGeometry { const fromPoint = new Vector3(); const toPoint = new Vector3(); + const sample1 = new Vector3(); + const sample2 = new Vector3(); + const bankedQuaternion = new Quaternion(); + const rollQuaternion = new Quaternion(); + for ( let i = 1; i <= divisions; i ++ ) { point.copy( curve.getPointAt( i / divisions ) ); @@ -365,6 +388,22 @@ class RollerCoasterLiftersGeometry extends BufferGeometry { quaternion.setFromAxisAngle( up, angle ); + // banking + + const bankDelta = 0.01; + const t = i / divisions; + + sample1.copy( curve.getTangentAt( ( ( t - bankDelta ) % 1 + 1 ) % 1 ) ); + sample2.copy( curve.getTangentAt( ( t + bankDelta ) % 1 ) ); + + let headingChange = Math.atan2( sample2.x, sample2.z ) - Math.atan2( sample1.x, sample1.z ); + if ( headingChange > Math.PI ) headingChange -= Math.PI * 2; + if ( headingChange < -Math.PI ) headingChange += Math.PI * 2; + + bankedQuaternion.copy( quaternion ); + rollQuaternion.setFromAxisAngle( tangent, - Math.atan( headingChange * 8 ) * 0.5 ); + bankedQuaternion.premultiply( rollQuaternion ); + // if ( point.y > 10 ) { @@ -402,12 +441,11 @@ class RollerCoasterLiftersGeometry extends BufferGeometry { } else { fromPoint.set( 0, - 0.2, 0 ); - fromPoint.applyQuaternion( quaternion ); + fromPoint.applyQuaternion( bankedQuaternion ); fromPoint.add( point ); - toPoint.set( 0, - point.y, 0 ); - toPoint.applyQuaternion( quaternion ); - toPoint.add( point ); + toPoint.copy( fromPoint ); + toPoint.y = 0; extrudeShape( tube3, fromPoint, toPoint ); diff --git a/examples/screenshots/webgpu_xr_rollercoaster.jpg b/examples/screenshots/webgpu_xr_rollercoaster.jpg index 02dbb2a2af89fa..bcd96ae05ee7e4 100644 Binary files a/examples/screenshots/webgpu_xr_rollercoaster.jpg and b/examples/screenshots/webgpu_xr_rollercoaster.jpg differ diff --git a/examples/screenshots/webxr_vr_rollercoaster.jpg b/examples/screenshots/webxr_vr_rollercoaster.jpg index 60ee28ddebe5f5..8726b12eda437e 100644 Binary files a/examples/screenshots/webxr_vr_rollercoaster.jpg and b/examples/screenshots/webxr_vr_rollercoaster.jpg differ diff --git a/examples/textures/memorial.png b/examples/textures/memorial.png deleted file mode 100644 index e2e0358487c8c4..00000000000000 Binary files a/examples/textures/memorial.png and /dev/null differ diff --git a/examples/webgpu_xr_rollercoaster.html b/examples/webgpu_xr_rollercoaster.html index 0d4ce8910ba317..b428643a5f5820 100644 --- a/examples/webgpu_xr_rollercoaster.html +++ b/examples/webgpu_xr_rollercoaster.html @@ -204,6 +204,10 @@ const lookAt = new THREE.Vector3(); + const tangent1 = new THREE.Vector3(); + const tangent2 = new THREE.Vector3(); + const bankQuaternion = new THREE.Quaternion(); + let velocity = 0; let progress = 0; @@ -235,6 +239,23 @@ velocity -= tangent.y * 0.0000001 * delta; velocity = Math.max( 0.00004, Math.min( 0.0002, velocity ) ); + // banking + + const bankDelta = 0.01; + const t1 = ( ( progress - bankDelta ) % 1 + 1 ) % 1; + const t2 = ( progress + bankDelta ) % 1; + + tangent1.copy( curve.getTangentAt( t1 ) ); + tangent2.copy( curve.getTangentAt( t2 ) ); + + let headingChange = Math.atan2( tangent2.x, tangent2.z ) - Math.atan2( tangent1.x, tangent1.z ); + if ( headingChange > Math.PI ) headingChange -= Math.PI * 2; + if ( headingChange < -Math.PI ) headingChange += Math.PI * 2; + + train.up.set( 0, 1, 0 ); + bankQuaternion.setFromAxisAngle( tangent, - Math.atan( headingChange * 8 ) * 0.5 ); + train.up.applyQuaternion( bankQuaternion ); + train.lookAt( lookAt.copy( position ).sub( tangent ) ); // diff --git a/examples/webxr_vr_rollercoaster.html b/examples/webxr_vr_rollercoaster.html index e65dcc89d63872..eca16657162674 100644 --- a/examples/webxr_vr_rollercoaster.html +++ b/examples/webxr_vr_rollercoaster.html @@ -203,6 +203,10 @@ const lookAt = new THREE.Vector3(); + const tangent1 = new THREE.Vector3(); + const tangent2 = new THREE.Vector3(); + const bankQuaternion = new THREE.Quaternion(); + let velocity = 0; let progress = 0; @@ -234,6 +238,23 @@ velocity -= tangent.y * 0.0000001 * delta; velocity = Math.max( 0.00004, Math.min( 0.0002, velocity ) ); + // banking + + const bankDelta = 0.01; + const t1 = ( ( progress - bankDelta ) % 1 + 1 ) % 1; + const t2 = ( progress + bankDelta ) % 1; + + tangent1.copy( curve.getTangentAt( t1 ) ); + tangent2.copy( curve.getTangentAt( t2 ) ); + + let headingChange = Math.atan2( tangent2.x, tangent2.z ) - Math.atan2( tangent1.x, tangent1.z ); + if ( headingChange > Math.PI ) headingChange -= Math.PI * 2; + if ( headingChange < -Math.PI ) headingChange += Math.PI * 2; + + train.up.set( 0, 1, 0 ); + bankQuaternion.setFromAxisAngle( tangent, - Math.atan( headingChange * 8 ) * 0.5 ); + train.up.applyQuaternion( bankQuaternion ); + train.lookAt( lookAt.copy( position ).sub( tangent ) ); // diff --git a/src/math/Plane.js b/src/math/Plane.js index c2ba3fd9cf984c..95287da599362e 100644 --- a/src/math/Plane.js +++ b/src/math/Plane.js @@ -210,9 +210,10 @@ class Plane { * * @param {Line3} line - The line to compute the intersection for. * @param {Vector3} target - The target vector that is used to store the method's result. - * @return {?Vector3} The intersection point. + * @param {boolean} [clampToLine=true] - Whether to clamp the intersection to the line segment. + * @return {?Vector3} The intersection point. Returns `null` if no intersection is detected. */ - intersectLine( line, target ) { + intersectLine( line, target, clampToLine = true ) { const direction = line.delta( _vector1 ); @@ -234,7 +235,7 @@ class Plane { const t = - ( line.start.dot( this.normal ) + this.constant ) / denominator; - if ( t < 0 || t > 1 ) { + if ( ( clampToLine === true ) && ( t < 0 || t > 1 ) ) { return null; diff --git a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js index c9971dbaf02a5c..d95761a4f894c1 100644 --- a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +++ b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js @@ -478,11 +478,11 @@ ${ flowData.code } if ( offsetSnippet ) { - snippet = `texelFetchOffset( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), ${ levelSnippet }, ${ offsetSnippet } )`; + snippet = `texelFetchOffset( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), int( ${ levelSnippet } ), ${ offsetSnippet } )`; } else { - snippet = `texelFetch( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), ${ levelSnippet } )`; + snippet = `texelFetch( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), int( ${ levelSnippet } ) )`; } @@ -490,11 +490,11 @@ ${ flowData.code } if ( offsetSnippet ) { - snippet = `texelFetchOffset( ${ textureProperty }, ${ uvIndexSnippet }, ${ levelSnippet }, ${ offsetSnippet } )`; + snippet = `texelFetchOffset( ${ textureProperty }, ${ uvIndexSnippet }, int( ${ levelSnippet } ), ${ offsetSnippet } )`; } else { - snippet = `texelFetch( ${ textureProperty }, ${ uvIndexSnippet }, ${ levelSnippet } )`; + snippet = `texelFetch( ${ textureProperty }, ${ uvIndexSnippet }, int( ${ levelSnippet } ) )`; } diff --git a/test/unit/src/math/Plane.tests.js b/test/unit/src/math/Plane.tests.js index 192bb9224c3469..46ebeb62e7e1d0 100644 --- a/test/unit/src/math/Plane.tests.js +++ b/test/unit/src/math/Plane.tests.js @@ -229,6 +229,17 @@ export default QUnit.module( 'Maths', () => { a.intersectLine( l1, point ); assert.ok( point.equals( new Vector3( 3, 0, 0 ) ), 'Passed!' ); + // plane lies outside the segment's endpoints + a = new Plane( new Vector3( 1, 0, 0 ), - 20 ); + const l2 = new Line3( new Vector3( - 10, 0, 0 ), new Vector3( 10, 0, 0 ) ); + + assert.strictEqual( a.intersectLine( l2, point ), null, 'Default clamps to segment and returns null' ); + assert.strictEqual( a.intersectLine( l2, point, true ), null, 'Explicit clampToLine=true returns null' ); + + const result = a.intersectLine( l2, point, false ); + assert.ok( result === point, 'clampToLine=false returns the target vector' ); + assert.ok( point.equals( new Vector3( 20, 0, 0 ) ), 'clampToLine=false returns infinite-line intersection' ); + } ); QUnit.test( 'intersectsBox', ( assert ) => {