Three.js animating with speed and friction - three.js

Im trying to make a 3D spinning wheel animation in three.js, where i press a key to accelerate the wheel and when i release the key it decelerates by friction until it stops.
I've experimented with different examples on this site but i just cant get it to run properly. I understand the physics but im having a hard time implementing it in the render loop.
Also in the future i would like to implement a deceleration curve.
Does anyone have a clue on how to make something like this?
Thanks in advance
Edit:
Finally got something working kinda like i want it! This is my code:`
var container, outputLeap;
var camera, scene, renderer;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
var sceneRoot = new THREE.Group();
var wheelSpin = new THREE.Group();
var wheelMesh;
var clock = new THREE.Clock();
var speed = 0.1;
var decelRate = 100;
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function init() {
outputLeap = document.getElementById('output-leap');
container = document.getElementById('container');
camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 80);
scene = new THREE.Scene();
var geometryWheel = new THREE.CylinderGeometry(3, 3, 0.05, 32);
var materialWheel = new THREE.MeshBasicMaterial({
color: 0xffff00,
wireframe: true
});
var wheelMesh = new THREE.Mesh(geometryWheel, materialWheel);
scene.add(sceneRoot);
sceneRoot.add(wheelSpin);
wheelSpin.add(wheelMesh);
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x000000);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false);
}
function render() {
var time = clock.getElapsedTime();
var delta = clock.getDelta();
camera.position.y = 15;
if (speed > 0)
speed = Math.max(0, speed - decelRate * delta);
else
speed = Math.min(0, speed + decelRate * delta);
camera.lookAt(scene.position);
wheelSpin.rotation.y -= speed;
outputLeap.innerHTML = 'Rotation: ' + speed;
renderer.render(scene, camera);
}
function animate() {
requestAnimationFrame(animate);
render();
}
init();
animate();
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
</head>
<body>
<div id="info-top">
<div id="output"></div>
<div id="output-leap"><br></div>
</div>
<div id="container"></div>
<script src="http://threejs.org/build/three.min.js">
</script>
<script src='js/THREEx.KeyboardState.js'></script>
<script>
</script>
</body>
</html>
It seems like delta was the key to making the animation work. But now i wonder why? I also wonder how you translate the speed and deceleration variables into more "realistic" values?

There are many ways to simulate friction and related accelerations.
Here is one via simple integration.
Friction (drag) is a force related to a exponent of velocity, that force acts against the mass of the moving (spinning) object.
We can ignore the fact that we have a spinning object as the location of the friction is at the center. Also one expects that the axis is rotating on a bearing or lubricated shaft, which for the most part can be described as drag.
If we remove all the coefficients, areas, viscosity, rolling friction, and blah blah from the drag function (they are all linear in relation) and look at the core function. drag = 1/2 * v*v where v is velocity. Multiply that by some adjustable coefficient and we get the force.
The velocity is the movement of the part of the wheel touching the axle and we get from the radius of the axle and rate of spin.
Thus we can set up the sim.
wheel = {
mass : 100, // mass
axleRadius : 40, // the lower the radius the less the drag
deltaRot : 0.3, // rate of turn per unit time.
dragCoff : 0.1, //coefficients of drag
}
Get the velocity against the axle
var velocity = wheel.deltaRot * axleRadius;
Get the drag from that velocity
var drag = 0.5 * velocity * velocity * wheel.dragCoff;
Apply that drag (as a force) on the wheel's mass f = ma
var accel = drag / wheel.mass;
Convert the acceleration back to velocity on the surface touching the axle
wheel.deltaRot -= accel / wheel.axleRadius;
And you have a good approximation of a wheel turning on a axle.
The axle radius has a big impact on the drag. The greater the radius the greater the drag. The dragCoff is all the factors we did not include, like surface area touching the axle, bearing rolling resistance, lubrication viscosity. They are all linear relations (as I assume you will not be changing the axle radius during the simulation) compared to the velocity squared so can be bundled as one number to suit your needs (less than one of course)
And the Mass, the greater the mass the longer the wheel will spin.
As a simple demo the function has been simplified a little
var wheel = {
mass : 100,
radius : 100, // has no effect on the sim
axleRadius : 30,
deltaRot : 1.3,
dragCoff : 0.2, //coefficients of drag
rotation : 0,
}
function updateWheel(w){
w.deltaRot -= ((0.5 * Math.pow(w.deltaRot * w.axleRadius,2) * w.dragCoff) / w.mass) / w.axleRadius;
w.rotation += w.deltaRot;
}
function drawCircle(radius,x=0,y=0){
ctx.beginPath();
ctx.arc(0,0,radius,0,Math.PI * 2);
ctx.fill();
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
updateWheel(wheel);
ctx.setTransform(1,0,0,1,cw,ch); // draw at center of canvas
ctx.rotate(wheel.rotation);
ctx.fillStyle = "black";
drawCircle(wheel.radius);
ctx.fillStyle = "red";
drawCircle(wheel.radius-10);
ctx.fillStyle = "black";
ctx.fillRect(0,-10,wheel.radius-5,20);
ctx.fillStyle = "white";
drawCircle(wheel.axleRadius+2);
ctx.fillStyle = "black";
drawCircle(wheel.axleRadius);
}
var w, h, cw, ch, canvas, ctx, globalTime = 0, firstRun = true;
;(function(){
var createCanvas, resizeCanvas;
createCanvas = function () {
var c, cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
wheel.deltaRot = 1.3;
}
function update() { // Main update loop
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();

Related

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)

Particle Sphere not fully drawn when looping through vertices

I am drawing a sphere in THREEJS with particles. That works good.
However i loop trough those points to animate(reposition them). Works just fine when i use widthSegments under 50(WidthSegement is the 2nd argument). Anything above 50 will stop being drawn.
For reference:
SphereGeometry(radius, widthSegments, heightSegments)
This would work fine:
let geometry = new THREE.SphereGeometry(30, 50, 20);
And produce this effect:
if i go with like 100 segments for example i would get this result:
However this only happens when i loop trough the points to alter their position.
It still animates the existing points. but not any others.
//Related to the audio api
let AudioContext = window.AudioContext || window.webkitAudioContext,
ctxAudio = new AudioContext(),
sampleRate = ctxAudio.sampleRate,
audio = document.getElementById("sound"),
audioSrc = ctxAudio.createMediaElementSource(audio),
analyser = ctxAudio.createAnalyser(),
bufferLength = analyser.frequencyBinCount,
dataArray = new Uint8Array(bufferLength);
analyser.fftSize = 2048 * 2;
analyser.smoothingTimeConstant = 0.75;
audioSrc.connect(analyser);
audioSrc.connect(ctxAudio.destination);
//Setting up the renderer
let renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//Defining the objects in the scene
let geometry = new THREE.SphereGeometry(30, 100, 20);
let loader = new THREE.TextureLoader();
loader.crossOrigin = true;
let particleTexture = loader.load('https://threejs.org/examples/textures/particle2.png');
let material = new THREE.PointsMaterial({
color: 0x20C9BD,
size: 1,
transparent: true,
blending: THREE.AdditiveBlending,
map: particleTexture,
depthWrite: false
});
let points = new THREE.Points(geometry, material);
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
//Setting the scene
scene.add(points);
camera.position.z = 100;
//Copy the default sphere into a vector
const def = new THREE.Vector3;
for (let i = 0; i < geometry.vertices.length; i++) {
def[i] = {
x: geometry.vertices[i].x,
y: geometry.vertices[i].y,
z: geometry.vertices[i].z
};
}
//Render the scene(looping through it 60 times a second)
//This is where the issue is
function render() {
requestAnimationFrame(render);
analyser.getByteFrequencyData(dataArray);
camera.lookAt(points.position);
for (let i = 0; i < geometry.vertices.length; i++) {
let particle = geometry.vertices[i];
let dx = def[i].x * (dataArray[i] / 255.0) + def[i].x;
let dy = def[i].y * (dataArray[i] / 255.0) + def[i].y;
let dz = def[i].z * (dataArray[i] / 255.0) + def[i].z;
particle.set(dx, dy, dz); //<--this is where the issue is.
}
geometry.verticesNeedUpdate = true;
renderer.render(scene, camera);
}
render();
<script src="https://rawgit.com/mrdoob/three.js/master/build/three.min.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../js/three.js"></script>
</head>
<body>
<audio id="sound" controls src="http://tackj.happyvisocoders.be/audio/DieForYou-Starset.mp3"></audio>
</body>
</html>
If you comment out the particle.set(dx,dy,dz) it draws perfectly.
However this animates the particles to move along with the music. So its important to keep this part.
Why doesn't the sphere draw completly and how can i fix this?
This has in fact little to do with three.js but more with the webaudio-API.
When you are creating the analyzer with an FFT-size of 2048, you will get 1024 frequency bins (see analyzer.frequencyBinCount), but your geometry has 1902 vertices. So for vertices with index 1024 to 1901, dataArray[i] is undefined, and the calculations all resolve to NaN. Now you are setting the vector as vector.set(NaN, NaN, NaN) and three.js has no Idea what to make of this so the points don't get rendered.
So if you replace that part of the loop with
let fftValue = (dataArray[i] / 255.0) || 0;
let dx = def[i].x * fftValue + def[i].x;
let dy = def[i].y * fftValue + def[i].y;
let dz = def[i].z * fftValue + def[i].z;
particle.set(dx, dy, dz);
or even simpler:
let fftValue = (dataArray[i] / 255.0) || 0;
geometry.vertices[i]
.copy(def)
.multiplyScalar(1 + fftValue)
you should be alright.

three js drag and drop Uncaught TypeError

I have been trying to implement the drag and drop functionality found here...
http://www.smartjava.org/tjscb/07-animations-physics/07.08-drag-n-drop-object-around-scene.html
Whenever I customise it slightly and use it in my project I get the following..
"Uncaught TypeError: Cannot read property 'point' of undefined"
whenever I try to drag a cube. The rotation isn't occurring so it must be recognising that I'm trying to drag an object and it relates to this line of code..
"selectedObject.position.copy(intersects[0].point.sub(offset))"
I assumed since I am new to all of this that I had messed up, so I copied all of the code from the link above into a new page (so should be identical) and ran it and I get the same thing (everything else works good)
Im probably missing something really stupid, I have searched for this and looked at other examples on how to achieve this, but since I was working my way through a book which explained everything I thought I would stick with this, and also it would be a good learning experience to figure out why its not working. If anyone could point me in the right direction I would appreciate it
<!DOCTYPE html>
<html>
<head>
<title>07.08 - Drag and drop object around scene</title>
<script type="text/javascript" src="js/threejs/three.min.js"></script>
<script type="text/javascript" src ="js/threejs/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script>
// global variables
var renderer;
var scene;
var camera;
var cube;
var control;
var orbit;
// used for drag and drop
var plane;
var selectedObject;
var offset = new THREE.Vector3();
var objects = [];
// based on http://mrdoob.github.io/three.js/examples/webgl_interactive_draggablecubes.html
function init() {
// create a scene, that will hold all our elements such as objects, cameras and lights.
scene = new THREE.Scene();
// create a camera, which defines where we're looking at.
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
// create a render, sets the background color and the size
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xffffff, 1.0);
renderer.setSize(window.innerWidth, window.innerHeight);
plane = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000, 18, 18), new THREE.MeshBasicMaterial({
color: 0x00ff00,
opacity: 0.25,
transparent: true
}));
plane.visible = false;
scene.add(plane);
var dirLight = new THREE.DirectionalLight();
dirLight.position.set(25, 23, 15);
scene.add(dirLight);
var dirLight2 = new THREE.DirectionalLight();
dirLight2.position.set(-25, 23, 15);
scene.add(dirLight2);
for (var i = 0; i < 200; i++) {
// create a cube and add to scene
var cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
var cubeMaterial = new THREE.MeshLambertMaterial({color: Math.random() * 0xffffff});
cubeMaterial.transparent = true;
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
objects.push(cube);
cube.scale.x = Math.random() + 0.5 * 2;
cube.scale.y = Math.random() + 0.5 * 2;
cube.scale.z = Math.random() + 0.5 * 2;
cube.position.x = Math.random() * 50 - 25;
cube.position.y = Math.random() * 50 - 25;
cube.position.z = Math.random() * 50 - 25;
cube.rotation.x = Math.random() * Math.PI * 2;
cube.rotation.y = Math.random() * Math.PI * 2;
cube.rotation.z = Math.random() * Math.PI * 2;
scene.add(cube);
}
// position and point the camera to the center of the scene
camera.position.x = 35;
camera.position.y = 35;
camera.position.z = 53;
camera.lookAt(scene.position);
// add some controls so we can rotate
orbit = new THREE.OrbitControls(camera);
// add the output of the renderer to the html element
document.body.appendChild(renderer.domElement);
// call the render function
render();
}
function render() {
renderer.render(scene, camera);
orbit.update();
requestAnimationFrame(render);
}
document.onmousemove = function (event) {
// make sure we don't access anything else
event.preventDefault();
// get the mouse positions
var mouse_x = ( event.clientX / window.innerWidth ) * 2 - 1;
var mouse_y = -( event.clientY / window.innerHeight ) * 2 + 1;
// get the 3D position and create a raycaster
var vector = new THREE.Vector3(mouse_x, mouse_y, 0.5);
vector.unproject(camera);
var raycaster = new THREE.Raycaster(camera.position,
vector.sub(camera.position).normalize());
// first check if we've already selected an object by clicking
if (selectedObject) {
// check the position where the plane is intersected
var intersects = raycaster.intersectObject(plane);
// reposition the selectedobject based on the intersection with the plane
selectedObject.position.copy(intersects[0].point.sub(offset));
} else {
// if we haven't selected an object, we check if we might need
// to reposition our plane. We need to do this here, since
// we need to have this position before the onmousedown
// to calculate the offset.
var intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
// now reposition the plane to the selected objects position
plane.position.copy(intersects[0].object.position);
// and align with the camera.
plane.lookAt(camera.position);
}
}
};
document.onmousedown = function (event) {
// get the mouse positions
var mouse_x = ( event.clientX / window.innerWidth ) * 2 - 1;
var mouse_y = -( event.clientY / window.innerHeight ) * 2 + 1;
// use the projector to check for intersections. First thing to do is unproject
// the vector.
var vector = new THREE.Vector3(mouse_x, mouse_y, 0.5);
// we do this by using the unproject function which converts the 2D mouse
// position to a 3D vector.
vector.unproject(camera);
// now we cast a ray using this vector and see what is hit.
var raycaster = new THREE.Raycaster(camera.position,
vector.sub(camera.position).normalize());
// intersects contains an array of objects that might have been hit
var intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
orbit.enabled = false;
// the first one is the object we'll be moving around
selectedObject = intersects[0].object;
// and calculate the offset
var intersects = raycaster.intersectObject(plane);
offset.copy(intersects[0].point).sub(plane.position);
}
};
document.onmouseup = function (event) {
orbit.enabled = true;
selectedObject = null;
}
// calls the init function when the window is done loading.
window.onload = init;
</script>
</head>
<body>
</body>
</html>
"Uncaught TypeError: Cannot read property 'point' of undefined"
"selectedObject.position.copy(intersects[0].point.sub(offset))"
This means, intersects[0] is undefined which means the array intersects has no element (length = 0). You are using raycasting and it isn't working properly.
You should share your modified code so that we can check what is going wrong in your raycasting.
Update: I think your three.js version is greater than 71 while three.js version of this website is 71 or less. In the 72th version, there is an update in the raycaster -
Ignore invisible objects. (#mrdoob, #tschw)
So, the problem is here -
var intersects = raycaster.intersectObject(plane);
Since the plane is invisible, the intersectObject is returning empty array.
Workaround: I found a workaround. You can remove the following line -
plane.visible = false;
You can hide the material of the plane instead in the following way -
plane = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000, 18, 18), new THREE.MeshBasicMaterial({
color: 0xffff00,
opacity: 0.50,
transparent: true,
visible: false
}));
In this way, the raycaster will work properly and the plane will be invisible as well.

How to express pixel width in Three.js world space?

Three.js offers a special renderer, examples/js/renderers/CSS2DRenderer, that allows html overlays on a standard WebGL-rendered scene (see the official demo, here.)
The CSS2DRenderer accomplishes the positioning of the html item with CSS transforms. Here is how the renderer relates world space to screen space:
vector.setFromMatrixPosition( object.matrixWorld );
vector.applyProjection( viewProjectionMatrix );
var element = object.element;
var style = 'translate(-50%,-50%) translate(' + ( vector.x * _widthHalf + _widthHalf ) + 'px,' + ( - vector.y * _heightHalf + _heightHalf ) + 'px)';
element.style.WebkitTransform = style;
element.style.MozTransform = style;
element.style.oTransform = style;
element.style.transform = style;
In the live snippet, below, I have positioned several text elements, alongside a grid, like axis labels in a data plot. My problem is to choose a position in three.js world space for the html labels that accounts for their pixel width. I have framed each label with a plane to show the gap to the edge of the grid – I need to eliminate that gap!
var renderer, labelRenderer, scene, camera, controls, sprite, stats, rot, planes, ctx, fontFamily, fontSize;
rot = 0; // this drives load(?)
init();
//animate();
render();
function init() {
fontFamily = "monospace";
fontSize = "10px";
stats = new Stats();
stats.showPanel(1);
document.body.appendChild(stats.dom);
var canvas = document.createElement('canvas')
ctx = canvas.getContext('2d')
ctx.font = fontSize + " " + fontFamily;
// renderer
renderer = new THREE.WebGLRenderer({
antialias: true
});
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);
// scene
scene = new THREE.Scene();
// camera
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.set(20, 20, 20);
// controls
controls = new THREE.OrbitControls(camera);
// ambient
scene.add(new THREE.AmbientLight(0x222222));
// light
var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(20, 20, 0);
scene.add(light);
// axes
scene.add(new THREE.AxisHelper(20));
var size = 5;
var step = 5;
var gridHelper = new THREE.GridHelper(size, step);
gridHelper.translateX(5);
gridHelper.translateZ(5);
scene.add(gridHelper);
var geometry, material, text, label;
planes = new Array(5);
var texts = ["one", "two", "three", "four", "five"];
for (var i = 0; i < 5; i++) {
geometry = new THREE.PlaneGeometry(2, 2);
material = new THREE.MeshBasicMaterial({
transparent: true,
opacity: 0
});
planes[i] = new THREE.Mesh(geometry, material);
planes[i].position.set(10 + 1, 1, i * 2 + 1)
planes[i].lookAt(camera.position)
scene.add(planes[i]);
scene.add(new THREE.EdgesHelper(planes[i]))
text = document.createElement('div');
text.className = 'label';
text.style.color = "white";
text.style["font-family"] = fontFamily;
text.style["font-size"] = fontSize;
text.textContent = texts[i];
var textWidth = ctx.measureText(texts[i]).width;
console.log("textWidth", textWidth);
label = new THREE.CSS2DObject(text);
label.position.copy(planes[i].position);
scene.add(label);
console.log("label", label);
}
}
function randomPos(scale) {
return scale * Math.random();
}
function render() {
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
var x = camera.position.x;
var z = camera.position.z;
camera.position.x = x * Math.cos(rot) + z * Math.sin(rot);
camera.position.z = z * Math.cos(rot) - x * Math.sin(rot);
camera.lookAt(scene.position);
requestAnimationFrame(render);
planes.forEach(function(plane) {
plane.lookAt(camera.position);
});
stats.update();
}
function animate() {
requestAnimationFrame(animate);
//controls.update();
renderer.render(scene, camera);
stats.update();
}
body {
background-color: #000;
margin: 0px;
overflow: hidden;
}
<script src="https://rawgit.com/mrdoob/three.js/dev/build/three.min.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/dev/examples/js/controls/OrbitControls.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/dev/examples/js/renderers/CSS2DRenderer.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/master/examples/js/libs/stats.min.js"></script>
Pixel Width isn't your issue here. It's origin. You're offsetting the position (10 + 1, i, i*2+1)... https://jsfiddle.net/7j4jypfL/1/
label = new THREE.CSS2DObject(text);
label.position.set(10+(0.5),0, (i * 2)+0.5);
scene.add(label);
You need to keep in mind that in OpenGL, things like billboards and the like are origin centered (i.e. -1,-1 to 1,1) and therefore (0,0) would be center.
YOU need you measure at scale 1 and when you zoom and the like it will line up correctly because of perspective math. Look at my fiddle where I change your positions to just pure (10, i, i*2) and look at how the text lines up. If you want to move it down from there, (10 + (0.5),0,(i*2)+0.5)

Resources