How to remove Box3 in ThreeJS? - three.js

I'm using Box3 to detect intersections so player could collect coins. I'd like the coin to be removed after detecting that it's intersecting with player but for some reason, I can't remove its (coin's) Box3.
I've read the documentation and deduced that Box3 is connected to the item's geometry, but removing the geometry and removing the item from the scene doesn't seem to remove Box3; it just stays in place, still interacting with player.
My code fiddle: https://jsfiddle.net/ImLost/g3mu1fqe/2/
Code:
function main() {
const canvas = document.querySelector('#canva');
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(window.innerWidth, window.innerHeight);
/* Camera */
const fov = 40;
const aspect = window.innerWidth / window.innerHeight;
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 65, -45);
camera.up.set(0, 0, 1);
camera.lookAt(0, 0, 0);
const scene = new THREE.Scene();
/* Lights */
const mainLight = new THREE.DirectionalLight(0xffffff, .85);
mainLight.position.set(0, 20, 0);
scene.add(mainLight);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
/* Board */
const boardGeometry = new THREE.PlaneGeometry(50, 50);
const boardMaterial = new THREE.MeshToonMaterial({ color: 0xEEEEEE, side: THREE.DoubleSide });
const board = new THREE.Mesh(boardGeometry, boardMaterial);
board.rotation.x = Math.PI / 2; //The board must be placed flat on the x axis
scene.add(board);
/* Player */
const playerBox = new THREE.Box3() // Used to determine collisions
const playerGeometry = new THREE.BoxGeometry(1, 1, 1.5);
const playerMaterial = new THREE.MeshToonMaterial({ color: 0xAAAAAA });
const player = new THREE.Mesh(playerGeometry, playerMaterial);
player.geometry.computeBoundingBox(playerBox);
scene.add(player);
/* Box helper */
const playerHelper = new THREE.Box3Helper(playerBox, 0xffff00);
scene.add(playerHelper);
/* Coin */
const smallCollectibleRadius = .4
const bigCollectibleRadius = .6
const coinBox = new THREE.Box3();
const coinGeometry = new THREE.SphereGeometry(smallCollectibleRadius, 100, 100);
const coinMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, emissive: 0xffff00 });
const coin = new THREE.Mesh(coinGeometry, coinMaterial);
coin.position.set(0, 0, 3)
scene.add(coin);
coin.geometry.computeBoundingBox(coinBox);
const coinHelper = new THREE.Box3Helper(coinBox, 0xffff00);
scene.add(coinHelper);
function checkCollision(box) {
var collision = playerBox.intersectsBox(box);
if (collision == true) {
return true
}
}
document.addEventListener("keypress", function (event) {
if (checkCollision(coinBox)) {
console.log("Yummy coin!")
coinGeometry.dispose()
coin.geometry.dispose()
scene.remove(coin)
}
});
function render(time) {
time *= 0.001;
const speed = 0.0005
const rotSpeed = 0.00005
const dir = new THREE.Vector3();
playerBox.copy(player.geometry.boundingBox).applyMatrix4(player.matrixWorld);
coinBox.copy(coin.geometry.boundingBox).applyMatrix4(coin.matrixWorld);
document.addEventListener("keypress", function (event) {
if (event.keyCode == 119) {
player.getWorldDirection(dir);
player.position.addScaledVector(dir, speed);
}
if (event.keyCode == 115) {
player.getWorldDirection(dir);
player.position.addScaledVector(dir, -speed);
}
if (event.keyCode == 97) {
player.rotation.y += rotSpeed
}
if (event.keyCode == 100) {
player.rotation.y -= rotSpeed
}
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();

coinGeometry.dispose()
coin.geometry.dispose()
scene.remove(coin)
The code above does not invalidate your coin object, nor its .geometry property--it simply discards the buffer and attribute data from memory. Other properties, like boundingBox still exist. Otherwise, you would be getting errors when you copy the bounding box into coinBox after the coin has been "consumed."
Now, you can invalidate the whole coin by setting it to null:
scene.remove(coin)
coin = null
However, JavaScript is garbage-collected, and you may still be able to access the object before it is actually removed from the heap. I would recommend a simple logical workaround:
scene.remove(coin)
coin.userData.consumed = true
coin = null
Then in your renderer and key event listener, add checks for the new property:
document.addEventListener("keypress", function (event) {
if (coin !== null && !('consumed' in coin.userData) && checkCollision(coinBox)) {
playerBox.copy(player.geometry.boundingBox).applyMatrix4(player.matrixWorld);
if( coin !== null && !('consumed' in coin.userData) ){
coinBox.copy(coin.geometry.boundingBox).applyMatrix4(coin.matrixWorld);
}

Related

Convert global coordinates to local

A particle system based on three.js' Points.
Internally I am treating the particles as global (with position and velocity vectors) and update the Points geometry accordingly.
It works nicely if the Points object is global (and static).
I need to change this so the Points object moves through the Scene (as child of the "particle emitter"). That requires converting global coordinates for each particle to local coordinates for the Points object geometry.
I have attempted the following:
local = globalParticle.position.clone().applyMatrix4( movingPointsObject.matrixWorld.invert() );
and
local = movingPointsObject.worldToLocal( globalParticle.position );
with different results.
The former seems to work in principle, but the particles appear to have duplicates depending on the Z-rotation of the Points object (they align when rotation is PI).
The latter causes the particles to rotate quickly, giving the appearance of a ring.
What's going on?
https://jsfiddle.net/b7nLorvf/
or
const renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(480, 480);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color("gray");
const camera = new THREE.OrthographicCamera(-2, 2, 2, -2, 1, 2);
camera.position.set(0, 0, 2);
scene.add(camera);
const light = new THREE.AmbientLight(0xffffff, 0.75); // soft white light
scene.add(light);
const bgeometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const bmaterial = new THREE.MeshBasicMaterial({
color: 0xff00ff
});
const cube = new THREE.Mesh(bgeometry, bmaterial);
scene.add(cube);
let pgeometry = new THREE.BufferGeometry();
//pgeometry.setAttribute("position", new THREE.Float32BufferAttribute([], 3));
const pmaterial = new THREE.PointsMaterial({
color: 0xff0000,
size: 2
}); //, depthTest:true } );
let points = new THREE.Points(pgeometry, pmaterial);
const axesHelper = new THREE.AxesHelper(1);
points.add(axesHelper);
//const global = true;
const global = false;
if (global) {
scene.add(points); // global
} else {
cube.add(points); // local
}
let particles = [];
let lt = 0,
dt = 0;
function animate(ms) {
dt = (ms - lt) / 1000;
lt = ms;
// move emitter
const a = ms / 1000;
cube.position.x = Math.cos(a) * 1;
cube.position.y = Math.sin(a) * 1;
cube.rotation.z = a;
// particles
particles.push({
position: cube.position.clone(),
velocity: new THREE.Vector3(2, 0, 0).applyEuler(cube.rotation),
life: 1
});
for (let p of particles) {
p.life -= dt;
}
particles = particles.filter(p => {
return p.life > 0.0;
});
let v = [];
for (let p of particles) {
p.position.add(p.velocity.clone().multiplyScalar(dt));
let vertex;
if (global) {
vertex = p.position;
} else {
//vertex = p.position.clone().applyMatrix4( points.matrixWorld.invert() );
vertex = points.worldToLocal( p.position );
}
v.push(vertex.x, vertex.y, vertex.z);
}
pgeometry.setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.148.0/three.min.js"></script>
The former works in conjunction with
movingPointsObject.updateWorldMatrix(true, true);
Inspired by worldToLocal, see three.js/Object3D.js#L256.

THREE.js Raycasting Points

I'm trying to implement raycasting for the Points object.
The problem is that the raycaster selection doesn't match the pointer position.
I took as reference these 2 examples from three:
webgl_interactive_raycasting_points
webgl_interactive_points
but i can't still figure out what i am doing wrong.
here is my code pen:
https://codepen.io/simone-tasca/pen/YzapWMN
let scene = new THREE.Scene()
const near = 0.1
const far = 5000
const fov = 30
const aspect = window.innerWidth / window.innerHeight
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
camera.position.set(-1.25, 0.8, -1.9)
let ambientLight = new THREE.AmbientLight('white', 1)
scene.add(ambientLight)
// POINTS CONTAINER ==========================================================
const worldCenter = [-1.07, 0, -6.85]
const rotationCorrection = [0.781, 4.305, 0.28]
const worldMap = new THREE.Object3D()
worldMap.position.set(...worldCenter)
worldMap.rotation.set(...rotationCorrection)
scene.add(worldMap)
// THREE.Points =============================================================
const PARTICLE_SIZE = 0.1
let particles, raycaster, INTERSECTED, pointer
let vertices = []
let names = []
let sizes = []
prepareData(sampleData).forEach(coords => {
vertices.push(...coords)
sizes.push(PARTICLE_SIZE)
})
const geometry = new THREE.BufferGeometry()
geometry.attributes.position = new THREE.Float32BufferAttribute(vertices, 3)
geometry.attributes.size = new THREE.Float32BufferAttribute(sizes, 1)
let material = new THREE.PointsMaterial({
color: 0xffffff,
transparent: true,
depthTest: true,
depthWrite: false
})
material.onBeforeCompile = shader => {
shader.vertexShader =
shader.vertexShader.replace('uniform float size;', 'attribute float size;')
}
particles = new THREE.Points(geometry, material)
worldMap.add(particles)
// RAYCASTER =============================================================
raycaster = new THREE.Raycaster()
pointer = new THREE.Vector2(99999, 99999)
document.addEventListener('pointermove', (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
})
document.addEventListener('pointerout', () => pointer.set(99999, 99999))
// RENDERING ==================================================================
let canvas = document.querySelector('#c')
const renderer = new THREE.WebGLRenderer({
canvas: canvas, alpha: false
})
function resizeRendererToDisplaySize(renderer) {
const width = canvas.clientWidth
const height = canvas.clientHeight
const needResize = canvas.width !== width || canvas.height !== height
if (needResize) renderer.setSize(width, height, false)
return needResize
}
function render(time) {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement
camera.aspect = canvas.clientWidth / canvas.clientHeight
camera.updateProjectionMatrix()
}
const geometry = particles.geometry
const attributes = geometry.attributes
raycaster.setFromCamera(pointer, camera)
let intersects = raycaster.intersectObject(particles)
if (intersects.length > 0) {
if (INTERSECTED != intersects[0].index) {
console.log(intersects[0].index)
attributes.size.array[INTERSECTED] = PARTICLE_SIZE
INTERSECTED = intersects[0].index
attributes.size.array[INTERSECTED] = PARTICLE_SIZE * 3
attributes.size.needsUpdate = true
}
} else if (INTERSECTED !== null) {
attributes.size.array[INTERSECTED] = PARTICLE_SIZE
attributes.size.needsUpdate = true
INTERSECTED = null
}
renderer.render(scene, camera)
requestAnimationFrame(render)
}
render()
You're very close. The only thing missing is to declare how "wide" you want your raycaster to be. Add this line after initiating the raycaster:
raycaster = new THREE.Raycaster()
raycaster.params.Points.threshold = 0.05;
The threshold is by default 1 unit wide. Think of this as painting with a very broad brush, the first particle you'll hit may not be the closest to your mouse pointer. So when you get intersects[0].index, it's going to be the first particle you hit with that broad ray (closest to the camera), not the closest one to your mouse. If you declare a narrower threshold, your ray will be more precise and you'll get more accurate results.
https://threejs.org/docs/#api/en/core/Raycaster.params

Three.js Move an object to front of camera

Hello I'm trying to move an object to front of camera, and when it reached to target position, I want it to stop. but it doesn't work.
function objectToCamera(mX, mY, object)
{
var vector = new THREE.Vector3(mX, mY, 1);
vector.unproject(camera);
vector.sub(object.position);
var dx = object.position.x - camera.position.x;
var dy = object.position.y - camera.position.y;
var dz = object.position.z - camera.position.z;
var distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
if(lastDistance < distance && lastDistance != -1)
keepOut = -1;
lastDistance = distance;
setTimeout(function(){
if( distance > 200 && keepOut == 1)
{
var amount = (1)*(indexForZoom/3);
amount = (amount>15) ? 15 : (1)*(indexForZoom/3);
if(distance - amount < 200)
amount = (distance-200)+1;
indexForZoom++;
object.translateZ(amount);
controls.target.addVectors(controls.target,vector.setLength(amount));
objectToCamera(mX, mY, object)
}
else
{
// stopForZoom = 1;
keepOut = -1;
objectClickHandler(object.name, object);
}
}, 10);
}
I'm checking the distance between camera and object, and if target distance has reached I'm letting it stop, but it doesn't work.
In coordinates, if i'm in positive X coordinates, distance is decreasing, and otherwise, distance is increasing.
I think, in my codes, distance should be decreasing always, but it is not.
Please help. Thanks.
you can use object.position.lerp(target, amount) to move an object toward target. Amount is a value from 0 to 1 with 1 = 100% all the way to target and 0.5 = 50% way to target.
If you want to move at a fixed speed then you can get the distance to the target
distance = object.position.distanceTo(target);
Say you want a max of 0.1 units per interation. then
moveSpeed = 0.1;
distance = object.position.distanceTo(target);
amount = Math.min(moveSpeed, distance) / distance;
object.position.lerp(target, amount)
All that's left is for you to choose a target.
The position in front of the camera is
const distanceFromCamera = 3; // 3 units
const target = new THREE.Vector3(0, 0, -distanceToCamera);
target.applyMatrix4(camera.matrixWorld);
So for example if you move the camera (drag with mouse, use scrollwheel). Note: in the code the speed is adjusted to be frame rate independent.
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new THREE.OrbitControls(camera, canvas);
controls.target.set(0, 0, 0);
controls.update();
const scene = new THREE.Scene();
scene.background = new THREE.Color('lightblue');
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
const gridHelper = new THREE.GridHelper(100, 10);
scene.add(gridHelper);
gridHelper.position.set(0, -5, 0);
const cube = new THREE.Mesh(
new THREE.BoxBufferGeometry(1, 1, 1),
new THREE.MeshPhongMaterial({color: 'red'}),
);
scene.add(cube);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cube.rotation.x = now;
cube.rotation.y = now * 1.1;
// move cube in front of camera
{
const distanceFromCamera = 3; // 3 units
const target = new THREE.Vector3(0, 0, -distanceFromCamera);
target.applyMatrix4(camera.matrixWorld);
const moveSpeed = 15; // units per second
const distance = cube.position.distanceTo(target);
if (distance > 0) {
const amount = Math.min(moveSpeed * deltaTime, distance) / distance;
cube.position.lerp(target, amount);
cube.material.color.set('green');
} else {
cube.material.color.set('red');
}
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body { margin: 0; }
#c { width: 100vw; height: 100vh; display: block; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/examples/js/controls/OrbitControls.js"></script>
<canvas id="c"></canvas>
Note, you might want to call camera.updateMatrixWorld() before all that math to make sure the target isn't one frame late.
If the object is in a hierarchy then there's more to do. You can do the math or you can use just attach the object to the scene and then attach it it back to its place in the hierarchy
const parent = object.parent;
// move object to scene without changing it's world orientation
scene.attach(object);
// do stuff above
// move object to parent without changing it's world orientation
parent.attach(object);

How does raycasting in three.js work with an offscreen canvas?

I can't seem to get raycasting to work in an offscreen canvas.
A click event sends data to the worker like this:
var r = document.getElementById('webGL').getBoundingClientRect()
offscreen.postMessage({
action: 'set_scene',
mesh: mesh.toJSON(),
camera: camera.toJSON(),
canvasSize: {
width: document.getElementById('webGL').clientWidth,
height: document.getElementById('webGL').clientHeight
},
coordinates: { x: e.originalEvent.clientX - r.x, y: e.originalEvent.clientY - r.y },
time: (new Date())
});

while the worker looks like this:
self.importScripts( './three.min.js' );
var loader = new THREE.ObjectLoader();
var scene = new THREE.Scene();
self.onmessage = function(e) {
// var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
var canvas = new OffscreenCanvas(e.data.canvasSize.width, e.data.canvasSize.height);
var renderer = new THREE.WebGLRenderer( { antialias: true, canvas: canvas, preserveDrawingBuffer: true } );
Promise.all([
(new Promise(function(resolve, reject) {
loader.parse(
e.data.mesh,
function ( obj ) {
resolve( obj );
})
})),
(new Promise(function(resolve, reject) {
loader.parse(
e.data.camera,
function ( obj ) {
resolve( obj );
})
}))
]).then(obj => {
var mesh, camera
[mesh, camera] = obj;
scene.add( mesh );
renderer.render( scene, camera );
var raycaster = new THREE.Raycaster();
p = { x: e.data.coordinates.x, y: e.data.coordinates.y };
var m = {};
m.x = (p.x) / e.data.canvasSize.width * 2 - 1;
m.y = 1 - (p.y) / e.data.canvasSize.height * 2;
raycaster.setFromCamera( m, camera );
var intersects = raycaster.intersectObjects( [ mesh ], true );
return intersects;
}).then(r => {
self.postMessage(r);
}).catch(e => {
console.log(e);
})
}
Same code onscreen works ok, and the values resulting from the transforms check out ok.
Is it possible to do such a thing at all, or what am I getting wrong?
I don't see the issue in your code but there is absolutely nothing special about picking offscreen. Here's a working example to prove it. It doesn't have any three or camera or mesh in the main page, only in the worker.
All the main page does is start the worker, transfer control of the canvas to the worker, then send resize and mouse events to the worker. That's it. Otherwise the code in the worker is 99% the same as the code would be in the main page. The only major difference is resizeCanvasToDisplaySize gets the display size from state.width and state.height. The non-offscreen version code comes from here
You need to post more code.
function main() {
const canvas = document.querySelector("#c");
if (!canvas.transferControlToOffscreen) {
alert('no offscreen canvas support');
return;
}
const offscreen = canvas.transferControlToOffscreen();
const workerScript = document.querySelector('#foo').text;
const blob = new Blob([workerScript], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
function sendSize() {
worker.postMessage({
type: 'size',
width: canvas.clientWidth,
height: canvas.clientHeight,
});
}
sendSize();
window.addEventListener('resize', sendSize);
function getNormaizedMousePosition(element, e) {
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
return {
x: x / canvas.clientWidth * 2 - 1,
y: y / canvas.clientHeight * -2 + 1,
}
}
canvas.addEventListener('mousemove', (e) => {
const pos = getNormaizedMousePosition(canvas, e);
worker.postMessage({
type: 'mousemove',
x: pos.x,
y: pos.y,
});
});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script type="foo" id="foo">
'use strict';
importScripts('https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js');
/* global THREE */
const state = {
width: 300,
height: 150,
mouse: {
x: -2,
y: -2,
},
};
function init(data) {
const {canvas} = data;
const renderer = new THREE.WebGLRenderer({
canvas
});
state.width = canvas.width;
state.height = canvas.height;
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 4;
const scene = new THREE.Scene();
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({
color
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
return cube;
}
const cubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
class PickHelper {
constructor() {
this.raycaster = new THREE.Raycaster();
this.pickedObject = null;
this.pickedObjectSavedColor = 0;
}
pick(normalizedPosition, scene, camera, time) {
// restore the color if there is a picked object
if (this.pickedObject) {
this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
this.pickedObject = undefined;
}
// cast a ray through the frustum
this.raycaster.setFromCamera(normalizedPosition, camera);
// get the list of objects the ray intersected
const intersectedObjects = this.raycaster.intersectObjects(scene.children);
if (intersectedObjects.length) {
// pick the first object. It's the closest one
this.pickedObject = intersectedObjects[0].object;
// save its color
this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
// set its emissive color to flashing red/yellow
this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
}
}
}
const pickHelper = new PickHelper();
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = state.width;
const height = state.height;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.width / canvas.height;
camera.updateProjectionMatrix();
}
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
pickHelper.pick(state.mouse, scene, camera, time);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function size(data) {
state.width = data.width;
state.height = data.height;
}
function mousemove(data) {
state.mouse.x = data.x;
state.mouse.y = data.y;
}
const handlers = {
init,
size,
mousemove,
};
self.onmessage = function(e) {
const fn = handlers[e.data.type];
if (!fn) {
throw new Error('no handler for type: ' + e.data.type);
}
fn(e.data);
};
</script>
For your particular case though you shouldn't need a camera. Send the ray. You don't need the canvas size either. Picking in three.js is CPU based
function main() {
const canvas = document.querySelector("#c");
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 4;
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
function makeInstance(geometry, color, x, name) {
const material = new THREE.MeshPhongMaterial({
color
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.name = name;
cube.position.x = x;
cube.rotation.x = x;
cube.rotation.z = x + .7;
return cube;
}
makeInstance(geometry, 0x44aa88, 0, 'cyan cube');
makeInstance(geometry, 0x8844aa, -2, 'purple cube');
makeInstance(geometry, 0xaa8844, 2, 'brown cube');
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.width / canvas.height;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
}
render();
window.addEventListener('resize', render);
const workerScript = document.querySelector('#foo').text;
const blob = new Blob([workerScript], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
const msgElem = document.querySelector('#msg');
worker.onmessage = (e) => {
msgElem.textContent = e.data;
};
worker.postMessage({type: 'init', scene: scene.toJSON()});
function getNormaizedMousePosition(element, e) {
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
return {
x: x / canvas.clientWidth * 2 - 1,
y: y / canvas.clientHeight * -2 + 1,
}
}
const raycaster = new THREE.Raycaster();
canvas.addEventListener('mousemove', (e) => {
const pos = getNormaizedMousePosition(canvas, e);
raycaster.setFromCamera(pos, camera);
worker.postMessage({
type: 'intersect',
origin: raycaster.ray.origin.toArray(),
direction: raycaster.ray.direction.toArray(),
});
});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
#msg {
position: absolute;
left: 1em;
top: 1em;
}
<canvas id="c"></canvas>
<div id="msg"></div>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js"></script>
<script type="foo" id="foo">
'use strict';
importScripts('https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js');
/* global THREE */
function loadObject(json) {
return new Promise((resolve) => {
const loader = new THREE.ObjectLoader();
loader.parse(json, resolve);
});
}
const renderer = new THREE.WebGLRenderer({
canvas: new OffscreenCanvas(1, 1),
});
// settings not important
const camera = new THREE.PerspectiveCamera(1, 1, 0.1, 100);
const raycaster = new THREE.Raycaster();
let scene;
let lastIntersectedObject;
async function init(data) {
scene = await loadObject(data.scene);
// we only need to render once to init the scene
renderer.render(scene, camera);
}
function intersect(data) {
raycaster.set(
new THREE.Vector3(...data.origin),
new THREE.Vector3(...data.direction));
const intersections = raycaster.intersectObjects(scene.children);
const intersectedObject = intersections.length
? intersections[0].object
: null;
if (intersectedObject !== lastIntersectedObject) {
lastIntersectedObject = intersectedObject;
log('intersection:', lastIntersectedObject ? lastIntersectedObject.name : 'none');
}
}
const handlers = {
init,
intersect,
};
self.onmessage = function(e) {
const fn = handlers[e.data.type];
if (!fn) {
throw new Error('no handler for type: ' + e.data.type);
}
fn(e.data);
};
function log(...args) {
postMessage([...args].join(' '));
}
</script>

Rotate at specific pivot point

I'm trying to pivot a piece of a headphone set. It's capable of doing such in my model (in Maya).. but I can't seem to figure it out in threejs.
I know I can rotate my objects X Y and Z by doing something like this:
object.rotateX(THREE.Math.degToRad(degreeX));
object.rotateY(THREE.Math.degToRad(degreeY));
object.rotateZ(THREE.Math.degToRad(degreeZ));
But how do I keep the pivot point stationary while the rests rotates/moves? So in my example, I'd want the ear piece to be able to move left and right based off of the black-ish screw you see in my picture.
You could nest your headphones Mesh inside another THREE.Group, reposition the headphones inside this group so the pivot is in the desired position, then rotate the parent.
// You take your headphones and nest them inside a Group
var headphones = new THREE.Mesh(geom, material);
var parent = new THREE.Group();
parent.add(headphones);
// Then you move your headphones to the desired pivot position
headphones.position.set(-5, 0.1, 0);
// Parent is going to rotate around it origin
parent.rotateX(THREE.Math.degToRad(degreeX));
Note that if you want the pivot to be at (5, -0.1, 0), you should move headphones in the opposite direction: (-5, 0.1, 0).
Parent your model to another THREE.Object3D but to make it easy use the SceneUtils.attach function.
Example:
Click then drag, each time you click the pivot object will be moved to that location and then the model (the cube) will be attached to the pivot by calling THREE.SceneUtils.attach(model, scene, pivot). When you let off the mouse the model is detached using THREE.SceneUtils.detach(model, pivot, scene).
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
// make the camera look down
camera.position.set(0, 10, 0);
camera.up.set(0, 0, -1);
camera.lookAt(0, 0, 0);
const scene = new THREE.Scene();
scene.background = new THREE.Color('black');
scene.add(new THREE.GridHelper(40, 40));
let model;
{
const cubeSize = 3;
const cubeGeo = new THREE.BoxBufferGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshBasicMaterial({color: 'red'});
model = new THREE.Mesh(cubeGeo, cubeMat);
model.position.set(.5, .5, .5);
scene.add(model);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
}
render();
let rotate = false;
const startPos = {x:0, y:0};
const raycaster = new THREE.Raycaster();
const pivot = new THREE.Object3D();
scene.add(pivot);
pivot.add(new THREE.AxesHelper(.5));
function setPivotPoint(e) {
startPos.x = e.clientX;
startPos.y = e.clientY;
const normalizedPosition = {
x: e.clientX / canvas.clientWidth * 2 - 1,
y: e.clientY / canvas.clientHeight * -2 + 1,
};
// this part is NOT important to the answer. The question
// is how to rotate from some point. This code is picking
// a point. Which point to pick was not part of the question
// but to demo the solution it's important to pick a point
// put the pivot where the mouse was clicked
raycaster.setFromCamera(normalizedPosition, camera);
const intersection = raycaster.intersectObjects(scene.children)[0];
if (intersection) {
if (rotate) {
removeFromPivot();
}
pivot.position.copy(intersection.point);
pivot.rotation.set(0,0,0);
pivot.updateMatrixWorld();
rotate = true;
// this the important part. We're making the cube
// a child of 'pivot' without it moving in world space
THREE.SceneUtils.attach(model, scene, pivot);
render();
}
}
function rotatePivot(e) {
e.preventDefault();
if (rotate) {
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
const maxDelta = Math.abs(dx) > Math.abs(dy) ? dx : dy;
pivot.rotation.y = maxDelta * 0.01;
render();
}
}
function removeFromPivot() {
if (rotate) {
rotate = false;
THREE.SceneUtils.detach(model, pivot, scene);
window.removeEventListener('mousemove', rotatePivot);
window.removeEventListener('mouseup', removeFromPivot);
}
}
canvas.addEventListener('mousedown', (e) => {
e.preventDefault();
setPivotPoint(e);
if (rotate) {
window.addEventListener('mousemove', rotatePivot);
window.addEventListener('mouseup', removeFromPivot);
}
});
}
main();
html, body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/js/utils/SceneUtils.js"></script>

Resources