首页 > 解决方案 > 强制 OrbitControls 围绕移动对象导航(几乎可以正常工作)

问题描述

我正在学习 Three.js,我正在玩太阳系模型来了解它是如何工作的。所以我有一个场景,地球围绕太阳旋转,月球围绕地球旋转。

现在我想专注于月球并使用控件围绕它旋转(同时让它一直位于屏幕的中心)。OrbitControls 似乎是理想的选择,但我无法让它们与移动的月球一起工作。

这是我的 3 次尝试(请忽略地球和月球是立方体)。

尝试 1 - 放置相机(jsfiddle

首先,我创建了一个场景,其中camera是月球的孩子(没有 OrbitControls)。

moon.add(camera);
camera.lookAt(0, 0, 0);

var camera, controls, scene, renderer, labelRenderer;
var solarPlane, earth, moon;
var angle = 0;

function buildScene() {
  scene = new THREE.Scene();
  solarPlane = createSolarPlane();
  earth = createBody("Earth");
  moon = createBody("Moon");

  scene.add(solarPlane);
  solarPlane.add(earth);
  earth.add(moon);

  moon.add(camera);
}

init();
animate();

function init() {

  renderer = new THREE.WebGLRenderer({
    antialias: false
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  labelRenderer = new THREE.CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0';
  labelRenderer.domElement.style.pointerEvents = 'none';
  document.body.appendChild(labelRenderer.domElement);

  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(13.670839104116506, 10.62941701834559, 0.3516419193657562);
  camera.lookAt(0, 0, 0);

  buildScene();
}

function animate(time) {

  angle = (angle + .005) % (2 * Math.PI);
  rotateBody(earth, angle, 1);
  rotateBody(moon, angle, 2);

  render();
  requestAnimationFrame(animate);

  function rotateBody(body, angle, radius) {
    body.rotation.x = angle;
    body.position.x = radius * Math.cos(angle);
    body.position.y = radius * Math.sin(angle);
    body.position.z = radius * Math.sin(angle);
  }
}

function render() {
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
}

function createBody(name, parent) {
  var geometry = new THREE.CubeGeometry(1, 1, 1);
  const body = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial());
  body.position.set(1, 1, 1);
  body.scale.set(.3, .3, .3);
  body.name = name;
  body.add(makeTextLabel(name));
  return body;
}

function createSolarPlane() {
  var solarPlane = new THREE.GridHelper(5, 10);
  solarPlane.add(makeTextLabel("solar plane"));
  return solarPlane;
}

function makeTextLabel(label) {
  var text = document.createElement('div');
  text.style.color = 'rgb(255, 255, 255)';
  text.textContent = label;
  return new THREE.CSS2DObject(text);
}
body {
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejs.org/examples/js/renderers/CSS2DRenderer.js"></script>

结果:它很好地将月亮置于中心,但显然你无法在场景中导航,因为我还没有雇用OrbitControls。但是这个尝试可以作为参考。

尝试 2 - 添加 OrbitControls ( jsfiddle )

然后我添加了OrbitControls.

var camera, controls, scene, renderer, labelRenderer;
var solarPlane, earth, moon;
var angle = 0;

function buildScene() {
  scene = new THREE.Scene();
  solarPlane = createSolarPlane();
  earth = createBody("Earth");
  moon = createBody("Moon");

  scene.add(solarPlane);
  solarPlane.add(earth);
  earth.add(moon);

  moon.add(camera);
}

init();
animate();

function init() {

  renderer = new THREE.WebGLRenderer({
    antialias: false
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  labelRenderer = new THREE.CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0';
  labelRenderer.domElement.style.pointerEvents = 'none';
  document.body.appendChild(labelRenderer.domElement);

  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(13.670839104116506, 10.62941701834559, 0.3516419193657562);
  camera.lookAt(0, 0, 0);

  buildScene();

  controls = new THREE.OrbitControls(camera, renderer.domElement);
  controls.enablePan = false;
  controls.enableDamping = false;
}

function animate(time) {

  angle = (angle + .005) % (2 * Math.PI);
  rotateBody(earth, angle, 1);
  rotateBody(moon, angle, 2);

  render();
  requestAnimationFrame(animate);

  function rotateBody(body, angle, radius) {
    body.rotation.x = angle;
    body.position.x = radius * Math.cos(angle);
    body.position.y = radius * Math.sin(angle);
    body.position.z = radius * Math.sin(angle);
  }
}

function render() {
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
}

function createBody(name, parent) {
  var geometry = new THREE.CubeGeometry(1, 1, 1);
  const body = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial());
  body.position.set(1, 1, 1);
  body.scale.set(.3, .3, .3);
  body.name = name;
  body.add(makeTextLabel(name));
  return body;
}

function createSolarPlane() {
  var solarPlane = new THREE.GridHelper(5, 10);
  solarPlane.add(makeTextLabel("solar plane"));
  return solarPlane;
}

function makeTextLabel(label) {
  var text = document.createElement('div');
  text.style.color = 'rgb(255, 255, 255)';
  text.textContent = label;
  return new THREE.CSS2DObject(text);
}
body {
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejs.org/examples/js/renderers/CSS2DRenderer.js"></script>

结果:月球已经从中心移到了一边(不知道为什么?)。当你开始用鼠标导航时,一切都变得疯狂。效果就像是OrbitControls围绕场景中心导航,而相机围绕其父对象(月亮)导航。实际上,它们不会以一致的方式更改状态,并且一切都变得疯狂。

尝试 3 - 控制轨道的目标 ( jsfiddle )

我尝试的最后一个选项是强制设置controls.target,使其始终指向月球。因为月亮不断地四处移动,所以我必须在每次渲染之前都这样做。

const p = new THREE.Vector3();
const q = new THREE.Quaternion();
const s = new THREE.Vector3();
moon.matrixWorld.decompose(p, q, s);

// now setting controls target to Moon's position (in scene's coordinates)
controls.target.copy(p); 

render();

var camera, controls, scene, renderer, labelRenderer;
var solarPlane, earth, moon;
var angle = 0;
const p = new THREE.Vector3();
const q = new THREE.Quaternion();
const s = new THREE.Vector3();

function buildScene() {
  scene = new THREE.Scene();
  solarPlane = createSolarPlane();
  earth = createBody("Earth");
  moon = createBody("Moon");

  scene.add(solarPlane);
  solarPlane.add(earth);
  earth.add(moon);

  moon.add(camera);
}

init();
animate();

function init() {

  renderer = new THREE.WebGLRenderer({
    antialias: false
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  labelRenderer = new THREE.CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0';
  labelRenderer.domElement.style.pointerEvents = 'none';
  document.body.appendChild(labelRenderer.domElement);

  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(13.670839104116506, 10.62941701834559, 0.3516419193657562);
  camera.lookAt(0, 0, 0);

  buildScene();

  controls = new THREE.OrbitControls(camera, renderer.domElement);
  controls.enablePan = false;
  controls.enableDamping = false;
}

function animate(time) {

  angle = (angle + .005) % (2 * Math.PI);
  rotateBody(earth, angle, 1);
  rotateBody(moon, angle, 2);

  moon.matrixWorld.decompose(p, q, s);
  controls.target.copy(p);

  render();
  requestAnimationFrame(animate);

  function rotateBody(body, angle, radius) {
    body.rotation.x = angle;
    body.position.x = radius * Math.cos(angle);
    body.position.y = radius * Math.sin(angle);
    body.position.z = radius * Math.sin(angle);
  }
}

function render() {
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
}

function createBody(name, parent) {
  var geometry = new THREE.CubeGeometry(1, 1, 1);
  const body = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial());
  body.position.set(1, 1, 1);
  body.scale.set(.3, .3, .3);
  body.name = name;
  body.add(makeTextLabel(name));
  return body;
}

function createSolarPlane() {
  var solarPlane = new THREE.GridHelper(5, 10);
  solarPlane.add(makeTextLabel("solar plane"));
  return solarPlane;
}

function makeTextLabel(label) {
  var text = document.createElement('div');
  text.style.color = 'rgb(255, 255, 255)';
  text.textContent = label;
  return new THREE.CSS2DObject(text);
}
body {
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejs.org/examples/js/renderers/CSS2DRenderer.js"></script>

结果:最初月亮位于屏幕的一侧(与第二次尝试中的位置相同),但是当您开始导航时,月亮“跳”到屏幕的中心,您可以在它周围导航。几乎完美。只要你不放大。当您放大/缩小时,您会开始看到月亮在缩放动作期间旋转。

问题

  1. 为什么不OrbitControls尊重相机的父母是月球这一事实,并一直围绕场景中心导航?
  2. 为什么添加后月亮会“跳”到屏幕的一侧OrbitControls
  3. 使它工作的优雅方式是什么?(由于缩放问题,强制target循环跟随月球既不优雅也不工作)?

河。98

编辑:编辑修改使句子更清晰。 编辑:升级到three.js r。109.

标签: three.js

解决方案


我通过引入一个假相机使它起作用,它的一切都与原始相机相同,除了camera.parent

fakeCamera = camera.clone(); // parent becomes null
controls = new THREE.OrbitControls(fakeCamera, renderer.domElement);

这种方式OrbitControls有一个带有自己坐标系的相机。

然后,在渲染之前,我将fakeCamera的值复制回用于渲染的真实相机。

camera.position.copy(fakeCamera.position);
camera.quaternion.copy(fakeCamera.quaternion);
camera.scale.copy(fakeCamera.scale);

render();

它运作良好。

编辑

我注意到

camera.position.copy(fakeCamera.position);
camera.quaternion.copy(fakeCamera.quaternion);
camera.scale.copy(fakeCamera.scale);

可以替换为

camera.copy(fakeCamera);

(以下代码已相应更新)

var camera, fakeCamera, controls, scene, renderer, labelRenderer;
var solarPlane, earth, moon;
var angle = 0;

function buildScene() {
  scene = new THREE.Scene();
  solarPlane = createSolarPlane();
  earth = createBody("Earth");
  moon = createBody("Moon");

  scene.add(solarPlane);
  solarPlane.add(earth);
  earth.add(moon);

  moon.add(camera);
}

init();
animate();

function init() {

  renderer = new THREE.WebGLRenderer({
    antialias: false
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  labelRenderer = new THREE.CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0';
  labelRenderer.domElement.style.pointerEvents = 'none';
  document.body.appendChild(labelRenderer.domElement);

  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(13.670839104116506, 10.62941701834559, 0.3516419193657562);
  camera.lookAt(0, 0, 0);

  buildScene();

  fakeCamera = camera.clone();
  controls = new THREE.OrbitControls(fakeCamera, renderer.domElement);
  controls.enablePan = false;
  controls.enableDamping = false;
}

function animate(time) {

  angle = (angle + .005) % (2 * Math.PI);
  rotateBody(earth, angle, 1);
  rotateBody(moon, angle, 2);

  camera.copy(fakeCamera);

  render();
  requestAnimationFrame(animate);

  function rotateBody(body, angle, radius) {
    body.rotation.x = angle;
    body.position.x = radius * Math.cos(angle);
    body.position.y = radius * Math.sin(angle);
    body.position.z = radius * Math.sin(angle);
  }
}

function render() {
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
}

function createBody(name, parent) {
  var geometry = new THREE.CubeGeometry(1, 1, 1);
  const body = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial());
  body.position.set(1, 1, 1);
  body.scale.set(.3, .3, .3);
  body.name = name;
  body.add(makeTextLabel(name));
  return body;
}

function createSolarPlane() {
  var solarPlane = new THREE.GridHelper(5, 10);
  solarPlane.add(makeTextLabel("solar plane"));
  return solarPlane;
}

function makeTextLabel(label) {
  var text = document.createElement('div');
  text.style.color = 'rgb(255, 255, 255)';
  text.textContent = label;
  return new THREE.CSS2DObject(text);
}
body {
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejs.org/examples/js/renderers/CSS2DRenderer.js"></script>


推荐阅读