I am here today because I would like your help.
I'm trying to create the earth globe with lat and long, but I can't get the geographic coordinates.
This is my code:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
scene.add(camera);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
renderer.render(scene, camera);
function anime() {
renderer.render(scene, camera);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
requestAnimationFrame(anime);
}
anime();
class Earth {
constructor(radius, scene, camera, renderer) {
this.scene = scene;
this.radius = radius;
this.camera = camera;
this.renderer = renderer;
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.geometry = new THREE.SphereGeometry(this.radius, 32, 32);
this.texture = new THREE.TextureLoader().load('https://i.ibb.co/4KGwCLD/earth-atmos-2048.jpg');
this.material = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: this.texture,
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.obj = new THREE.Object3D();
}
initObject() {
// this.scene.add(this.scene);
this.obj.add(this.mesh);
this.obj.name = 'earth';
this.scene.add(this.obj);
}
onClick(event) {
event.preventDefault();
let canvas = this.renderer.domElement;
let vector = new THREE.Vector3(
((event.offsetX) / canvas.width) * 2 - 1, -((event.offsetY) / canvas.height) * 2 + 1,
0.5);
vector.unproject(this.camera);
this.raycaster.set(this.camera.position, vector.sub(this.camera.position).normalize());
let intersects = this.raycaster.intersectObjects(this.scene.children);
if (intersects.length > 0) {
console.log(intersects[0].point.x);
console.log(intersects[0].point.y);
console.log(intersects[0].point.z);
}
}
earthRotate() {
this.obj.rotation.y += 0.01;
requestAnimationFrame(this.earthRotate.bind(this));
}
}
const earth = new Earth(3, scene, camera, renderer);
earth.initObject();
addEventListener("click", (event) => {
earth.onClick(event);
});
/* earth.earthRotate(); */
body {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r99/three.min.js" integrity="sha512-0tlhMhMGPohLm/YwaskxH7jJuUGqU/XPTl+HE0dWrhGbpEBRIZYMQdbHC0CmyNPzZKTBd8JoVZnvMcL7hzlFOg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r99/three.js" integrity="sha512-RixpleQfVXfhJUrmXlxWwitZGvMWk13+KhCsaYdeod5xryBN6gqo3RJ9xvaHn8VUeNuUnYfvzKBhLBnJnpEsgA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Is it possible to have geographic coordinates on a sphere with ThreeJS.
If so do you know how to do it?
Thank you in advance for your help !
Intersections are returned in world coordinates, so if you click the same place over and over as your globe is spinning, you will get (about) the same hit position every time.
THAT SAID, these hit points are based on the geometry of the sphere, so a low-poly sphere could actually return a position that does not match a lat/lon position as if the sphere were perfect.
To get a better result, you'll need to take a couple more steps.
1. Create a sphere representation
Use a Sphere to create a mathematically perfect representation of your globe.
2. Use Ray to find the intersection point
The Three Raycaster is a great tool for easily setting up raycasting. But you can use Raycaster.ray (Ray) to perform manual steps, too.
For example, use Ray.intersectSphere against your perfect sphere to get the "real" intersection point.
3. Convert to local coordinates
You were getting the same(-ish) click point because the intersection is in world coordinates. To convert to local coordinates, use the very convenient Object3D.worldToLocal to transform the returned world hit point into a local hit point (the Vector3 is changed using this method, so keep this in mind!).
4. Get your lat/lon
Pick a point on your globe which will represent the intersection point of your equator and prime meridian (0, 0). For a sphere where north is +Y, your origin could be new Vector3( 0, 0, sphere.radius )
Assuming you rotate about the Y axis...
For longitude, remove the Y component of your hit vector (hit.y = 0), set the length of the vector to your sphere radius (hit.setLength( sphere.radius )), then use Vector3.angleTo to find the angle (in radians) between the hit vector and and your "zero" vector, and convert to degrees. Use the X component to determine the East/West component of your value.
For latitude, create a copy of your hit vector. Remove the Y component from the copy and set its length to the sphere's radius, just like for longitude. But instead of comparing to the origin vector, use angleTo to get the angle between the copy and the hit vectors. Convert to degrees. Use the Y value to determine the North/South component of your value.
It's possible to use normalized vectors to perform these comparisons instead, but I think it's easier to visualize if you picture the vectors representing points on the surface of your sphere.
Related
I have the following project below created using ThreeJS. You will notice the gold object creates a shadow behind it on a sphere where I'm only rendering the backside so we can see the objects inside. I'm using a point light in the very center of the eye model to create the shadow evenly in all directions. This is the reason the shadow is curved.
I need to know how to get the 3D coordinates (x,y,z) of each pixel of this shadow that was created. For reference here is the code that creates the shadow with a lot removed for simplicity.
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap//THREE.PCFSoftShadowMap;
const light = new THREE.PointLight( 0xffffff, 20, 0 );
light.position.set( 0, 0, 0 );
light.castShadow = true;
light.shadow.mapSize.width = 512;
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 500;
scene.add( light );
const sphereGeometry = new THREE.SphereGeometry( 25, 32, 32 );
const sphereMaterial = new THREE.MeshStandardMaterial( { color: 0xffffff } );
sphereMaterial.side=THREE.BackSide;
const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
sphere.castShadow = false;
sphere.receiveShadow = true;
scene.add( sphere );
I have researched some into this and I think it may be storing the shadow information in the matrix property of the model but this is not clear for sure in any documentation, so I'm not sure where to look to get this information. Any help is appreciated!
--- Extra not important info ---
Also, in case you are curious, the reason I need the shadow coordinates is because I will use those to raycast back into the eye and create a different kind of shadow on an azimuthal equidistant project on the right (it's complicated...), but just know that if I have the 3D coordinates of the shadow pixels I can do this :). I'm already doing it for the muscles of the eye for example.
You can't extract the shadow into a new geometry because this is all calculated in the GPU shaders upon rendertime, so JavaScript doesn't really have access to the shadowMap positions. However, there is a solution.
Assuming your point light is at (0, 0, 0), and it's at the center of the sphere, you could iterate through the vertices of the gold object and project these positions onto the sphere:
// Sphere radius
const radius = 25;
const vec3 = new THREE.Vector3();
// Get the vertex position array
const vertices = goldObject.geometry.getAttribute("position").array;
// Loop that iterates through all vertex positions
for (let i3 = 0; i3 < vertices.length; i3 += 3) {
// Set this vertex into our vec3
vec3.set(
vertices[i3 + 0], // x
vertices[i3 + 1], // y
vertices[i3 + 2] // z
);
// Set vector magnitude to 1
vec3.normalize();
// Set vector magnitude to radius of sphere
vec3.multiplyScalar(sphereRadius);
// Now you have the spherical projection of this vertex!
console.log(vec3);
}
Since the light source is the exact center of the sphere, you could take the position of each vertex of the gold object, normalize it, then multiply it by the radius of the sphere. Now that you have the vec3 on each iteration, you could add it to your own array to build your own THREE.BufferGeometry that's pushed against the sphere.
Of course, if you've translated or rotated the gold object, then that will affect the vertex positions, so you'd have to undo those translations, rotations, etc. when iterating through all the vertices.
I have created an example project and read a great tutorial on three.js but one thing is not described well when creating the perpsective camera.
When an object is created an added into the scene for a default location of (0,0,0) and the camera is set and moved back 10 units to make the object easily visible, I have specified a near and far plane of 0.1 and 1000. I am not sure which axis this is specified on, however whichever axis it is specified on, none of the axis's on the default object are >= 0.1 to be visible given that the near and far planes specify that the visible objects must be between these planes.
Could someone please explain why my object is visible in the scene, which axis the near and far plane are or provide a very useful link describing it as I cannot find a link to explain well.
Code below if interested.
import * as THREE from 'three';
import 'bootstrap';
import css from '../css/custom_css.css';
let scene = new THREE.Scene();
/* Constants */
let WIDTH = window.innerWidth;
let HEIGHT = window.innerHeight;
/* Camera */
let field_of_view = 75;
let aspect_ratio = WIDTH/HEIGHT;
let near_plane = 0.1;
let far_plane = 1000;
let camera = new THREE.PerspectiveCamera(field_of_view, aspect_ratio, near_plane, far_plane);
// every object is initially created at ( 0, 0, 0 )
// we'll move the camera back a bit so that we can view the scene
camera.position.set( 0, 0, 10 );
/* Renderer */
let renderer = new THREE.WebGLRenderer({antialias:true, canvas: my_canvas });
renderer.setSize(WIDTH, HEIGHT);
renderer.setClearColor(0xE8E2DD, 1);
// Create the shape
let geometry = new THREE.BoxGeometry(1, 1, 1);
// Create a material, colour or image texture
let material = new THREE.MeshBasicMaterial( {
color: 0xFF0000,
wireframe: true
});
// Cube
let cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// Game Logic
let update = function(){
cube.rotation.x += 0.01;
cube.rotation.y += 0.005;
};
// Draw Scene
let render = function(){
renderer.render(scene, camera);
};
// Run game loop, update, render, repeat
let gameLoop = function(){
requestAnimationFrame(gameLoop);
update();
render();
};
gameLoop();
The near and far planes for a perspective camera are relative to the camera itself, they are not on one of the global scene axes.
In your case, the near plane is a plane 0.1 units away from the camera along the camera's central axis or "look" vector, and the far plane is a plane 1000 units away. Things rendered must be between these two planes inside the camera's "view frustum".
So in your example, since the camera is looking at the object and the object is 10 units away, it's within the view frustum and is therefore visible.
See this youtube video for a more visual representation: https://www.youtube.com/watch?v=KyTaxN2XUyQ
I'm detecting collision in my three.js demo using a Raycaster which points out the front of my camera (which can only move in the direction it's facing - though the mouse steers the camera in any direction):
var ray = new THREE.Raycaster( camera.position, cameraDirectionVector );
var collisionResults = ray.intersectObjects( scene.children );
if ( collisionResults.length > 0 )
{
if (collisionResults[0].distance < 0.15) {
var crossAxis = new THREE.Vector3( 0, 0, 1 );
crossAxis.crossVectors(cameraDirectionVector, collisionResults[0].face.normal)
camera.rotateOnAxis( crossAxis, 0.2); // this second parameter is the number I'd like to calculate correctly
}
}
So when there's a collision, I'm using the cross product of the collision face normal and the camera's direction of travel as the axis of rotation, with the aim of effectively spinning the camera away from the collision.
I'd like to know how to correctly calculate the direction of the rotation around that axis; Depending on the direction of crossAxis Vector3 relative to the face normal and direction of travel, the value might either need to be positive or negative.
I should point out this collision detection is aiming only to be very rudimentary - and limited by the fact that the player can only move forward.
How would I figure out if the camera needs to rotate clockwise or anticlockwise around that cross product axis? (Once I know how this number can be calculated I can have the collision expressed more 'realistically'; eg, by having the rotation value decrease over a set amount of frames).
Any help, much appreciated!
Maybe I missunderstood your question, but depending on what direction the camera is approaching the collision face normal, the cross product will be facing in different direction, and thereby changing the rotation direction.
In the example below, the blue arrow would be your collision face normal, the red arrow is your cameras direction, and the green arrow is the cross product betweeen the two (red X blue). The cameras direction is coming in towards the face normal in different directions, thus creating a different cross product each time.
You'll notice that each time the angle between the red and blue arrow passes 180 degrees of eachother, the rotation direction of the white sphere changes. This is because I am using the green arrow (the cross product of red and blue arrow) as the axis of rotation, and when the axis of rotation changes side, the rotation direction changes aswell due to the right hand rule.
Because of this, you should not have to put any sign infront of the second parameter, the direction of rotation is already given in the (cross product) vector that you use as rotation axis.
But since your asking this question, and you are providing some example code, I assume that the solution you have provided is not what you want. Maybe you mean that you always want the camera to rotate away from the collision normal, have you tried simply to put a negative value as the second parameter?
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
var origin = new THREE.Vector3(0, 0, 0)
var a = new THREE.Vector3(1, 0, 0);
var arrowHelperA = new THREE.ArrowHelper( a, origin, 1, 0xff0000 );
scene.add( arrowHelperA );
var b = new THREE.Vector3(0, 1, 0);
var arrowHelperB = new THREE.ArrowHelper( b, origin, 1, 0x0000ff );
scene.add( arrowHelperB );
var crossAB = new THREE.Vector3().crossVectors(a, b);
var arrowHelperCrossAB = new THREE.ArrowHelper( crossAB, origin, 1, 0x00ff00 );
scene.add( arrowHelperCrossAB );
var geometry = new THREE.SphereGeometry( 0.02, 32, 32 );
var material = new THREE.MeshBasicMaterial( {color: 0xffffff} );
var sphere = new THREE.Mesh( geometry, material );
scene.add( sphere );
sphere.position.copy(crossAB);
sphere.position.x = 0.3;
camera.position.z = 2;
camera.position.x = 2;
camera.position.y = 2;
camera.lookAt(origin);
var animate = function () {
requestAnimationFrame( animate );
a.applyAxisAngle(new THREE.Vector3(0, 0, 1), 0.01);
arrowHelperA.setDirection(a);
crossAB = new THREE.Vector3().crossVectors(a, b);
arrowHelperCrossAB.setDirection(crossAB);
arrowHelperCrossAB.setLength(crossAB.length())
sphere.position.z = crossAB.z;
sphere.position.applyAxisAngle(crossAB.normalize(), .1);
renderer.render(scene, camera);
};
animate();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/86/three.min.js"></script>
I'm working on a solar system in three.js and am curious if there is an easy way to make the labels for the planets I have below all show up the same size regardless of how far they are from the camera? I can't seem to find a solution to this. I figure you could calculate the distance from each label to the camera then come up with some sort of scaling factor based on that. Seems like there would be an easier way to accomplish this?
Thanks!
Updated with answer from prisoner849. Works excellent!
I figure you could calculate the distance from each label to the camera then come up with some sort of scaling factor based on that.
And it's very simple. Let's say, a THREE.Sprite() object (label) is a child of a THREE.Mesh() object (planet), then in your animation loop you need to do
var scaleVector = new THREE.Vector3();
var scaleFactor = 4;
var sprite = planet.children[0];
var scale = scaleVector.subVectors(planet.position, camera.position).length() / scaleFactor;
sprite.scale.set(scale, scale, 1);
I've made a very simple example of the Solar System, using this technique.
For the benefit of future visitors, the transform controls example does exactly this:
https://threejs.org/examples/misc_controls_transform.html
Here's how its done in the example code:
var factor;
if ( this.camera.isOrthographicCamera ) {
factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom;
} else {
factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 );
}
handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 7 );
Finally I found the answer to your question:
First, create a DOM Element:
<div class="element">Not Earth</div>
Then set CSS styles for it:
.element {position: absolute; top:0; left:0; color: white}
// |-------------------------------| |-----------|
// make the element on top of canvas is
// the canvas black, so text
// must be white
After that, create moveDom() function and run it every time you render the scene requestAnimationFrame()
geometry is the geometry of the mesh
cube is the mesh you want to create label
var moveDom = function(){
vector = geometry.vertices[0].clone();
vector.applyMatrix4(cube.matrix);
vector.project(camera);
vector.x = (vector.x * innerWidth/2) + innerWidth/2;
vector.y = -(vector.y * innerHeight/2) + innerHeight/2;
//Get the DOM element and apply transforms on it
document.querySelectorAll(".element")[0].style.webkitTransform = "translate("+vector.x+"px,"+vector.y+"px)";
document.querySelectorAll(".element")[0].style.transform = "translate("+vector.x+"px,"+vector.y+"px)";
};
You can create a for loop to set label for all the mesh in your scene.
Because this trick only set 2D position of DOM Element, the size of label is the same even if you zoom (the label is not part of three.js scene).
Full test case: https://jsfiddle.net/0L1rpayz/1/
var renderer, scene, camera, cube, vector, geometry;
var ww = window.innerWidth,
wh = window.innerHeight;
function init(){
renderer = new THREE.WebGLRenderer({canvas : document.getElementById('scene')});
renderer.setSize(ww,wh);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(50,ww/wh, 0.1, 10000 );
camera.position.set(0,0,500);
scene.add(camera);
light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set( 0, 0, 500 );
scene.add(light);
//Vector use to get position of vertice
vector = new THREE.Vector3();
//Generate Not Earth
geometry = new THREE.BoxGeometry(50,50,50);
var material = new THREE.MeshLambertMaterial({color: 0x00ff00});
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
//Render my scene
render();
}
var moveDom = function(){
vector = geometry.vertices[0].clone();
vector.applyMatrix4(cube.matrix);
vector.project(camera);
vector.x = (vector.x * ww/2) + ww/2;
vector.y = -(vector.y * wh/2) + wh/2;
//Get the DOM element and apply transforms on it
document.querySelectorAll(".element")[0].style.webkitTransform = "translate("+vector.x+"px,"+vector.y+"px)";
document.querySelectorAll(".element")[0].style.transform = "translate("+vector.x+"px,"+vector.y+"px)";
};
var counter = 0;
var render = function (a) {
requestAnimationFrame(render);
counter++;
//Move my cubes
cube.position.x = Math.cos((counter+1*150)/200)*(ww/6+1*80);
cube.position.y = Math.sin((counter+1*150)/200)*(70+1*80);
cube.rotation.x += .001*1+.002;
cube.rotation.y += .001*1+.02;
//Move my dom elements
moveDom();
renderer.render(scene, camera);
};
init();
body,html, canvas{width:100%;height:100%;padding:0;margin:0;overflow: hidden;}
.element{color:white;position:absolute;top:0;left:0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.min.js"></script>
<!-- My scene -->
<canvas id="scene"></canvas>
<div class="element">
<h1>Not Earth</h1>
</div>
If you downvote this, please tell me why. I will try my best to improve my posts.
If you are using spriteMaterial to present your text, you could try to set the sizeAttenuation attribute to false.
var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff, sizeAttenuation:false } );
See more information from here:
https://threejs.org/docs/index.html#api/en/materials/SpriteMaterial.sizeAttenuation
im stuck with a work that i need to do, basicly first i needed to create a pendulum that basicly is 2 spheres and 1 cylinder the 2 spheres are in the up and down a bigger 1 and a smaller 1, what i need to do is the pendulum should move not around his own center but about a center that is 0.85 the height of my cylinder, my idea was to create a pivot point, but since i dont know very good how the pivot point work i tryed a lot of things, i tryed first to add the cylinder to the scene and the spheres to the cylinder, then i created the pivot point and add the pivot point to the cylinder, in the animate function i just tried to rotate the pivot point in the x axis but nothing happens :/
Here is my code guys hope someone can give me a hand
var scene, camera, renderer;
var caixaGrande, caixaPequena1, caixaPequena2,cylinder,Cylinder2,esferaGrande;
var pivotPoint1, pivotPoint2;
const RAIOCILINDRO = 2.5;
const ALTURACILINDRO = 100;
const RAIOESFERAGRANDE = 15;
const RAIOESFERAPEQUENA = 5;
var rotacao = Math.PI/180;
window.onload = function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
camera.up=new THREE.Vector3(0,1,0);
camera.position.set(150,50,50);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// Add a directional light to show off the object
var light = new THREE.DirectionalLight( 0xffffff, 1.5);
// Position the light out from the scene, pointing at the origin
light.position.set(1.5, 1.5, 1);
scene.add( light );
var textureCylindro = new THREE.TextureLoader().load("CylinderTexture.png");
var textureSphere = new THREE.TextureLoader().load("SphereTexture.png");
geometry = new THREE.CylinderGeometry(RAIOCILINDRO,RAIOCILINDRO,ALTURACILINDRO);
material = new THREE.MeshPhongMaterial( {color: 0xffffff , map:textureCylindro} );
cylinder = new THREE.Mesh(geometry,material);
scene.add(cylinder);
pivotPoint1 = new THREE.Object3D();
pivotPoint1.position.y=ALTURACILINDRO*0.15;
cylinder.add(pivotPoint1);
var geometry = new THREE.SphereGeometry(RAIOESFERAGRANDE);
var material = new THREE.MeshPhongMaterial( {color: 0xffffff,map:textureSphere} );
esferaGrande = new THREE.Mesh( geometry, material );
esferaGrande.position.y = -ALTURACILINDRO/2;
cylinder.add( esferaGrande );
var geometry = new THREE.SphereGeometry(RAIOESFERAPEQUENA);
var material = new THREE.MeshPhongMaterial( {color: 0xffffff,map:textureSphere} );
var esferaPequena = new THREE.Mesh( geometry, material );
esferaPequena.position.y = ALTURACILINDRO/2;
cylinder.add( esferaPequena );
Cylinder2 = cylinder.clone();
Cylinder2.position.z = 3 * RAIOESFERAGRANDE;
scene.add(Cylinder2);
renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
animate();
}
function animate() {
pivotPoint1.rotation.x += 10;
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
What you should do is create an Object3D that you add the other 3 elements to, then you add that Object3D to the scene. Then when you want to rotate the whole thing, you rotate your Object3D.
So for example:
var axis = new Object3D();
sphere1.position.y = -.15 * pendulumLength;
sphere2.position.y = .85 * pendulumLength;
cylinder.position.y = .35 * pendulumLength;
//assuming the cylinder is pendulumLength long, this^ puts it right between the two balls
axis.add(sphere1);
axis.add(sphere2);
axis.add(cylinder);
scene.add(axis);
then in your animate() function, simply rotate your Axis:
axis.rotation.z += .01;
EDIT:
So, here is my bad drawing of what happens. If you look here, the circle when positioned at 0,0 when rotated, rotates around the axis. Again when you move it up to 1,1 it rotates around its center point, because rotation is relative to the center point of the circle.
Again the same is true of the cylinder. At 0,0 it rotates around its center. At .5,.5 it also rotates around its center. It doesn't care where it is, it will rotate around its position point.
So if we want to rotate these two as a group relative to some other point, we need to make them the children of another object, because when we move the parent, the children maintain their relationship to the parent, even though their positions are still 1,1 and .5,.5 respectively.
The reason that they get rotated in the manner on the right is because their relationship to the parent object is 1,1 and .5,.5 relative to a parent object with a rotation of 0 radians. When we rotate the parent object by some number of radians, they need to move in order to maintain their original relationship.
Another way to think of it: You have a wooden board with a nail perfectly through the center and you have a shoe. You set the shoe in the upper right hand corner of the board with the toe pointing away from you. If you rotate the wooden board Math.PI/2 radians, the shoe will not stay in the upper right hand corner (relative to you), even though that's where you put it, because the shoe has been added to the board, and you have moved the board. Now, the shoe should be in the bottom right, and will be facing to your right. Object3D.add() is like the process of putting the shoe on the board.