Hello,
I am currently working on a rotation ring that allows the user to click and drag to rotate an object.
Here is a demo: RotationTest - CodeSandbox
To rotate the object, I wait until the user clicks on the sphere, rotation ring, or plane (the plane is only visible for demo reasons, the opacity can be zero).
Here is a code excerpt from line 175:
var angle =
Math.atan2(previousPosition.y, previousPosition.x) -
Math.atan2(currentPosition.y, currentPosition.x);
if (angle) {
// Rotate the mesh by this amount
targetMesh.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), angle);
rotationSphereXZ.position.set(
4 * Math.sin(targetMesh.rotation.y),
0,
4 * Math.cos(targetMesh.rotation.y)
);
}
With this rotation scheme, if you click on the sphere and try to rotate it around the entire ring, the sphere will bounce off the x-axis and not go into the negative x-direction.
Note that the initial rotation is triggered by clicking on the sphere, but the mouse moving rotation is controlled by raycasting the plane or the sphere.
If you replace the rotation with the following:
// targetMesh.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), angle);
// Instead use:
targetMesh.rotation.y += angle;
This works without the bouncing effect, but this is a local rotation. If I were to rotate the object on the x or z-axis, it would not rotate along just the y-axis and the rotation would become incorrect to the rotation ring scheme I am creating.
So how can I use the world rotation, without this domain issue?
Full code for demo:
import "./styles.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {
AmbientLight,
BoxGeometry,
GridHelper,
Mesh,
MeshPhongMaterial,
SphereGeometry,
Vector2
} from "three";
import Stats from "stats.js";
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
let sceneMeshes = [];
const ringGeometry = new THREE.RingGeometry(4, 3.9, 100);
const ringMaterial = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
const rotationRingXY = new THREE.Mesh(ringGeometry, ringMaterial);
const rotationRingXZ = new THREE.Mesh(ringGeometry, ringMaterial);
const rotationRingYZ = new THREE.Mesh(ringGeometry, ringMaterial);
rotationRingXZ.rotation.x = Math.PI / 2;
rotationRingYZ.rotation.y = Math.PI / 2;
var currentRotationType = "";
var mouseMoveIntersectObjects = [];
sceneMeshes.push(rotationRingXY);
let targetMesh = new Mesh(
new BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
scene.add(targetMesh);
camera.position.set(1, 1, 1);
controls.update();
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
// These two vectors keep track of the mouse's position from one frame to another
let previousPosition = new THREE.Vector2();
let currentPosition = new THREE.Vector2();
let mouseMoveRaycast = false;
let mouseClickRaycast = false;
/**
*
* When the pointer is clicked, set up the pointer vector for the raycast
* Enable the mouse move raycaster
*
* @param {*} event
*/
function onPointerClick(event) {
// calculate pointer position in normalized device coordinates
// (-1 to +1) for both components
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
mouseClickRaycast = true;
window.requestAnimationFrame(render);
}
function onPointerMove(event) {
if (mouseMoveRaycast) {
// calculate pointer position in normalized device coordinates
// (-1 to +1) for both components
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
window.requestAnimationFrame(render);
}
// When the mouse click is released, disable the raycasting and stop rotating the object
function onMouseUp(event) {
mouseClickRaycast = false;
mouseMoveRaycast = false;
previousPosition = new Vector2(0, 0);
currentPosition = new Vector2(0, 0);
controls.enableRotate = true;
currentRotationType = "";
}
let rotationPlaneXZ = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new MeshPhongMaterial({
transparent: true,
opacity: 0.5,
color: 0x0ffff0,
side: THREE.DoubleSide
})
);
// The following spheres are used as a marking of the rotation of the shape.
const rotationSphereGeometry = new SphereGeometry(0.3);
const rotationSphereMaterial = new THREE.MeshNormalMaterial();
let rotationSphereXZ = new Mesh(rotationSphereGeometry, rotationSphereMaterial);
rotationSphereXZ.position.set(
4 * Math.sin(targetMesh.rotation.y),
0,
4 * Math.cos(targetMesh.rotation.y)
);
rotationPlaneXZ.rotation.x = Math.PI / 2;
scene.add(rotationPlaneXZ, rotationRingXZ, rotationSphereXZ);
scene.add(new AmbientLight());
window.addEventListener("pointermove", onPointerMove);
function render() {
controls.update();
/**
* This raycaster is used to find the initial reference point for the rotation
*/
if (mouseClickRaycast) {
raycaster.setFromCamera(pointer, camera);
// Only intersect with the ring.
const intersects = raycaster.intersectObjects([rotationSphereXZ]);
if (intersects.length > 0) {
mouseMoveIntersectObjects = [rotationPlaneXZ, rotationSphereXZ];
previousPosition = new THREE.Vector2(
intersects[0].point.x,
intersects[0].point.z
);
currentPosition = new THREE.Vector2(
intersects[0].point.x,
intersects[0].point.z
);
// Set the previous and current reference point as the same thing.
// Enable the mouse move raycaster
mouseMoveRaycast = true;
// Disable rotation of camera for click and drag
controls.enableRotate = false;
mouseClickRaycast = false;
} else {
onMouseUp();
}
}
// If the mouse raycaster is enabled, we can rotate the target mesh
if (mouseMoveRaycast) {
raycaster.setFromCamera(pointer, camera);
// User can rotate on mouse move with either the rotation ring or the green plane
const intersects = raycaster.intersectObjects(mouseMoveIntersectObjects);
if (intersects.length > 0) {
// Grab where the users mouse is currently
currentPosition = new THREE.Vector2(
intersects[0].point.x,
intersects[0].point.z
);
// Calculate the change in angle between the previous intersection from mouse (or click) and this
var angle =
Math.atan2(previousPosition.y, previousPosition.x) -
Math.atan2(currentPosition.y, currentPosition.x);
if (angle) {
// Rotate the mesh by this amount
targetMesh.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), angle);
// If you instead try the rotation below, it will work
// targetMesh.rotation.y += angle;
rotationSphereXZ.position.set(
4 * Math.sin(targetMesh.rotation.y),
0,
4 * Math.cos(targetMesh.rotation.y)
);
}
previousPosition = currentPosition.clone();
}
}
// rotationPlaneXZ.rotation.y += 0.001;
renderer.render(scene, camera);
window.requestAnimationFrame(render);
}
render();
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("mousedown", onPointerClick);
window.addEventListener("mouseup", onMouseUp);
// scene.add(new GridHelper());
scene.add(new THREE.AxesHelper(10, 10, 10));