Skip to content

Commit b03e036

Browse files
authored
SVGRenderer: Add near/far plane clipping support. (#32210)
1 parent 000d4fb commit b03e036

File tree

2 files changed

+245
-33
lines changed

2 files changed

+245
-33
lines changed

examples/jsm/renderers/Projector.js

Lines changed: 243 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class Projector {
139139
_face, _faceCount, _facePoolLength = 0,
140140
_line, _lineCount, _linePoolLength = 0,
141141
_sprite, _spriteCount, _spritePoolLength = 0,
142-
_modelMatrix;
142+
_modelMatrix, _clipInput = [], _clipOutput = [];
143143

144144
const
145145

@@ -159,8 +159,20 @@ class Projector {
159159

160160
_frustum = new Frustum(),
161161

162-
_objectPool = [], _vertexPool = [], _facePool = [], _linePool = [], _spritePool = [];
162+
_objectPool = [], _vertexPool = [], _facePool = [], _linePool = [], _spritePool = [],
163163

164+
_clipVertexPool = [],
165+
_clipPos1 = new Vector4(),
166+
_clipPos2 = new Vector4(),
167+
_clipPos3 = new Vector4(),
168+
_screenVertexPool = [],
169+
_clipInputVertices = [ null, null, null ],
170+
171+
_clipPlanes = [
172+
{ sign: + 1 },
173+
{ sign: - 1 }
174+
];
175+
164176
//
165177

166178
function RenderList() {
@@ -298,48 +310,165 @@ class Projector {
298310
const v2 = _vertexPool[ b ];
299311
const v3 = _vertexPool[ c ];
300312

301-
if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return;
313+
// Get homogeneous clip space positions (before perspective divide)
314+
_clipPos1.copy( v1.positionWorld ).applyMatrix4( _viewProjectionMatrix );
315+
_clipPos2.copy( v2.positionWorld ).applyMatrix4( _viewProjectionMatrix );
316+
_clipPos3.copy( v3.positionWorld ).applyMatrix4( _viewProjectionMatrix );
317+
318+
// Check if triangle needs clipping
319+
const nearDist1 = _clipPos1.z + _clipPos1.w;
320+
const nearDist2 = _clipPos2.z + _clipPos2.w;
321+
const nearDist3 = _clipPos3.z + _clipPos3.w;
322+
const farDist1 = - _clipPos1.z + _clipPos1.w;
323+
const farDist2 = - _clipPos2.z + _clipPos2.w;
324+
const farDist3 = - _clipPos3.z + _clipPos3.w;
325+
326+
// Check if completely outside
327+
if ( ( nearDist1 < 0 && nearDist2 < 0 && nearDist3 < 0 ) ||
328+
( farDist1 < 0 && farDist2 < 0 && farDist3 < 0 ) ) {
329+
330+
return; // Triangle completely clipped
331+
332+
}
333+
334+
// Check if completely inside (no clipping needed)
335+
if ( nearDist1 >= 0 && nearDist2 >= 0 && nearDist3 >= 0 &&
336+
farDist1 >= 0 && farDist2 >= 0 && farDist3 >= 0 ) {
337+
338+
// No clipping needed - use original path
339+
if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return;
340+
341+
if ( material.side === DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) {
342+
343+
_face = getNextFaceInPool();
302344

303-
if ( material.side === DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) {
345+
_face.id = object.id;
346+
_face.v1.copy( v1 );
347+
_face.v2.copy( v2 );
348+
_face.v3.copy( v3 );
349+
_face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
350+
_face.renderOrder = object.renderOrder;
351+
352+
// face normal
353+
_vector3.subVectors( v3.position, v2.position );
354+
_vector4.subVectors( v1.position, v2.position );
355+
_vector3.cross( _vector4 );
356+
_face.normalModel.copy( _vector3 );
357+
_face.normalModel.applyMatrix3( normalMatrix ).normalize();
304358

305-
_face = getNextFaceInPool();
359+
for ( let i = 0; i < 3; i ++ ) {
306360

307-
_face.id = object.id;
308-
_face.v1.copy( v1 );
309-
_face.v2.copy( v2 );
310-
_face.v3.copy( v3 );
311-
_face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
312-
_face.renderOrder = object.renderOrder;
361+
const normal = _face.vertexNormalsModel[ i ];
362+
normal.fromArray( normals, arguments[ i ] * 3 );
363+
normal.applyMatrix3( normalMatrix ).normalize();
313364

314-
// face normal
315-
_vector3.subVectors( v3.position, v2.position );
316-
_vector4.subVectors( v1.position, v2.position );
317-
_vector3.cross( _vector4 );
318-
_face.normalModel.copy( _vector3 );
319-
_face.normalModel.applyMatrix3( normalMatrix ).normalize();
365+
const uv = _face.uvs[ i ];
366+
uv.fromArray( uvs, arguments[ i ] * 2 );
320367

321-
for ( let i = 0; i < 3; i ++ ) {
368+
}
369+
370+
_face.vertexNormalsLength = 3;
371+
372+
_face.material = material;
373+
374+
if ( material.vertexColors ) {
375+
376+
_face.color.fromArray( colors, a * 3 );
322377

323-
const normal = _face.vertexNormalsModel[ i ];
324-
normal.fromArray( normals, arguments[ i ] * 3 );
325-
normal.applyMatrix3( normalMatrix ).normalize();
378+
}
326379

327-
const uv = _face.uvs[ i ];
328-
uv.fromArray( uvs, arguments[ i ] * 2 );
380+
_renderData.elements.push( _face );
329381

330382
}
331383

332-
_face.vertexNormalsLength = 3;
384+
return;
385+
386+
}
387+
388+
// Triangle needs clipping
389+
_clipInputVertices[ 0 ] = _clipPos1;
390+
_clipInputVertices[ 1 ] = _clipPos2;
391+
_clipInputVertices[ 2 ] = _clipPos3;
392+
const clippedCount = clipTriangle( _clipInputVertices );
393+
394+
if ( clippedCount < 3 ) return; // Triangle completely clipped
333395

334-
_face.material = material;
396+
// Perform perspective divide on clipped vertices and create screen vertices
397+
for ( let i = 0; i < clippedCount; i ++ ) {
335398

336-
if ( material.vertexColors ) {
399+
const cv = _clipInput[ i ];
400+
401+
// Get or create renderable vertex from pool
402+
let sv = _screenVertexPool[ i ];
403+
if ( ! sv ) {
337404

338-
_face.color.fromArray( colors, a * 3 );
405+
sv = new RenderableVertex();
406+
_screenVertexPool[ i ] = sv;
339407

340408
}
341409

342-
_renderData.elements.push( _face );
410+
// Perform perspective divide
411+
const invW = 1 / cv.w;
412+
sv.positionScreen.set( cv.x * invW, cv.y * invW, cv.z * invW, 1 );
413+
414+
// Interpolate world position (simplified - using weighted average based on barycentric-like coords)
415+
// For a proper implementation, we'd need to track interpolation weights
416+
sv.positionWorld.copy( v1.positionWorld );
417+
418+
sv.visible = true;
419+
420+
}
421+
422+
// Triangulate the clipped polygon (simple fan triangulation)
423+
for ( let i = 1; i < clippedCount - 1; i ++ ) {
424+
425+
const tv1 = _screenVertexPool[ 0 ];
426+
const tv2 = _screenVertexPool[ i ];
427+
const tv3 = _screenVertexPool[ i + 1 ];
428+
429+
if ( material.side === DoubleSide || checkBackfaceCulling( tv1, tv2, tv3 ) === true ) {
430+
431+
_face = getNextFaceInPool();
432+
433+
_face.id = object.id;
434+
_face.v1.copy( tv1 );
435+
_face.v2.copy( tv2 );
436+
_face.v3.copy( tv3 );
437+
_face.z = ( tv1.positionScreen.z + tv2.positionScreen.z + tv3.positionScreen.z ) / 3;
438+
_face.renderOrder = object.renderOrder;
439+
440+
// face normal - use original triangle's normal
441+
_vector3.subVectors( v3.position, v2.position );
442+
_vector4.subVectors( v1.position, v2.position );
443+
_vector3.cross( _vector4 );
444+
_face.normalModel.copy( _vector3 );
445+
_face.normalModel.applyMatrix3( normalMatrix ).normalize();
446+
447+
// Use original vertex normals and UVs (simplified - proper impl would interpolate)
448+
for ( let j = 0; j < 3; j ++ ) {
449+
450+
const normal = _face.vertexNormalsModel[ j ];
451+
normal.fromArray( normals, arguments[ j ] * 3 );
452+
normal.applyMatrix3( normalMatrix ).normalize();
453+
454+
const uv = _face.uvs[ j ];
455+
uv.fromArray( uvs, arguments[ j ] * 2 );
456+
457+
}
458+
459+
_face.vertexNormalsLength = 3;
460+
461+
_face.material = material;
462+
463+
if ( material.vertexColors ) {
464+
465+
_face.color.fromArray( colors, a * 3 );
466+
467+
}
468+
469+
_renderData.elements.push( _face );
470+
471+
}
343472

344473
}
345474

@@ -858,6 +987,92 @@ class Projector {
858987

859988
}
860989

990+
// Sutherland-Hodgman triangle clipping in homogeneous clip space
991+
// Returns count of vertices in clipped polygon (0 if completely clipped, 3+ if partially clipped)
992+
// Result vertices are in _clipInput array
993+
function clipTriangle( vertices ) {
994+
995+
// Initialize input with the three input vertices
996+
_clipInput[ 0 ] = vertices[ 0 ];
997+
_clipInput[ 1 ] = vertices[ 1 ];
998+
_clipInput[ 2 ] = vertices[ 2 ];
999+
1000+
let inputCount = 3;
1001+
let outputCount = 0;
1002+
1003+
for ( let p = 0; p < _clipPlanes.length; p ++ ) {
1004+
1005+
const plane = _clipPlanes[ p ];
1006+
outputCount = 0;
1007+
1008+
if ( inputCount === 0 ) break;
1009+
1010+
for ( let i = 0; i < inputCount; i ++ ) {
1011+
1012+
const v1 = _clipInput[ i ];
1013+
const v2 = _clipInput[ ( i + 1 ) % inputCount ];
1014+
1015+
const d1 = plane.sign * v1.z + v1.w;
1016+
const d2 = plane.sign * v2.z + v2.w;
1017+
1018+
const v1Inside = d1 >= 0;
1019+
const v2Inside = d2 >= 0;
1020+
1021+
if ( v1Inside && v2Inside ) {
1022+
1023+
// Both inside - add v1
1024+
_clipOutput[ outputCount ++ ] = v1;
1025+
1026+
} else if ( v1Inside && ! v2Inside ) {
1027+
1028+
// v1 inside, v2 outside - add v1 and intersection
1029+
_clipOutput[ outputCount ++ ] = v1;
1030+
1031+
const t = d1 / ( d1 - d2 );
1032+
let intersection = _clipVertexPool[ outputCount ];
1033+
if ( ! intersection ) {
1034+
1035+
intersection = new Vector4();
1036+
_clipVertexPool[ outputCount ] = intersection;
1037+
1038+
}
1039+
1040+
intersection.lerpVectors( v1, v2, t );
1041+
_clipOutput[ outputCount ++ ] = intersection;
1042+
1043+
} else if ( ! v1Inside && v2Inside ) {
1044+
1045+
// v1 outside, v2 inside - add intersection only
1046+
const t = d1 / ( d1 - d2 );
1047+
let intersection = _clipVertexPool[ outputCount ];
1048+
if ( ! intersection ) {
1049+
1050+
intersection = new Vector4();
1051+
_clipVertexPool[ outputCount ] = intersection;
1052+
1053+
}
1054+
1055+
intersection.lerpVectors( v1, v2, t );
1056+
_clipOutput[ outputCount ++ ] = intersection;
1057+
1058+
}
1059+
1060+
// Both outside - add nothing
1061+
1062+
}
1063+
1064+
// Swap input/output
1065+
const temp = _clipInput;
1066+
_clipInput = _clipOutput;
1067+
_clipOutput = temp;
1068+
inputCount = outputCount;
1069+
1070+
}
1071+
1072+
return inputCount;
1073+
1074+
}
1075+
8611076
function clipLine( s1, s2 ) {
8621077

8631078
let alpha1 = 0, alpha2 = 1;

examples/jsm/renderers/SVGRenderer.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import {
88
SRGBColorSpace,
99
Vector3
1010
} from 'three';
11+
1112
import {
1213
Projector,
1314
RenderableFace,
1415
RenderableLine,
1516
RenderableSprite
16-
} from '../renderers/Projector.js';
17+
} from './Projector.js';
1718

1819
/**
1920
* Can be used to wrap SVG elements into a 3D object.
@@ -367,10 +368,6 @@ class SVGRenderer {
367368

368369
_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;
369370

370-
if ( _v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1 ) continue;
371-
if ( _v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1 ) continue;
372-
if ( _v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1 ) continue;
373-
374371
_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
375372
_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
376373
_v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;

0 commit comments

Comments
 (0)