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.
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
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" /> |
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 > |
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
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(), |
011 |
this .scene = new THREE.Scene(); |
012 |
this .scene.fog = new THREE.FogExp2(0xcce0ff, 0.0003); |
014 |
var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight; |
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)); |
024 |
this .renderer = new THREE.WebGLRenderer({ antialias: true }); |
025 |
this .renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); |
026 |
this .renderer.setClearColor( this .scene.fog.color); |
029 |
this .container = document.createElement( 'div' ); |
030 |
document.body.appendChild( this .container); |
031 |
this .container.appendChild( this .renderer.domElement); |
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 ); |
040 |
this .controls = new THREE.OrbitControls( this .camera); |
041 |
this .controls.target = new THREE.Vector3(0, 0, 0); |
042 |
this .controls.maxDistance = 150; |
045 |
this .clock = new THREE.Clock(); |
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 ); |
056 |
this .scene.add( new THREE.AmbientLight(0x444444)); |
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); |
065 |
addSkybox: function () { |
068 |
onDocumentMouseDown: function (event) { |
071 |
onDocumentMouseMove: function (event) { |
074 |
onDocumentMouseUp: function (event) { |
081 |
requestAnimationFrame(animate); |
088 |
var delta = lesson10.clock.getDelta(); |
090 |
lesson10.controls.update(delta); |
091 |
lesson10.stats.update(); |
096 |
if (lesson10.renderer) { |
097 |
lesson10.renderer.render(lesson10.scene, lesson10.camera); |
102 |
function initializeLesson() { |
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:
02 |
"varying vec3 vWorldPosition;" , |
04 |
" vec4 worldPosition = modelMatrix * vec4( position, 1.0 );" , |
05 |
" vWorldPosition = worldPosition.xyz;" , |
06 |
" gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );" , |
11 |
"uniform vec3 topColor;" , |
12 |
"uniform vec3 bottomColor;" , |
13 |
"uniform float offset;" , |
14 |
"uniform float exponent;" , |
15 |
"varying vec3 vWorldPosition;" , |
17 |
" float h = normalize( vWorldPosition + offset ).y;" , |
18 |
" gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( h, exponent ), 0.0 ) ), 1.0 );" , |
So both shaders (vertex and fragment shaders) will be added to our page. Now we can add
right after we added our light. Here is the code for the ‘addSkybox’ function:
01 |
addSkybox: function () { |
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} |
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); |
Additional objects
After the skybox, we can add spheres with random radius and position:
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); |
10 |
radius = Math.random() * 4 + 2; |
11 |
object.scale.x = radius; |
12 |
object.scale.y = radius; |
13 |
object.scale.z = radius; |
15 |
object.position.x = Math.random() * 50 - 25; |
16 |
object.position.y = Math.random() * 50 - 25; |
17 |
object.position.z = Math.random() * 50 - 25; |
19 |
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):
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) { |
03 |
var mouseX = (event.clientX / window.innerWidth) * 2 - 1; |
04 |
var mouseY = -(event.clientY / window.innerHeight) * 2 + 1; |
07 |
var vector = new THREE.Vector3(mouseX, mouseY, 1); |
08 |
vector.unproject(lesson10.camera); |
11 |
lesson10.raycaster.set( lesson10.camera.position, vector.sub( lesson10.camera.position ).normalize() ); |
14 |
var intersects = lesson10.raycaster.intersectObjects(lesson10.objects); |
16 |
if (intersects.length > 0) { |
18 |
lesson10.controls.enabled = false ; |
21 |
lesson10.selection = intersects[0].object; |
24 |
var intersects = lesson10.raycaster.intersectObject(lesson10.plane); |
25 |
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:
01 |
onDocumentMouseMove: function (event) { |
02 |
event.preventDefault(); |
05 |
var mouseX = (event.clientX / window.innerWidth) * 2 - 1; |
06 |
var mouseY = -(event.clientY / window.innerHeight) * 2 + 1; |
09 |
var vector = new THREE.Vector3(mouseX, mouseY, 1); |
10 |
vector.unproject(lesson10.camera); |
13 |
lesson10.raycaster.set( lesson10.camera.position, vector.sub( lesson10.camera.position ).normalize() ); |
15 |
if (lesson10.selection) { |
17 |
var intersects = lesson10.raycaster.intersectObject(lesson10.plane); |
19 |
lesson10.selection.position.copy(intersects[0].point.sub(lesson10.offset)); |
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); |
onDocumentMouseUp
When we release our mouse button, we only need to enable the controls again, and reset the selection:
1 |
onDocumentMouseUp: function (event) { |
3 |
lesson10.controls.enabled = true ; |
4 |
lesson10.selection = null ; |
That’s it for today. Hope you find our tutorial useful.