Three.js FirstPersonControl does nothing - three.js

I implemented the following short script:
var screenWidth = window.innerWidth;
var screenHeight = window.innerHeight;
var camera;
var controls;
var scene;
var renderer;
var container;
var controls;
var keyboard = new THREEx.KeyboardState();
var clock = new THREE.Clock();
var light;
var floor;
var movingGeometry;
function setup()
{
var viewAngle = 45;
var aspect = screenWidth / screenHeight;
var near = 0.1;
var far = 20000;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(viewAngle, aspect, near, far);
camera.position.set(0,150,400);
camera.lookAt(scene.position);
scene.add(camera);
controls = new THREE.FirstPersonControls(camera);
controls.movementSpeed = 70;
controls.lookSpeed = 0.05;
controls.noFly = true;
controls.lookVertical = false;
renderer = new THREE.WebGLRenderer();
renderer.setSize(screenWidth, screenHeight);
container = document.getElementById('canvas');
container.appendChild( renderer.domElement );
createLight();
createFloor();
createSkyBox();
createGeometry();
animate();
}
function createLight()
{
light = new THREE.PointLight(0xffffff);
light.position.set(0,250,0);
scene.add(light);
}
function createFloor()
{
var floorMaterial = new THREE.MeshLambertMaterial({color: 0x00FF00});
floor = new THREE.Mesh(new THREE.BoxGeometry(1000, 1000, 3, 1, 1, 1), floorMaterial);
floor.position.y = -0.5;
floor.rotation.x = Math.PI / 2;
scene.add(floor);
}
function createSkyBox()
{
var skyBoxGeometry = new THREE.BoxGeometry(10000, 10000, 10000);
var skyBoxMaterial = new THREE.MeshBasicMaterial({color: 0x0000FF, side: THREE.BackSide});
var skyBox = new THREE.Mesh(skyBoxGeometry, skyBoxMaterial);
scene.add(skyBox);
}
function createGeometry()
{
var material = new THREE.MeshNormalMaterial();
var geometry = new THREE.BoxGeometry(50, 50, 50);
movingGeometry = new THREE.Mesh(geometry, material);
movingGeometry.position.set(0, 28, 0);
scene.add(movingGeometry);
}
function animate()
{
requestAnimationFrame(animate);
render();
update();
}
function render()
{
renderer.render(scene, camera);
controls.update();
}
function update()
{
var delta = clock.getDelta(); // seconds.
var moveDistance = 200 * delta; // 200 pixels per second
var rotateAngle = Math.PI / 2 * delta; // pi/2 radians (90 degrees) per second
if (keyboard.pressed("W"))
{
movingGeometry.translateZ(-moveDistance);
}
if (keyboard.pressed("S"))
{
movingGeometry.translateZ(moveDistance);
}
if (keyboard.pressed("A"))
{
movingGeometry.rotateOnAxis(new THREE.Vector3(0,1,0), rotateAngle);
}
if (keyboard.pressed("D"))
{
movingGeometry.rotateOnAxis(new THREE.Vector3(0,1,0), -rotateAngle);
}
var relativeCameraOffset = new THREE.Vector3(0,50,200);
var cameraOffset = relativeCameraOffset.applyMatrix4(movingGeometry.matrixWorld);
camera.position.x = cameraOffset.x;
camera.position.y = cameraOffset.y;
camera.position.z = cameraOffset.z;
camera.lookAt(movingGeometry.position);
}
I wanted to implement a camera which is sticking to an object. If i use 'w', 'a', 's', 'd' i can move the object and the camera follows. But i also want to be able to rotate the camera (at its position) by leftclick + dragging and i also want to rotate the object by rightclick + dragging (the typical first person behaviour).
So i added the FirstPersonControls from Three.js to the camera. The result: nothing happens when i use the mouse or click or anything and i also have no idea what i need to do to rotate the object by rightclicking and dragging.
Can someone help?

At first sight it seems like you have a problem with overwriting the cameras lookAt
Since in update() you do :
camera.lookAt(movingGeometry.position);
List item
Your order of execution order is:
animate
(your) render
(threejs) render
(threejs) controls update
(your) update
and in your update you overwrite the cameras lookat from the first person controls.

Related

I am having a problem with three.js collision detection

We are creating a three.js based game where players can eat food, currently we have a collision script but it is not working properly. Any help to get it working so our players can eat would be greatly appreciated.
The code is below.
Code snippet:
//sorry in advance for the crazy code structure o.o
//variables
var scene, renderer, rayCaster;
var WORLD, floor, FOOD, MWORLD;
var plr, camera, controls;
function debugupdate()
{
window.plr = plr
window.floor = floor
window.WORLD = WORLD
window.camera = camera
window.controls = controls
window.scene = scene
window.FOOD = FOOD
}
setInterval(debugupdate, 1000)
//setup scene for gameplay
function InitGame()
{
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer();
rayCaster = new THREE.Raycaster();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
camera.position.y = 8;
camera.position.z = 8;
controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.autoRotate = false;
controls.enablePan = false;
//controls.update() must be called after any manual changes to the camera's transform
//camera.position.set( 0, 20, 100 );
//controls.update();
//MWORLD = stuff mouse can detect
MWORLD = new THREE.Object3D();
MWORLD.name = 'MWORLD'
var floorgeo = new THREE.BoxGeometry(30, 0.5, 30);
var floormat = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
floor = new THREE.Mesh( floorgeo, floormat );
MWORLD.add(floor)
WORLD = new THREE.Object3D();
WORLD.add( MWORLD );
WORLD.name = 'WORLD';
scene.add(WORLD);
}
InitGame();
//Mouse Stuff
var MousePos;
var PlrTarget;
document.addEventListener('mousemove', MouseToWorld, false);
function MouseToWorld(event) {
event.preventDefault();
var mouse = {};
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
var vector = new THREE.Vector3(mouse.x, mouse.y, 0.5);
vector.unproject( camera );
var dir = vector.sub( camera.position ).normalize();
var distance = - camera.position.z / dir.z;
MousePos = camera.position.clone().add( dir.multiplyScalar( distance ) );
//console.log(MousePos)
rayCaster.setFromCamera(mouse, camera);
var intersects = rayCaster.intersectObjects(WORLD.getObjectByName('MWORLD').children, true);
if (intersects.length > 0)
// console.log(intersects[0].point);
PlrTarget = intersects[0].point
// Make the sphere follow the mouse
// mouseMesh.position.set(event.clientX, event.clientY, 0);
};
//Food Parent
FOOD = new THREE.Object3D();
FOOD.name = 'FOOD'
WORLD.add(FOOD)
var fid = -1
//Add Food Object should this be different?
function AddFood()
{
fid = fid + 1
var colors = ['red', 'blue', 'orange', 'yellow', 'pink', 'cyan'];
var geometry = new THREE.SphereGeometry( 0.05 * 1.5, 32 / 4, 32 / 4 );
var material = new THREE.MeshBasicMaterial( {color: colors[Math.floor(Math.random() * colors.length)]} );
var sphere = new THREE.Mesh( geometry, material );
var geometry = new THREE.BoxGeometry( 0.1 * 1.5, 10, 0.1 * 1.5);//BoxGeometry for collision detection spheres were lagging like crazy :(
var material = new THREE.MeshBasicMaterial( {color: 'red'} );
material.transparent = true
material.opacity = 0.2
var cube = new THREE.Mesh( geometry, material );
var foodrange = 15
cube.add(sphere);
cube.position.y = 0.25
cube.position.z = ((Math.random() * foodrange + 1) * (Math.round(Math.random()) * 2 - 1));
cube.position.x = ((Math.random() * foodrange + 1) * (Math.round(Math.random()) * 2 - 1));
cube.name = 'f' + fid
WORLD.getObjectByName('FOOD').add(cube)
}
//adds lots of food
function InitFood()
{
var i
for(i = 0; i < 150; i++)
{
AddFood();
}
}
InitFood();
//Eats the food working I think...
function ConsumeFood(fid)
{
FOOD.remove(FOOD.getObjectByName(fid))
plr.scale.x = plr.scale.x + 0.01
plr.scale.y = plr.scale.y + 0.01
plr.scale.z = plr.scale.z + 0.01
}
//Creates Player
function CreatePlr()
{
var geometry = new THREE.SphereGeometry( 0.5, 32, 32);//32 / 2
var material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
var sphere = new THREE.Mesh( geometry, material );
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( {color: 'blue'} );
material.transparent = true
material.opacity = 0.2
var cube = new THREE.Mesh( geometry, material );
plr = new THREE.Object3D();
plr.add(sphere);
plr.add(cube);
scene.add(plr)
controls.target = plr.position
}
CreatePlr();
setTimeout(Eat, 1500)
//DETECT FOOD PLEASE HELP :(
//sometimes works ok you have to have the food fairly deep within the player to detect
//never eats as soon as you touch it
//sometimes totally fails to detect piece of food until you go over it multiple times
//sometimes random pieces of food are eaten even though they are not touched
function Eat() {
var originPoint = plr.position.clone();
for (var vertexIndex = 0; vertexIndex < plr.children[1].geometry.vertices.length; vertexIndex++)
{
var localVertex = plr.children[1].geometry.vertices[vertexIndex].clone();
var globalVertex = localVertex.applyMatrix4( plr.children[1].matrix );
var directionVector = globalVertex.sub( plr.position );
var ray = new THREE.Raycaster( originPoint, directionVector.clone().normalize() );
var collisionResults = ray.intersectObjects( FOOD.children );
if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() ) //if your touching the food or its in your player eat it
collisionResults.forEach(function(food){
console.log(food.object)//shows in console that food was detected and what piece of food it was
ConsumeFood(food.object.name)//consume food based on name (f1, f2, f3)
})
}
setTimeout(Eat, 50)
}
var Time = new THREE.Clock();
function PlrLerpSpeed(speed)
{
var distance = plr.position.distanceTo(PlrTarget);
var finalSpeed = (distance / speed);
return Time.deltaTime / finalSpeed
}
var animate = function () {
requestAnimationFrame( animate );
if(PlrTarget){
plr.lookAt(PlrTarget)
//plr.position.lerp(PlrTarget, PlrLerpSpeed(1));
plr.position.lerp(PlrTarget, 0.01 / (plr.position.distanceTo(PlrTarget) / 2));
}
controls.update();
//plr.position = MousePos
/*var speed = 5; // units a second, the speed we want
var currentPoint = new THREE.Vector3(); // we will re-use it
// this part is in a function of event listener of, for example, a button
currentPoint.copy(plr.position); // cube is the object to move
var distance = currentPoint.distanceTo(MousePos)
var duration = (distance / speed) * 1000; // in milliseconds
new TWEEN.Tween(plr.position)
.to(MousePos, duration) // destinationPoint is the object of destination
.start();
*/
renderer.render( scene, camera );
};
animate();
body { margin: 0; }
canvas { display: block; }
<script src="https://threejs.org/build/three.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
Since your player and food objects are basically spheres, you can implement a super fast and accurate collision detection with bounding spheres. Try it like so:
//variables
var scene, renderer, rayCaster;
var WORLD, floor, FOOD, MWORLD;
var plr, camera, controls;
function debugupdate() {
window.plr = plr
window.floor = floor
window.WORLD = WORLD
window.camera = camera
window.controls = controls
window.scene = scene
window.FOOD = FOOD
}
setInterval(debugupdate, 1000)
//setup scene for gameplay
function InitGame() {
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer();
rayCaster = new THREE.Raycaster();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 8;
camera.position.z = 8;
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.autoRotate = false;
controls.enablePan = false;
//controls.update() must be called after any manual changes to the camera's transform
//camera.position.set( 0, 20, 100 );
//controls.update();
//MWORLD = stuff mouse can detect
MWORLD = new THREE.Object3D();
MWORLD.name = 'MWORLD'
var floorgeo = new THREE.BoxGeometry(30, 0.5, 30);
var floormat = new THREE.MeshBasicMaterial({
color: 0x00ff00
});
floor = new THREE.Mesh(floorgeo, floormat);
MWORLD.add(floor)
WORLD = new THREE.Object3D();
WORLD.add(MWORLD);
WORLD.name = 'WORLD';
scene.add(WORLD);
}
InitGame();
//Mouse Stuff
var MousePos;
var PlrTarget;
document.addEventListener('mousemove', MouseToWorld, false);
function MouseToWorld(event) {
event.preventDefault();
var mouse = {};
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
var vector = new THREE.Vector3(mouse.x, mouse.y, 0.5);
vector.unproject(camera);
var dir = vector.sub(camera.position).normalize();
var distance = -camera.position.z / dir.z;
MousePos = camera.position.clone().add(dir.multiplyScalar(distance));
//console.log(MousePos)
rayCaster.setFromCamera(mouse, camera);
var intersects = rayCaster.intersectObjects(WORLD.getObjectByName('MWORLD').children, true);
if (intersects.length > 0)
// console.log(intersects[0].point);
PlrTarget = intersects[0].point
// Make the sphere follow the mouse
// mouseMesh.position.set(event.clientX, event.clientY, 0);
};
//Food Parent
FOOD = new THREE.Object3D();
FOOD.name = 'FOOD'
WORLD.add(FOOD)
var fid = -1
//Add Food Object should this be different?
function AddFood() {
fid = fid + 1
var colors = ['red', 'blue', 'orange', 'yellow', 'pink', 'cyan'];
var geometry = new THREE.SphereGeometry(0.05 * 1.5, 32 / 4, 32 / 4);
var material = new THREE.MeshBasicMaterial({
color: colors[Math.floor(Math.random() * colors.length)]
});
var sphere = new THREE.Mesh(geometry, material);
var foodrange = 15
sphere.position.y = 0.25
sphere.position.z = ((Math.random() * foodrange + 1) * (Math.round(Math.random()) * 2 - 1));
sphere.position.x = ((Math.random() * foodrange + 1) * (Math.round(Math.random()) * 2 - 1));
sphere.name = 'f' + fid
WORLD.getObjectByName('FOOD').add(sphere)
}
//adds lots of food
function InitFood() {
var i
for (i = 0; i < 150; i++) {
AddFood();
}
}
InitFood();
//Eats the food working I think...
function ConsumeFood(fid) {
FOOD.remove(FOOD.getObjectByName(fid))
plr.scale.x = plr.scale.x + 0.01
plr.scale.y = plr.scale.y + 0.01
plr.scale.z = plr.scale.z + 0.01
}
//Creates Player
function CreatePlr() {
var geometry = new THREE.SphereGeometry(0.5, 32, 32); //32 / 2
var material = new THREE.MeshBasicMaterial({
color: 0xffff00
});
plr = new THREE.Mesh(geometry, material);
scene.add(plr)
controls.target = plr.position
}
CreatePlr();
setTimeout(Eat, 1500)
var spherePlayer = new THREE.Sphere();
var sphereFood = new THREE.Sphere();
//DETECT FOOD PLEASE HELP :(
//sometimes works ok you have to have the food fairly deep within the player to detect
//never eats as soon as you touch it
//sometimes totally fails to detect piece of food until you go over it multiple times
//sometimes random pieces of food are eaten even though they are not touched
function Eat() {
spherePlayer.copy( plr.geometry.boundingSphere ).applyMatrix4( plr.matrixWorld );
for ( var i = 0; i < FOOD.children.length; i ++ ) {
var food = FOOD.children[ i ];
sphereFood.copy( food.geometry.boundingSphere ).applyMatrix4( food.matrixWorld );
if ( spherePlayer.intersectsSphere( sphereFood ) === true ) {
ConsumeFood(food.name);
}
}
setTimeout(Eat, 50)
}
var Time = new THREE.Clock();
function PlrLerpSpeed(speed) {
var distance = plr.position.distanceTo(PlrTarget);
var finalSpeed = (distance / speed);
return Time.deltaTime / finalSpeed
}
var animate = function() {
requestAnimationFrame(animate);
if (PlrTarget) {
plr.lookAt(PlrTarget)
//plr.position.lerp(PlrTarget, PlrLerpSpeed(1));
plr.position.lerp(PlrTarget, 0.01 / (plr.position.distanceTo(PlrTarget) / 2));
}
controls.update();
//plr.position = MousePos
/*var speed = 5; // units a second, the speed we want
var currentPoint = new THREE.Vector3(); // we will re-use it
// this part is in a function of event listener of, for example, a button
currentPoint.copy(plr.position); // cube is the object to move
var distance = currentPoint.distanceTo(MousePos)
var duration = (distance / speed) * 1000; // in milliseconds
new TWEEN.Tween(plr.position)
.to(MousePos, duration) // destinationPoint is the object of destination
.start();
*/
renderer.render(scene, camera);
};
animate();
body { margin: 0; }
canvas { display: block; }
<script src="https://cdn.jsdelivr.net/npm/three#0.116.1/build/three.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.116.1/examples/js/controls/OrbitControls.js"></script>

How to change the camera position for multiple views in Three JS

I've tried to change the camera view (say: Front View, Left View, Right View, Back View, Bottom View and Top View ) in button click. I have achieved by changing the camera position for each view where I have a concern that what if camera position or the mesh position get changes. I have given the mesh position to the camera position in the initial View and the mesh disappeared from the scene.
So is there any alternate to achieve this without hard coding the camera position.
Kindly, help me out with the issue.
Here's the fiddle https://jsfiddle.net/8L10qkzt/1/ and my piece of code
var camera, scene, renderer;
var views;
init();
animate();
function init() {
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
camera.position.z = 1;
scene = new THREE.Scene();
var geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
var material = new THREE.MeshNormalMaterial();
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
setTimeout(() => {
controls.enableDamping = false;
controls.reset();
}, 5000);
document.querySelector('#frontView').addEventListener('click', () => {
console.log("frontview");
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 1;
scene.add(mesh);
controls.update();
render();
});
document.querySelector('#sideView').addEventListener('click', () => {
console.log("Side View");
camera.position.x = 1;
camera.position.y = 1;
camera.position.z = 1;
scene.add(mesh);
controls.update();
render();
});
document.querySelector('#backView').addEventListener('click', () => {
console.log("Back View");
});
}
function render() {
for (var ii = 0; ii < views; ++ii) {
var view = views[ii];
var camera = view.camera;
view.updateCamera(camera, scene, mouseX, mouseY);
var left = Math.floor(windowWidth * view.left);
var top = Math.floor(windowHeight * view.top);
var width = Math.floor(windowWidth * view.width);
var height = Math.floor(windowHeight * view.height);
renderer.setViewport(left, top, width, height);
renderer.setScissor(left, top, width, height);
renderer.setScissorTest(true);
renderer.setClearColor(view.background);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
}
}
function animate() {
render();
requestAnimationFrame(animate);
renderer.render(scene, camera);
}

Making breakout game in three.js - detect end of the board

i'm trying to learn Three.js by making game, sadly most resources I would be interested in are outdated, since library seem to change so often.
Currently I am able to move my paddle with my mouse and launch the ball on mouse click, however I've got no clue how to stop paddle to go over the board and make ball bounce from the edges.
Can anyone point me in the right direction? Here is my current code:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
//renderer
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//board
geometry = new THREE.BoxGeometry(10, 6, 0.00001)
material= new THREE.MeshBasicMaterial({color: "blue"});
var board = new THREE.Mesh(geometry,material);
scene.add(board);
//paddle
geometry = new THREE.BoxGeometry(1, 0.1, 0.2);
material = new THREE.MeshBasicMaterial({ color: "red" });
var paddle = new THREE.Mesh(geometry, material);
paddle.position.set(0, -2.5, 0);
scene.add(paddle);
camera.position.z = 5;
//ball
var geometry = new THREE.SphereGeometry(0.1, 32, 32);
var material = new THREE.MeshBasicMaterial({ color: 0xffff00 });
var ball = new THREE.Mesh(geometry, material);
ball.moving = false;
ball.position.set(0, -2.3, 0);
var velocityY = 0.05; //ball Y speed
scene.add(ball);
//mouse movements
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseMove(e) {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (e.clientY / window.innerHeight) * 2 + 1;
paddle.position.x = mouse.x * 5.5;
}
function onMouseClick(e) {
ball.moving = true;
}
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('click', onMouseClick, false);
function animate() {
requestAnimationFrame(animate);
// ball.position.y += -0.01;
if (ball.moving === false) {
// console.log('jest false')
ball.position.x = paddle.position.x;
} else {
ball.position.y += velocityY;
ball.position.x += 0.01;
}
renderer.render(scene, camera);
}
animate();
Go here:
https://threejs.org/editor/
Load the arkanoid example..
Select the Scene object in the list on the right...
Click edit on the Game Logic script at the bottom...

three is - how to limit pan in OrbitControls for OrthographicCamera so that object (texture image) is always in the scene

I am using OrbitControls to control an Orthographic camera. The scene has a 2d texture image.
I want to limit the pan of the camera, when reaching the edge of the image.
For example, when the camera is panned to the left, and the image is shifted to the right in the view window, when the left edge of the image is to the right of the visible window, stop the left panning (see the attached diagram)
Here and here the pan limit is hard coded, but in my case the limit depends on the zoom (and I assume that also on the size of the image). For example, when the image is shifted all the way to left,
when zoomed-in, scope.target.x ~= ~100
when zoomed-out, scope.target.x ~= ~800
How can I disable panning to the left when the left side of the image reaches the left edge of the visible window?
Thanks,
Avner
EDIT:
#Rabbid76 thanks for your suggestions. With some modifications to the example code, I solved the problem, i.e. the image always covers the view window.
See here for details
You can manually limit the pan.
Consider you have an OrthographicCamera, which looks onto the xy-plane.
e.g.
camera = new THREE.OrthographicCamera(-5*aspect, 5*aspect, -5, 5, -100, 100);
camera.position.set(0, 0, -1);
And you have a mesh (object), from which you can get the bounding box (THREE.Box3):
var bbox = new THREE.Box3().setFromObject(object);
With this information the minimum and maximum x and y coordinates of the object can be calculated:
var min_x = camera.left - bbox.min.x;
var max_x = camera.right - bbox.max.x;
var min_y = camera.top - bbox.min.y;
var max_y = camera.bottom - bbox.max.y;
The current camera position and target can be clamped to the limiting range:
let pos_x = Math.min(max_x, Math.max(min_x, camera.position.x));
let pos_y = Math.min(max_y, Math.max(min_y, camera.position.y));
Update the OrthographicCamera and the OrbitControls:
camera.position.set(pos_x, pos_y, camera.position.z);
camera.lookAt(pos_x, pos_y, orbitControls.target.z);
orbitControls.target.x = pos_x;
orbitControls.target.y = pos_y;
orbitControls.update();
See the example:
(function onLoad() {
var container, loader, camera, scene, renderer, orbitControls, object, bbox;
init();
animate();
function init() {
container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
aspect = window.innerWidth / window.innerHeight;
camera = new THREE.OrthographicCamera(-5*aspect, 5*aspect, -5, 5, -100, 100);
camera.position.set(0, 0, -1);
loader = new THREE.TextureLoader();
loader.setCrossOrigin("");
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
scene.add(camera);
window.onresize = resize;
orbitControls = new THREE.OrbitControls(camera);
orbitControls.enabled = true;
orbitControls.enableRotate = false;
orbitControls.screenSpacePanning = true;
orbitControls.mouseButtons = {
LEFT: THREE.MOUSE.RIGHT,
MIDDLE: THREE.MOUSE.MIDDLE,
RIGHT: THREE.MOUSE.LEFT
}
addGridHelper();
createModel();
}
function createModel() {
var material = new THREE.MeshBasicMaterial({color:'#ff4040'});
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
object = new THREE.Mesh(geometry, material);
bbox = new THREE.Box3().setFromObject(object);
scene.add(object);
}
function addGridHelper() {
var helper = new THREE.GridHelper(100, 100);
helper.rotation.x = Math.PI / 2;
helper.material.opacity = 0.25;
helper.material.transparent = true;
scene.add(helper);
var axis = new THREE.AxesHelper(1000);
scene.add(axis);
}
function resize() {
var aspect = window.innerWidth / window.innerHeight;
renderer.setSize(window.innerWidth, window.innerHeight);
camera.left = -5*aspect;
camera.right = 5*aspect;
camera.updateProjectionMatrix();
}
function animate() {
requestAnimationFrame(animate);
var min_x = camera.left - bbox.min.x;
var max_x = camera.right - bbox.max.x;
var min_y = camera.top - bbox.min.y;
var max_y = camera.bottom - bbox.max.y;
let pos_x = Math.min(max_x, Math.max(min_x, camera.position.x));
let pos_y = Math.min(max_y, Math.max(min_y, camera.position.y));
camera.position.set(pos_x, pos_y, camera.position.z);
camera.lookAt(pos_x, pos_y, orbitControls.target.z);
orbitControls.target.x = pos_x;
orbitControls.target.y = pos_y;
orbitControls.update();
render();
}
function render() {
renderer.render(scene, camera);
}
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<div id="container"></div>

Forcing OrbitControls to navigate around a moving object (almost working)

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 OrbitControls not 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 target to 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.
I made it work by introducing a fake camera, which has everything the same as the original camera, except for camera.parent
fakeCamera = camera.clone(); // parent becomes null
controls = new THREE.OrbitControls(fakeCamera, renderer.domElement);
This way OrbitControls has a camera with its own coordinate system.
Then, before rendering, I copy fakeCamera's values back to the real camera, which is used for rendering.
camera.position.copy(fakeCamera.position);
camera.quaternion.copy(fakeCamera.quaternion);
camera.scale.copy(fakeCamera.scale);
render();
and it works well.
EDIT
I noticed that
camera.position.copy(fakeCamera.position);
camera.quaternion.copy(fakeCamera.quaternion);
camera.scale.copy(fakeCamera.scale);
can be replaced with
camera.copy(fakeCamera);
(the code below has been updated accordingly)
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>
I think your workaround is a nice solution because it does not require modifying imported code. Also, an extra camera is not expensive to maintain as long as it is not used for rendering. Here is an OrbitControls subclass that can be applied, based on the same principle. Note that the localTarget property is just an alias for the target property. There is no globalTarget property.
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
}
});
My previous solution of merely converting the local target to world space before applying lookAt was not correct. The problem seems to be that lookAt orients the camera according to its world-space up direction (camera.up or object.up) on every update. This problem does not exist with the placeholder/fakeCamera solution. (See PR https://github.com/mrdoob/three.js/pull/16374)

Resources