Browse Source

SVGRenderer: Add depth sorting for Sprites and SVGObjects (#32212)

* SVGRenderer: Implemented Sprite and SVGObject sorting.

* Added OrbitControls to svg_sandbox.

* Updated screenshot.
mrdoob 2 months ago
parent
commit
43324e6a8a

+ 25 - 2
examples/jsm/renderers/Projector.js

@@ -577,7 +577,7 @@ class Projector {
 
 			if ( sortObjects === true ) {
 
-				_renderData.objects.sort( painterSort );
+				painterSortStable( _renderData.objects, 0, _renderData.objects.length );
 
 			}
 
@@ -843,7 +843,7 @@ class Projector {
 
 			if ( sortElements === true ) {
 
-				_renderData.elements.sort( painterSort );
+				painterSortStable( _renderData.elements, 0, _renderData.elements.length );
 
 			}
 
@@ -987,6 +987,29 @@ class Projector {
 
 		}
 
+		function painterSortStable( array, start, length ) {
+
+			// A stable insertion sort for sorting render items
+			// This avoids the GC overhead of Array.prototype.sort()
+
+			for ( let i = start + 1; i < start + length; i ++ ) {
+
+				const item = array[ i ];
+				let j = i - 1;
+
+				while ( j >= start && painterSort( array[ j ], item ) > 0 ) {
+
+					array[ j + 1 ] = array[ j ];
+					j --;
+
+				}
+
+				array[ j + 1 ] = item;
+
+			}
+
+		}
+
 		// 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

+ 190 - 54
examples/jsm/renderers/SVGRenderer.js

@@ -88,6 +88,8 @@ class SVGRenderer {
 
 			_svgNode,
 			_pathCount = 0,
+			_svgObjectCount = 0,
+			_renderListCount = 0,
 
 			_precision = null,
 			_quality = 1,
@@ -114,6 +116,8 @@ class SVGRenderer {
 			_viewProjectionMatrix = new Matrix4(),
 
 			_svgPathPool = [],
+			_svgObjectsPool = [],
+			_renderListPool = [],
 
 			_projector = new Projector(),
 			_svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
@@ -276,6 +280,49 @@ class SVGRenderer {
 
 		}
 
+		function renderSort( a, b ) {
+
+			const aOrder = a.data.renderOrder !== undefined ? a.data.renderOrder : 0;
+			const bOrder = b.data.renderOrder !== undefined ? b.data.renderOrder : 0;
+
+			if ( aOrder !== bOrder ) {
+
+				return aOrder - bOrder;
+
+			} else {
+
+				const aZ = a.data.z !== undefined ? a.data.z : 0;
+				const bZ = b.data.z !== undefined ? b.data.z : 0;
+
+				return bZ - aZ; // Painter's algorithm: far to near
+
+			}
+
+		}
+
+		function arraySortStable( array, start, length ) {
+
+			// A stable insertion sort for sorting the render list
+			// This avoids the GC overhead of Array.prototype.sort()
+
+			for ( let i = start + 1; i < start + length; i ++ ) {
+
+				const item = array[ i ];
+				let j = i - 1;
+
+				while ( j >= start && renderSort( array[ j ], item ) > 0 ) {
+
+					array[ j + 1 ] = array[ j ];
+					j --;
+
+				}
+
+				array[ j + 1 ] = item;
+
+			}
+
+		}
+
 		/**
 		 * Performs a manual clear with the defined clear color.
 		 */
@@ -328,10 +375,7 @@ class SVGRenderer {
 
 			calculateLights( _lights );
 
-			 // reset accumulated path
-
-			_currentPath = '';
-			_currentStyle = '';
+			_renderListCount = 0;
 
 			for ( let e = 0, el = _elements.length; e < el; e ++ ) {
 
@@ -340,84 +384,126 @@ class SVGRenderer {
 
 				if ( material === undefined || material.opacity === 0 ) continue;
 
-				_elemBox.makeEmpty();
+				getRenderItem( _renderListCount ++, 'element', element, material );
 
-				if ( element instanceof RenderableSprite ) {
+			}
 
-					_v1 = element;
-					_v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf;
+			_svgObjectCount = 0;
 
-					renderSprite( _v1, element, material );
+			scene.traverseVisible( function ( object ) {
 
-				} else if ( element instanceof RenderableLine ) {
+				if ( object.isSVGObject ) {
 
-					_v1 = element.v1; _v2 = element.v2;
+					_vector3.setFromMatrixPosition( object.matrixWorld );
+					_vector3.applyMatrix4( _viewProjectionMatrix );
 
-					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
-					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
+					if ( _vector3.z < - 1 || _vector3.z > 1 ) return;
 
-					_elemBox.setFromPoints( [ _v1.positionScreen, _v2.positionScreen ] );
+					const x = _vector3.x * _svgWidthHalf;
+					const y = - _vector3.y * _svgHeightHalf;
 
-					if ( _clipBox.intersectsBox( _elemBox ) === true ) {
+					const svgObject = getSVGObjectData( _svgObjectCount ++ );
 
-						renderLine( _v1, _v2, material );
+					svgObject.node = object.node;
+					svgObject.x = x;
+					svgObject.y = y;
+					svgObject.z = _vector3.z;
+					svgObject.renderOrder = object.renderOrder;
 
-					}
+					getRenderItem( _renderListCount ++, 'svgObject', svgObject, null );
 
-				} else if ( element instanceof RenderableFace ) {
+				}
 
-					_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;
+			} );
 
-					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
-					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
-					_v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;
+			if ( this.sortElements ) {
 
-					if ( this.overdraw > 0 ) {
+				arraySortStable( _renderListPool, 0, _renderListCount );
 
-						expand( _v1.positionScreen, _v2.positionScreen, this.overdraw );
-						expand( _v2.positionScreen, _v3.positionScreen, this.overdraw );
-						expand( _v3.positionScreen, _v1.positionScreen, this.overdraw );
+			}
 
-					}
+			// Reset accumulated path
+			_currentPath = '';
+			_currentStyle = '';
 
-					_elemBox.setFromPoints( [
-						_v1.positionScreen,
-						_v2.positionScreen,
-						_v3.positionScreen
-					] );
+			// Render in sorted order
+			for ( let i = 0; i < _renderListCount; i ++ ) {
 
-					if ( _clipBox.intersectsBox( _elemBox ) === true ) {
+				const item = _renderListPool[ i ];
 
-						renderFace3( _v1, _v2, _v3, element, material );
+				if ( item.type === 'svgObject' ) {
 
-					}
+					flushPath(); // Flush any accumulated paths before inserting SVG node
 
-				}
+					const svgObject = item.data;
+					const node = svgObject.node;
+					node.setAttribute( 'transform', 'translate(' + svgObject.x + ',' + svgObject.y + ')' );
+					_svg.appendChild( node );
 
-			}
+				} else {
 
-			flushPath(); // just to flush last svg:path
+					const element = item.data;
+					const material = item.material;
 
-			scene.traverseVisible( function ( object ) {
+					_elemBox.makeEmpty();
 
-				 if ( object.isSVGObject ) {
+					if ( element instanceof RenderableSprite ) {
 
-					_vector3.setFromMatrixPosition( object.matrixWorld );
-					_vector3.applyMatrix4( _viewProjectionMatrix );
+						_v1 = element;
+						_v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf;
 
-					if ( _vector3.z < - 1 || _vector3.z > 1 ) return;
+						renderSprite( _v1, element, material );
 
-					const x = _vector3.x * _svgWidthHalf;
-					const y = - _vector3.y * _svgHeightHalf;
+					} else if ( element instanceof RenderableLine ) {
 
-					const node = object.node;
-					node.setAttribute( 'transform', 'translate(' + x + ',' + y + ')' );
+						_v1 = element.v1; _v2 = element.v2;
 
-					_svg.appendChild( node );
+						_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
+						_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
+
+						_elemBox.setFromPoints( [ _v1.positionScreen, _v2.positionScreen ] );
+
+						if ( _clipBox.intersectsBox( _elemBox ) === true ) {
+
+							renderLine( _v1, _v2, material );
+
+						}
+
+					} else if ( element instanceof RenderableFace ) {
+
+						_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;
+
+						_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
+						_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
+						_v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;
+
+						if ( this.overdraw > 0 ) {
+
+							expand( _v1.positionScreen, _v2.positionScreen, this.overdraw );
+							expand( _v2.positionScreen, _v3.positionScreen, this.overdraw );
+							expand( _v3.positionScreen, _v1.positionScreen, this.overdraw );
+
+						}
+
+						_elemBox.setFromPoints( [
+							_v1.positionScreen,
+							_v2.positionScreen,
+							_v3.positionScreen
+						] );
+
+						if ( _clipBox.intersectsBox( _elemBox ) === true ) {
+
+							renderFace3( _v1, _v2, _v3, element, material );
+
+						}
+
+					}
 
 				}
 
-			} );
+			}
+
+			flushPath(); // Flush any remaining paths
 
 		};
 
@@ -657,21 +743,71 @@ class SVGRenderer {
 
 		function getPathNode( id ) {
 
-			if ( _svgPathPool[ id ] == null ) {
+			let path = _svgPathPool[ id ];
 
-				_svgPathPool[ id ] = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
+			if ( path === undefined ) {
+
+				path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
 
 				if ( _quality == 0 ) {
 
-					_svgPathPool[ id ].setAttribute( 'shape-rendering', 'crispEdges' ); //optimizeSpeed
+					path.setAttribute( 'shape-rendering', 'crispEdges' ); //optimizeSpeed
 
 				}
 
-				return _svgPathPool[ id ];
+				_svgPathPool[ id ] = path;
+
+			}
+
+			return path;
+
+		}
+
+		function getSVGObjectData( id ) {
+
+			let svgObject = _svgObjectsPool[ id ];
+
+			if ( svgObject === undefined ) {
+
+				svgObject = {
+					node: null,
+					x: 0,
+					y: 0,
+					z: 0,
+					renderOrder: 0
+				};
+
+				_svgObjectsPool[ id ] = svgObject;
 
 			}
 
-			return _svgPathPool[ id ];
+			return svgObject;
+
+		}
+
+		function getRenderItem( id, type, data, material ) {
+
+			let item = _renderListPool[ id ];
+
+			if ( item === undefined ) {
+
+				item = {
+					type: type,
+					data: data,
+					material: material
+				};
+
+				_renderListPool[ id ] = item;
+
+				return item;
+
+			}
+
+			item.type = type;
+			item.data = data;
+			item.material = material;
+
+			return item;
 
 		}
 

BIN
examples/screenshots/svg_sandbox.jpg


+ 7 - 5
examples/svg_sandbox.html

@@ -26,12 +26,13 @@
 			import * as THREE from 'three';
 
 			import Stats from 'three/addons/libs/stats.module.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 			import { SVGRenderer, SVGObject } from 'three/addons/renderers/SVGRenderer.js';
 
 			THREE.ColorManagement.enabled = false;
 
-			let camera, scene, renderer, stats;
+			let camera, scene, renderer, stats, controls;
 
 			let group;
 
@@ -218,6 +219,9 @@
 				renderer.setQuality( 'low' );
 				document.body.appendChild( renderer.domElement );
 
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableDamping = true;
+
 				stats = new Stats();
 				document.body.appendChild( stats.dom );
 
@@ -251,12 +255,10 @@
 
 				const time = Date.now() * 0.0002;
 
-				camera.position.x = Math.sin( time ) * 500;
-				camera.position.z = Math.cos( time ) * 500;
-				camera.lookAt( scene.position );
-
 				group.rotation.x += 0.01;
 
+				controls.update();
+
 				renderer.render( scene, camera );
 
 			}

粤ICP备19079148号