6

I know we can make the object have a new parent to act as the pivot, or we could adjust the geometry position within the mesh.

But how can we achieve this mathematically without reparenting the object or modifying the object's parent, and without modifying the object's geometry (if it is a mesh)?

In other words, what would we have to do to its transform matrix (or the parts, rotation, position, quaternion, etc) to achieve the same, with the above requirement of not touching parents or geometry?

trusktr
  • 34,715
  • 41
  • 148
  • 226
  • You could use quaternion – soju Mar 12 '19 at 08:06
  • @soju got an example? – trusktr Mar 12 '19 at 14:48
  • I don't get what you're asking. If you want to move with the pivot somewhere else consider temporarily reparenting it and using [SceneUtils attach and detach](https://threejs.org/docs/#examples/utils/SceneUtils). You can put a temp object where you want the pivot to be, detach the object you want to rotate, attach to the pivot object, rotate the pivot, now detach the object from the pivot and put it back where it was in the scene with attach. In fact I think I wrote an example here https://stackoverflow.com/questions/53503767/rotate-at-specific-pivot-point/53512529#53512529 – gman Mar 12 '19 at 15:41
  • Possible duplicate: https://stackoverflow.com/questions/31953608/rotate-object-on-specific-axis-anywhere-in-three-js-including-outside-of-mesh – WestLangley Mar 12 '19 at 16:29
  • The method @WestLangley linked to does not work if the object is not in the root of the scene. – gman Mar 13 '19 at 02:25
  • @gman The inline comments specify the requirements: _assumes object does not have a rotated parent_. I believe that requirement is correct. :-) – WestLangley Mar 13 '19 at 03:20
  • Sure, but then that's not really solving this question is it? To put it another way it's great reference but not a duplicate? This one specifically mentions parents. – gman Mar 13 '19 at 03:24
  • 1
    @gman Maybe _possibly useful_ would have been better. :-) – WestLangley Mar 13 '19 at 15:45

1 Answers1

3
  1. Take pivot matrix and inverse it. Inversed matrix, when applied, will place pivot to world origin and your object to somewhere else. Now your object is relative to pivot point [0,0,0].

  2. Apply transforms, that you would like to make relative to pivot point.

  3. Reapply initial pivot matrix (hey, not inversed!) to place object where it was before.

My example does all steps separate, mainly to explain the logic. Of course, you should not transform pivot object (maybe you don't even have one). And all steps can be compressed in one line formula:

object.matrix = inverse(pivot.matrix)*someTranformationMatrix*pivot.matrix

Working demo you find here: https://jsfiddle.net/mmalex/hd8ex0ok/

// example for https://stackoverflow.com/questions/55116131/how-can-we-change-the-rotation-origin-pivot-point-of-a-three-js-object-without

let renderer;
let camera;
let controls;

let scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(54, window.innerWidth / window.innerHeight, 0.1, 1000);

renderer = new THREE.WebGLRenderer({
    antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(new THREE.Color(0xfefefe));
document.body.appendChild(renderer.domElement);

camera.position.x = 4;
camera.position.y = 10;
camera.position.z = 4;
camera.lookAt(0, 0, 0);

controls = new THREE.OrbitControls(camera);

// white spotlight shining from the side, casting a shadow
let spotLight = new THREE.SpotLight(0xffffff, 2.5, 25, Math.PI / 6);
spotLight.position.set(9, 10, 1);
scene.add(spotLight);
var light = new THREE.AmbientLight(0x202020); // soft white light
scene.add(light);

// example starts here
let gridHelper = new THREE.GridHelper(4, 4);
scene.add(gridHelper);
var axesHelper = new THREE.AxesHelper(1);
axesHelper.applyMatrix(new THREE.Matrix4().makeTranslation(1.5, 0, -1.5));
axesHelper.updateMatrixWorld(true);
scene.add(axesHelper);

document.changePivot = function() {
 axesHelper.position.set(-2 + 4*Math.random(), -2 + 4*Math.random(), -2 + 4*Math.random());
 axesHelper.updateMatrixWorld(true);
}

const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshStandardMaterial({
    color: 0xff0000
});
const topBox = new THREE.Mesh(geometry, material);
topBox.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / 8));
topBox.applyMatrix(new THREE.Matrix4().makeTranslation(0.5, 1, -0.5));
scene.add(topBox);

let animate = function() {
    requestAnimationFrame(animate);

    // get world transforms from desired pivot
    var pivot_matrix = axesHelper.matrixWorld.clone();
    // inverse it to know how to move pivot to [0,0,0]
    let pivot_inv = new THREE.Matrix4().getInverse(pivot_matrix, false);

    // place pivot to [0,0,0]
    // apply same transforms to object
    axesHelper.applyMatrix(pivot_inv);
    topBox.applyMatrix(pivot_inv);

    // say, we want to rotate 0.1deg around Y axis of pivot
    var desiredTransform = new THREE.Matrix4().makeRotationY(Math.PI / 180);
    axesHelper.applyMatrix(desiredTransform);
    topBox.applyMatrix(desiredTransform);

    // and put things back, i.e. apply pivot initial transformation
    axesHelper.applyMatrix(pivot_matrix);
    topBox.applyMatrix(pivot_matrix);

    controls.update();
    renderer.render(scene, camera);
};

animate();
body {
    margin: 0;
}
<button onclick="changePivot()">set random pivot</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/91/three.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

rotate three.js object around pivot point

let renderer;
let camera;
let controls;

let scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(54, window.innerWidth / window.innerHeight, 0.1, 1000);

renderer = new THREE.WebGLRenderer({
    antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(new THREE.Color(0xfefefe));
document.body.appendChild(renderer.domElement);

camera.position.x = 5;
camera.position.y = 15.5;
camera.position.z = 5.5;
camera.lookAt(0, 0, 0);

controls = new THREE.OrbitControls(camera);

// white spotlight shining from the side, casting a shadow
let spotLight = new THREE.SpotLight(0xffffff, 2.5, 25, Math.PI / 6);
spotLight.position.set(9, 10, 1);
scene.add(spotLight);
var light = new THREE.AmbientLight(0x202020); // soft white light
scene.add(light);

// example starts here
let gridHelper = new THREE.GridHelper(4, 4);
scene.add(gridHelper);
var axesHelper = new THREE.AxesHelper(1);
axesHelper.applyMatrix(new THREE.Matrix4().makeTranslation(1.5, 0, -1.5));
scene.add(axesHelper);

const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshStandardMaterial({
    color: 0xff0000
});
const topBox = new THREE.Mesh(geometry, material);
topBox.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / 8));
topBox.applyMatrix(new THREE.Matrix4().makeTranslation(0.5, 1, -0.5));
scene.add(topBox);

let animate = function() {
    requestAnimationFrame(animate);

    // get world transforms from desired pivot
    axesHelper.updateMatrixWorld(true);
    var pivot_matrix = axesHelper.matrixWorld.clone();
    // inverse it to know how to move pivot to [0,0,0]
    let pivot_inv = new THREE.Matrix4().getInverse(pivot_matrix, false);

    // place pivot to [0,0,0]
    // apply same transforms to object
    axesHelper.applyMatrix(pivot_inv);
    topBox.applyMatrix(pivot_inv);

    // say, we want to rotate 0.1deg around Y axis of pivot
    var desiredTransform = new THREE.Matrix4().makeRotationY(Math.PI / 180);
    axesHelper.applyMatrix(desiredTransform);
    topBox.applyMatrix(desiredTransform);

    // and put things back, i.e. apply pivot initial transformation
    axesHelper.applyMatrix(pivot_matrix);
    topBox.applyMatrix(pivot_matrix);

    controls.update();
    renderer.render(scene, camera);
};

animate();
Alex Khoroshylov
  • 1,811
  • 1
  • 10
  • 25
  • Is there a way to do this if I am currently relying on `position` and `rotation` properties of Three.js Object3D, which are combined into a matrix during render? Seems like this feature should live inside Matrix4, or maybe in Object3D when it composes a matrix out of the components, one of them being `origin`. Maybe as PoC I need to extend Object3D, Matrix4, or both, and that way all I have to do is `object.origin.set(20, 30, 40)` where the numbers are relative to the objects initial (0,0,0) origin, or similar. – trusktr Mar 13 '19 at 16:38
  • In other words, I'd like to be able to apply origin to objects of any scene without breaking how they work (most of them start off using `position`, `rotation`, etc, and let the renderer composes the parts into a matrix). I guess I'd have to modify other code to get them to use a new Object3D class, or monkey patch `THREE` global before they use it, but at least I can leave all existing logic of all components in place this way, then apply origin to some of them. – trusktr Mar 13 '19 at 16:42
  • I want to avoid re-writing code that uses position/rotation, thus avoid having to make that code manipulate Matrix4 instead. Know what I mean? I want to keep things simple in existing code. Basically, I want to make it as simple as it is to set `transform-origin` on HTML elements using CSS (a one liner), but in this case similar feature on Three.js objects. EDIT: I see BabylonJS has this feature built in! https://doc.babylonjs.com/how_to/pivots That's what I'm aiming to make (in terms of simplicity to be make it re-usable). – trusktr Mar 13 '19 at 16:45
  • sorry, combining two approaches is not possible – Alex Khoroshylov Mar 13 '19 at 16:48
  • btw ```.position``` and ```.rotation``` are very limited per se. I wonder if this is possible at all, to solve your problem not jumping into matrix world. – Alex Khoroshylov Mar 13 '19 at 16:55
  • Yeah, that's what it looks like. What I'm thinking is to extend `Matrix4` to change the `Matrix4.compose( position, quaternion, scale )` signature to `Matrix4.compose( position, quaternion, scale, origin )`. The I should be able to patch `THREE.Object3D` without affecting existing code, and I'll implement something like your example in the `compose` method, then in any existing code I can simply go write `object.origin.set(1,2,3)` where needed. – trusktr Mar 13 '19 at 17:07
  • Thanks for your example! Very insightful on how implementation could be inside Matrix4. :) – trusktr Mar 13 '19 at 17:07
  • Right, this may possibly work, however it already goes away from pivot question. Better create another question to keep topics detached from each other. – Alex Khoroshylov Mar 13 '19 at 17:08
  • Alex, do you have a GitHub username, so I can mention you in the feature idea? – trusktr Mar 13 '19 at 17:21
  • https://github.com/nmalex, you can also check my channel https://www.youtube.com/user/MRIYArender – Alex Khoroshylov Mar 13 '19 at 17:26