'How to rotate camera around object without centering to it

I would like to make a camera rotate around object, but without shifting pivot to it's center. A good example I made with blender: Link to gif (In this example camera rotates around cursor, but it works as an example)

So what I want is when I click a certain object, I want to rotate around it, but without centering camera pivot to objects position, basically retaining objects position on screen. I found many examples on rotating around objects center, but I can seem to find anything for my problem.

Currently I have working camera rotation and movement, but I don't know how to approach this. I am working in OpenGL with Cinder framework. I would be grateful for a simple explanation on how would I be able to do it :)

My current code:

void HandleUICameraRotate() {
//selectedObj <- object...has position etc..

float deltaX = (mMousePos.x - mInitialMousePos.x) / -100.0f;
float deltaY = (mMousePos.y - mInitialMousePos.y) / 100.0f;

// Camera direction vector
glm::vec3 mW = glm::normalize(mInitialCam.getViewDirection());
bool invertMotion = (mInitialCam.getOrientation() * mInitialCam.getWorldUp()).y < 0.0f;

// Right axis vector
vec3 mU = normalize(cross(mInitialCam.getWorldUp(), mW));

if (invertMotion) {
    deltaX = -deltaX;
    deltaY = -deltaY;
}

glm::vec3 rotatedVec = glm::angleAxis(deltaY, mU) * (-mInitialCam.getViewDirection() * mInitialPivotDistance);
rotatedVec = glm::angleAxis(deltaX, mInitialCam.getWorldUp()) * rotatedVec;


mCamera.setEyePoint(mInitialCam.getEyePoint() + mInitialCam.getViewDirection() * mInitialPivotDistance + rotatedVec);
mCamera.setOrientation(glm::angleAxis(deltaX, mInitialCam.getWorldUp()) * glm::angleAxis(deltaY, mU) * mInitialCam.getOrientation());
}


Solution 1:[1]

This is how you can do this rotation (look at the function orbit(...) in the code below).

The basic idea is to rotate the position and the lookAt direction of the camera about the target position. When you run the code demo, use the mouse right button to select the target, and move the mouse to rotate the camera around the target.

Hit me up if you need any clarifications.

let renderer;
let canvas;
let camera;
let scene;

const objects = [];
const highlightGroup = new THREE.Group();

const xaxis = new THREE.Vector3(1, 0, 0);
const yaxis = new THREE.Vector3(0, 1, 0);
const zaxis = new THREE.Vector3(0, 0, 1);
const radius = 10;
const fov = 40;
const tanfov = Math.tan(fov * Math.PI / 360.0);


function initCamera() {
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 2000;
  camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.set(0, 0, 500);
}

function initLights() {
  const color = 0xFFFFFF;
  const intensity = 1;
  const light = new THREE.PointLight(color, intensity);
  light.position.set(0,0,200)
  scene.add(light);  

  const light1 = new THREE.PointLight(color, intensity);
  light1.position.set(100,200,-200)
  scene.add(light1);  
}

function initObjects() {
  const geometry = new THREE.SphereBufferGeometry( radius, 13, 13 );
  const yellowMat = new THREE.MeshPhongMaterial( {color: 0xffff00} );
  const redMat = new THREE.MeshPhongMaterial( {color: 0xff0000} );
  const greenMat = new THREE.MeshPhongMaterial( {color: 0x00ff00} );
  const blueMat = new THREE.MeshPhongMaterial( {color: 0x0000ff} );
  const magentaMat = new THREE.MeshPhongMaterial( {color: 0xff00ff} );
  const cyanMat = new THREE.MeshPhongMaterial( {color: 0x00ffff} );
  const lblueMat = new THREE.MeshPhongMaterial( {color: 0x6060ff} );

  let sphere
  sphere = new THREE.Mesh( geometry, yellowMat );
  sphere.position.set(0, 0, 0);
  objects.push(sphere);
  scene.add(sphere)

  sphere = new THREE.Mesh( geometry, redMat );
  sphere.position.set(50, 0, 0);
  objects.push(sphere);
  scene.add(sphere)

  sphere = new THREE.Mesh( geometry, blueMat );
  sphere.position.set(0, 0, 50);
  objects.push(sphere);
  scene.add(sphere)

  sphere = new THREE.Mesh( geometry, greenMat );
  sphere.position.set(0, 50, 0);
  objects.push(sphere);
  scene.add(sphere)

  sphere = new THREE.Mesh( geometry, magentaMat );
  sphere.position.set(0, -50, 0);
  objects.push(sphere);
  scene.add(sphere)

  sphere = new THREE.Mesh( geometry, cyanMat );
  sphere.position.set(-50, 0, 0);
  objects.push(sphere);
  scene.add(sphere);
  
  sphere = new THREE.Mesh( geometry, lblueMat );
  sphere.position.set(0, 0, -50);
  objects.push(sphere);
  scene.add(sphere);
  
  scene.add( highlightGroup );  
}

function createRenderLoop() {
  function render(time) {
    time *= 0.001;
    renderer.render(scene, camera);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}

function initEventHandlers() {
  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
  }
  window.addEventListener( 'resize', onWindowResize, false );
  onWindowResize()
  
  canvas.addEventListener('contextmenu', event => event.preventDefault());
}

function initOrbitCam() {
  const diffToAngle = 0.01;
  const hscale = 1.05;
  const highlightMat = new THREE.MeshBasicMaterial({
    color: 0xffffff,
    transparent: true,
    opacity: 0.2,
  });
  let isMouseButtonDown = -1;
  let mouseDownPos;
  let rightDownDragging = false;
  let savedCamPos;
  let savedCamLookAt = new THREE.Vector3();
  let orbitTarget;

  function absScrDist(pos1, pos2) {
    return Math.abs(pos1[0] - pos2[0]) + Math.abs(pos1[1] - pos2[1]);
  }
  
  function addHighlight(obj) {
      const objCopy = obj.clone();
      objCopy.material = highlightMat;
      objCopy.scale.set(hscale, hscale, hscale);
      highlightGroup.add(objCopy);
  }
  
  function emptyHighlightGroup() {
    highlightGroup.children.slice(0).forEach(child => {
      highlightGroup.remove(child);
    })
  }
  
  function getTarget(camera, event) {
    const [x, y] = [event.offsetX, event.offsetY];
    const [cw, ch] = [canvas.width, canvas.height];
    const mouse3D = new THREE.Vector3( ( x / cw ) * 2 - 1,   
                                    -( y / ch ) * 2 + 1,  
                                    0.5 );     
    const raycaster =  new THREE.Raycaster();                                        
    raycaster.setFromCamera( mouse3D, camera );
    const intersects = raycaster.intersectObjects( objects );
    console.log(intersects)
    if ( intersects.length > 0 ) {
      addHighlight(intersects[0].object);
      return intersects[0].object.position.clone();
    }    

    const nv = new THREE.Vector3();
    camera.getWorldDirection(nv);
    return camera.position.clone().add(nv.clone().multiplyScalar(500));
  }

  function onCanvasMouseDown(event) {
    isMouseButtonDown = event.button;
    mouseDownPos = [event.offsetX, event.offsetY];
    orbitTarget = getTarget(camera, event);
    event.preventDefault();
    event.stopPropagation();
  }
  canvas.addEventListener("mousedown", onCanvasMouseDown, false);

  function onCanvasMouseUp(event) {
    isMouseButtonDown = -1;
    rightDownDragging = false;
    emptyHighlightGroup();
    event.preventDefault();
    event.stopPropagation();
  }
  canvas.addEventListener("mouseup", onCanvasMouseUp, false);

  function onCanvasMouseMove(event) {
    if (rightDownDragging === false) {
      if (isMouseButtonDown === 2) {
        const currPos = [event.clientX, event.clientY];
        const dragDist = absScrDist(mouseDownPos, currPos);
        if (dragDist >= 5) {
          rightDownDragging = true;
          savedCamPos = camera.position.clone();
          camera.getWorldDirection( savedCamLookAt );
        }
      }
    } else {
      const xdiff = event.clientX - mouseDownPos[0];
      const ydiff = event.clientY - mouseDownPos[1];
      const yAngle = xdiff * diffToAngle;
      const xAngle = ydiff * diffToAngle;
      orbit(-xAngle, -yAngle, savedCamPos.clone(), savedCamLookAt.clone(), orbitTarget)
    } 
  }
  canvas.addEventListener("mousemove", onCanvasMouseMove, false);

  function orbit(xRot, yRot, camPos, camLookAt, target) {
    const newXAxis = camLookAt.clone();
    const lx = camLookAt.x;
    const lz = camLookAt.z;
    newXAxis.x = -lz;
    newXAxis.z = lx;
    newXAxis.y = 0;

    const newCamPos = camPos
      .sub(target)
      .applyAxisAngle( newXAxis, xRot )
      .applyAxisAngle( yaxis, yRot )
      .add(target);
    camera.position.set(...newCamPos.toArray());
    

    const relLookAt = camLookAt
      .applyAxisAngle( newXAxis, xRot )
      .applyAxisAngle( yaxis, yRot )
      .add(newCamPos);
    camera.lookAt(...relLookAt.toArray());

    camera.updateProjectionMatrix();
  }
}

function setup() {
  canvas = document.querySelector('#c');
  renderer = new THREE.WebGLRenderer({canvas});
  scene = new THREE.Scene();
  initCamera();
  initLights();
  initObjects();
  initEventHandlers();
  initOrbitCam();
  createRenderLoop();
}

setup();
#c {
  width: 100vw;
  height: 100vh;
  display: block;
}
<canvas id="c"></canvas>
  
<script src="https://unpkg.com/[email protected]/examples/js/libs/stats.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
  <script src="https://unpkg.com/[email protected]/examples/js/controls/OrbitControls.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>

Solution 2:[2]

I don't exactly understand what you want to do... But maybe this helps...

Transformations in 3d space happen through matrcices, there are different kind of transformation matrices (i.e. translation, scale, rotation, ...) if you want to rotate an object around an axis which is not its own, you will have to move the object to this axis, rotate it this position and than move it back. What will happen is you multply the coordinates of whatever object you want to rotate around something, by the translation matrix, then mutltiply with a rotation matrix and than again multiple with a translation matrix. Luckily according to the rules of linear algebra, we can simply multiply all of these matrices in order, than multply it with the coordinates...

instead of this:

translationMatrix * somePosition;

rotationMatrix * somePosition;

anotherTranslationMatrix * somePosition;

this:

translationMatrix * rotationMatrix * anotherTranslationMatrix * somePosition;

It is a bit vague to explain this like that, but the idea is there. This might seem a like a lot of work, but GPUs are highly optimised to perform matrix multiplications, so if you succeed in lettling the GPU perform these, it will not be an issue performance wise...

If you already knew this: welp... If you did not know this, research some linear algebra, specifically: coordinate spaces, matrix multiplication and transformation matrices.

cheers!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Plegeus