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

01 <!DOCTYPE html>
02 <html lang="en" >
03   <head>
04     <meta charset="utf-8" />
05     <meta name="author" content="Script Tutorials" />
06     <title>WebGL With Three.js - Lesson 10 - Drag and Drop Objects | Script Tutorials</title>
07     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
08     <link href="css/main.css" rel="stylesheet" type="text/css" />
09   </head>
10   <body>
11     <script src="js/three.min.71.js"></script>
12     <script src="js/THREEx.WindowResize.js"></script>
13     <script src="js/OrbitControls.js"></script>
14     <script src="js/stats.min.js"></script>
15     <script src="js/script.js"></script>
16   </body>
17 </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

001 var lesson10 = {
002   scene: null, camera: null, renderer: null,
003   container: null, controls: null,
004   clock: null, stats: null,
005   plane: null, selection: null, offset: new THREE.Vector3(), objects: [],
006   raycaster: new THREE.Raycaster(),
007
008   init: function() {
009
010     // Create main scene
011     this.scene = new THREE.Scene();
012     this.scene.fog = new THREE.FogExp2(0xcce0ff, 0.0003);
013
014     var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight;
015
016     // Prepare perspective camera
017     var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 1, FAR = 1000;
018     this.camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
019     this.scene.add(this.camera);
020     this.camera.position.set(100, 0, 0);
021     this.camera.lookAt(new THREE.Vector3(0,0,0));
022
023     // Prepare webgl renderer
024     this.renderer = new THREE.WebGLRenderer({ antialias:true });
025     this.renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
026     this.renderer.setClearColor(this.scene.fog.color);
027
028     // Prepare container
029     this.container = document.createElement('div');
030     document.body.appendChild(this.container);
031     this.container.appendChild(this.renderer.domElement);
032
033     // Events
034     THREEx.WindowResize(this.renderer, this.camera);
035     document.addEventListener('mousedown'this.onDocumentMouseDown, false);
036     document.addEventListener('mousemove'this.onDocumentMouseMove, false);
037     document.addEventListener('mouseup'this.onDocumentMouseUp, false);
038
039     // Prepare Orbit controls
040     this.controls = new THREE.OrbitControls(this.camera);
041     this.controls.target = new THREE.Vector3(0, 0, 0);
042     this.controls.maxDistance = 150;
043
044     // Prepare clock
045     this.clock = new THREE.Clock();
046
047     // Prepare stats
048     this.stats = new Stats();
049     this.stats.domElement.style.position = 'absolute';
050     this.stats.domElement.style.left = '50px';
051     this.stats.domElement.style.bottom = '50px';
052     this.stats.domElement.style.zIndex = 1;
053     this.container.appendChild( this.stats.domElement );
054
055     // Add lights
056     this.scene.add( new THREE.AmbientLight(0x444444));
057
058     var dirLight = new THREE.DirectionalLight(0xffffff);
059     dirLight.position.set(200, 200, 1000).normalize();
060     this.camera.add(dirLight);
061     this.camera.add(dirLight.target);
062
063     ....
064   },
065   addSkybox: function() {
066     ....
067   },
068   onDocumentMouseDown: function (event) {
069     ....
070   },
071   onDocumentMouseMove: function (event) {
072     ....
073   },
074   onDocumentMouseUp: function (event) {
075     ....
076   }
077 };
078
079 // Animate the scene
080 function animate() {
081   requestAnimationFrame(animate);
082   render();
083   update();
084 }
085
086 // Update controls and stats
087 function update() {
088   var delta = lesson10.clock.getDelta();
089
090   lesson10.controls.update(delta);
091   lesson10.stats.update();
092 }
093
094 // Render the scene
095 function render() {
096   if (lesson10.renderer) {
097     lesson10.renderer.render(lesson10.scene, lesson10.camera);
098   }
099 }
100
101 // Initialize lesson on page load
102 function initializeLesson() {
103   lesson10.init();
104   animate();
105 }
106
107 if (window.addEventListener)
108   window.addEventListener('load', initializeLesson, false);
109 else if (window.attachEvent)
110   window.attachEvent('onload', initializeLesson);
111 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:

01 sbVertexShader = [
02 "varying vec3 vWorldPosition;",
03 "void main() {",
04 "  vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
05 "  vWorldPosition = worldPosition.xyz;",
06 "  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
07 "}",
08 ].join("\n");
09
10 sbFragmentShader = [
11 "uniform vec3 topColor;",
12 "uniform vec3 bottomColor;",
13 "uniform float offset;",
14 "uniform float exponent;",
15 "varying vec3 vWorldPosition;",
16 "void main() {",
17 "  float h = normalize( vWorldPosition + offset ).y;",
18 "  gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( h, exponent ), 0.0 ) ), 1.0 );",
19 "}",
20 ].join("\n");

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

1 // Display skybox
2 this.addSkybox();

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

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

Additional objects

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

01 // Add 100 random objects (spheres)
02 var object, material, radius;
03 var objGeometry = new THREE.SphereGeometry(1, 24, 24);
04 for (var i = 0; i < 50; i++) {
05   material = new THREE.MeshPhongMaterial({color: Math.random() * 0xffffff});
06   material.transparent = true;
07   object = new THREE.Mesh(objGeometry.clone(), material);
08   this.objects.push(object);
09
10   radius = Math.random() * 4 + 2;
11   object.scale.x = radius;
12   object.scale.y = radius;
13   object.scale.z = radius;
14
15   object.position.x = Math.random() * 50 - 25;
16   object.position.y = Math.random() * 50 - 25;
17   object.position.z = Math.random() * 50 - 25;
18
19   this.scene.add(object);
20 }

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):

1 // Plane, that helps to determinate an intersection position
2 this.plane = new THREE.Mesh(new THREE.PlaneBufferGeometry(500, 500, 8, 8), new THREE.MeshBasicMaterial({color: 0xffffff}));
3 this.plane.visible = false;
4 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:

01 onDocumentMouseDown: function (event) {
02   // Get mouse position
03   var mouseX = (event.clientX / window.innerWidth) * 2 - 1;
04   var mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
05
06   // Get 3D vector from 3D mouse position using 'unproject' function
07   var vector = new THREE.Vector3(mouseX, mouseY, 1);
08   vector.unproject(lesson10.camera);
09
10   // Set the raycaster position
11   lesson10.raycaster.set( lesson10.camera.position, vector.sub( lesson10.camera.position ).normalize() );
12
13   // Find all intersected objects
14   var intersects = lesson10.raycaster.intersectObjects(lesson10.objects);
15
16   if (intersects.length > 0) {
17     // Disable the controls
18     lesson10.controls.enabled = false;
19
20     // Set the selection - first intersected object
21     lesson10.selection = intersects[0].object;
22
23     // Calculate the offset
24     var intersects = lesson10.raycaster.intersectObject(lesson10.plane);
25     lesson10.offset.copy(intersects[0].point).sub(lesson10.plane.position);
26   }
27 }

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:

01 onDocumentMouseMove: function (event) {
02   event.preventDefault();
03
04   // Get mouse position
05   var mouseX = (event.clientX / window.innerWidth) * 2 - 1;
06   var mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
07
08   // Get 3D vector from 3D mouse position using 'unproject' function
09   var vector = new THREE.Vector3(mouseX, mouseY, 1);
10   vector.unproject(lesson10.camera);
11
12   // Set the raycaster position
13   lesson10.raycaster.set( lesson10.camera.position, vector.sub( lesson10.camera.position ).normalize() );
14
15   if (lesson10.selection) {
16     // Check the position where the plane is intersected
17     var intersects = lesson10.raycaster.intersectObject(lesson10.plane);
18     // Reposition the object based on the intersection point with the plane
19     lesson10.selection.position.copy(intersects[0].point.sub(lesson10.offset));
20   else {
21     // Update position of the plane if need
22     var intersects = lesson10.raycaster.intersectObjects(lesson10.objects);
23     if (intersects.length > 0) {
24       lesson10.plane.position.copy(intersects[0].object.position);
25       lesson10.plane.lookAt(lesson10.camera.position);
26     }
27   }
28 }

onDocumentMouseUp

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

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

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


Live Demo