How can I render a lego model (as .ldr or .io file) using ThreeJS?

Hi!

Short version:

I want to build a website where I can load and render/view a 3D lego model from a .io or .ldr file.

So I only need a view window within the html file where I can view, zoom and rotate the lego model.

What is the easiert way for that?

More detailed version:

Here are 2 example files (I can open and view the .io file in Studio 2.0 from Brick Link):

BUILD_Schere-Stein-Papier_v1.0.io (205.8 KB)
BUILD_Schere-Stein-Papier_v1.0.ldr (10.7 KB)

Note: The .ldr file is obtained by conversion from the .io file (so I cannot 100% guarantee that it is correct)

I already downloaded the parts from the official site LDraw.org website and tried some renderers from the internet for .ldr files (including ThreeJS and LDraw). None of them worked. In the web tools command line, I got some missing parts errors, even tough I downloaded all parts I could find. It seemed there were some case sensitive naming issues (some parts where named .DAT but the lego “compiler” seemed to search for them as .dat and couldn’t find them). But there where too many parts to rename them manually. So the error seemed strange. Why could Studio 2.0 render the file and the Web renderer not?

Can anyone help with this problem?
So basically, I need a .io or .ldr viewer for web pages.
And, if possible, I need a place where I can test if my models are correct (at least the converted .ldr file) → but only if I go for .ldr rendering (the .io file should be fine)

Here is how I tried to use three.js webgl - LDrawLoader:

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - LDrawLoader</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<style>
			body {
				font-family: Monospace;
				background-color: #000;
				color: #fff;
				margin: 0px;
				overflow: hidden;
			}
			#info {
				color: #fff;
				position: absolute;
				top: 10px;
				width: 100%;
				text-align: center;
				z-index: 100;
				display:block;
			}
			#info a, .button { color: #f00; font-weight: bold; text-decoration: underline; cursor: pointer }
		</style>
	</head>

	<body>
		<div id="info">
		<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - LDrawLoader
		</div>

		<script src="../build/three.js"></script>

		<script src="js/loaders/LDrawLoader.js"></script>
		<script src="js/controls/OrbitControls.js"></script>

		<script src='js/libs/dat.gui.min.js'></script>

		<script>

			var container, stats, progressBar, progressBarDiv;

			var camera, scene, renderer, pointLight, controls, gui, guiData;

			var modelFileList, model, textureCube;

			var envMapActivated = false;

			var ldrawPath = 'models/ldraw/officialLibrary/';

			var modelFileList = {
				'SSP': 'models/BUILD_Schere-Stein-Papier_v1.0.ldr',
				'Car': 'models/car.ldr_Packed.mpd',
				'Lunar Vehicle': 'models/1621-1-LunarMPVVehicle.mpd_Packed.mpd',
				'Radar Truck': 'models/889-1-RadarTruck.mpd_Packed.mpd',
				'Trailer': 'models/4838-1-MiniVehicles.mpd_Packed.mpd',
				'Bulldozer': 'models/4915-1-MiniConstruction.mpd_Packed.mpd',
				'Helicopter': 'models/4918-1-MiniFlyers.mpd_Packed.mpd',
				'Plane': 'models/5935-1-IslandHopper.mpd_Packed.mpd',
				'Lighthouse': 'models/30023-1-Lighthouse.ldr_Packed.mpd',
				'X-Wing mini': 'models/30051-1-X-wingFighter-Mini.mpd_Packed.mpd',
				'AT-ST mini': 'models/30054-1-AT-ST-Mini.mpd_Packed.mpd',
				'AT-AT mini': 'models/4489-1-AT-AT-Mini.mpd_Packed.mpd',
				'Shuttle': 'models/4494-1-Imperial Shuttle-Mini.mpd_Packed.mpd',
				'TIE Interceptor': 'models/6965-1-TIEIntercep_4h4MXk5.mpd_Packed.mpd',
				'Star fighter': 'models/6966-1-JediStarfighter-Mini.mpd_Packed.mpd',
				'X-Wing': 'models/7140-1-X-wingFighter.mpd_Packed.mpd',
				'AT-ST': 'models/10174-1-ImperialAT-ST-UCS.mpd_Packed.mpd'
			};

			init();
			animate();


			function init() {

				container = document.createElement( 'div' );
				document.body.appendChild( container );

				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 );
				camera.position.set( 150, 200, 250 );

				// scene

				scene = new THREE.Scene();

				var ambientLight = new THREE.AmbientLight( 0xcccccc, 0.4 );
				scene.add( ambientLight );

				pointLight = new THREE.PointLight( 0xffffff, 1 );
				pointLight.position.set( -1000, 1200, 1500 );
				scene.add( pointLight );

				//

				renderer = new THREE.WebGLRenderer();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				container.appendChild( renderer.domElement );

				controls = new THREE.OrbitControls( camera, renderer.domElement );

				//

				guiData = {
					modelFileName: modelFileList[ 'SSP' ],
					envMapActivated: false
				};

				gui = new dat.GUI( { width: 350 } );

				var modelFolder = gui.addFolder( "Model" );

				modelFolder.add( guiData, 'modelFileName', modelFileList ).name( 'Model' ).onFinishChange( function () {

					reloadObject( true );

				} );

				modelFolder.open();

				var graphicsFolder = gui.addFolder( "Graphics" );

				graphicsFolder.add( guiData , 'envMapActivated' ).name( 'Env. map' ).onChange( function ( value ) {

					envMapActivated = value;

					reloadObject( false );

				} );

				graphicsFolder.open();

				window.addEventListener( 'resize', onWindowResize, false );

				progressBarDiv = document.createElement( 'div' );
				progressBarDiv.innerText = "Loading...";
				progressBarDiv.style.fontSize = "3em";
				progressBarDiv.style.display = "block";
				progressBarDiv.style.position = "absolute";
				progressBarDiv.style.top = "50%";
				progressBarDiv.style.left = "50%";

				// load materials and then the model

				reloadObject( true );

			}

			function reloadObject ( resetCamera ) {

				if ( model ) {

					scene.remove( model );

				}

				model = null;

				updateProgressBar( 0 );
				showProgressBar();

				var lDrawLoader = new THREE.LDrawLoader();
				lDrawLoader
					.setPath( ldrawPath )
					.load( guiData.modelFileName, function ( group2 ) {

						if ( model ) {

							scene.remove( model );

						}

						model = group2;

						// Convert from LDraw coordinates: rotate 180 degrees around OX
						model.rotation.x = Math.PI;

						scene.add( model );

						// Adjust materials

						var materials = lDrawLoader.materials;

						if ( envMapActivated ) {

							if ( ! textureCube ) {

								// Envmap texture
								var r = "textures/cube/Bridge2/";
								var urls = [ r + "posx.jpg", r + "negx.jpg",
											r + "posy.jpg", r + "negy.jpg",
											r + "posz.jpg", r + "negz.jpg" ];
								textureCube = new THREE.CubeTextureLoader().load( urls );
								textureCube.format = THREE.RGBFormat;
								textureCube.mapping = THREE.CubeReflectionMapping;

							}

							for ( var i = 0, n = materials.length; i < n; i ++ ) {

								var material = materials[ i ];

								if ( material.userData.canHaveEnvMap ) {

										material.envMap = textureCube;

								}

							};

						}

						// Adjust camera and light

						var bbox = new THREE.Box3().setFromObject( model );
						var size = bbox.getSize( new THREE.Vector3() );
						var radius = Math.max( size.x, Math.max( size.y, size.z ) ) * 0.5;

						if ( resetCamera ) {

							controls.target0.copy( bbox.getCenter( new THREE.Vector3() ) );
							controls.position0.set( - 2.3, 2, 2 ).multiplyScalar( radius ).add( controls.target0 );
							controls.reset();

						}

						pointLight.position.normalize().multiplyScalar( radius * 3 );

						hideProgressBar();

					}, onProgress, onError );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			//

			function animate() {

				requestAnimationFrame( animate );
				render();

			}

			function render() {

				renderer.render( scene, camera );

			}

			function onProgress ( xhr ) {

				if ( xhr.lengthComputable ) {

					updateProgressBar( xhr.loaded / xhr.total );

					console.log( Math.round( xhr.loaded / xhr.total * 100, 2 ) + '% downloaded' );

				}

			}

			function onError ( error ) {

				var message = "Error loading model";
				progressBarDiv.innerText = message;
				console.log( message );

			}

			function showProgressBar () {

				document.body.appendChild( progressBarDiv );

			}

			function hideProgressBar () {

				document.body.removeChild( progressBarDiv );

			}

			function updateProgressBar ( fraction ) {

				progressBarDiv.innerText = 'Loading... ' + Math.round( fraction * 100, 2 ) + '%';

			}

		</script>

		<!-- LDraw.org CC BY 2.0 Parts Library attribution -->
		<div style="display: block; position: absolute; bottom: 0px; right: 0px; width: 160px; padding: 2px; border: #838A92 1px solid; background-color: #F3F7F8;">
			<center>
				<a href="http://www.ldraw.org"><img style="width: 145px" src="files/ldraw_org_logo/Stamp145.png"></a>
				<br />
				<a href="http://www.ldraw.org/">This software uses the LDraw Parts Library</a>
			</center>
		</div>

	</body>
</html>

It worked with some demo .mpd files (that I did not need), but I didn’t work with my .ldr file (the Missing Parts Error came up)

I have zero experience with Lego models, as well as with .ldr and .io files. However, the attached .ldr file is incomplete – when you look at its contents (with a plain text editor) it really references other parts, that are missing.

Also, it is uncompressed text, but it is still 20 times smaller than the corresponding .io file (which is actually a compressed file).

My uneducated guess is that you have to convert the full .io into .ldr before trying to load it in Three.js. Either you have to tweak some options of the converter, or use another converter. When I checked other .ldr files, the also have references to other Lego parts, but they are also included in the .ldr file.

If your model uses external parts, check whether the loader can access theie files. Did you set correct paths and filenames in LDrawLoader’s setPartsLibraryPath, setPath and setFileMap?

@PavelBoytchev:

The .ldr file had 3 errors (which I corrected), and I can check the model in the ldr viewer: LDraw.org - LDraw Model Viewer

Basically, that viewer is what I want for my webpage and it is not working with the model.
As the model is ok, my viewer must be broken.

I will check the code again tomorrow.

Thx!

You need to set the parts library path using LDrawLoader.setPartsLibraryPath.

2 Likes

@gkjohnson: Thx!

Today I tried a new, more simple approach.

I just created a whole new repo and went to three.js docs and did this:

  1. Install threejs and set up project (three.js docs)
  2. Create a scene with cam, renderer, etc. (three.js docs)
  3. Set a path to parts and model and load model using the basic LDrawLoader (three.js docs)

Here is the javascript code for that:

import * as THREE from 'three';
import { LDrawLoader } from 'three/addons/loaders/LDrawLoader.js';
import WebGL from 'three/addons/capabilities/WebGL.js';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
const loader = new LDrawLoader();

renderer.setSize(734, 296);

// Get container for viewer window
const container = document.getElementById('webgl-container');


function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement); // Add renderer to container
camera.position.z = 5;

// Pfad zu den Teilen
var partsLibraryPath = "/models/ldraw/officialLibrary/";
loader.setPartsLibraryPath(partsLibraryPath);

// Lade eine LDraw-Ressource
loader.load(
  // Ressourcen-URL
  'models/BUILD_Schere-Stein-Papier_v1.0.ldr',
  // Aufgerufen, wenn die Ressource geladen ist
  function (group) {
    // Füge die Gruppe zur Szene hinzu
    scene.add(group);
  },
  // Aufgerufen während des Ladevorgangs
  function (xhr) {
    console.log((xhr.loaded / xhr.total * 100) + '% geladen');
  },
  // Aufgerufen, wenn beim Laden Fehler auftreten
  function (error) {
    console.log('Ein Fehler ist aufgetreten');
  }
);

// Fehlerbehandlung
if (WebGL.isWebGLAvailable()) {
  // Initiiere Funktion oder andere Initialisierungen hier
  animate();
} else {
  const warning = WebGL.getWebGLErrorMessage();
  container.appendChild(warning);
}

And this is how I use it in my html:

<head>
....
<script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.157.0/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
      }
    }
  </script>
  <style>
    ...
    /* Size of ldr viewer */
    canvas {
      /* Some styles that might affect the size */
      width: 80% !important;
      height: 80% !important;
    }    
  </style>
....
</head>
<body>
...
<div class="col-sm-8 text-left"> 
      <h2>Bots Viewer</h2>
      <hr>

      <div id="webgl-container"></div>
      <script type="module" src="/views/main.js"></script>

    </div>
....
</body>

The problem now is, that I see a scene, but the web dev terminal shows some "Missing parts " and “Material properties not available” errors.
For example:

XHRGET
http://localhost:3000/models/ldraw/officialLibrary/models/p/48/1-6cyli.dat
[HTTP/1.1 404 Not Found 1ms]

and

LDrawLoader: Material properties for code 15 not available. 3 LDrawLoader.js:2117:14

Now, the path set to the parts is correct.
Also, it’s the newest parts data.
If I load the same model on the offical ldr viewer from lego, it works and they use the same parts.

My implementation only shows a black background and some violet lines belonging to my model, but not the full model.

If I search for the apparently missing parts manually, they are in the folder!

Some parts are in the “parts” folder and some parts in the “p” folder (inside of officialParts Folder).

As I read in the docs, you only need to provide the parts folder and it will automacally search the parts and p folders.
But for some reason, it appers to not be doing that.
For example, the part “1-6cyli.dat” is searched for in the p folder, but it is located in the part folder. So instead of searching also in the part folder, it shows a error message, that it cannot find the part.

I don’t really get what I’m doing wrong.
Anyone some ideas?

Sorry, you’ll also have to preload a material file since those are not embedded in the model files, either. And the loader has to search through multiple folders in order to find the parts so you’ll see some failures in the network tab but the pieces should still be found.

Here’s a link to a list of parts on github that has a few lines on how to prep the materials and parts in the README:

const loader = new LDrawLoader();
loader.setPartsLibraryPath( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/complete/ldraw/' );
await loader.preloadMaterials( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/colors/ldcfgalt.ldr' );

loader.load( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/complete/ldraw/10030-1%20-%20Imperial%20Star%20Destroyer%20-%20UCS.mpd' );

Thanks, @gkjohnson : It works now!