diff --git a/examples/files.json b/examples/files.json index f1938c7f5fa5c8..d28bc4b2785979 100644 --- a/examples/files.json +++ b/examples/files.json @@ -67,6 +67,7 @@ "webgl_lines_colors", "webgl_lines_dashed", "webgl_lines_fat", + "webgl_lines_fat_raycasting", "webgl_lines_fat_wireframe", "webgl_lines_sphere", "webgl_loader_3dm", diff --git a/examples/jsm/lines/LineMaterial.js b/examples/jsm/lines/LineMaterial.js index 0eccd145626f1a..a4074f8841f84d 100644 --- a/examples/jsm/lines/LineMaterial.js +++ b/examples/jsm/lines/LineMaterial.js @@ -219,7 +219,7 @@ ShaderLib[ 'line' ] = { vec4 clip = projectionMatrix * worldPos; // shift the depth of the projected points so the line - // segements overlap neatly + // segments overlap neatly vec3 clipPose = ( position.y < 0.5 ) ? ndcStart : ndcEnd; clip.z = clipPose.z * clip.w; diff --git a/examples/jsm/lines/LineSegments2.js b/examples/jsm/lines/LineSegments2.js index 0a9d00dc1a52a3..137745c7986352 100644 --- a/examples/jsm/lines/LineSegments2.js +++ b/examples/jsm/lines/LineSegments2.js @@ -29,17 +29,19 @@ const _box = new Box3(); const _sphere = new Sphere(); const _clipToWorldVector = new Vector4(); +let _ray, _instanceStart, _instanceEnd, _lineWidth; + // Returns the margin required to expand by in world space given the distance from the camera, // line width, resolution, and camera projection -function getWorldSpaceHalfWidth( camera, distance, lineWidth, resolution ) { +function getWorldSpaceHalfWidth( camera, distance, resolution ) { // transform into clip space, adjust the x and y values by the pixel width offset, then // transform back into world space to get world offset. Note clip space is [-1, 1] so full // width does not need to be halved. _clipToWorldVector.set( 0, 0, - distance, 1.0 ).applyMatrix4( camera.projectionMatrix ); _clipToWorldVector.multiplyScalar( 1.0 / _clipToWorldVector.w ); - _clipToWorldVector.x = lineWidth / resolution.width; - _clipToWorldVector.y = lineWidth / resolution.height; + _clipToWorldVector.x = _lineWidth / resolution.width; + _clipToWorldVector.y = _lineWidth / resolution.height; _clipToWorldVector.applyMatrix4( camera.projectionMatrixInverse ); _clipToWorldVector.multiplyScalar( 1.0 / _clipToWorldVector.w ); @@ -47,6 +49,170 @@ function getWorldSpaceHalfWidth( camera, distance, lineWidth, resolution ) { } +function raycastWorldUnits( lineSegments, intersects ) { + + for ( let i = 0, l = _instanceStart.count; i < l; i ++ ) { + + _line.start.fromBufferAttribute( _instanceStart, i ); + _line.end.fromBufferAttribute( _instanceEnd, i ); + + const pointOnLine = new Vector3(); + const point = new Vector3(); + + _ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine ); + const isInside = point.distanceTo( pointOnLine ) < _lineWidth * 0.5; + + if ( isInside ) { + + intersects.push( { + point, + pointOnLine, + distance: _ray.origin.distanceTo( point ), + object: lineSegments, + face: null, + faceIndex: i, + uv: null, + uv2: null, + } ); + + } + + } + +} + +function raycastScreenSpace( lineSegments, camera, intersects ) { + + const projectionMatrix = camera.projectionMatrix; + const material = lineSegments.material; + const resolution = material.resolution; + const matrixWorld = lineSegments.matrixWorld; + + const geometry = lineSegments.geometry; + const instanceStart = geometry.attributes.instanceStart; + const instanceEnd = geometry.attributes.instanceEnd; + + const near = - camera.near; + + // + + // pick a point 1 unit out along the ray to avoid the ray origin + // sitting at the camera origin which will cause "w" to be 0 when + // applying the projection matrix. + _ray.at( 1, _ssOrigin ); + + // ndc space [ - 1.0, 1.0 ] + _ssOrigin.w = 1; + _ssOrigin.applyMatrix4( camera.matrixWorldInverse ); + _ssOrigin.applyMatrix4( projectionMatrix ); + _ssOrigin.multiplyScalar( 1 / _ssOrigin.w ); + + // screen space + _ssOrigin.x *= resolution.x / 2; + _ssOrigin.y *= resolution.y / 2; + _ssOrigin.z = 0; + + _ssOrigin3.copy( _ssOrigin ); + + _mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld ); + + for ( let i = 0, l = instanceStart.count; i < l; i ++ ) { + + _start4.fromBufferAttribute( instanceStart, i ); + _end4.fromBufferAttribute( instanceEnd, i ); + + _start4.w = 1; + _end4.w = 1; + + // camera space + _start4.applyMatrix4( _mvMatrix ); + _end4.applyMatrix4( _mvMatrix ); + + // skip the segment if it's entirely behind the camera + const isBehindCameraNear = _start4.z > near && _end4.z > near; + if ( isBehindCameraNear ) { + + continue; + + } + + // trim the segment if it extends behind camera near + if ( _start4.z > near ) { + + const deltaDist = _start4.z - _end4.z; + const t = ( _start4.z - near ) / deltaDist; + _start4.lerp( _end4, t ); + + } else if ( _end4.z > near ) { + + const deltaDist = _end4.z - _start4.z; + const t = ( _end4.z - near ) / deltaDist; + _end4.lerp( _start4, t ); + + } + + // clip space + _start4.applyMatrix4( projectionMatrix ); + _end4.applyMatrix4( projectionMatrix ); + + // ndc space [ - 1.0, 1.0 ] + _start4.multiplyScalar( 1 / _start4.w ); + _end4.multiplyScalar( 1 / _end4.w ); + + // screen space + _start4.x *= resolution.x / 2; + _start4.y *= resolution.y / 2; + + _end4.x *= resolution.x / 2; + _end4.y *= resolution.y / 2; + + // create 2d segment + _line.start.copy( _start4 ); + _line.start.z = 0; + + _line.end.copy( _end4 ); + _line.end.z = 0; + + // get closest point on ray to segment + const param = _line.closestPointToPointParameter( _ssOrigin3, true ); + _line.at( param, _closestPoint ); + + // check if the intersection point is within clip space + const zPos = MathUtils.lerp( _start4.z, _end4.z, param ); + const isInClipSpace = zPos >= - 1 && zPos <= 1; + + const isInside = _ssOrigin3.distanceTo( _closestPoint ) < _lineWidth * 0.5; + + if ( isInClipSpace && isInside ) { + + _line.start.fromBufferAttribute( instanceStart, i ); + _line.end.fromBufferAttribute( instanceEnd, i ); + + _line.start.applyMatrix4( matrixWorld ); + _line.end.applyMatrix4( matrixWorld ); + + const pointOnLine = new Vector3(); + const point = new Vector3(); + + _ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine ); + + intersects.push( { + point: point, + pointOnLine: pointOnLine, + distance: _ray.origin.distanceTo( point ), + object: lineSegments, + face: null, + faceIndex: i, + uv: null, + uv2: null, + } ); + + } + + } + +} + class LineSegments2 extends Mesh { constructor( geometry = new LineSegmentsGeometry(), material = new LineMaterial( { color: Math.random() * 0xffffff } ) ) { @@ -57,7 +223,7 @@ class LineSegments2 extends Mesh { } - // for backwards-compatability, but could be a method of LineSegmentsGeometry... + // for backwards-compatibility, but could be a method of LineSegmentsGeometry... computeLineDistances() { @@ -88,31 +254,27 @@ class LineSegments2 extends Mesh { raycast( raycaster, intersects ) { - if ( raycaster.camera === null ) { + const worldUnits = this.material.worldUnits; + const camera = raycaster.camera; + + if ( camera === null && ! worldUnits ) { - console.error( 'LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2.' ); + console.error( 'LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2 while worldUnits is set to false.' ); } const threshold = ( raycaster.params.Line2 !== undefined ) ? raycaster.params.Line2.threshold || 0 : 0; - const ray = raycaster.ray; - const camera = raycaster.camera; - const projectionMatrix = camera.projectionMatrix; + _ray = raycaster.ray; const matrixWorld = this.matrixWorld; const geometry = this.geometry; const material = this.material; - const resolution = material.resolution; - const lineWidth = material.linewidth + threshold; - const instanceStart = geometry.attributes.instanceStart; - const instanceEnd = geometry.attributes.instanceEnd; + _lineWidth = material.linewidth + threshold; - // camera forward is negative - const near = - camera.near; - - // + _instanceStart = geometry.attributes.instanceStart; + _instanceEnd = geometry.attributes.instanceEnd; // check if we intersect the sphere bounds if ( geometry.boundingSphere === null ) { @@ -122,162 +284,65 @@ class LineSegments2 extends Mesh { } _sphere.copy( geometry.boundingSphere ).applyMatrix4( matrixWorld ); - const distanceToSphere = Math.max( camera.near, _sphere.distanceToPoint( ray.origin ) ); // increase the sphere bounds by the worst case line screen space width - const sphereMargin = getWorldSpaceHalfWidth( camera, distanceToSphere, lineWidth, resolution ); - _sphere.radius += sphereMargin; + let sphereMargin; + if ( worldUnits ) { - if ( raycaster.ray.intersectsSphere( _sphere ) === false ) { + sphereMargin = _lineWidth * 0.5; - return; + } else { - } - - // - - // check if we intersect the box bounds - if ( geometry.boundingBox === null ) { - - geometry.computeBoundingBox(); + const distanceToSphere = Math.max( camera.near, _sphere.distanceToPoint( _ray.origin ) ); + sphereMargin = getWorldSpaceHalfWidth( camera, distanceToSphere, material.resolution ); } - _box.copy( geometry.boundingBox ).applyMatrix4( matrixWorld ); - const distanceToBox = Math.max( camera.near, _box.distanceToPoint( ray.origin ) ); - - // increase the box bounds by the worst case line screen space width - const boxMargin = getWorldSpaceHalfWidth( camera, distanceToBox, lineWidth, resolution ); - _box.max.x += boxMargin; - _box.max.y += boxMargin; - _box.max.z += boxMargin; - _box.min.x -= boxMargin; - _box.min.y -= boxMargin; - _box.min.z -= boxMargin; + _sphere.radius += sphereMargin; - if ( raycaster.ray.intersectsBox( _box ) === false ) { + if ( _ray.intersectsSphere( _sphere ) === false ) { return; } - // - - // pick a point 1 unit out along the ray to avoid the ray origin - // sitting at the camera origin which will cause "w" to be 0 when - // applying the projection matrix. - ray.at( 1, _ssOrigin ); - - // ndc space [ - 1.0, 1.0 ] - _ssOrigin.w = 1; - _ssOrigin.applyMatrix4( camera.matrixWorldInverse ); - _ssOrigin.applyMatrix4( projectionMatrix ); - _ssOrigin.multiplyScalar( 1 / _ssOrigin.w ); - - // screen space - _ssOrigin.x *= resolution.x / 2; - _ssOrigin.y *= resolution.y / 2; - _ssOrigin.z = 0; - - _ssOrigin3.copy( _ssOrigin ); - - _mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld ); - - for ( let i = 0, l = instanceStart.count; i < l; i ++ ) { - - _start4.fromBufferAttribute( instanceStart, i ); - _end4.fromBufferAttribute( instanceEnd, i ); - - _start4.w = 1; - _end4.w = 1; - - // camera space - _start4.applyMatrix4( _mvMatrix ); - _end4.applyMatrix4( _mvMatrix ); - - // skip the segment if it's entirely behind the camera - const isBehindCameraNear = _start4.z > near && _end4.z > near; - if ( isBehindCameraNear ) { - - continue; - - } - - // trim the segment if it extends behind camera near - if ( _start4.z > near ) { - - const deltaDist = _start4.z - _end4.z; - const t = ( _start4.z - near ) / deltaDist; - _start4.lerp( _end4, t ); - - } else if ( _end4.z > near ) { - - const deltaDist = _end4.z - _start4.z; - const t = ( _end4.z - near ) / deltaDist; - _end4.lerp( _start4, t ); - - } - - // clip space - _start4.applyMatrix4( projectionMatrix ); - _end4.applyMatrix4( projectionMatrix ); - - // ndc space [ - 1.0, 1.0 ] - _start4.multiplyScalar( 1 / _start4.w ); - _end4.multiplyScalar( 1 / _end4.w ); - - // screen space - _start4.x *= resolution.x / 2; - _start4.y *= resolution.y / 2; + // check if we intersect the box bounds + if ( geometry.boundingBox === null ) { - _end4.x *= resolution.x / 2; - _end4.y *= resolution.y / 2; + geometry.computeBoundingBox(); - // create 2d segment - _line.start.copy( _start4 ); - _line.start.z = 0; + } - _line.end.copy( _end4 ); - _line.end.z = 0; + _box.copy( geometry.boundingBox ).applyMatrix4( matrixWorld ); - // get closest point on ray to segment - const param = _line.closestPointToPointParameter( _ssOrigin3, true ); - _line.at( param, _closestPoint ); + // increase the box bounds by the worst case line width + let boxMargin; + if ( worldUnits ) { - // check if the intersection point is within clip space - const zPos = MathUtils.lerp( _start4.z, _end4.z, param ); - const isInClipSpace = zPos >= - 1 && zPos <= 1; + boxMargin = _lineWidth * 0.5; - const isInside = _ssOrigin3.distanceTo( _closestPoint ) < lineWidth * 0.5; + } else { - if ( isInClipSpace && isInside ) { + const distanceToBox = Math.max( camera.near, _box.distanceToPoint( _ray.origin ) ); + boxMargin = getWorldSpaceHalfWidth( camera, distanceToBox, material.resolution ); - _line.start.fromBufferAttribute( instanceStart, i ); - _line.end.fromBufferAttribute( instanceEnd, i ); + } - _line.start.applyMatrix4( matrixWorld ); - _line.end.applyMatrix4( matrixWorld ); + _box.expandByScalar( boxMargin ); - const pointOnLine = new Vector3(); - const point = new Vector3(); + if ( _ray.intersectsBox( _box ) === false ) { - ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine ); + return; - intersects.push( { + } - point: point, - pointOnLine: pointOnLine, - distance: ray.origin.distanceTo( point ), + if ( worldUnits ) { - object: this, - face: null, - faceIndex: i, - uv: null, - uv2: null, + raycastWorldUnits( this, intersects ); - } ); + } else { - } + raycastScreenSpace( this, camera, intersects ); } diff --git a/examples/jsm/lines/Wireframe.js b/examples/jsm/lines/Wireframe.js index a9d90c955db9f6..91f82188be5f6d 100644 --- a/examples/jsm/lines/Wireframe.js +++ b/examples/jsm/lines/Wireframe.js @@ -20,7 +20,7 @@ class Wireframe extends Mesh { } - // for backwards-compatability, but could be a method of LineSegmentsGeometry... + // for backwards-compatibility, but could be a method of LineSegmentsGeometry... computeLineDistances() { diff --git a/examples/screenshots/webgl_lines_fat_raycasting.jpg b/examples/screenshots/webgl_lines_fat_raycasting.jpg new file mode 100644 index 00000000000000..341b30b87edfc4 Binary files /dev/null and b/examples/screenshots/webgl_lines_fat_raycasting.jpg differ diff --git a/examples/tags.json b/examples/tags.json index e6b1dbb7dfca34..6180be65917e9a 100644 --- a/examples/tags.json +++ b/examples/tags.json @@ -29,6 +29,7 @@ "webgl_lights_hemisphere": [ "directional" ], "webgl_lights_pointlights": [ "multiple" ], "webgl_lines_fat": [ "gpu", "stats", "panel" ], + "webgl_lines_fat_raycasting": [ "gpu", "stats", "panel", "raycast" ], "webgl_loader_ttf": [ "text", "font" ], "webgl_loader_pdb": [ "molecules", "css2d" ], "webgl_loader_ldraw": [ "lego" ], diff --git a/examples/webgl_lines_fat_raycasting.html b/examples/webgl_lines_fat_raycasting.html new file mode 100644 index 00000000000000..f923378abdffd6 --- /dev/null +++ b/examples/webgl_lines_fat_raycasting.html @@ -0,0 +1,380 @@ + + +
+