Rotating viewport in threejs on keyboard like in threejs holding left mouse's key event

I have an option to move forward/backward and right/left

And i wanted to have an option to rotate viewport
like it does threejs on holding left mouse’s key

currently for rotation i have a code:

if (keys['home']) {
  camera.rotation.y -= rotationSpeed * deltaTime; // Rotate left
}
if (keys['delete']) {
  camera.rotation.y += rotationSpeed * deltaTime; // Rotate right
}

but as you see in video, it diagonally moves viewport

is there way to emulate rotation like we have on holding left mouse’s key
or may be there another option?

i also wanted to move forward/backward/left/right after rotating like it does original rotating on holding left mouse’s key

like in this case:

but when rotating camera target then there should be an option to move forward/backward/left/right into side of camera target, not an original side
and there is a bug with using left mouse, it breaks position to original value in some cases

From your .mp4 one can tell, that the camera is not looking horizontally, but at a downwards angle. Rotating the camera about its local(!) y-axis, which is not perpendicular wrt the world, gives you the diagonal tilting of the horizon.

You need to decide wether you want to

  • orbit the camera about the world’s y-axis through the camera.target, keeping the scene fixed, or
  • rotate the scene “underneath” a fixed camera/target about the world’s y-axis through the camera.target

which are two fundamentally different options.

It’s more of a hack, but since we can only control the OrbtiControls rotation through the animation, we can manipulate the animation itself.

You can see it in action in this demo, and here’s the code:

const controls = new OrbitControls(camera, renderer.domElement);
const rotationSpeed = 10;
const leftKeys = {
  ArrowLeft: "left",
  KeyA: "left", // QWERTY
  KeyQ: "left", // AZERTY
}
const righKeys = {
  ArrowRight: "right",
  KeyD: "right",
}
const directionsKeys = { ...leftKeys, ...righKeys }

document.body.onkeydown = (event) => {
  const key = event.code

  if (!directionsKeys[key]) return

  controls.autoRotate = true

  if (leftKeys[key]) controls.autoRotateSpeed = rotationSpeed
  else controls.autoRotateSpeed = -rotationSpeed
}

document.body.onkeyup = () => {
  controls.autoRotate = false
}
3 Likes

vielzutun.ch

i need this case

i think we compute fixed target in center of the scene

scene fixed,

  1. camera rotating on the circular orbit
    looking into fixed target in the center of the scene

  2. camera moving a/w/s/d perpendicular to viewport

Fennec

in your demo
rotating using mouse is correct
and camera should move perpendicular forward/backward/left/right on /w/s/a/d

Something like this: Three.js: MapControls with keyboard controls - JSFiddle - Code Playground

I’m using the default controls keys for the panning w/s/a/d:

controls.keys = {
    LEFT: 'KeyA',
    UP: 'KeyW', 
    RIGHT: 'KeyD', 
    BOTTOM: 'KeyS' 
  }
controls.keyPanSpeed = 20;
controls.listenToKeyEvents(domElementKeyListener)

And custom listener for the orbit rotation KeyQ/KeyE:

const rotationSpeed = 10;
const rotationLeftKey = "KeyQ"
const rotationRightKey = "KeyE"

function onKeyDown(event) {
    const key = event.code

    if (key === rotationLeftKey) controls.autoRotateSpeed = rotationSpeed
    else if(key === rotationRightKey) controls.autoRotateSpeed = -rotationSpeed
    else return;

    controls.autoRotate = true;
}

function onKeyUp() {
    controls.autoRotate = false
}
  
domElementKeyListener.addEventListener("keydown", onKeyDown);
domElementKeyListener.addEventListener("keyup", onKeyUp);

1 Like

@Fennec yes, it exactly what is required!

and i need to disable holding left mouse’s button and holding right mouse’s button
cause they may be missclicked (i tried to use standard events click, but it called after original threejs pan event called, so preventDefault didnt work)

zoomPlus/zoomMinus I may add by myself I just need to change camera.position.y, right?

You could disconnect the controls right after initializing them, here’s a demo showing that. It removes all the mouse event listeners, but at that point things start to feel pretty hacky, and you’re basically stripping away everything MapControls is meant to do.

A cleaner way might be to just skip OrbitControls or MapControls entirely and use a simple custom camera controller, especially if you don’t need mouse input. Something like the following class, you can see it in action in this demo.

class CameraKeyboardController {
  camera: THREE.PerspectiveCamera;
  domElement: HTMLElement;

  target: THREE.Vector3 = new THREE.Vector3();
  moveSpeed = 5;
  rotationSpeed = Math.PI;

  private keys: Record<string, boolean> = {};
  private clock = new THREE.Clock();
  private forward = new THREE.Vector3();
  private move = new THREE.Vector3();
  private right = new THREE.Vector3();
  private offset = new THREE.Vector3();
  private yUP = new THREE.Vector3();

  constructor(camera: THREE.PerspectiveCamera, domElement: HTMLElement) {
    this.camera = camera;
    this.domElement = domElement;

    this.initEventListeners();
    this.clock.start();
  }

  private initEventListeners() {
    window.addEventListener("keydown", (e) => (this.keys[e.code] = true));
    window.addEventListener("keyup", (e) => (this.keys[e.code] = false));
  }

  update() {
    const delta = this.clock.getDelta();

    this.forward.setScalar(0);
    this.move.setScalar(0);
    this.right.setScalar(0);

    this.camera.getWorldDirection(this.forward);
    this.forward.y = 0;
    this.forward.normalize();

    this.right.crossVectors(this.forward, this.camera.up).normalize();

    if (this.keys["KeyW"]) this.move.add(this.forward);
    if (this.keys["KeyS"]) this.move.sub(this.forward);
    if (this.keys["KeyD"]) this.move.add(this.right);
    if (this.keys["KeyA"]) this.move.sub(this.right);

    if (this.move.lengthSq() > 0) {
      this.move.normalize().multiplyScalar(this.moveSpeed * delta);
      this.camera.position.add(this.move);
      this.target.add(this.move);
    }

    if (this.keys["KeyQ"] || this.keys["KeyE"]) {
      const angle = (this.keys["KeyQ"] ? 1 : 0) - (this.keys["KeyE"] ? 1 : 0);
      const theta = angle * this.rotationSpeed * delta;

      this.offset
        .subVectors(this.camera.position, this.target)
        .applyAxisAngle(this.yUP, theta);
      this.camera.position.copy(this.target).add(this.offset);

      this.camera.lookAt(this.target);
    } else {
      this.camera.lookAt(this.target);
    }
  }

  dispose() {
    window.removeEventListener("keydown", this.keyDownHandler);
    window.removeEventListener("keyup", this.keyUpHandler);
  }

  private keyDownHandler = (e: KeyboardEvent) => {
    this.keys[e.code] = true;
  };

  private keyUpHandler = (e: KeyboardEvent) => {
    this.keys[e.code] = false;
  };
}