I hope you're having a marvellous May everyone! This month's Creative Coding Club project theme is Sugar, Sugar and below I've outlined a step-by-step project walkthrough that will result in a 3D visualization of random rotating sugar particles using three.js.

This tutorial is meant for those who have some experience with JavaScript, but I will do my very best to outline it clearly for beginners.


Before we begin: research!

For this project especially, I wanted to replicate what sugar looks like up close - so I started with a quick google image search:

sugar particles up close sugar particles up close

Sugar is not a perfect cube, cylinder or other pure geometric shape - the particles are chipped, inconsistent and imperfect. They are also somewhat translucent, and can be colourful in the right light. This will be important to remember as we manipulate our geometry later in three.js.


Step 1: Create a scene with three.js

If you haven't done so already, be sure to include the three.js library in your project. I will also be referencing the three.js Documentation throughout the tutorial.

Since our canvas element is generated with JS, you don't need to write any HTML for this project, but we will need a little bit of CSS to ensure that our <canvas> is full-screen, and to give our <body> a nice "cotton candy" coloured gradient for a background.

  body { 
  margin: 0; 
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: linear-gradient(to bottom, #614385, #516395);
}

canvas { 
  width: 100%; 
  height: 100%;
}


That's it for CSS though, from here on out we're building this project with our good pal JavaScript. Y'all ready for this?

Woman in neck brace at computer, cracking her fingers and getting ready.


If you reference the three.js docs, they will step you through creating a scene with a Perspective Camera. Our code will start off very similar to the docs, but we are using an Orthographic Camera to ensure our sugar particles remain constant (not distorted) regardless of their distance from the camera.

  // create the scene
var scene = new THREE.Scene();

// set up the orthographic camera 
var camera = new THREE.OrthographicCamera(window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, - 500, 1000 );
camera.zoom = 5; // we will change this later
camera.updateProjectionMatrix();

// create the renderer with {alpha: true} to make it transprent
var renderer = new THREE.WebGLRenderer({alpha: true});
renderer.setSize( window.innerWidth, window.innerHeight );

// append it to the DOM
document.body.appendChild( renderer.domElement );

// render the scene and camera
function render() {
    requestAnimationFrame( render );
    renderer.render( scene, camera );
}
render();


With the above JS, you will have a blank transparent canvas (with the gradient behind) that is the entire width and height of the viewport. Now that everything is set up, let's add our first sugar particle.


Geometry in three.js

The three.js docs walk you through how to render a scene with a cube in the middle using something called BoxGeometry. There are countless other fun three.js geometries to use, including: cones, spheres, cylinders, icosahedrons, torus (donut-shapes) . . . and the one we'll use in our project: ExtrudeGeometry.

I stumbled across ExtrudeGeometry while perusing the docs, and thought the example shape (a bevelled rectangular prism) would be perfect as our starting shape for each sugar particle. So, to start this off let's copy the example shape directly from the docs and take it from there:

  // establish variable for our mesh object (sugar particle)
var mesh;

// determine a length and width for each particle
// we will randomize these values later
var length = 12, width = 8;

// what is the shape that you want to be extruded?
// we will randomize some of these values later
var shape = new THREE.Shape();
shape.moveTo( 0,0 ); 
shape.lineTo( 0, width );
shape.lineTo( length, width );
shape.lineTo( length, 0 );
shape.lineTo( 0, 0 );

// how do you want to extrude that shape?
// we will randomize some of these values later
var extrudeSettings = {
  steps: 2,
  amount: 16,
  bevelEnabled: true,
  bevelThickness: 1,
  bevelSize: 1,
  bevelSegments: 1
};

// define the geometry based on the settings above
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );

// define a material for the mesh
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );

// geometry + material = mesh (sugar particle)
mesh = new THREE.Mesh(geometry, material) ;

// add it to the scene
scene.add(mesh);


And it's... a green rectangle. That's because you're looking at it from only one side. Let's rotate it:

  function render() {
    requestAnimationFrame( render );
    // add a rotating animation along the x and y axis
    mesh.rotation.x += 0.03;
    mesh.rotation.y += 0.03;
    renderer.render( scene, camera );
}


Okay, looks pretty good - let's just add one more thing before we move on. Right now, our sugar particle isn't rotating from the centre and it's driving me bananas. Let's fix that.

  // get the centre of each sugar particle so it rotates in the middle
mesh.geometry.center();


We now have this vibrant green 3D shape rotating on a purple background. Your project should look like this:


Step 2: Change the material

Three.js has some neat default materials built in already. Right now, our project is using MeshBasicMaterial. I secretly love the default look of MeshNormalMaterial. It's vibrant and pretty, but doesn't quite look like a "real" sugar particle. For this project, we want the particles to be white, textured, with a transparency.

In order to do this, we have to use a texture - download the texture for this project here.


Textures in three.js

Using a texture on your mesh object will probably be a process of trial-and-error at first. At least, it was for me when I built this project. I designed and exported a transparent PNG image and then proceeded to once again follow the three.js documentation to add a texture to my mesh.

  // create a threeJS TextureLoader
var textureLoader = new THREE.TextureLoader();
// to fight any cross-origin issues with images
textureLoader.crossOrigin = true;

textureLoader.load('/path/to/sugar-texture-4.png', function(texture) {
  // repeat the pattern
  texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping;

  // assign the texture via the MeshBasicMaterial map property
  var material = new THREE.MeshBasicMaterial({map: texture});

  mesh = new THREE.Mesh(geometry, material) ;
  mesh.geometry.center();
  scene.add(mesh);

  render();
}); 


The texture pattern should be showing up on the mesh, but it's too small, and it's not transparent.

    // zoom in on the pattern so it's not so small when it repeats
  texture.repeat.set(0.1,0.1);

  var material = new THREE.MeshBasicMaterial({
    map: texture,
    // apply some other properties to increase transparency
    transparent: true,
    premultipliedAlpha: true,
    side: THREE.DoubleSide,
    blending: THREE.AdditiveBlending
  });


Now our "sugar particle" is starting to look a little bit more like an actual sugar crystal:


Step 3: Randomize the shape and size

We're going to manipulate a few different properties in order to create realistic looking sugar particles that have imperfect shapes and sizes. The first step is to create a function to randomize those properties.

  // make a function that gets a random number between two values
function randBetween(min, max) {
  return (Math.random() * (max - min)) + min;
}


Next, use the new randBetween() function to randomize the length, width, shape and extrudeSettings values:

    var length  = randBetween(1, 6), 
      width   = randBetween(1, 8);

  var shape = new THREE.Shape();
  shape.moveTo( randBetween(0,1.5),randBetween(0,1.5) );
  shape.lineTo( randBetween(0,1.5), width );
  shape.lineTo( length, width );
  shape.lineTo( length, randBetween(0,1.5) );
  shape.lineTo( randBetween(0,1.5), randBetween(0,1.5) );

  var extrudeSettings = {
      steps: 2,
      amount: randBetween(3,6),
      bevelEnabled: true,
      bevelThickness: 1,
      bevelSize: randBetween(1,3),
      bevelSegments: 1.5
  };


Awesome! Now every time we reload the project, the sugar particle will take on a new random shape:


Step 4: Multiply the sugar particles

Now that we have our sugar particle textured and randomizing in shape and size, lets make lots of them. We need to randomize each sugar crystal, which means each one requires a unique length, width, shape, extrudeSettings etc. We then need to create a function that returns a unique sugar particle.

Let's call it createMesh():

  // create a function that returns a unique sugar particle
function createMesh() {
  var length  = randBetween(1, 6), 
      width   = randBetween(1, 8);

  var shape = new THREE.Shape();
  shape.moveTo( randBetween(0,1.5),randBetween(0,1.5) );
  shape.lineTo( randBetween(0,1.5), width );
  shape.lineTo( length, width );
  shape.lineTo( length, randBetween(0,1.5) );
  shape.lineTo( randBetween(0,1.5), randBetween(0,1.5) );

  var extrudeSettings = {
      steps: 2,
      amount: randBetween(3,6),
      bevelEnabled: true,
      bevelThickness: 1,
      bevelSize: randBetween(1,3),
      bevelSegments: 1.5
  };

  var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );

  var textureLoader = new THREE.TextureLoader();
  textureLoader.crossOrigin = true;
  textureLoader.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/160607/sugar-texture-4.png', function(texture) {
    texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping;
    texture.repeat.set(0.1,0.1);

    var material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      premultipliedAlpha: true,
      side: THREE.DoubleSide,
      blending: THREE.AdditiveBlending
    });

    mesh = new THREE.Mesh(geometry, material) ;
    mesh.geometry.center();
    scene.add(mesh);

  }); 
}


Create a loop to generate more than one sugar particle - test it out with a small number to start:

  for ( var i = 0; i < 3; i ++ ) {
  createMesh();
}


And move the render() outside of the new createMesh() function to the very end of your JS:

  render();


You should now see three sugar crystals stacked on top of one another - and it seems like only one is spinning. To caputre all the random meshes that are generated, let's create an array called "crystals":

  // create an array for all the random crystals
var crystals = [];

  
    scene.add( mesh );
    // add each random crystal to the array
    crystals.push(mesh);
  });
}


Wrap the rotation animation in a .forEach function to ensure every particle rotates:

    crystals.forEach(function(mesh) {
    mesh.rotation.x += 0.03;
    mesh.rotation.y += 0.03;
  });


Great - now we have more than one sugar particle, and they're all rotating. But they're in the same position on the canvas and they're all rotating at the same time.


Step 5: Randomize the position / rotation

Finding out how to randomize the mesh position took a bit of digging in the mesh documentation. It inherits properties from Object3D such as position.set:

      mesh = new THREE.Mesh(geometry, material) ;

    // randomize the position
    var x = randBetween(-50,50),
        y = randBetween(-50,50),
        z = randBetween(-50,50);

    mesh.position.set(x,y,z);


And let's also randomize the rotation speed of each particle:

      mesh.geometry.center();

    //randomize the rotation speed of each sugar particle
    mesh.rotateAt = randBetween(0.01, 0.04);

    scene.add( mesh );

    // update the rotate animaton to be a random speed
  crystals.forEach(function(mesh) {
    mesh.rotation.x += mesh.rotateAt;
    mesh.rotation.y += mesh.rotateAt;
  });



Step 6: Final touches and more crystals!

Well, what do you know - it's starting to look pretty sweet, but let's make it even sweeter!

Wil Wheaton smiling cheeky

  // instead of 3 crystals, gimme 300!!
for ( var i = 0; i < 300; i ++ ) {
  createMesh();
}


And let's spread them out a bit more - it's okay if they fall off the edge of the screen, we have overflow:hidden on the body anyway.

      var mesh = new THREE.Mesh( geometry, material );

    // spread them out a bit more (increase from 50 to 120)
    var x = randBetween(-120,120),
        y = randBetween(-120,120),
        z = randBetween(-120,120);

    mesh.position.set(x,y,z);


Totally optional - but I also zoomed the camera out a bit:

  // zoom out a bit
camera.zoom = 3.5;


And your final project should look something like this!


References

This was my first three.js project, and I still have a lot to learn. I leaned on the documentation, and I really appreciated reading articles by Rach Smith - especially this one: Beginning with 3D WebGL.

It was an awesome challenge and I had so much fun.


Extensions

As always, I invite you to fork my pens and make artistic decisions, improvements and modifications to the project as much as you like! I think it would be super cute to see a similar particle-esque project with torus shapes or doughnuts. Haha.

Until next month - have fun, nerds!


5,745 4 90