I am learning Three.js and I am playing with the model of solar system to learn how it works. So I have a scene in which the Earth rotates around the Sun, and the Moon around the Earth.
Now I would like to focus on the Moon and use controls to rotate around it (while having it all the time in the center of the screen). OrbitControls seem to be ideal for that, but I cannot get them to work with the moving Moon.
Here are my 3 attempts (please ignore that the Earth and the Moon are cubes).
Attempt 1 - Placing camera (jsfiddle)
First, I created a scene where camera is a child of the Moon (without 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>
Result: it nicely puts the Moon in the center, but obviously you cannot navigate the scene, because I haven't employed OrbitControls yet. But this attempt acts as a reference.
Attempt 2 - Adding OrbitControls (jsfiddle)
Then I added 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>
Result: the Moon has been moved from the center to the side (no idea why?). And when you start navigating with the mouse, everything goes crazy. The effect is as if OrbitControls navigates around the center of the scene and the camera around its parent (the Moon). Effectively they don't change state in a consistent manner, and everything goes wild.
Attempt 3 - Controlling Orbits' target (jsfiddle)
Last option I tried was to forcefully set controls.target so that it always points at the Moon. Because the Moon constantly moves around, I had to do it before each rendering.
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>
Result: Initially the Moon is located on the side of the screen (same position as in the second attempt), but then when you start navigate, the Moon "jumps" to the center of the screen, and you can navigate around it. Almost perfect. As long you don't zoom. When you zoom in/zoom out, you start seeing that the Moon rotates during the zooming action.
Questions
- why does
OrbitControlsnot respect the fact that camera's parent is the Moon, and keeps navigating around the center of the scene? - why did the Moon "jump" to the side of the screen after adding
OrbitControls? - what would be the elegant way of making it work? (forcing
targetto follow the Moon in a loop is neither elegant nor working due to the zooming issue)?
r. 98
Edit: editorial changes to make a sentence more clear. Edit: upgrade to three.js r. 109.