Three.js Interaktif
Tidak seperti HTML yang punya event handler bawaan, di 3D kamu harus mendeteksi interaksi sendiri menggunakan Raycaster — "sinar" virtual yang ditembakkan dari kamera menembus layar.
Raycaster — Menembak Sinar ke Scene
Raycaster mendeteksi objek 3D mana yang "tertembak" oleh sinar dari posisi mouse:
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener("mousemove", (e) => {
// Konversi posisi mouse ke koordinat normalized (-1 sampai 1)
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
Membongkar rumus (e.clientX / window.innerWidth) * 2 - 1:
e.clientX / window.innerWidth— posisi mouse dalam rentang 0..1 (0 = pojok kiri, 1 = pojok kanan).* 2— skala jadi rentang 0..2.- 1— geser jadi rentang -1..1 (yang disebut Normalized Device Coordinates / NDC).
Kenapa harus NDC? Karena WebGL (dan Three.js di atasnya) bekerja di ruang terstandarisasi di mana layar selalu dipetakan ke rentang -1..1 — independen dari resolusi. Raycaster butuh NDC supaya tahu arah sinar yang benar dari kamera.
Sumbu Y di-flip (-(e.clientY / window.innerHeight) * 2 + 1) karena di browser Y=0 di atas, sedangkan di WebGL Y=0 di tengah dan nilai positif ke atas.
Jadi: pojok kiri-bawah = (-1, -1), tengah = (0, 0), pojok kanan-atas = (1, 1).
Hati-hati canvas yang tidak full-screen: kalau canvas kamu berukuran kecil (bukan
window.innerWidth), gunakancanvas.getBoundingClientRect()untuk dapat ukuran & offset yang benar, jangan pakaiwindow.innerWidth.
Deteksi Hover pada Objek 3D
const interactiveObjects = [cube, sphere, torus];
let hoveredObject = null;
function checkIntersection() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(interactiveObjects);
// Reset semua objek
interactiveObjects.forEach(obj => {
obj.material.emissive.setHex(0x000000);
});
if (intersects.length > 0) {
hoveredObject = intersects[0].object;
hoveredObject.material.emissive.setHex(0x333333); // highlight
document.body.style.cursor = "pointer";
} else {
hoveredObject = null;
document.body.style.cursor = "default";
}
}
function animate() {
requestAnimationFrame(animate);
checkIntersection();
renderer.render(scene, camera);
}
animate();
intersectObjects() mengembalikan array objek yang ditembak sinar, diurutkan dari yang terdekat. intersects[0] adalah objek pertama yang terkena.
Klik pada Objek 3D
window.addEventListener("click", () => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(interactiveObjects);
if (intersects.length > 0) {
const clicked = intersects[0].object;
console.log("Objek diklik:", clicked.name);
// Contoh: ubah warna random saat diklik
clicked.material.color.setHex(Math.random() * 0xffffff);
// Contoh: animasi dengan GSAP
gsap.to(clicked.scale, {
x: 1.5, y: 1.5, z: 1.5,
duration: 0.2,
yoyo: true,
repeat: 1
});
}
});
Informasi dari Intersection
Object intersection memberikan banyak data berguna:
const intersects = raycaster.intersectObjects(interactiveObjects);
if (intersects.length > 0) {
const hit = intersects[0];
console.log(hit.object); // objek yang terkena
console.log(hit.point); // titik 3D di mana sinar mengenai objek
console.log(hit.distance); // jarak dari kamera
console.log(hit.face); // face (segitiga) yang terkena
console.log(hit.faceIndex); // index face
console.log(hit.uv); // koordinat UV di titik hit
}
Object Selection — Pilih dan Sorot Objek
let selectedObject = null;
const outlineMaterial = new THREE.MeshBasicMaterial({
color: 0xffff00,
side: THREE.BackSide
});
window.addEventListener("click", () => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(interactiveObjects);
// Hapus outline dari selection sebelumnya
if (selectedObject && selectedObject.userData.outline) {
selectedObject.remove(selectedObject.userData.outline);
selectedObject.userData.outline = null;
}
if (intersects.length > 0) {
selectedObject = intersects[0].object;
// Buat outline
const outline = new THREE.Mesh(
selectedObject.geometry.clone(),
outlineMaterial
);
outline.scale.setScalar(1.05);
selectedObject.add(outline);
selectedObject.userData.outline = outline;
} else {
selectedObject = null;
}
});
Drag & Drop 3D
let isDragging = false;
let dragObject = null;
const dragPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // bidang horizontal
const intersection = new THREE.Vector3();
window.addEventListener("mousedown", (e) => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(interactiveObjects);
if (intersects.length > 0) {
isDragging = true;
dragObject = intersects[0].object;
controls.enabled = false; // nonaktifkan orbit controls saat drag
}
});
window.addEventListener("mousemove", () => {
if (!isDragging || !dragObject) return;
raycaster.setFromCamera(mouse, camera);
if (raycaster.ray.intersectPlane(dragPlane, intersection)) {
dragObject.position.x = intersection.x;
dragObject.position.z = intersection.z;
}
});
window.addEventListener("mouseup", () => {
isDragging = false;
dragObject = null;
controls.enabled = true;
});
Hover Effect dengan Animasi
function checkIntersection() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(interactiveObjects);
interactiveObjects.forEach(obj => {
if (!obj.userData.originalScale) {
obj.userData.originalScale = obj.scale.clone();
}
});
if (intersects.length > 0) {
const hovered = intersects[0].object;
gsap.to(hovered.scale, {
x: 1.2, y: 1.2, z: 1.2,
duration: 0.3
});
document.body.style.cursor = "pointer";
}
interactiveObjects.forEach(obj => {
if (!intersects.find(i => i.object === obj)) {
gsap.to(obj.scale, {
x: 1, y: 1, z: 1,
duration: 0.3
});
}
});
}
Interaktivitas 3D membuka pintu untuk pengalaman web yang imersif — product configurator, virtual showroom, atau portfolio interaktif!