Three.js – Drag and Drop Objects

Three.js – Drag and Drop Objects

1 235110
Three.js – Drag and Drop Objects

The Drag and Drop feature is one of important features of nearly every interactive environment. Use the mouse to interact and drag and drop objects is very native. However this feature is not supported in three.js by default. In our tenth lesson we explain how to implement this drag-and-drop function.

Live Demo

The demo generates randomly located spheres that you can move with your mouse. Simply drag a sphere, move it to another place, and then drop it. To rotate the scene, you can use the mouse as well (we use THREE.OrbitControls).

Preparation

To start with, please create a new index.html file with the following code:

index.html

<!DOCTYPE html>
<html lang="en" >
  <head>
    <meta charset="utf-8" />
    <meta name="author" content="Script Tutorials" />
    <title>WebGL With Three.js - Lesson 10 - Drag and Drop Objects | Script Tutorials</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <link href="css/main.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <script src="js/three.min.71.js"></script>
    <script src="js/THREEx.WindowResize.js"></script>
    <script src="js/OrbitControls.js"></script>
    <script src="js/stats.min.js"></script>
    <script src="js/script.js"></script>
  </body>
</html>

This basic code connects all the necessary libraries.

Skeleton of the scene

Now create another file – ‘script.js’ and put it into ‘js’ folder. Place the following code into the file:

js/script.js

var lesson10 = {
  scene: null, camera: null, renderer: null,
  container: null, controls: null,
  clock: null, stats: null,
  plane: null, selection: null, offset: new THREE.Vector3(), objects: [],
  raycaster: new THREE.Raycaster(),
  init: function() {
    // Create main scene
    this.scene = new THREE.Scene();
    this.scene.fog = new THREE.FogExp2(0xcce0ff, 0.0003);
    var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight;
    // Prepare perspective camera
    var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 1, FAR = 1000;
    this.camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
    this.scene.add(this.camera);
    this.camera.position.set(100, 0, 0);
    this.camera.lookAt(new THREE.Vector3(0,0,0));
    // Prepare webgl renderer
    this.renderer = new THREE.WebGLRenderer({ antialias:true });
    this.renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
    this.renderer.setClearColor(this.scene.fog.color);
    // Prepare container
    this.container = document.createElement('div');
    document.body.appendChild(this.container);
    this.container.appendChild(this.renderer.domElement);
    // Events
    THREEx.WindowResize(this.renderer, this.camera);
    document.addEventListener('mousedown', this.onDocumentMouseDown, false);
    document.addEventListener('mousemove', this.onDocumentMouseMove, false);
    document.addEventListener('mouseup', this.onDocumentMouseUp, false);
    // Prepare Orbit controls
    this.controls = new THREE.OrbitControls(this.camera);
    this.controls.target = new THREE.Vector3(0, 0, 0);
    this.controls.maxDistance = 150;
    // Prepare clock
    this.clock = new THREE.Clock();
    // Prepare stats
    this.stats = new Stats();
    this.stats.domElement.style.position = 'absolute';
    this.stats.domElement.style.left = '50px';
    this.stats.domElement.style.bottom = '50px';
    this.stats.domElement.style.zIndex = 1;
    this.container.appendChild( this.stats.domElement );
    // Add lights
    this.scene.add( new THREE.AmbientLight(0x444444));
    var dirLight = new THREE.DirectionalLight(0xffffff);
    dirLight.position.set(200, 200, 1000).normalize();
    this.camera.add(dirLight);
    this.camera.add(dirLight.target);
    ....
  },
  addSkybox: function() {
    ....
  },
  onDocumentMouseDown: function (event) {
    ....
  },
  onDocumentMouseMove: function (event) {
    ....
  },
  onDocumentMouseUp: function (event) {
    ....
  }
};
// Animate the scene
function animate() {
  requestAnimationFrame(animate);
  render();
  update();
}
// Update controls and stats
function update() {
  var delta = lesson10.clock.getDelta();
  lesson10.controls.update(delta);
  lesson10.stats.update();
}
// Render the scene
function render() {
  if (lesson10.renderer) {
    lesson10.renderer.render(lesson10.scene, lesson10.camera);
  }
}
// Initialize lesson on page load
function initializeLesson() {
  lesson10.init();
  animate();
}
if (window.addEventListener)
  window.addEventListener('load', initializeLesson, false);
else if (window.attachEvent)
  window.attachEvent('onload', initializeLesson);
else window.onload = initializeLesson;

With this code, we prepared the scene – camera (THREE.PerspectiveCamera), renderer (THREE.WebGLRenderer), controller (THREE.OrbitControls), light (THREE.DirectionalLight), and various event handlers (empty for now, we will add it’s code further).

Skybox

Now, let’s add the skybox – blue-white gradient with shader. First of all, add two shaders in the beginning of our script.js file:

sbVertexShader = [
"varying vec3 vWorldPosition;",
"void main() {",
"  vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
"  vWorldPosition = worldPosition.xyz;",
"  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
"}",
].join("\n");
sbFragmentShader = [
"uniform vec3 topColor;",
"uniform vec3 bottomColor;",
"uniform float offset;",
"uniform float exponent;",
"varying vec3 vWorldPosition;",
"void main() {",
"  float h = normalize( vWorldPosition + offset ).y;",
"  gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( h, exponent ), 0.0 ) ), 1.0 );",
"}",
].join("\n");

So both shaders (vertex and fragment shaders) will be added to our page. Now we can add

    // Display skybox
    this.addSkybox();

right after we added our light. Here is the code for the ‘addSkybox’ function:

  addSkybox: function() {
    var iSBrsize = 500;
    var uniforms = {
      topColor: {type: "c", value: new THREE.Color(0x0077ff)}, bottomColor: {type: "c", value: new THREE.Color(0xffffff)},
      offset: {type: "f", value: iSBrsize}, exponent: {type: "f", value: 1.5}
    }
    var skyGeo = new THREE.SphereGeometry(iSBrsize, 32, 32);
    skyMat = new THREE.ShaderMaterial({vertexShader: sbVertexShader, fragmentShader: sbFragmentShader, uniforms: uniforms, side: THREE.DoubleSide, fog: false});
    skyMesh = new THREE.Mesh(skyGeo, skyMat);
    this.scene.add(skyMesh);
  },

Additional objects

After the skybox, we can add spheres with random radius and position:

    // Add 100 random objects (spheres)
    var object, material, radius;
    var objGeometry = new THREE.SphereGeometry(1, 24, 24);
    for (var i = 0; i < 50; i++) {
      material = new THREE.MeshPhongMaterial({color: Math.random() * 0xffffff});
      material.transparent = true;
      object = new THREE.Mesh(objGeometry.clone(), material);
      this.objects.push(object);
      radius = Math.random() * 4 + 2;
      object.scale.x = radius;
      object.scale.y = radius;
      object.scale.z = radius;
      object.position.x = Math.random() * 50 - 25;
      object.position.y = Math.random() * 50 - 25;
      object.position.z = Math.random() * 50 - 25;
      this.scene.add(object);
    }

As you see, all the objects were added to the ‘objects’ array. We will use this array for raycaster (THREE.Raycaster) to determinate if an object is intersected with our mouse.

Now pay attention, in order to implement the drag and drop function, we need to determine at what axis (plane) we need to move the selected object. As we know, mouse moves in two dimensions, while our scene works in three. To find an offset of dragging, we will use an invisible ‘helper’ – plane (add this code below the code where we add the spheres):

    // Plane, that helps to determinate an intersection position
    this.plane = new THREE.Mesh(new THREE.PlaneBufferGeometry(500, 500, 8, 8), new THREE.MeshBasicMaterial({color: 0xffffff}));
    this.plane.visible = false;
    this.scene.add(this.plane);

onDocumentMouseMove

Now we need to implement the first event handler: onDocumentMouseMove. When we press the mouse button, we need to define position where we pressed the key, then we create a 3D vector of this point, unproject it, set the raycaster position, and find all intersected objects (where we clicked the mouse button). Then we disable the controls (we don’t need to rotate the scene while we are dragging the selection). The first visible element will be set as the selection, and we need to save the offset:

  onDocumentMouseDown: function (event) {
    // Get mouse position
    var mouseX = (event.clientX / window.innerWidth) * 2 - 1;
    var mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
    // Get 3D vector from 3D mouse position using 'unproject' function
    var vector = new THREE.Vector3(mouseX, mouseY, 1);
    vector.unproject(lesson10.camera);
    // Set the raycaster position
    lesson10.raycaster.set( lesson10.camera.position, vector.sub( lesson10.camera.position ).normalize() );
    // Find all intersected objects
    var intersects = lesson10.raycaster.intersectObjects(lesson10.objects);
    if (intersects.length > 0) {
      // Disable the controls
      lesson10.controls.enabled = false;
      // Set the selection - first intersected object
      lesson10.selection = intersects[0].object;
      // Calculate the offset
      var intersects = lesson10.raycaster.intersectObject(lesson10.plane);
      lesson10.offset.copy(intersects[0].point).sub(lesson10.plane.position);
    }
  }

onDocumentMouseMove

Having the selection (sphere), we can change position of the sphere (to another position where our mouse pointer is). But if there is no any selection, we need to update position of our help plane. It always needs to look directly at our camera position:

  onDocumentMouseMove: function (event) {
    event.preventDefault();
    // Get mouse position
    var mouseX = (event.clientX / window.innerWidth) * 2 - 1;
    var mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
    // Get 3D vector from 3D mouse position using 'unproject' function
    var vector = new THREE.Vector3(mouseX, mouseY, 1);
    vector.unproject(lesson10.camera);
    // Set the raycaster position
    lesson10.raycaster.set( lesson10.camera.position, vector.sub( lesson10.camera.position ).normalize() );
    if (lesson10.selection) {
      // Check the position where the plane is intersected
      var intersects = lesson10.raycaster.intersectObject(lesson10.plane);
      // Reposition the object based on the intersection point with the plane
      lesson10.selection.position.copy(intersects[0].point.sub(lesson10.offset));
    } else {
      // Update position of the plane if need
      var intersects = lesson10.raycaster.intersectObjects(lesson10.objects);
      if (intersects.length > 0) {
        lesson10.plane.position.copy(intersects[0].object.position);
        lesson10.plane.lookAt(lesson10.camera.position);
      }
    }
  }

onDocumentMouseUp

When we release our mouse button, we only need to enable the controls again, and reset the selection:

  onDocumentMouseUp: function (event) {
    // Enable the controls
    lesson10.controls.enabled = true;
    lesson10.selection = null;
  }

That’s it for today. Hope you find our tutorial useful.


Live Demo

[sociallocker]

download in package

[/sociallocker]

SIMILAR ARTICLES

Understanding Closures

0 23115

1 COMMENT

Leave a Reply