<!-- Three.js source -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/93/three.min.js'></script>
<!-- OrbitControls.js source-->
<script src="http://threejs.org/examples/js/controls/OrbitControls.js"></script>
body { 
  margin: 0; 
  width: 100%;
  height: 100%;
  overflow: hidden;
}

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

///////////////////////////////////////////////////
///////////   3D VERLET TRAPPED CUBE    ///////////
///////////////////////////////////////////////////



////// VERLET COMPONENTS //////


///---Initiation---///

//components
var points = [];
var spans = [];
var pointCount = 0;

//settings
var containerWidth = 300;
var cubeWidth = 50;
var cubeDropHeight = 200;
var initialSpin = 10;
var jumpStrength = 30;
var jumpFrequency = 3000;  //jump frequency in milliseconds
var jumpTimeStamp = Math.floor(Date.now());  //logs time of last jump in miliseconds
var timeNow = Math.floor(Date.now());  //tracks time in miliseconds
var rigidity = 5;  //iterations of shape-fidelity refinement
var gravity = .25;  //rate of y-velocity increase per frame
var friction = 0.999;  //proportion of previous velocity after frame refresh
var bounceLoss = 0.9;  //proportion of previous velocity after bouncing
var skidLoss = 0.5;  //proportion of previous velocity if touching the ground


///---Objects---///

//point constructor
function Point(x,y,z) {
  this.cx = x;        //current x value
  this.cy = y;        //current y value
  this.cz = z;        //current z value
  this.px = this.cx;  //previous x value
  this.py = this.cy;  //previous y value
  this.pz = this.cz;  //previous z value
  this.id = pointCount;
  pointCount += 1;
  this.outOfTheBox = false;
}

//span constructor
function Span(point1, point2) {
  this.p1 = point1;
  this.p2 = point2;
  this.l = distance(this.p1,this.p2) //length
}


///---functions---///

//builds cube's underlying verlet structure  
function buildCube(width) {
  //clears old points and spans arrays if populated from previous cube
  points = []; pointCount = 0;
  spans = [];
  //points (from center at 0,0,0)
  var hcw = cubeWidth/2;  //half cube width
  var l = cubeDropHeight-(containerWidth/2);  //change to y-values to lift cube to drop height above floor
  addPt(hcw,hcw+l,hcw); addPt(hcw,hcw+l,-hcw); addPt(hcw,-hcw+l,hcw); addPt(hcw,-hcw+l,-hcw);
  addPt(-hcw,hcw+l,-hcw); addPt(-hcw,hcw+l,hcw); addPt(-hcw,-hcw+l,-hcw); addPt(-hcw,-hcw+l,hcw); 
  //spans
  addSp(0,1); addSp(1,2); addSp(2,3); addSp(3,0);  //bottom edges
  addSp(0,4); addSp(1,5); addSp(2,6); addSp(3,7);  //side edges
  addSp(4,5); addSp(5,6); addSp(6,7); addSp(7,4);  //top edges
  addSp(0,2); addSp(0,5); addSp(1,6); addSp(2,7); addSp(3,4); addSp(4,6);  //surface braces
  addSp(0,6); addSp(1,7); addSp(2,4); addSp(3,5);  //internal braces 
  //initial spin
  spin(initialSpin);
  //resets jump timestamps
  jumpTimeStamp = Math.floor(Date.now());  //logs time of last jump in miliseconds
  timeNow = Math.floor(Date.now());  //tracks time in miliseconds
}

//gets a point by id number
function getPt(id) {
  for (var i=0; i<points.length; i++) { 
    if (points[i].id == id) { return points[i]; }
  }
}

//gets distance between two points (pythogorian theorum twice)
function distance(point_1, point_2) { 
  var x_difference = point_2.cx - point_1.cx;
  var	y_difference = point_2.cy - point_1.cy;
  var z_difference = point_2.cz - point_1.cz;
  var xy_distance = Math.sqrt( x_difference*x_difference + y_difference*y_difference);
  var xz_distance = Math.sqrt( xy_distance*xy_distance + z_difference*z_difference);
  return xz_distance;
}

//generates random number between a minimum and maximum value
function randNumBetween(min,max) {
  return Math.floor(Math.random()*(max-min+1))+min;
}

//creates a point object instance
function addPt(x,y,z) {
  points.push( new Point(x,y,z) );
}

//creates a span object instance
function addSp(point1id,point2id) {
  spans.push( new Span( getPt(point1id), getPt(point2id) ) );
}

//updates coordinate positions
function updateVerlet() {
  updatePoints();
  //refines point positions for position accuracy & shape rigidity
  for (var i=0; i<rigidity; i++) {
    bounce();
    updateSpans();
  }
}

//updates points based on verlet velocity (i.e., current coord minus previous coord)
function updatePoints() {
  for(var i=0; i<points.length; i++) {
    var p = points[i];  //point object
    var	xv = (p.cx - p.px) * friction;	//x velocity
    var	yv = (p.cy - p.py) * friction;	//y velocity
    var	zv = (p.cz - p.pz) * friction;	//z velocity
    var floorY = -(containerWidth/2)
    if (p.py < floorY+1 && p.py >= floorY) { xv *= skidLoss; zv *= skidLoss };
    p.px = p.cx;  //updates previous x as current x
    p.py = p.cy;  //updates previous y as current y
    p.pz = p.cz;  //updates previous z as current z
    p.cx += xv;  //updates current x with new velocity
    p.cy += yv;  //updates current y with new velocity
    p.cz += zv;  //updates current z with new velocity
    p.cy -= gravity;  //adds gravity to y
  }
}

//inverts velocity for bounce if a point reaches floor or wall
function bounce() {
  for (var i=0; i<points.length; i++) {
    var p = points[i];
    //tracks whether point is out of the box or not
    var hcw = containerWidth/2;  //half coontainer width
    if (p.cy > hcw + cubeWidth) {
      p.outOfTheBox = true;
    } else if (p.cx < hcw && p.cx > -hcw && p.cy > -hcw && p.cz < hcw && p.cz > -hcw) {
      p.outOfTheBox = false;
    }
    //left/right walls
    if (p.cx > hcw && cubeInTheBox() ) {
      p.cx = hcw;
      p.px = p.cx + (p.cx - p.px) * bounceLoss;
    } else if (p.cx < -hcw && cubeInTheBox() ) {
      p.cx = -hcw;
      p.px = p.cx + (p.cx - p.px) * bounceLoss;
    }
    //floor
    if (p.cy < -hcw && cubeInTheBox() ) {
      p.cy = -hcw;
      p.py = p.cy + (p.cy - p.py) * bounceLoss;
    }
    //front/back walls
    if (p.cz > hcw && cubeInTheBox() ) {
      p.cz = hcw;
      p.pz = p.cz + (p.cz - p.pz) * bounceLoss;
    } else if (p.cz < -hcw && cubeInTheBox() ) {
      p.cz = -hcw;
      p.pz = p.cz + (p.cz - p.pz) * bounceLoss;
    }
  }
}

//updates spans between points
function updateSpans() {
  for (var i=0; i<spans.length; i++) {
    var s = spans[i];
    var dx = s.p2.cx - s.p1.cx;  // distance between x values
    var	dy = s.p2.cy - s.p1.cy;  // distance between y values
    var	dz = s.p2.cz - s.p1.cz;  // distance between z values
    var d = distance(s.p1, s.p2)  // distance between the points
    var	r = s.l / d;	// ratio (span length over distance between points)
    var	mx = s.p1.cx + dx / 2;  // midpoint between x values 
    var my = s.p1.cy + dy / 2;  // midpoint between y values
    var mz = s.p1.cz + dz / 2;  // midpoint between z values
    var ox = dx / 2 * r;  // offset of each x value (compared to span length)
    var oy = dy / 2 * r;  // offset of each y value (compared to span length)
    var oz = dz / 2 * r;  // offset of each z value (compared to span length)
    s.p1.cx = mx - ox;  // updates span's first point x value
    s.p1.cy = my - oy;  // updates span's first point y value
    s.p1.cz = mz - oz;  // updates span's first point z value
    s.p2.cx = mx + ox;  // updates span's second point x value
    s.p2.cy = my + oy;  // updates span's second point y value
    s.p2.cz = mz + oz;  // updates span's second point z value
  }
}

//adds spin
function spin(intensity) {
  points[0].px = points[0].cx+randNumBetween(-intensity,intensity);
  points[0].py = points[0].cy+randNumBetween(-intensity,intensity);
  points[0].pz = points[0].cz+randNumBetween(-intensity,intensity);
  points[6].px = points[6].cx+randNumBetween(-intensity,intensity);
  points[6].py = points[6].cy+randNumBetween(-intensity,intensity);
  points[6].pz = points[6].cz+randNumBetween(-intensity,intensity);
}

//checks whether cube is in the box
function cubeInTheBox() {
  var allPointsIn = true;
  for (i=0; i<points.length; i++) {
    if (points[i].outOfTheBox == true) { allPointsIn = false } 
  }
  return allPointsIn;
}

//makes cube jump
function jump(intensity,frequency) {
  timeNow = Math.floor(Date.now());
  if (timeNow-jumpTimeStamp>frequency) { 
    //upward motion
    points[0].py = points[0].cy+randNumBetween(-intensity,-intensity/2);
    points[1].py = points[1].cy+randNumBetween(-intensity,-intensity/2);
    points[2].py = points[2].cy+randNumBetween(-intensity,-intensity/2);
    points[3].py = points[3].cy+randNumBetween(-intensity,-intensity/2);
    //leftward/rightward motion
    points[0].px = points[0].cx+randNumBetween(-intensity,intensity);
    //forward/backward motion
    points[0].pz = points[0].cz+randNumBetween(-intensity,intensity);
    //resets timer
    jumpTimeStamp = Math.floor(Date.now());
  }
}







////// THREE.JS COMPONENTS //////

///---Initiation---///

//object variables
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 100000 );
var renderer = new THREE.WebGLRenderer( { antialias: true } );
controls = new THREE.OrbitControls(camera, renderer.domElement);

//background
renderer.setClearColor (0x70d0ff, 1);

//html
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

//camera
camera.position.y = 500;
camera.position.x = 0;
camera.position.z = 1000;
camera.rotation.x = -0.48; //(in radians)
camera.zoom += 1.2;

//cube
var geometry = new THREE.BoxGeometry( 20, 20, 20);
var material = new THREE.MeshLambertMaterial( { color: 0xdb471e, wireframe: false } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );

//container
var geometry = new THREE.BoxGeometry( containerWidth, containerWidth, containerWidth);
var material = new THREE.MeshPhongMaterial( { 
  side: THREE.DoubleSide,
  color: 0xCCCCCC, 
  transparent: true, 
  opacity: 0.25,
  depthWrite: false
} );
var container = new THREE.Mesh( geometry, material );
container.geometry.faces.splice(4, 2);  //removes container top
scene.add( container );

//light sources
var lightP1 = new THREE.PointLight( 0xFFFFFF, .25, 0, 0 );
lightP1.position.set( 0, 0, 0 );
var lightP2 = new THREE.PointLight( 0xFFFFFF, .25, 0, 2 );
lightP2.position.set( 500, 0, 500 );
var lightP3 = new THREE.PointLight( 0xFFFFFF, .25, 0, 2 );
lightP3.position.set( -500, 0, 500 );
var lightA1 = new THREE.AmbientLight( 0xFFFFFF, 0.75 );
scene.add( lightP1 );
scene.add( lightP2 );
scene.add( lightP3 );
scene.add( lightA1 );


///---functions---///

//sets vertices to verlet point positions
function updateVertices() {
  for (var i=0; i<points.length; i++) {
    cube.geometry.vertices[i].x = points[i].cx;
    cube.geometry.vertices[i].y = points[i].cy;
    cube.geometry.vertices[i].z = points[i].cz;
  }
}

///---rendering---///

function render() {  
  cube.geometry.verticesNeedUpdate = true;
  if ( points[0].cy < -50000 ) { buildCube(cubeWidth); }
  updateVerlet();
  updateVertices();
  if ( cubeInTheBox() ) { jump(jumpStrength,jumpFrequency); }
  camera.updateProjectionMatrix();
  renderer.render(scene,camera);
  requestAnimationFrame(render);
};

buildCube(cubeWidth);
render();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.