OrbitControls on a shared camera cannot handle panning

Viewed 30

I am trying to create two renderers on two different div containers. Each renderer wish has its own OrbitControls for updating the associated camera. The camera is shared between the two renderers.

The goal is to sync the camera for OrbitControl (so that two containers can have the same camera view) and render between the two render loops.

I noticed that the general rotation could be shared automatically. However, when I do panning, it behaves a bit strange. Here is a video:

https://user-images.githubusercontent.com/5498964/170499367-44c2581d-8089-492e-bc90-a6675976bb44.mov

This is the minimum reproducible code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>

    <div id="rend1" style="height: 400px; width: 400px;"></div>
    <div id="rend2" style="height: 400px; width: 400px;"></div>

<script type="module">
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.121.1/examples/jsm/controls/OrbitControls.js';

const rend1 = document.getElementById("rend1")
const rend2 = document.getElementById("rend2")

const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 1000);
camera.position.copy(new THREE.Vector3(0, 1, 2));
camera.lookAt(new THREE.Vector3(0, 0, 0));

const r1 = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true,
    precision: "highp",
    alpha: true,
  });
r1.setPixelRatio(window.devicePixelRatio);
const s1 = new THREE.Scene();
rend1.append(r1.domElement);

const c1 =  new OrbitControls(camera, r1.domElement)
s1.add(new THREE.AxesHelper(1));
s1.add(camera);
const g1 = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
const m1 = new THREE.MeshBasicMaterial( {color: 0x0080ff} );
const cube1 = new THREE.Mesh(g1, m1);
s1.add(cube1)

const r2 = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true,
    precision: "highp",
    alpha: true,
  });
r2.setPixelRatio(window.devicePixelRatio);
const s2 = new THREE.Scene();
rend2.append(r2.domElement);
const c2 =  new OrbitControls(camera, r2.domElement)
s2.add(new THREE.AxesHelper(1));
s2.add(camera);
const g2 = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
const m2 = new THREE.MeshBasicMaterial( {color: 0x0080ff} );
const cube2 = new THREE.Mesh(g2, m2);
s2.add(cube2)



const render1 = () => {
    r1.setSize(400, 400);
    c1.update();
    r1.render(s1, camera);
    window.requestAnimationFrame(() => render1());
};
const render2 = () => {
    r2.setSize(400, 400);
    c2.update();
    r2.render(s2, camera);
    window.requestAnimationFrame(() => render2());
};
render1 ();
render2 ();
</script>
</body>
</html>

What is the reason and how could we achieve desired behavior?

Thank you!

2 Answers

You don't need to add your camera to any scene. The camera can be independent. Additionally, an Object3D can only have one parent, so when you do

s1.add(camera);
s2.add(camera);

... then the camera only gets added to s2, because it automatically gets removed from s1 when you assign a new parent.

Try removing all the .add(camera) from your code.

Edit:

Additionally, you're adding two CameraControls to the same camera, then you're calling c1.update() and c2.update(). These two are giving conflicting commands to the camera because one control tells it to move, while the other one tells it to stay still. You should only use one control if you want to manipulate a single camera.

The actual solution for this problem is to sync cameras and orbit control's target explicitly as they change. See c1.addEventListener("change", () => { ... }} and c2.addEventListener("change", () => { ... }}.

Here is a complete code for solving the issue:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>

    <div id="rend1" style="height: 400px; width: 400px;"></div>
    <div id="rend2" style="height: 400px; width: 400px;"></div>

<script type="module">
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.121.1/examples/jsm/controls/OrbitControls.js';

const rend1 = document.getElementById("rend1")
const rend2 = document.getElementById("rend2")

const camera1 = new THREE.PerspectiveCamera(40, 1, 0.01, 1000);
camera1.position.copy(new THREE.Vector3(0, 1, 2));
camera1.lookAt(new THREE.Vector3(0, 0, 0));
const camera2 = new THREE.PerspectiveCamera(40, 1, 0.01, 1000);
camera2.position.copy(new THREE.Vector3(0, 1, 2));
camera2.lookAt(new THREE.Vector3(0, 0, 0));
const r1 = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true,
    precision: "highp",
    alpha: true,
  });
r1.setPixelRatio(window.devicePixelRatio);
const s1 = new THREE.Scene();
rend1.append(r1.domElement);

const c1 =  new OrbitControls(camera1, r1.domElement)
s1.add(new THREE.AxesHelper(1));
s1.add(camera1);
const g1 = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
const m1 = new THREE.MeshBasicMaterial( {color: 0x0080ff} );
const cube1 = new THREE.Mesh(g1, m1);
s1.add(cube1)
c1.addEventListener("change", () => {
    camera2.position.copy(camera1.position)
    camera2.rotation.copy(camera1.rotation)
    c2.target.copy(c1.target)
})

const r2 = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true,
    precision: "highp",
    alpha: true,
  });
r2.setPixelRatio(window.devicePixelRatio);
const s2 = new THREE.Scene();
rend2.append(r2.domElement);
const c2 =  new OrbitControls(camera2, r2.domElement)
s2.add(new THREE.AxesHelper(1));
s2.add(camera2);
const g2 = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
const m2 = new THREE.MeshBasicMaterial( {color: 0x0080ff} );
const cube2 = new THREE.Mesh(g2, m2);
s2.add(cube2)
c2.addEventListener("change", () => {
    camera1.position.copy(camera2.position)
    camera1.rotation.copy(camera2.rotation)
    c1.target.copy(c2.target)
})




const render1 = () => {
    r1.setSize(400, 400);
    c1.update();
    r1.render(s1, camera1);
    window.requestAnimationFrame(() => render1());
};
const render2 = () => {
    r2.setSize(400, 400);
    c2.update();
    r2.render(s2, camera2);
    window.requestAnimationFrame(() => render2());
};
render1 ();
render2 ();
</script>
</body>
</html>
Related