mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
101.58k stars 35.29k forks source link

Camera rotation flicker issue when linked to mouse move #25280

Closed coolcomfort closed 1 year ago

coolcomfort commented 1 year ago

Description

I have noticed a common three js problem that appears to span all FPS camera examples in Chrome. The camera appears to flicker or rotate randomly (on x axis), when I move my mouse continuously to the left or right. Here is a video demonstration:

https://streamable.com/4135wl

Notice the three abrupt skips that take place in the first half of the video. I have done a bit of research and the two most common solutions presented appear to be changing the camera order to "YXZ" and as well as implementing "yaw" and "pitch" values for mouse move in order to prevent gimbal lock. I have tried these solutions along with several others and have had no luck in fixing the issue. One last thing to point out is the issue does seem to become more apparent with a higher tessellated environment, but I have had it happen in low tessellated environments as well.

Below, I have created a simplified FPS example based off the one on the Three JS website. This uses no imported meshes, no lighting, no fog, just a scene, camera, three js geometry along with three js provided collision detection. I have placed random boxes around the world to help notice the skip. I have also increased the widthSegments and heightSegments of each box to 250 in order to help with a replicating the bug easier. Feel free to increase or lower these in order to find a correlation.

Reproduction steps

  1. Create a camera and attach it to the scene
  2. Attach a mousemove event listener that can rotate the camera based on mouse event
  3. Add objects or some type of noticeable detail to the scene in order to notice the camera issue

Code

import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { Octree } from "three/addons/math/Octree.js";
import { Capsule } from "three/addons/math/Capsule.js";

const clock = new THREE.Clock();

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x88ccee);

const camera = new THREE.PerspectiveCamera(
  70,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.rotation.order = "YXZ";
scene.add(camera);

const container = document.getElementById("app");

const renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

container.appendChild(renderer.domElement);

const stats = new Stats();
stats.domElement.style.position = "absolute";
stats.domElement.style.top = "0px";
container.appendChild(stats.domElement);

const GRAVITY = 30;

const STEPS_PER_FRAME = 5;

const worldOctree = new Octree();

const playerCollider = new Capsule(
  new THREE.Vector3(0, 13.35, 0),
  new THREE.Vector3(0, 1, 0),
  0.35
);

const playerVelocity = new THREE.Vector3();
const playerDirection = new THREE.Vector3();

let playerOnFloor = false;

const keyStates = {};

document.addEventListener("keydown", (event) => {
  keyStates[event.code] = true;
});

document.addEventListener("keyup", (event) => {
  keyStates[event.code] = false;
});

container.addEventListener("mousedown", () => {
  document.body.requestPointerLock();
});

document.body.addEventListener("mousemove", (event) => {
  camera.rotation.y -= event.movementX / 500;
  camera.rotation.x -= event.movementY / 500;
});

window.addEventListener("resize", onWindowResize);

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

function playerCollisions() {
  const result = worldOctree.capsuleIntersect(playerCollider);

  playerOnFloor = false;

  if (result) {
    playerOnFloor = result.normal.y > 0;

    if (!playerOnFloor) {
      playerVelocity.addScaledVector(
        result.normal,
        -result.normal.dot(playerVelocity)
      );
    }

    playerCollider.translate(result.normal.multiplyScalar(result.depth));
  }
}

function updatePlayer(deltaTime) {
  let damping = Math.exp(-4 * deltaTime) - 1;

  if (!playerOnFloor) {
    playerVelocity.y -= GRAVITY * deltaTime;

    // small air resistance
    damping *= 0.1;
  }

  playerVelocity.addScaledVector(playerVelocity, damping);

  const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime);
  playerCollider.translate(deltaPosition);

  playerCollisions();

  camera.position.copy(playerCollider.end);
}

function getForwardVector() {
  camera.getWorldDirection(playerDirection);
  playerDirection.y = 0;
  playerDirection.normalize();

  return playerDirection;
}

function getSideVector() {
  camera.getWorldDirection(playerDirection);
  playerDirection.y = 0;
  playerDirection.normalize();
  playerDirection.cross(camera.up);

  return playerDirection;
}

function controls(deltaTime) {
  // gives a bit of air control
  const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);

  if (keyStates["KeyW"]) {
    playerVelocity.add(getForwardVector().multiplyScalar(speedDelta));
  }

  if (keyStates["KeyS"]) {
    playerVelocity.add(getForwardVector().multiplyScalar(-speedDelta));
  }

  if (keyStates["KeyA"]) {
    playerVelocity.add(getSideVector().multiplyScalar(-speedDelta));
  }

  if (keyStates["KeyD"]) {
    playerVelocity.add(getSideVector().multiplyScalar(speedDelta));
  }

  if (playerOnFloor) {
    if (keyStates["Space"]) {
      playerVelocity.y = 15;
    }
  }
}

const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const plane = new THREE.PlaneGeometry(300, 300);
const planeMesh = new THREE.Mesh(plane, planeMaterial);
planeMesh.position.y = -5;
planeMesh.rotation.x = -Math.PI / 2;

worldOctree.fromGraphNode(planeMesh);
scene.add(planeMesh);

for (let i = 0; i < 30; i++) {
  const boxGeometry = new THREE.BoxGeometry(
    Math.random() * 10 + 1, // random width
    Math.random() * 10 + 1, // random height
    Math.random() * 10 + 1, // random depth
    250,
    250
  );

  const boxMaterial = new THREE.MeshLambertMaterial({
    color: Math.random() * 0xffffff,
  });
  const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);

  boxMesh.position.set(
    Math.random() * 30 - 15, // random x position
    Math.random() * 30 - 15, // random y position
    Math.random() * 30 - 15 // random z position
  );
  scene.add(boxMesh);
}

animate();

function teleportPlayerIfOob() {
  if (camera.position.y <= -25) {
    playerCollider.start.set(0, 0.35, 0);
    playerCollider.end.set(0, 1, 0);
    playerCollider.radius = 0.35;
    camera.position.copy(playerCollider.end);
    camera.rotation.set(0, 0, 0);
  }
}

function animate() {
  const deltaTime = Math.min(0.05, clock.getDelta()) / STEPS_PER_FRAME;

  for (let i = 0; i < STEPS_PER_FRAME; i++) {
    controls(deltaTime);

    updatePlayer(deltaTime);

    teleportPlayerIfOob();
  }

  controls(deltaTime);
  updatePlayer(deltaTime);
  teleportPlayerIfOob();

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  stats.update();
}

Live example

Screenshots

https://streamable.com/4135wl

Version

r148

Device

Desktop

Browser

Chrome

OS

Windows

Mugen87 commented 1 year ago

I can not reproduce the issue in any of the linked examples. I have test this with Chrome 109.0.5414.87 on macOS. So maybe this is a device related issue.

Do you mind testing whether you see jumps in the following example? https://mdn.github.io/dom-examples/pointer-lock/

coolcomfort commented 1 year ago

Issue does not seem to replicate on the pointer lock example. I would also like to point out that I have tested this in other 3D engines, such as Babylon, Play Canvas and WebGL Unity and have not had the same issue. For these reasons, along with others not being able to reproduce this on their own devices, leads me to believe it may come down to the Three JS engine along with hardware.

Since this post I have tested this issue in FireFox, and instead of skipping, FireFox seems to lag on mousemove whenever the skipping would normally take place. The lag is occurring at about the same rate and just as affecting if not worse. I will take a video on that as well if needed.

Specs: Processor: Intel Core i7-1160G7 Processor Graphics Card: Intel Iris Xe Graphics

Mugen87 commented 1 year ago

Can you try to update the Intel driver to the latest version? I know it sounds silly but could you also test with a different mouse?

coolcomfort commented 1 year ago

I did have a pending driver update for my graphics card, but after installing, this did not fix the issue. When I first started studying three js I assumed the issue was the mouse, so I went ahead and purchased a new one. However, after exploring other 3D game engines for JS and not seeing the same issue, I am more inclined to believe this has to do with possibly the internals of three js. More than willing to share more details.

Mugen87 commented 1 year ago

I've worked and tested PointerLockControls on different devices using different operating systems. I also use a similar implementation for first-person games where I have never encountered this particular issue (nor it was reported by users).

If we (the maintainers) are not able to reproduce the issue on our devices, I'm afraid we can't investigate the root cause. In such cases, usually the OP has to find the exact line(s) of code causing the issue.

kkota1 commented 1 year ago

I believe this only happens if the mouse has a polling rate > 250Hz. I implemented this solution a few months ago and have had no issues since.

https://stackoverflow.com/a/69672260/4577551

Basically you need to exclude any movementX or movementY values whose absolute value is greater than window.innerWidth/3 or window.innerHeight/3, respectively

Mugen87 commented 1 year ago

Thanks for the feedback! Unfortunately, this does not explain why the OP experienced the issue only with three.js apps and not with other JS engines.

In any event, this issue is going to be closed since there are too less information for further investigation.

kkota1 commented 1 year ago

It’s a 3+ year old OS bug. Not three specific

On Wed, Mar 29, 2023 at 1:06 AM Michael Herzog @.***> wrote:

Thanks for the feedback! Unfortunately, this does not explain why the OP experienced the issue only with three.js apps and not with other JS engines.

In any event, this issue is going to be closed since there are too less information for further investigation.

— Reply to this email directly, view it on GitHub https://github.com/mrdoob/three.js/issues/25280#issuecomment-1488126643, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJ3FPG56PXDQ6XXRD2ZUJITW6PUQ7ANCNFSM6AAAAAATZ6A7YY . You are receiving this because you commented.Message ID: <mrdoob/three. @.***>

coolcomfort commented 1 year ago

This is definitely pertaining to Xiris graphics card and likely nothing else. I've tested this issue on multiple devices and haven't had the same issue.

Mugen87 commented 6 months ago

FYI: There is a workaround now that fixes the stutter: https://github.com/mrdoob/three.js/issues/27747#issuecomment-1944413572