diff --git a/examples/jsm/renderers/Projector.js b/examples/jsm/renderers/Projector.js index 2d3882984412fc..b86258288f186b 100644 --- a/examples/jsm/renderers/Projector.js +++ b/examples/jsm/renderers/Projector.js @@ -139,7 +139,7 @@ class Projector { _face, _faceCount, _facePoolLength = 0, _line, _lineCount, _linePoolLength = 0, _sprite, _spriteCount, _spritePoolLength = 0, - _modelMatrix; + _modelMatrix, _clipInput = [], _clipOutput = []; const @@ -159,8 +159,20 @@ class Projector { _frustum = new Frustum(), - _objectPool = [], _vertexPool = [], _facePool = [], _linePool = [], _spritePool = []; + _objectPool = [], _vertexPool = [], _facePool = [], _linePool = [], _spritePool = [], + _clipVertexPool = [], + _clipPos1 = new Vector4(), + _clipPos2 = new Vector4(), + _clipPos3 = new Vector4(), + _screenVertexPool = [], + _clipInputVertices = [ null, null, null ], + + _clipPlanes = [ + { sign: + 1 }, + { sign: - 1 } + ]; + // function RenderList() { @@ -298,48 +310,165 @@ class Projector { const v2 = _vertexPool[ b ]; const v3 = _vertexPool[ c ]; - if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return; + // Get homogeneous clip space positions (before perspective divide) + _clipPos1.copy( v1.positionWorld ).applyMatrix4( _viewProjectionMatrix ); + _clipPos2.copy( v2.positionWorld ).applyMatrix4( _viewProjectionMatrix ); + _clipPos3.copy( v3.positionWorld ).applyMatrix4( _viewProjectionMatrix ); + + // Check if triangle needs clipping + const nearDist1 = _clipPos1.z + _clipPos1.w; + const nearDist2 = _clipPos2.z + _clipPos2.w; + const nearDist3 = _clipPos3.z + _clipPos3.w; + const farDist1 = - _clipPos1.z + _clipPos1.w; + const farDist2 = - _clipPos2.z + _clipPos2.w; + const farDist3 = - _clipPos3.z + _clipPos3.w; + + // Check if completely outside + if ( ( nearDist1 < 0 && nearDist2 < 0 && nearDist3 < 0 ) || + ( farDist1 < 0 && farDist2 < 0 && farDist3 < 0 ) ) { + + return; // Triangle completely clipped + + } + + // Check if completely inside (no clipping needed) + if ( nearDist1 >= 0 && nearDist2 >= 0 && nearDist3 >= 0 && + farDist1 >= 0 && farDist2 >= 0 && farDist3 >= 0 ) { + + // No clipping needed - use original path + if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return; + + if ( material.side === DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) { + + _face = getNextFaceInPool(); - if ( material.side === DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) { + _face.id = object.id; + _face.v1.copy( v1 ); + _face.v2.copy( v2 ); + _face.v3.copy( v3 ); + _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3; + _face.renderOrder = object.renderOrder; + + // face normal + _vector3.subVectors( v3.position, v2.position ); + _vector4.subVectors( v1.position, v2.position ); + _vector3.cross( _vector4 ); + _face.normalModel.copy( _vector3 ); + _face.normalModel.applyMatrix3( normalMatrix ).normalize(); - _face = getNextFaceInPool(); + for ( let i = 0; i < 3; i ++ ) { - _face.id = object.id; - _face.v1.copy( v1 ); - _face.v2.copy( v2 ); - _face.v3.copy( v3 ); - _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3; - _face.renderOrder = object.renderOrder; + const normal = _face.vertexNormalsModel[ i ]; + normal.fromArray( normals, arguments[ i ] * 3 ); + normal.applyMatrix3( normalMatrix ).normalize(); - // face normal - _vector3.subVectors( v3.position, v2.position ); - _vector4.subVectors( v1.position, v2.position ); - _vector3.cross( _vector4 ); - _face.normalModel.copy( _vector3 ); - _face.normalModel.applyMatrix3( normalMatrix ).normalize(); + const uv = _face.uvs[ i ]; + uv.fromArray( uvs, arguments[ i ] * 2 ); - for ( let i = 0; i < 3; i ++ ) { + } + + _face.vertexNormalsLength = 3; + + _face.material = material; + + if ( material.vertexColors ) { + + _face.color.fromArray( colors, a * 3 ); - const normal = _face.vertexNormalsModel[ i ]; - normal.fromArray( normals, arguments[ i ] * 3 ); - normal.applyMatrix3( normalMatrix ).normalize(); + } - const uv = _face.uvs[ i ]; - uv.fromArray( uvs, arguments[ i ] * 2 ); + _renderData.elements.push( _face ); } - _face.vertexNormalsLength = 3; + return; + + } + + // Triangle needs clipping + _clipInputVertices[ 0 ] = _clipPos1; + _clipInputVertices[ 1 ] = _clipPos2; + _clipInputVertices[ 2 ] = _clipPos3; + const clippedCount = clipTriangle( _clipInputVertices ); + + if ( clippedCount < 3 ) return; // Triangle completely clipped - _face.material = material; + // Perform perspective divide on clipped vertices and create screen vertices + for ( let i = 0; i < clippedCount; i ++ ) { - if ( material.vertexColors ) { + const cv = _clipInput[ i ]; + + // Get or create renderable vertex from pool + let sv = _screenVertexPool[ i ]; + if ( ! sv ) { - _face.color.fromArray( colors, a * 3 ); + sv = new RenderableVertex(); + _screenVertexPool[ i ] = sv; } - _renderData.elements.push( _face ); + // Perform perspective divide + const invW = 1 / cv.w; + sv.positionScreen.set( cv.x * invW, cv.y * invW, cv.z * invW, 1 ); + + // Interpolate world position (simplified - using weighted average based on barycentric-like coords) + // For a proper implementation, we'd need to track interpolation weights + sv.positionWorld.copy( v1.positionWorld ); + + sv.visible = true; + + } + + // Triangulate the clipped polygon (simple fan triangulation) + for ( let i = 1; i < clippedCount - 1; i ++ ) { + + const tv1 = _screenVertexPool[ 0 ]; + const tv2 = _screenVertexPool[ i ]; + const tv3 = _screenVertexPool[ i + 1 ]; + + if ( material.side === DoubleSide || checkBackfaceCulling( tv1, tv2, tv3 ) === true ) { + + _face = getNextFaceInPool(); + + _face.id = object.id; + _face.v1.copy( tv1 ); + _face.v2.copy( tv2 ); + _face.v3.copy( tv3 ); + _face.z = ( tv1.positionScreen.z + tv2.positionScreen.z + tv3.positionScreen.z ) / 3; + _face.renderOrder = object.renderOrder; + + // face normal - use original triangle's normal + _vector3.subVectors( v3.position, v2.position ); + _vector4.subVectors( v1.position, v2.position ); + _vector3.cross( _vector4 ); + _face.normalModel.copy( _vector3 ); + _face.normalModel.applyMatrix3( normalMatrix ).normalize(); + + // Use original vertex normals and UVs (simplified - proper impl would interpolate) + for ( let j = 0; j < 3; j ++ ) { + + const normal = _face.vertexNormalsModel[ j ]; + normal.fromArray( normals, arguments[ j ] * 3 ); + normal.applyMatrix3( normalMatrix ).normalize(); + + const uv = _face.uvs[ j ]; + uv.fromArray( uvs, arguments[ j ] * 2 ); + + } + + _face.vertexNormalsLength = 3; + + _face.material = material; + + if ( material.vertexColors ) { + + _face.color.fromArray( colors, a * 3 ); + + } + + _renderData.elements.push( _face ); + + } } @@ -858,6 +987,92 @@ class Projector { } + // Sutherland-Hodgman triangle clipping in homogeneous clip space + // Returns count of vertices in clipped polygon (0 if completely clipped, 3+ if partially clipped) + // Result vertices are in _clipInput array + function clipTriangle( vertices ) { + + // Initialize input with the three input vertices + _clipInput[ 0 ] = vertices[ 0 ]; + _clipInput[ 1 ] = vertices[ 1 ]; + _clipInput[ 2 ] = vertices[ 2 ]; + + let inputCount = 3; + let outputCount = 0; + + for ( let p = 0; p < _clipPlanes.length; p ++ ) { + + const plane = _clipPlanes[ p ]; + outputCount = 0; + + if ( inputCount === 0 ) break; + + for ( let i = 0; i < inputCount; i ++ ) { + + const v1 = _clipInput[ i ]; + const v2 = _clipInput[ ( i + 1 ) % inputCount ]; + + const d1 = plane.sign * v1.z + v1.w; + const d2 = plane.sign * v2.z + v2.w; + + const v1Inside = d1 >= 0; + const v2Inside = d2 >= 0; + + if ( v1Inside && v2Inside ) { + + // Both inside - add v1 + _clipOutput[ outputCount ++ ] = v1; + + } else if ( v1Inside && ! v2Inside ) { + + // v1 inside, v2 outside - add v1 and intersection + _clipOutput[ outputCount ++ ] = v1; + + const t = d1 / ( d1 - d2 ); + let intersection = _clipVertexPool[ outputCount ]; + if ( ! intersection ) { + + intersection = new Vector4(); + _clipVertexPool[ outputCount ] = intersection; + + } + + intersection.lerpVectors( v1, v2, t ); + _clipOutput[ outputCount ++ ] = intersection; + + } else if ( ! v1Inside && v2Inside ) { + + // v1 outside, v2 inside - add intersection only + const t = d1 / ( d1 - d2 ); + let intersection = _clipVertexPool[ outputCount ]; + if ( ! intersection ) { + + intersection = new Vector4(); + _clipVertexPool[ outputCount ] = intersection; + + } + + intersection.lerpVectors( v1, v2, t ); + _clipOutput[ outputCount ++ ] = intersection; + + } + + // Both outside - add nothing + + } + + // Swap input/output + const temp = _clipInput; + _clipInput = _clipOutput; + _clipOutput = temp; + inputCount = outputCount; + + } + + return inputCount; + + } + function clipLine( s1, s2 ) { let alpha1 = 0, alpha2 = 1; diff --git a/examples/jsm/renderers/SVGRenderer.js b/examples/jsm/renderers/SVGRenderer.js index 44e12f581334c2..9c58defed7c9b3 100644 --- a/examples/jsm/renderers/SVGRenderer.js +++ b/examples/jsm/renderers/SVGRenderer.js @@ -8,12 +8,13 @@ import { SRGBColorSpace, Vector3 } from 'three'; + import { Projector, RenderableFace, RenderableLine, RenderableSprite -} from '../renderers/Projector.js'; +} from './Projector.js'; /** * Can be used to wrap SVG elements into a 3D object. @@ -367,10 +368,6 @@ class SVGRenderer { _v1 = element.v1; _v2 = element.v2; _v3 = element.v3; - if ( _v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1 ) continue; - if ( _v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1 ) continue; - if ( _v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1 ) continue; - _v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf; _v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf; _v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;