In this section, we'll elevate our interactivity skills by implementing drag-and-drop functionality for 3D objects in our Three.js scene. This allows users to pick up objects, move them around, and place them in new positions, adding a dynamic and engaging layer to your 3D experiences. We'll break this down into several key steps: detecting when a user clicks on an object, tracking the mouse movement to drag the object, and updating the object's position in the 3D world.
The core of drag-and-drop in 3D involves a few fundamental concepts. Firstly, we need to determine which object the user is interacting with. This is typically done using raycasting. A ray is cast from the camera through the mouse's position on the screen into the 3D scene, and we check for intersections with our objects. Secondly, once an object is 'picked up', we need to translate the 2D mouse movement into 3D movement. This involves projecting the mouse coordinates onto a plane in the 3D scene.
Let's start with the raycasting to identify the object. We'll need a Raycaster instance and an array of objects to check against. When the user clicks, we'll calculate the mouse coordinates relative to the viewport and use them to set the raycaster's origin and direction.
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObject = null;
function onPointerDown(event) {
// calculate mouse position in normalized device coordinates (-1 to +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
selectedObject = intersects[0].object;
// Store initial offset if needed
// console.log('Selected object:', selectedObject.name);
}
}Next, we need to handle the dragging. When the mouse moves after an object has been selected, we'll update the object's position. To do this effectively, we'll cast another ray from the camera through the current mouse position and intersect it with a plane that passes through the selected object. This plane can be positioned at the object's current depth or at a fixed distance.
const plane = new THREE.Plane();
const offset = new THREE.Vector3();
const intersectionPoint = new THREE.Vector3();
function onPointerMove(event) {
if (selectedObject) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// Define the plane to intersect with. We'll use a plane parallel to the camera's near plane, passing through the object.
plane.setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 0, 1), selectedObject.position);
if (raycaster.ray.intersectPlane(plane, intersectionPoint)) {
selectedObject.position.copy(intersectionPoint);
}
}
}Finally, when the user releases the mouse button, we need to stop dragging the object. This involves clearing the selectedObject variable.
function onPointerUp(event) {
selectedObject = null;
// console.log('Dropped object');
}Putting it all together, we'll add event listeners for pointerdown, pointermove, and pointerup to our renderer's DOM element. It's also good practice to add a small visual cue to indicate that an object is selected, perhaps by changing its material color or adding an outline.
graph TD
A[User Clicks Object] --> B{Is Object Clicked?}
B -- Yes --> C[Set selectedObject]
C --> D{Mouse Moves}
D -- Yes --> E[Cast Ray from Camera]
E --> F[Intersect with Plane]
F --> G[Update Object Position]
G --> D
D -- No --> H[User Releases Mouse]
H --> I[Clear selectedObject]
B -- No --> J[Do Nothing]
A more robust implementation might consider:
- Constraining movement: Limiting the drag to a specific axis or plane.
- Snapping: Allowing objects to snap to a grid or to other objects.
- Performance: Optimizing raycasting, especially with a large number of objects.
- Touch support: Adapting the logic for touch events on mobile devices.