Prechádzať zdrojové kódy

LDrawLoader example: Add option to merge geometries after loading (#23173)

* GPGPU Protoplanets example: fix pixel trails

* webgl_gpgpu_water.html: Remove mention to orbiting the camera.

* LDrawLoader example: Add option to merge geometries after loading

* Re-enable conditional lines option when merge model option is on

* Remove unnecessary segment vertices permutations

* Fix code format

* Move the mergeObject function into LDrawLoader

* Extract mergeObject() to LDrawUtils.js

* Documentation file for LDrawLoader

* Add comment about using LDrawUtils
Juan Jose Luna Espinosa 3 rokov pred
rodič
commit
c9daf113eb

+ 188 - 0
docs/examples/en/loaders/LDrawLoader.html

@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:Loader] &rarr;
+
+		<h1>[name]</h1>
+
+		<p class="desc"> A loader for <em>LDraw</em> resources. <br /><br />
+		[link:https://ldraw.org LDraw] (LEGO Draw) is an
+		[link:https://ldraw.org/article/218.html open format specification] for describing LEGO
+		and other construction set 3D models.</p>
+
+		<p>An LDraw asset (a text file usually with extension .ldr, .dat or .txt) can describe
+		just a single construction piece, or an entire model.
+		In the case of a model the LDraw file can reference other LDraw files, which are loaded
+		from a library path set with [page:Function setPartsLibraryPath]. You usually download
+		the LDraw official parts library, extract to a folder and point setPartsLibraryPath to it.
+		</p>
+
+		<p>Library parts will be loaded by trial and error in subfolders 'parts', 'p' and 'models'.
+		These file accesses are not optimal for web environment, so a script tool has been made
+		to pack an LDraw file with all its dependencies into a single file, which loads much faster.
+		See section 'Packing LDraw models'. The LDrawLoader example loads several packed files.
+		The official parts library is not included due to its large size.</p>
+
+
+		<h2>Extensions</h2>
+
+		<p>
+			LDrawLoader supports the following extensions:
+		</p>
+
+		<ul>
+			<li>!COLOUR: Colour and surface finish declarations.</li>
+			<li>BFC: Back Face Culling specification.</li>
+			<li>!CATEGORY: Model/part category declarations.</li>
+			<li>!KEYWORDS: Model/part keywords declarations.</li>
+		</ul>
+
+		<h2>Code Example</h2>
+
+		<code>
+		// Instantiate a loader
+		const loader = new LDrawLoader();
+
+		// Optionally set library parts path
+		// loader.setPartsLibraryPath( path to library );
+
+		// Load a LDraw resource
+		loader.load(
+			// resource URL
+			'models/car.ldr_Packed.mpd',
+			// called when the resource is loaded
+			function ( group ) {
+
+				// Optionally, use LDrawUtils.mergeObject() from
+				// 'examples/jsm/utils/LDrawUtils.js' to merge all
+				// geometries by material (it gives better runtime
+				// performance, but construction steps are lost)
+				// group = LDrawUtils.mergeObject( group );
+
+				scene.add( group );
+
+			},
+			// called while loading is progressing
+			function ( xhr ) {
+
+				console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
+
+			},
+			// called when loading has errors
+			function ( error ) {
+
+				console.log( 'An error happened' );
+
+			}
+		);
+		</code>
+
+		<h2>Examples</h2>
+
+		<p>
+			[example:webgl_loader_ldraw]
+		</p>
+
+		<h2>Packing LDraw models</h2>
+
+		<p>To pack a model with all its referenced files, download the
+		[link:https://www.ldraw.org/parts/latest-parts.html Official LDraw parts library]
+		and use the following Node script:
+		[link:https://github.com/mrdoob/three.js/blob/master/utils/packLDrawModel.js utils/packLDrawModel.js]
+		It contains instructions on how to setup the files and execute it.</p>
+
+		<h2>Metadata in .userData</h2>
+
+		<p>LDrawLoader returns a [page:Group] object which contains an object hierarchy. Depending of each subobject
+		type, its .userData member will contain the following members: <br />
+		In a [page:Group], the userData member will contain: <br />
+		<ul>
+			<li>.numConstructionSteps: Only in the root [page:Group], Indicates total number of construction steps in
+			the model. These can be used to set visibility of objects to show different construction steps, which is
+			done in the example.</li>
+			<li>.constructionStep: Indicates the construction index of this step.</li>
+			<li>.category: Contains, if not null, the [page:String] category for this piece or model.</li>
+			<li>.keywords: Contains, if not null, an array of [page:String] keywords for this piece or model.</li>
+		</ul>
+		</p>
+		<p>In a [page:Material], the userData member will contain:
+		<ul>
+			<li>.code: Indicates the LDraw code for this material.</li>
+			<li>.edgeMaterial: Only in a [page:Mesh] material, indicates the [page:LineBasicMaterial] belonging to edges
+			of the same color code (in the LDraw format, each surface material is also related to an edge material)</li>
+			<li>.conditionalEdgeMaterial: Only in a [page:LineSegments] material, indicates the [page:Material] belonging
+			to conditional edges of the same color code.</li>
+		<ul>
+		</p>
+
+		<br>
+		<hr>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]( [param:LoadingManager manager] )</h3>
+		<p>
+		[page:LoadingManager manager] — The [page:LoadingManager loadingManager] for the loader to use. Default is [page:LoadingManager THREE.DefaultLoadingManager].
+		</p>
+		<p>
+		Creates a new [name].
+		</p>
+
+		<h2>Properties</h2>
+		<p>See the base [page:Loader] class for common properties.</p>
+
+		<h2>Methods</h2>
+		<p>See the base [page:Loader] class for common methods.</p>
+
+		<h3>[method:undefined load]( [param:String url], [param:Function onLoad], [param:Function onProgress], [param:Function onError] )</h3>
+		<p>
+		[page:String url] — A string containing the path/URL of the LDraw file.<br />
+		[page:Function onLoad] — A function to be called after the loading is successfully completed. The function receives the loaded JSON response returned from [page:Function parse].<br />
+		[page:Function onProgress] — (optional) A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, that contains .[page:Integer total] and .[page:Integer loaded] bytes. If the server does not set the Content-Length header; .[page:Integer total] will be 0.<br />
+		[page:Function onError] — (optional) A function to be called if an error occurs during loading. The function receives error as an argument.<br />
+		</p>
+		<p>
+		Begin loading from url and call the callback function with the parsed response content.
+		</p>
+
+		<h3>[method:this setPartsLibraryPath]( [param:String path] )</h3>
+		<p>
+		[page:String path] —  Path to library parts files to load referenced parts from. This is different from [page:Loader.setPath], which indicates the path to load the main asset from.<br />
+		</p>
+		<p>
+		This method must be called prior to [page:.load] unless the model to load does not reference library parts (usually it will be a model with all its parts packed in a single file)
+		</p>
+
+		<h3>[method:this setFileMap]( [param:Map fileMap] )</h3>
+		<p>
+		[page:Map map] — Set a map from [page:String] to [page:String] which maps referenced library filenames to new filenames. If a fileMap is not specified (the default), library parts will be accessed by trial and error in subfolders 'parts', 'p' and 'models'.
+		</p>
+
+		<h3>[method:undefined parse]( [param:String text], [param:String path], [param:Function onLoad], [param:Function onError] )</h3>
+		<p>
+		[page:String text] — LDraw asset to parse, as string.<br />
+		[page:String path] — The base path from which to find other referenced LDraw asset files.<br />
+		[page:Function onLoad] — A function to be called when parse completes.<br />
+		</p>
+		<p>
+		Parse a LDraw file contents as a String and fire [page:Function onLoad] callback when complete. The argument to [page:Function onLoad] will be an [page:Group] that contains hierarchy of [page:Group], [page:Mesh] and [page:LineSegments] (with other part data in .userData fields).
+		</p>
+
+		<h3>[method:Material getMaterial]( [param:String colourCode] )</h3>
+		<p>
+		[page:String colourCode] — For an already loaded LDraw asset, returns the [page:Material] associated with the parameter colour code.
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/LDrawLoader.js examples/jsm/loaders/LDrawLoader.js]
+		</p>
+	</body>
+</html>

+ 1 - 0
docs/list.json

@@ -366,6 +366,7 @@
 				"FontLoader": "examples/en/loaders/FontLoader",
 				"GLTFLoader": "examples/en/loaders/GLTFLoader",
 				"KTX2Loader": "examples/en/loaders/KTX2Loader",
+				"LDrawLoader": "examples/en/loaders/LDrawLoader",
 				"MMDLoader": "examples/en/loaders/MMDLoader",
 				"MTLLoader": "examples/en/loaders/MTLLoader",
 				"OBJLoader": "examples/en/loaders/OBJLoader",

+ 1 - 0
examples/jsm/loaders/LDrawLoader.js

@@ -7,6 +7,7 @@ import {
 	LineBasicMaterial,
 	LineSegments,
 	Loader,
+	Matrix3,
 	Matrix4,
 	Mesh,
 	MeshStandardMaterial,

+ 195 - 0
examples/jsm/utils/LDrawUtils.js

@@ -0,0 +1,195 @@
+import {
+	BufferAttribute,
+	BufferGeometry,
+	Group,
+	LineSegments,
+	Matrix3,
+	Mesh
+} from '../../../build/three.module.js';
+
+import { mergeBufferGeometries } from './BufferGeometryUtils.js';
+
+class LDrawUtils {
+
+	static mergeObject( object ) {
+
+		// Merges geometries in object by materials and returns new object. Use on not indexed geometries.
+		// The object buffers reference the old object ones.
+		// Special treatment is done to the conditional lines generated by LDrawLoader.
+
+		function extractGroup( geometry, group, elementSize, isConditionalLine ) {
+
+			// Extracts a group from a geometry as a new geometry (with attribute buffers referencing original buffers)
+
+			const newGeometry = new BufferGeometry();
+
+			const originalPositions = geometry.getAttribute( 'position' ).array;
+			const originalNormals = elementSize === 3 ? geometry.getAttribute( 'normal' ).array : null;
+
+			const numVertsGroup = Math.min( group.count, Math.floor( originalPositions.length / 3 ) - group.start );
+			const vertStart = group.start * 3;
+			const vertEnd = ( group.start + numVertsGroup ) * 3;
+
+			const positions = originalPositions.subarray( vertStart, vertEnd );
+			const normals = originalNormals !== null ? originalNormals.subarray( vertStart, vertEnd ) : null;
+
+			newGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
+			if ( normals !== null ) newGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
+
+			if ( isConditionalLine ) {
+
+				const controlArray0 = geometry.getAttribute( 'control0' ).array.subarray( vertStart, vertEnd );
+				const controlArray1 = geometry.getAttribute( 'control1' ).array.subarray( vertStart, vertEnd );
+				const directionArray = geometry.getAttribute( 'direction' ).array.subarray( vertStart, vertEnd );
+
+				newGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );
+				newGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );
+				newGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );
+
+			}
+
+			return newGeometry;
+
+		}
+
+		function addGeometry( mat, geometry, geometries ) {
+
+			const geoms = geometries[ mat.uuid ];
+			if ( ! geoms ) {
+
+				geometries[ mat.uuid ] = {
+					mat: mat,
+					arr: [ geometry ]
+				};
+
+			}
+			else geoms.arr.push( geometry );
+
+		}
+
+		function permuteAttribute( attribute, elemSize ) {
+
+			// Permutes first two vertices of each attribute element
+
+			if ( ! attribute ) return;
+
+			const verts = attribute.array;
+			const numVerts = Math.floor( verts.length / 3 );
+			let offset = 0;
+			for ( let i = 0; i < numVerts; i ++ ) {
+
+				const x = verts[ offset ];
+				const y = verts[ offset + 1 ];
+				const z = verts[ offset + 2 ];
+
+				verts[ offset ] = verts[ offset + 3 ];
+				verts[ offset + 1 ] = verts[ offset + 4 ];
+				verts[ offset + 2 ] = verts[ offset + 5 ];
+
+				verts[ offset + 3 ] = x;
+				verts[ offset + 4 ] = y;
+				verts[ offset + 5 ] = z;
+
+				offset += elemSize * 3;
+
+			}
+
+		}
+
+		// Traverse the object hierarchy collecting geometries and transforming them to world space
+
+		const meshGeometries = {};
+		const linesGeometries = {};
+		const condLinesGeometries = {};
+
+		object.updateMatrixWorld( true );
+		const normalMatrix = new Matrix3();
+
+		object.traverse( c => {
+
+			if ( c.isMesh | c.isLineSegments ) {
+
+				const elemSize = c.isMesh ? 3 : 2;
+
+				const matrixIsInverted = c.matrixWorld.determinant() < 0;
+				if ( matrixIsInverted ) {
+
+					permuteAttribute( c.geometry.attributes.position, elemSize );
+					permuteAttribute( c.geometry.attributes.normal, elemSize );
+
+				}
+
+				c.geometry.applyMatrix4( c.matrixWorld );
+
+				if ( c.isConditionalLine ) {
+
+					c.geometry.attributes.control0.applyMatrix4( c.matrixWorld );
+					c.geometry.attributes.control1.applyMatrix4( c.matrixWorld );
+					normalMatrix.getNormalMatrix( c.matrixWorld );
+					c.geometry.attributes.direction.applyNormalMatrix( normalMatrix );
+
+				}
+
+				const geometries = c.isMesh ? meshGeometries : ( c.isConditionalLine ? condLinesGeometries : linesGeometries );
+
+				if ( Array.isArray( c.material ) ) {
+
+					for ( const groupIndex in c.geometry.groups ) {
+
+						const group = c.geometry.groups[ groupIndex ];
+						const mat = c.material[ group.materialIndex ];
+						const newGeometry = extractGroup( c.geometry, group, elemSize, c.isConditionalLine );
+						addGeometry( mat, newGeometry, geometries );
+
+					}
+
+				}
+				else addGeometry( c.material, c.geometry, geometries );
+
+			}
+
+		} );
+
+		// Create object with merged geometries
+
+		const mergedObject = new Group();
+
+		const meshMaterialsIds = Object.keys( meshGeometries );
+		for ( const i in meshMaterialsIds ) {
+
+			const meshGeometry = meshGeometries[ meshMaterialsIds[ i ] ];
+			const mergedGeometry = mergeBufferGeometries( meshGeometry.arr );
+			mergedObject.add( new Mesh( mergedGeometry, meshGeometry.mat ) );
+
+		}
+
+		const linesMaterialsIds = Object.keys( linesGeometries );
+		for ( const i in linesMaterialsIds ) {
+
+			const lineGeometry = linesGeometries[ linesMaterialsIds[ i ] ];
+			const mergedGeometry = mergeBufferGeometries( lineGeometry.arr );
+			mergedObject.add( new LineSegments( mergedGeometry, lineGeometry.mat ) );
+
+		}
+
+		const condLinesMaterialsIds = Object.keys( condLinesGeometries );
+		for ( const i in condLinesMaterialsIds ) {
+
+			const condLineGeometry = condLinesGeometries[ condLinesMaterialsIds[ i ] ];
+			const mergedGeometry = mergeBufferGeometries( condLineGeometry.arr );
+			const condLines = new LineSegments( mergedGeometry, condLineGeometry.mat );
+			condLines.isConditionalLine = true;
+			mergedObject.add( condLines );
+
+		}
+
+		mergedObject.userData.constructionStep = 0;
+		mergedObject.userData.numConstructionSteps = 1;
+
+		return mergedObject;
+
+	}
+
+}
+
+export { LDrawUtils };

+ 11 - 0
examples/webgl_loader_ldraw.html

@@ -30,6 +30,7 @@
 			import { RoomEnvironment } from './jsm/environments/RoomEnvironment.js';
 
 			import { LDrawLoader } from './jsm/loaders/LDrawLoader.js';
+			import { LDrawUtils } from './jsm/utils/LDrawUtils.js';
 
 			let container, progressBarDiv;
 
@@ -98,6 +99,7 @@
 					constructionStep: 0,
 					noConstructionSteps: 'No steps.',
 					flatColors: false,
+					mergeModel: false
 				};
 
 				window.addEventListener( 'resize', onWindowResize );
@@ -213,6 +215,9 @@
 
 						}
 
+						// Merge model geometries by material
+						if ( guiData.mergeModel ) model = LDrawUtils.mergeObject( model );
+
 						// Convert from LDraw coordinates: rotate 180 degrees around OX
 						model.rotation.x = Math.PI;
 
@@ -275,6 +280,12 @@
 
 				} );
 
+				gui.add( guiData, 'mergeModel' ).name( 'Merge model' ).onChange( function () {
+
+					reloadObject( false );
+
+				} );
+
 				if ( model.userData.numConstructionSteps > 1 ) {
 
 					gui.add( guiData, 'constructionStep', 0, model.userData.numConstructionSteps - 1 ).step( 1 ).name( 'Construction step' ).onChange( updateObjectsVisibility );