In this blog post we’ll cover the code required to set up a clickable 3D object using three.js that behave like the objects on our clickable three.js object demo page. Let’s get into 3D objects with click handlers.
Looking to get a head start on your next software interview? Pickup a copy of the best book to prepare: Cracking The Coding Interview!
The three.js javascript library is a powerful library that uses WebGL to render 3D animations in modern browsers. Once we have included the three.js script <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.min.js"></script>
we can instantiate a 3D object using the following code:
// three.js variables var mesh, mesh2, mesh3, camera, scene, renderer; var maxRotation = 2 * Math.PI; function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function init() { camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); camera.position.z = 400; scene = new THREE.Scene(); var texture = new THREE.TextureLoader().load('https://pericror.com/wp-content/uploads/2018/04/pericrorLogoBox.png'); var material = new THREE.MeshBasicMaterial({ map: texture }); var objectSize = 100; // Create a cube var boxGeometry = new THREE.BoxGeometry(objectSize, objectSize, objectSize); mesh = new THREE.Mesh(boxGeometry, material); mesh.position.set(objectSize * -2, 0, 0); mesh.callback = objectClickHandler; scene.add(mesh); renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); var container = document.getElementById('canvasContainer'); container.appendChild(renderer.domElement); window.addEventListener('resize', onWindowResize, false); } window.onload = function() { init(); };
To add additional shapes, we use different mesh geometries:
// Create a cube var boxGeometry = new THREE.BoxGeometry(objectSize, objectSize, objectSize); mesh = new THREE.Mesh(boxGeometry, material); mesh.position.set(objectSize * -2, 0, 0); mesh.callback = objectClickHandler; // create a sphere var sphereGeometry = new THREE.SphereGeometry( objectSize / 2, 32, 32 ); mesh2 = new THREE.Mesh(sphereGeometry, material); mesh2.position.set(0, 0, 0); mesh2.callback = objectClickHandler; // create a cylinder var cylinderGeometry = new THREE.CylinderGeometry( objectSize / 4, objectSize / 4, 20, 32 ); mesh3 = new THREE.Mesh(cylinderGeometry, material); mesh3.position.set(objectSize * 2, 0, 0); mesh3.callback = objectClickHandler; scene.add(mesh); scene.add(mesh2); scene.add(mesh3);
To make the objects move, we animate them by rotating them on the y axis:
function animate() { requestAnimationFrame(animate); mesh.rotation.y = (mesh.rotation.y + 0.005) % maxRotation; mesh2.rotation.y = (mesh2.rotation.y + 0.005) % maxRotation; mesh3.rotation.y = (mesh3.rotation.y + 0.005) % maxRotation; renderer.render(scene, camera); } window.onload = function() { init(); animate();
Now that we have the shapes created and animated, we need to make them clickable. A solution from StackOverflow shows us how we can use a raycaster to test if our mouse intersects a 3D object:
// Default click handler for our three.js objects function objectClickHandler() { window.open('https://pericror.com/', '_blank'); } window.onload = function() { init(); animate(); var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); // See https://stackoverflow.com/questions/12800150/catch-the-click-event-on-a-specific-mesh-in-the-renderer // Handle all clicks to determine of a three.js object was clicked and trigger its callback function onDocumentMouseDown(event) { event.preventDefault(); mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; mouse.y = - (event.clientY / renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); meshObjects = [mesh, mesh2, mesh3]; // three.js objects with click handlers we are interested in var intersects = raycaster.intersectObjects(meshObjects); if (intersects.length > 0) { intersects[0].object.callback(); } } document.addEventListener('mousedown', onDocumentMouseDown, false); };
We can also use the same intersection logic to optionally add feedback in the object motion when a user mouses over the object:
// Using the same logic as above, determine if we are currently mousing over a three.js object, // and adjust the animation to provide visual feedback accordingly function onDocumentMouseMove(event) { event.preventDefault(); mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; mouse.y = - (event.clientY / renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); var intersects = raycaster.intersectObjects([mesh, mesh2, mesh3]); var canvas = document.body.getElementsByTagName('canvas')[0]; if (intersects.length > 0) { intersects[0].object.rotation.x += .005; canvas.style.cursor = "pointer"; } else { canvas.style.cursor = "default"; } } document.addEventListener('mousedown', onDocumentMouseDown, false); document.addEventListener('mousemove', onDocumentMouseMove, false);
The full code to produce the objects above:
// three.js variables var mesh, mesh2, mesh3, camera, scene, renderer; var maxRotation = 2 * Math.PI; // Default click handler for our three.js objects function objectClickHandler() { window.open('https://pericror.com/', '_blank'); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function init() { camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); camera.position.z = 400; scene = new THREE.Scene(); var texture = new THREE.TextureLoader().load('https://pericror.com/wp-content/uploads/2018/04/pericrorLogoBox.png'); var material = new THREE.MeshBasicMaterial({ map: texture }); var objectSize = 100; // Create a cube var boxGeometry = new THREE.BoxGeometry(objectSize, objectSize, objectSize); mesh = new THREE.Mesh(boxGeometry, material); mesh.position.set(objectSize * -2, 0, 0); mesh.callback = objectClickHandler; // create a sphere var sphereGeometry = new THREE.SphereGeometry( objectSize / 2, 32, 32 ); mesh2 = new THREE.Mesh(sphereGeometry, material); mesh2.position.set(0, 0, 0); mesh2.callback = objectClickHandler; // create a cylinder var cylinderGeometry = new THREE.CylinderGeometry( objectSize / 4, objectSize / 4, 20, 32 ); mesh3 = new THREE.Mesh(cylinderGeometry, material); mesh3.position.set(objectSize * 2, 0, 0); mesh3.callback = objectClickHandler; scene.add(mesh); scene.add(mesh2); scene.add(mesh3); renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); var container = document.getElementById('canvasContainer'); container.appendChild(renderer.domElement); window.addEventListener('resize', onWindowResize, false); } function animate() { requestAnimationFrame(animate); mesh.rotation.y = (mesh.rotation.y + 0.005) % maxRotation; mesh2.rotation.y = (mesh2.rotation.y + 0.005) % maxRotation; mesh3.rotation.y = (mesh3.rotation.y + 0.005) % maxRotation; renderer.render(scene, camera); } window.onload = function() { init(); animate(); var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); // See https://stackoverflow.com/questions/12800150/catch-the-click-event-on-a-specific-mesh-in-the-renderer // Handle all clicks to determine of a three.js object was clicked and trigger its callback function onDocumentMouseDown(event) { event.preventDefault(); mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; mouse.y = - (event.clientY / renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); meshObjects = [mesh, mesh2, mesh3]; // three.js objects with click handlers we are interested in var intersects = raycaster.intersectObjects(meshObjects); if (intersects.length > 0) { intersects[0].object.callback(); } } // Using the same logic as above, determine if we are currently mousing over a three.js object, // and adjust the animation to provide visual feedback accordingly function onDocumentMouseMove(event) { event.preventDefault(); mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; mouse.y = - (event.clientY / renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); var intersects = raycaster.intersectObjects([mesh, mesh2, mesh3]); var canvas = document.body.getElementsByTagName('canvas')[0]; if (intersects.length > 0) { intersects[0].object.rotation.x += .005; canvas.style.cursor = "pointer"; } else { canvas.style.cursor = "default"; } } document.addEventListener('mousedown', onDocumentMouseDown, false); document.addEventListener('mousemove', onDocumentMouseMove, false); };
The full code for this blog post can be found on our Github. This is a great way to quickly create 3D objects with click handlers.
Elevate your software skills
Ergonomic Mouse |
Custom Keyboard |
SW Architecture |
Clean Code |
if (intersects.length &gt; 0) {
intersects[0].object.callback();
}
how this command works and why exactly are you using this?
please explain.
Thank You.
Imagine there is a camera looking at the 3D scene positioned where the mouse is. We shoot a ray into the scene and if it intersects with a object we fire the callback for the object.
This is a good example.
How can i have each object linked to different urls?
Hi, you can change the mesh callback to a new click handler function that is defined with the other link (i.e. mesh2.callback = objectClickHandler2;)
Good implementation of event handlers of 3d objects.