Заставить 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/98/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/98/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/98/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

Изменить: редакционные изменения, чтобы сделать предложение более четким.

1 ответ

Решение

Я сделал это, представив поддельную камеру, которая имеет все то же самое, что и оригинальная камера, за исключением 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/98/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, который можно применять, основываясь на том же принципе. Обратите внимание, что localTarget свойство это просто псевдоним для target имущество. Здесь нет globalTarget имущество.

THREE.OrbitControlsLocal = function ( realObject, domElement ) {
    this.realObject = realObject;
    //Camera and Object3D have different forward direction:
    let placeholderObject = realObject.isCamera ? new THREE.PerspectiveCamera() : new THREE.Object3D;
    this.placeholderObject = placeholderObject;

    THREE.OrbitControls.call( this, placeholderObject, domElement );

    let globalUpdate = this.update;
    this.globalUpdate = globalUpdate;
    this.update = function() {

        //This responds to changes made to realObject from outside the controls:
        placeholderObject.position.copy( realObject.position );
        placeholderObject.quaternion.copy( realObject.quaternion);
        placeholderObject.scale.copy( realObject.scale );
        placeholderObject.up.copy( realObject.up );

        var retval = globalUpdate();
        realObject.position.copy( placeholderObject.position );
        realObject.quaternion.copy( placeholderObject.quaternion);
        realObject.scale.copy( placeholderObject.scale );
        return retval ;

    };

    this.update();
};

THREE.OrbitControlsLocal.prototype = Object.create(THREE.OrbitControls.prototype);
THREE.OrbitControlsLocal.prototype.constructor = THREE.OrbitControlsLocal;

Object.defineProperties(THREE.OrbitControlsLocal.prototype, {
    localTarget: {
        get: ()=>this.target,
        set: v=>this.target=v
    }
});

Мое предыдущее решение простой конвертации локальной цели в мировое пространство перед применением lookAt было неверным. Кажется, проблема в том, что при каждом обновлении look ориентирует камеру в соответствии с ее направлением вверх (camera.up или object.up). Эта проблема не существует с решением заполнителя /fakeCamera. (См. PR https://github.com/mrdoob/three.js/pull/16374)

Другие вопросы по тегам