If you’ve visited my personal site before then you may have found this (somewhat) hidden lego model.

Double clicking it rotates it but there’s also orbital controls for you to zoom in and view it from all angles.

At the time, I was interested in playing with 3D models in JavaScript and had known about Mr Doob’s popular ThreeJS library.

But I had (still have and will probably continue to have…) no experience with modelling software like Blender.

But I do know my Lego!

BrickLink a popular community driven Lego marketplace (which fun fact is owned by Lego now) has a tool for building digital Lego models called Studio.

After downloading the tool, you’ll find it relatively simple to use. If you know your lego pieces, you can find the appropriate bricks you need to build whatever you want. You can even import real Lego sets! But many sets like ones with special pieces, colours or stickers are likely to fail importing all the required parts. I have found the BrickHeadz line for the most part successful.

OK getting side tracked - the point is that this is the simplest way to digitize your Lego creations!

Here’s an inefficient timelapse of me building my Lego Batman Logo! You can see me struggling but in the end I got what I wanted :)

Enter ThreeJS

After you’ve finished building your model make sure to export it as an Ldraw file (File -> Export As -> Export As LDraw...).

  1. Now create a standard html file like this:
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My Lego</title>
        <script async src="model.js" type="module"></script>
      </head>
      <body>
      </body>
    </html>
    
  2. Create a model.js file in the same directory of this project that this page runs when it loads. Note: Like the html template above, ensure the <script> tag is of type module (ThreeJS requires this file to be an ES Module).
  3. Install ThreeJS

Packing the LDraw Model

ThreeJS’s LDrawLoader is what we use to load our model but by default it actually loads packed LDraw models. Meaning it doesn’t support pulling a single LDraw model.

Thankfullly, the ThreeJS docs do have section on Packing Ldraw Models, where you download the latest LDraw parts library and run this official script on your LDraw model you exported from Studio.

Note: make sure to read the instructions written in the script before running it - good rule of thumb is to read any script you get, official or not.

Importing the LDraw Model

The following JavaScript code (model.js) uses this ThreeJS GLTFLoader tutorial as a template. The main modifications are changing the loader to LDrawLoader and adding a pivot so that we can rotate around the object’s center rather than the world’s center (a problem I encountered and solved thanks to this SO post).

import * as THREE from "three";

// orbit controls so you can drag and play around with the model in space
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

// import the loader
import { LDrawLoader } from "three/examples/jsm/loaders/LDrawLoader";

// import the packed model
import batman from "./batman.ldr_Packed.mpd";

// performance stats that you can see on the top left corner
import Stats from "three/examples/jsm/libs/stats.module";

// need to create a scene to add the model first
const scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(5));

// adding a light source (used to highlight a specific part of the model)
const light = new THREE.PointLight();
// position depends on how your model loads
light.position.set(0.8, 1.4, 1.0);
scene.add(light);

// adding another light source but this one lights the entire scene
const ambientLight = new THREE.AmbientLight("yellow");
scene.add(ambientLight);

// create the view
const camera = new THREE.PerspectiveCamera(
  100,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
// position depends on how and where the model loads
// might need to tinker with these values
camera.position.set(0, 50, 400);

// adding the render space
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// adding the orbit controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 1, 0);

// initializing the loader
const loader = new LDrawLoader();
let loadedModel;

// create a pivot to rotate the on model
let pivot = new THREE.Group();
scene.add(pivot);

// load the model
loader.load(
  batman,
  (object) => {
    loadedModel = object;
    pivot.add(object);
  },
  (xhr) => {
    console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
  },
  (error) => {
    console.log(error);
  }
);

// resize window settings
window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  render();
}

// only rotate the model on double click
let rotateModel = false;
window.addEventListener("dblclick", () => (rotateModel = !rotateModel));

// add the stats
const stats = Stats();
document.body.appendChild(stats.dom);

// animate the model (this acts as an event loop)
// only after the model has loaded (and the user double clicks)
// will the model be able to animate
function animate() {
  requestAnimationFrame(animate);

  if (loadedModel && rotateModel) {
    pivot.rotation.y += 0.02;
  }
  controls.update();

  render();

  stats.update();
}

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

animate();