<script src="https://threejs.org/build/three.js"></script>

<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

<div id="scene-container"></div>
body {
  margin: 0px;
  overflow: hidden;
  color: white;
  text-align: center;
}

h1 {
  position: absolute;
  width: 100%;
  z-index: 1;
  font-size: 1.5rem;
}

a {
  color: white;
}
a:hover{
  color: purple;
}

#scene-container {
  position: absolute;
  width: 100%;
  height: 100%;
}
function init() {

  const container = document.querySelector( '#scene-container' );

  const scene = new THREE.Scene();
  scene.background = new THREE.Color( 0x8FBCD4 );

  const camera = new THREE.PerspectiveCamera( 35, container.clientWidth / container.clientHeight, 0.1, 1000 );
  camera.position.set( -40, 40, 10 );
  const controls = new THREE.OrbitControls( camera, container );
  
  const lights = createLights();
  const materials = createMaterials();
  const meshes = createMeshes( materials );
  
  scene.add(
    
    lights.ambient,
    lights.main,

    ...meshes.boxes,
    
  );
  
  const renderer = createRenderer( container );
  
  setupOnWindowResize( camera, container, renderer );

  renderer.setAnimationLoop( () => {
    
    renderer.render( scene, camera );

  } );
  
  setupSelectAndZoom( camera, container, controls, materials, meshes );

}

function createLights() {

  const ambient = new THREE.HemisphereLight( 0xddeeff, 0x0f0e0d, 5 );

  const main = new THREE.DirectionalLight( 0xffffff, 5 );
  main.position.set( 10, 10, 10 );

  return {
    ambient,
    main,
  }

}

function createMaterials() {

  const main = new THREE.MeshStandardMaterial( {
    color: 0xcccccc,
    flatShading: true,
    transparent: true,
    opacity: 0.8,
  } );

  main.color.convertSRGBToLinear();

  const highlight = new THREE.MeshStandardMaterial( {
    color: 0xff4444,
    flatShading: true,
  } );

  highlight.color.convertSRGBToLinear();

  return {

    main,
    highlight,

  };

}

function createMeshes( materials ) {
  
  const geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
  geometry.translate( 0, 0.5, 0 );
  const boxes = [];

  for ( let i = 0; i < 500; i ++ ) {
    
    const mesh = new THREE.Mesh( geometry, materials.main );
    mesh.position.x = Math.random() * 60 - 30;
    mesh.position.y = 0;
    mesh.position.z = Math.random() * 60 - 30;
    mesh.scale.y = Math.random() * 10;
    mesh.updateMatrix();
    mesh.matrixAutoUpdate = false;
    
    boxes.push( mesh );
    
  }
  
  return {
    boxes,
  };
}

function createRenderer( container ) {

  const renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setSize( container.clientWidth, container.clientHeight );

  renderer.setPixelRatio( window.devicePixelRatio );

  renderer.gammaFactor = 2.2;
  renderer.gammaOutput = true;

  renderer.physicallyCorrectLights = true;

  container.appendChild( renderer.domElement );
  
  return renderer;

}

function setupOnWindowResize( camera, container, renderer ) {
  
  window.addEventListener( 'resize', () => {
                          
    camera.aspect = container.clientWidth / container.clientHeight;
    camera.updateProjectionMatrix();

    renderer.setSize( container.clientWidth, container.clientHeight );
  
  } );

}

init();


function setupSelectAndZoom( camera, container, controls, materials, meshes ) {
  
  const selection = [];
  
  let isDragging = false;
  const mouse = new THREE.Vector2();
  const raycaster = new THREE.Raycaster();
  
  container.addEventListener('mousedown', () => { 
    isDragging = false;
  }, false );
  
  container.addEventListener('mousemove', () => {
    isDragging = true;
  }, false );
  
  window.addEventListener('mouseup', ( event ) => {
    
    if ( isDragging ) {
      isDragging = false;
      return;
    }
    
    isDragging = false;
    
    mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
    raycaster.setFromCamera( mouse, camera );
    
    const intersects = raycaster.intersectObjects( meshes.boxes );
    
    if ( intersects.length > 0 ) {
      
      const mesh = intersects[ 0 ].object;
      
      if ( selection.includes( mesh ) ) {
        
        mesh.material = materials.main;
        selection.splice( selection.indexOf( mesh ), 1 );
        
      } else {
        
        selection.push( mesh );
        mesh.material = materials.highlight;
        
      }
      
      if( selection.length > 0 ) zoomCameraToSelection( camera, controls, selection );

    }
    
    
    
  }, false );

}

function zoomCameraToSelection( camera, controls, selection, fitRatio = 1.2 ) {
  
  const box = new THREE.Box3();
  
  for( const object of selection ) box.expandByObject( object );
  
  const size = box.getSize( new THREE.Vector3() );
  const center = box.getCenter( new THREE.Vector3() );
  
  const maxSize = Math.max( size.x, size.y, size.z );
  const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * camera.fov / 360 ) );
  const fitWidthDistance = fitHeightDistance / camera.aspect;
  const distance = fitRatio * Math.max( fitHeightDistance, fitWidthDistance );
  
  const direction = controls.target.clone()
    .sub( camera.position )
    .normalize()
    .multiplyScalar( distance );

  controls.maxDistance = distance * 10;
  controls.target.copy( center );
  
  camera.near = distance / 100;
  camera.far = distance * 100;
  camera.updateProjectionMatrix();

  camera.position.copy( controls.target ).sub(direction);
  
  controls.update();
  
}
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.