Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="three"></div>

<div id=controls>
  <div class="btn decr">-</div>
  <div class="count">4</div>
  <div class="btn incr">+</div>
</div>
              
            
!

CSS

              
                body {
  margin: 0;
  overflow: hidden;
}

#three {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
}

#controls {
  color: white;
  font-size: 24px;
  font-family: monospace;
  position: absolute;
  left: 0;
  bottom: 0;
  display: flex;
  width: 100%;
  justify-content: center;
  padding-bottom: 16px;
  
  display: none;
}

#controls div {
  display: inline-block;
  padding: 0 16px;
  text-align: center;
}

.btn {
  cursor: pointer;
}
.btn.disabled {
  color: gray;
  pointer-events: none;
}

.count {
  pointer-events: none;
}

              
            
!

JS

              
                // setup three.js

var renderer = new THREE.WebGLRenderer({
  antialias: true
});
var camera = new THREE.PerspectiveCamera(80);
var scene = new THREE.Scene();

document.querySelector('#three').appendChild(renderer.domElement);

// camera config

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

camera.position.z = 200;

// lights

var light;

light = new THREE.DirectionalLight(0xff0000, 1);
light.position.set(-1, 1, 1);
scene.add(light);

light = new THREE.DirectionalLight(0x0000ff, 1);
light.position.set(1, 1, 1);
scene.add(light);

light = new THREE.PointLight(0x00ff00, 1, 200);
scene.add(light);

// create meshes

var cubes;
var geometry;
var material;

var config = {
  // size of the grid in world units
  gridSize: 100,
  // length of each axis
  gridLength: 12,
  // translation delta on the x axis
  deltaX: 300,
  // animation duration for each mesh
  duration: 2.0,
  // startTime for each mesh will be based on the total delay
  totalDelay: 1.0
}

function createCubes() {
  // dispose previous cubes
  dispose();
  
  // determine size of each cube based size and length
  var cubeSize = config.gridSize / (config.gridLength * 1.125);
  var gridHalfSize = config.gridSize * 0.5;

  // total cubes based on grid length on each axis
  var cubeCount = config.gridLength * config.gridLength * config.gridLength;
  
  console.log('CREATING ' + cubeCount + ' CUBES');
  
  // create the geometry that will be repeated in the buffer geometry
  // I refer to this 'base' geometry as a prefab
  var prefab = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  
  // create the buffer geometry where a set number of prefabs are repeated
  // PrefabBufferGeometry offers some utility methods for working with such geometries
  geometry = new THREE.BAS.PrefabBufferGeometry(prefab, cubeCount);
  
  // create a buffer for start positions per prefab, with an item size of 3 (x, y, z)
  var startPositionBuffer = geometry.createAttribute('startPosition', 3);
  // create a buffer for end positions per prefab, with an item size of 3 (x, y, z)
  var endPositionBuffer = geometry.createAttribute('endPosition', 3);
  // create a buffer for duration per prefab, with the item size of 1
  var durationBuffer = geometry.createAttribute('duration', 1);
  // create a buffer for start time per prefab, with the item size of 1
  var startTimeBuffer = geometry.createAttribute('startTime', 1);

  // create a buffer for rotation with 4 components per vertex
  // we use x, y and z to store an axis
  // and w to store the rotation around that axis
  var rotationBuffer = geometry.createAttribute('rotation', 4);
  
  // populate the buffers

  var cubeIndex = 0;
  // reuse the same array and vector each loop iteration
  var tmpa = [];
  var tmpv = new THREE.Vector3();
  
  for (var x = 0; x < config.gridLength; x++) {
    for (var y = 0; y < config.gridLength; y++) {
      for (var z = 0; z < config.gridLength; z++) {

        // calculate start position spread around the x, y, and z axes, offset by half delta on the x axis
        // the x, y and z values are stored in the temporary array
        tmpa[0] = THREE.Math.mapLinear(x, 0, config.gridLength - 1, gridHalfSize, -gridHalfSize) - config.deltaX * 0.5;
        tmpa[1] = THREE.Math.mapLinear(y, 0, config.gridLength - 1, gridHalfSize, -gridHalfSize);
        tmpa[2] = THREE.Math.mapLinear(z, 0, config.gridLength - 1, gridHalfSize, -gridHalfSize);
        // geometry.setPrefabData will use the array to set the same values for each vertex in a prefab based on an index 
        geometry.setPrefabData(startPositionBuffer, cubeIndex, tmpa);
        
        // repeat the same steps for the end position
        tmpa[0] = THREE.Math.mapLinear(x, 0, config.gridLength - 1, gridHalfSize, -gridHalfSize) + config.deltaX * 0.5;
        tmpa[1] = THREE.Math.mapLinear(y, 0, config.gridLength - 1, gridHalfSize, -gridHalfSize);
        tmpa[2] = THREE.Math.mapLinear(z, 0, config.gridLength - 1, gridHalfSize, -gridHalfSize);
        
        geometry.setPrefabData(endPositionBuffer, cubeIndex, tmpa);
        
        // repeat the same steps for duration
        // in this example, the duration is the same for each prefab, so it could be set as a uniform
        tmpa[0] = config.duration;
    
        geometry.setPrefabData(durationBuffer, cubeIndex, tmpa);
        
        // repeat the sa5me steps for start time
        tmpa[0] = (config.totalDelay / cubeCount) * cubeIndex;
        
        geometry.setPrefabData(startTimeBuffer, cubeIndex, tmpa);
        
        // calculate rotation using a random axis and a rotation of 360 degrees around that axis
        
        // get a random axis
        THREE.BAS.Utils.randomAxis(tmpv);
        // store the x, y and z values in the array
        tmpv.toArray(tmpa);
        // set the rotation to 2 PI (can be anything)
        tmpa[3] = Math.PI * 4;
        
        geometry.setPrefabData(rotationBuffer, cubeIndex, tmpa);
        
        // increment the cubeIndex for the next iteration
        cubeIndex++;
      }
    }
  }
  
  // create the animation material
  // it 'extends' THREE.MeshPhongMaterial by injecting arbitrary GLSL code at key places in the shader code
  material = new THREE.BAS.PhongAnimationMaterial({
    shading: THREE.FlatShading,
    // define a time uniform that will control the state of the animation
    // the uniform will be the same for each vertex
    uniforms: {
      time: {value: 0}
    },
    // add GLSL definitions for the uniform and the 4 attributes we defined on the geometry
    // the names and types must be the same as defined above
    // we use vec3 for attributes with an item size of 3
    // we use float for attributes with an item size of 1
    vertexParameters: [
      'uniform float time;',
      
      'attribute vec3 startPosition;',
      'attribute vec3 endPosition;',
      'attribute float startTime;',
      'attribute float duration;',
      // don't forget to add the new rotation attribute here!
      // also note the type vec4
      'attribute vec4 rotation;',
    ],
    // add definitions for functions to be used in the vertex shader
    vertexFunctions: [
      // the ease functions follow an underscore deliminated naming convention.
      THREE.BAS.ShaderChunk['ease_cubic_in_out'],
      // quatFromAxisAngle and rotateVector functions
      THREE.BAS.ShaderChunk['quaternion_rotation']
    ],
    // add the GLSL animation update logic
    vertexPosition: [
      // progress is calculated based on the time uniform, and the duration and startTime attributes
      'float progress = clamp(time - startTime, 0.0, duration) / duration;',
      
      // use the single argument variant of the easing function to ease the progress
      // the function names are camel cased by convention
      'progress = easeCubicInOut(progress);',
      
      // calculate the quaternion representing the desired rotation
      // we use the axis stored in the attribute and calculate rotation based on progress
      'vec4 quat = quatFromAxisAngle(rotation.xyz, rotation.w * progress);',

      // 'transformed' is a variable defined by THREE.js.
      // it is used throughout the vertex shader to transform the vertex position
      
      // rotate the vertex by applying the quaternion
      'transformed = rotateVector(quat, transformed);',
      
      // scale based on progress
      // progress 0.0 = scale 0.0
      // progress 0.5 = scale 1.0
      // progress 1.0 = scale 0.0
      'float scl = progress * 2.0 - 1.0;',
      'scl = 1.0 - scl * scl;',
      'transformed *= scl;',
      
      // 'mix' is a built-in GLSL method that performs linear interpolation
      'transformed += mix(startPosition, endPosition, progress);'
    ]
  });
  
  // once the geometry and metrials are defined we can use them to create one single mesh, and add it to the scene
  cubes = new THREE.Mesh(geometry, material);
  scene.add(cubes);
}

function dispose() {
  cubes && scene.remove(cubes);
  geometry && geometry.dispose();
  material && material.dispose();
}

createCubes();

// loop

// var stats = new Stats();
// document.body.appendChild(stats.dom);

function tick() {
  // stats.begin();
  update();
  render();
  // stats.end();
  
  requestAnimationFrame(tick);
}

function update() {
  // instead of updating a global time variable, we update the time uniform defined on the material
  
  // increment global time
  cubes.material.uniforms.time.value += 1/60;
  // reset time when it exceeds the total duration
  cubes.material.uniforms.time.value %= (config.duration + config.totalDelay);
}

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

requestAnimationFrame(tick);

// resize

function resize() {
  var w = window.innerWidth;
  var h = window.innerHeight;
  
  renderer.setSize(w, h);
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
}

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

// html controls

var btnIncr = document.querySelector('.btn.incr');
var btnDecr = document.querySelector('.btn.decr');
var counter = document.querySelector('.count');

btnIncr.addEventListener('click', function() {
  config.gridLength++;

  updateCount();
  createCubes();
});

btnDecr.addEventListener('click', function() {
  if (config.gridLength === 4) return;
  
  config.gridLength = Math.min(4, config.gridLength - 1);

  updateCount();
  createCubes();
});

function updateCount() {
  counter.innerHTML = 'grid: ' + config.gridLength + ', total: ' + (config.gridLength * config.gridLength * config.gridLength);
} 

updateCount();

              
            
!
999px

Console