I'm visualizing UTM/WGS84 coordinates in three.js. My problem is that the granularity of the trajectories are very fine grained, meaning that I can't see any differences in the movement behaviour. I'm looking for a clean way to plot a Space-Time-Cube (X and Y is space, Z is time) but I can't figure out how to project the trajectory data into the scene that I can actually see the location changes (I normalized the data which kinda worked but I would rather prefer a more fancy method). I'm loading the trajectory info from a CSV which is stored in the variable data. I have 1500 of these tuples, with LAT, LON (EPSG 4326) and ascending seconds. As you can see the movement is very fine grained (I have movement data from an object moving over a size of approx. four football fields)
12.4309352,48.4640973,0
12.4301431,48.4655268,15
12.4288555,48.4658138,30
12.4266812,48.4653488,45
12.4245049,48.4648678,60
12.4228305,48.4639438,75
12.4217859,48.4625038,90
... ... ...
Here is my code so far with comments:
var data = $.csv.toArrays(csv);
var renderer,
scene,
camera,
controls
//using terrainSize was an experiment, it didn't change much
var terrainSize = 60;
if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
renderer = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild( renderer.domElement );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColorHex( 0xeeeeee, 1.0 );
scene = new THREE.Scene();
var material = new THREE.LineBasicMaterial({
color: 0xff00cc,
fog: true
});
var geometry = new THREE.Geometry();
var x = []
var y = []
var z = []
var count = 0;
for(var row in data) {
count += parseInt(data[row][2]);
x.push(parseFloat(data[row][0]));
y.push(parseFloat(data[row][1]));
z.push(parseFloat(count));
}
//I normalize the seconds that everything is visible on the map
z_stretch = stretch_array(z,10,1)
function stretch_array(my_stretched_array, given_stretch, multiplier) {
ratio = Math.max.apply(this, my_stretched_array) / given_stretch,
l = my_stretched_array.length;
for ( i = 0; i < l; i++ ) {
my_stretched_array[i] = my_stretched_array[i] / ratio;
}
for ( i = 0; i < my_stretched_array.length; i++) {
my_stretched_array[i] = multiplier * my_stretched_array[i];
}
return my_stretched_array;
}
//I zip the data back together
var data_stretched = []
for ( i = 0; i < data.length; i++ ) {
data_stretched.push([x[i], y[i], z_stretch[i]]);
}
//I tried using d3.js but I couldn't figure out how to stretch the data accordingly
var projection = d3.geo.transverseMercator()
.translate([terrainSize / 2, terrainSize / 2])
.scale(10)
.center([12.4309352,48.4640973]);
//Looping through the data, translating it and adding each tuple to the geometry
for (var row in data_stretched) {
var x = data_stretched[row][0]
var y = data_stretched[row][2]
var z = data_stretched[row][2]
coord = translate(projection([y, x]));
geometry.vertices.push(new THREE.Vector3(parseFloat(coord[0]), parseFloat(z), parseFloat(coord[1])));
}
// Another experiment
function translate(point) {
return [point[0] - (terrainSize / 2), (terrainSize / 2) - point[1]];
}
// Plotting the line
var line = new THREE.Line(geometry, material);
scene.add( line );
// camera and control settings..
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, -terrainSize / 2, terrainSize / 2);
controls = new THREE.TrackballControls( camera );
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 0.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
animate();
function animate() {
requestAnimationFrame( animate );
controls.update();
renderer.render( scene, camera );
}
And this is how I want it to look like (I just stretched the values for this one, which is kinda ugly..)
Solved, I had to scale the coordinates.
var projection = d3.geo.transverseMercator()
.translate([window.innerWidth, window.innerHeight])
.scale(30000000);
Related
three.js beginner here! I'm working through several tutorials to learn how to animate 3D objects with three.js, so parts of the code here will seem familiar.
What I'd like to do: find a way to change the scale of each object over a period of time (3 to 5 seconds) in a loop. Ideally, these objects will change in size (and, possibly, shape).
What I have:
var Decoration = function() {
// Run the Group constructor with the given arguments
THREE.Group.apply(this, arguments);
this.rotationSpeed = Math.random() * 0.01 + .001 + .001;
this.rotationPosition = Math.random() * 0.01 + .001 + .001;
this.scale = Math.random() * 0.21 + .051 + .021;
this.rotation.x += 0.01;
this.rotation.y += 0.02;
this.scale.x += 1;
this.scale.y += 1;
this.scale.z = 1;
// A random color assignment
var colors = ['#ff0051', '#f56762','#a53c6c','#f19fa0','#72bdbf','#47689b'];
// The main bauble is an Octahedron
var bauble = new THREE.Mesh(
addNoise(new THREE.OctahedronGeometry(15,0), 0),
new THREE.MeshStandardMaterial( {
color: colors[Math.floor(Math.random()*colors.length)],
shading: THREE.FlatShading ,
metalness: 0,
roughness: 0.8,
refractionRatio: 0.25
} )
);
bauble.castShadow = true;
bauble.receiveShadow = true;
bauble.rotateZ(Math.random()*Math.PI*2);
bauble.rotateY(Math.random()*Math.PI*2);
this.add(bauble);
};
Decoration.prototype = Object.create(THREE.Group.prototype);
Decoration.prototype.constructor = Decoration;
Decoration.prototype.updatePosition = function() {
this.rotationPosition += this.rotationSpeed;
this.rotation.y = (Math.sin(this.rotationPosition));
};
var clock = new THREE.Clock();
// Create a scene which will hold all our meshes to be rendered
var scene = new THREE.Scene();
// Create and position a camera
var camera = new THREE.PerspectiveCamera(
60, // Field of view
window.innerWidth/window.innerHeight, // Aspect ratio
0.1, // Near clipping pane
1000 // Far clipping pane
);
// Reposition the camera
camera.position.set(0,30,50);
// Point the camera at a given coordinate
camera.lookAt(new THREE.Vector3(0,15,0))
// Create a renderer
var renderer = new THREE.WebGLRenderer({ antialias: true });
// Size should be the same as the window
renderer.setSize( window.innerWidth, window.innerHeight );
// Set a near white clear color (default is black)
renderer.setClearColor( 0xfff6e6 );
// Append to the document
document.body.appendChild( renderer.domElement );
var decorations = [];
// Add some new instances of our decoration
var decoration1 = new Decoration();
decoration1.position.y += 10;
scene.add(decoration1);
decorations.push(decoration1);
var decoration2 = new Decoration();
decoration2.position.set(20,15,-10);
decoration2.scale.set(.8,.8,.8);
scene.add(decoration2);
decorations.push(decoration2);
var decoration3 = new Decoration();
decoration3.position.set(20,10,-10);
scene.add(decoration3);
decorations.push(decoration3);
var decoration4 = new Decoration();
decoration3.position.set(20,10,-10);
scene.add(decoration4);
decorations.push(decoration4);
// Render the scene/camera combnation
renderer.render(scene, camera);
requestAnimationFrame(render);
function render() {
controls.update();
var t = clock.getElapsedTime();
if (t >= 3.0)
{
clock = new THREE.Clock();
this.scale.set(1,1,1);
}
else
{
this.scale.x = 1-(t/3.0);
this.scale.y = 1-(t/3.0);
this.scale.z = 1-(t/3.0);
}
// Update the decoration positions
for(var d = 0; d < decorations.length; d++) {
decorations[d].updatePosition();
}
// Render the scene/camera combnation
renderer.render(scene, camera);
// Repeat
requestAnimationFrame(render);
}
Any help here would be greatly appreciated!! Thank you :)
The animation/render for any decoration element you have set up happens in the render function. By adding your sought after transitions to the decoration elements in your render function, you can apply it to multiple variables.
Try this:
First, declare your clock variable:
var clock = new THREE.Clock();
Then, update your render function to:
function render() {
for(var d = 0; d < decorations.length; d++) {
decorations[d].rotation.x += 0.01;
decorations[d].rotation.y += 0.01;
}
var t = clock.getElapsedTime();
if (t >= 3.0)
{
clock = new THREE.Clock();
// Update the decoration positions
for(var d = 0; d < decorations.length; d++) {
decorations[d].scale.set(1,1,1);
}
}
else
{
// Update the decoration positions
for(var d = 0; d < decorations.length; d++) {
decorations[d].scale.x = 1-(t/3.0);
decorations[d].scale.y = 1-(t/3.0);
decorations[d].scale.z = 1-(t/3.0);
}
}
// Render the scene/camera combnation
renderer.render(scene, camera);
// Repeat
requestAnimationFrame(render);
}
The short version: How can one make a camera follow an object controlled by physics within a Three.js scene?
The long version: I'm working on a Three.js scene in which the W,A,S,D keys move a sphere along a plane. So far, however, I haven't figured out how to make the camera follow behind the sphere.
In the example below, the camera follows the sphere perfectly if one only presses the W key. However, if one presses A or D, the sphere starts to turn, and the camera is no longer behind the ball. If the sphere starts to turn, I want the camera to turn with it, so the camera is always following just behind the sphere, and is always a constant distance from the sphere. As users continue to press W, the ball will continue rolling forward relative to the camera.
In a previous scene [demo], I was able to implement this behavior by creating the sphere, adding that sphere to a group, then using the following bit of code each frame:
var relativeCameraOffset = new THREE.Vector3(0,50,200);
var cameraOffset = relativeCameraOffset.applyMatrix4(sphereGroup.matrixWorld);
camera.position.x = cameraOffset.x;
camera.position.y = cameraOffset.y;
camera.position.z = cameraOffset.z;
camera.lookAt(sphereGroup.position);
The key in the demo above was to rotate the sphere while keeping the sphereGroup unrotated, so I could compute the cameraOffset on the un-rotated sphereGroup.
In the demo below, the sphere's position is controlled by the Cannon.js physics library, which translates and rotates the sphere as forces are applied to the body. Does anyone know how I can make the camera follow behind the sphere in the scene below?
/**
* Generate a scene object with a background color
**/
function getScene() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
return scene;
}
/**
* Generate the camera to be used in the scene. Camera args:
* [0] field of view: identifies the portion of the scene
* visible at any time (in degrees)
* [1] aspect ratio: identifies the aspect ratio of the
* scene in width/height
* [2] near clipping plane: objects closer than the near
* clipping plane are culled from the scene
* [3] far clipping plane: objects farther than the far
* clipping plane are culled from the scene
**/
function getCamera() {
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 10000);
camera.position.set(0, 2000, -5000);
camera.lookAt(scene.position);
return camera;
}
/**
* Generate the light to be used in the scene. Light args:
* [0]: Hexadecimal color of the light
* [1]: Numeric value of the light's strength/intensity
* [2]: The distance from the light where the intensity is 0
* #param {obj} scene: the current scene object
**/
function getLight(scene) {
var light = new THREE.PointLight( 0xffffff, 0.6, 0, 0 )
light.position.set( -2000, 1000, -2100 );
scene.add( light );
var light = new THREE.PointLight( 0xffffff, 0.15, 0, 0 )
light.position.set( -190, 275, -1801 );
light.castShadow = true;
scene.add( light );
// create some ambient light for the scene
var ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
return light;
}
/**
* Generate the renderer to be used in the scene
**/
function getRenderer() {
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
// Add support for retina displays
renderer.setPixelRatio(window.devicePixelRatio);
// Specify the size of the canvas
renderer.setSize(window.innerWidth, window.innerHeight);
// Enable shadows
renderer.shadowMap.enabled = true;
// Specify the shadow type; default = THREE.PCFShadowMap
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Add the canvas to the DOM
document.body.appendChild(renderer.domElement);
return renderer;
}
/**
* Generate the controls to be used in the scene
* #param {obj} camera: the three.js camera for the scene
* #param {obj} renderer: the three.js renderer for the scene
**/
function getControls(camera, renderer) {
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
/**
* Get stats
**/
function getStats() {
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
stats.domElement.style.right = '0px';
document.body.appendChild( stats.domElement );
return stats;
}
/**
* Get grass
**/
function getGrass() {
var texture = loader.load('http://4.bp.blogspot.com/-JiJEc7lH1Is/UHJs3kn261I/AAAAAAAADYA/gQRAxHK2q_w/s1600/tileable_old_school_video_game_grass.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
var material = new THREE.MeshLambertMaterial({
map: texture,
side: THREE.DoubleSide,
});
return material;
}
function getPlanes(scene, loader) {
var planes = [];
var material = getGrass();
[ [4000, 2000, 0, 0, -1000, 0] ].map(function(p) {
var geometry = new THREE.PlaneGeometry(p[0], p[1]);
var plane = new THREE.Mesh(geometry, material);
plane.position.x = p[2];
plane.position.y = p[3];
plane.position.z = p[4];
plane.rotation.y = p[5];
plane.rotation.x = Math.PI / 2;
plane.receiveShadow = true;
planes.push(plane);
scene.add(plane);
})
return planes;
}
/**
* Add background
**/
function getBackground(scene, loader) {
var imagePrefix = 'sky-parts/';
var directions = ['right', 'left', 'top', 'bottom', 'front', 'back'];
var imageSuffix = '.bmp';
var geometry = new THREE.BoxGeometry( 4000, 4000, 4000 );
// Add each of the images for the background cube
var materialArray = [];
for (var i = 0; i < 6; i++)
materialArray.push( new THREE.MeshBasicMaterial({
//map: loader.load(imagePrefix + directions[i] + imageSuffix),
color: 0xff0000,
side: THREE.BackSide
}));
var sky = new THREE.Mesh( geometry, materialArray );
scene.add(sky);
return sky;
}
/**
* Add a character
**/
function getSphere(scene) {
var geometry = new THREE.SphereGeometry( 30, 12, 9 );
var material = new THREE.MeshPhongMaterial({
color: 0xd0901d,
emissive: 0xaa0000,
side: THREE.DoubleSide,
flatShading: true
});
var sphere = new THREE.Mesh( geometry, material );
// allow the sphere to cast a shadow
sphere.castShadow = true;
sphere.receiveShadow = false;
// create a group for translations and rotations
var sphereGroup = new THREE.Group();
sphereGroup.add(sphere)
sphereGroup.castShadow = true;
sphereGroup.receiveShadow = false;
scene.add(sphereGroup);
return [sphere, sphereGroup];
}
/**
* Initialize physics engine
**/
function getPhysics() {
world = new CANNON.World();
world.gravity.set(0, -400, 0); // earth = -9.82 m/s
world.broadphase = new CANNON.NaiveBroadphase();
world.broadphase.useBoundingBoxes = true;
var solver = new CANNON.GSSolver();
solver.iterations = 7;
solver.tolerance = 0.1;
world.solver = solver;
world.quatNormalizeSkip = 0;
world.quatNormalizeFast = false;
world.defaultContactMaterial.contactEquationStiffness = 1e9;
world.defaultContactMaterial.contactEquationRelaxation = 4;
return world;
}
/**
* Generate the materials to be used for contacts
**/
function getPhysicsMaterial() {
var physicsMaterial = new CANNON.Material('slipperyMaterial');
var physicsContactMaterial = new CANNON.ContactMaterial(
physicsMaterial, physicsMaterial, 0.0, 0.3)
world.addContactMaterial(physicsContactMaterial);
return physicsMaterial;
}
/**
* Add objects to the world
**/
function addObjectPhysics() {
addFloorPhysics()
addSpherePhysics()
}
function addFloorPhysics() {
floors.map(function(floor) {
var q = floor.quaternion;
floorBody = new CANNON.Body({
mass: 0, // mass = 0 makes the body static
material: physicsMaterial,
shape: new CANNON.Plane(),
quaternion: new CANNON.Quaternion(-q._x, q._y, q._z, q._w)
});
world.addBody(floorBody);
})
}
function addSpherePhysics() {
sphereBody = new CANNON.Body({
mass: 1,
material: physicsMaterial,
shape: new CANNON.Sphere(30),
linearDamping: 0.5,
position: new CANNON.Vec3(1000, 500, -2000)
});
world.addBody(sphereBody);
}
/**
* Store all currently pressed keys & handle window resize
**/
function addListeners() {
window.addEventListener('keydown', function(e) {
pressed[e.key.toUpperCase()] = true;
})
window.addEventListener('keyup', function(e) {
pressed[e.key.toUpperCase()] = false;
})
window.addEventListener('resize', function(e) {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (typeof(controls) != 'undefined') controls.handleResize();
})
}
/**
* Update the sphere's position
**/
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var rotateAngle = Math.PI / 2 * delta; // 90 deg per second
// move forwards, backwards, left, or right
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.z += moveDistance;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.z -= moveDistance;
}
if (pressed['A'] || pressed['ARROWLEFT']) {
sphereBody.velocity.x += moveDistance;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
sphereBody.velocity.x -= moveDistance;
}
}
/**
* Follow the sphere
**/
function moveCamera() {
camera.position.x = sphereBody.position.x + 0;
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z + -200;
camera.lookAt(sphereGroup.position);
}
function updatePhysics() {
world.step(1/60);
sphereGroup.position.copy(sphereBody.position);
sphereGroup.quaternion.copy(sphereBody.quaternion);
}
// Render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
moveSphere();
updatePhysics();
if (typeof(controls) === 'undefined') moveCamera();
if (typeof(controls) !== 'undefined') controls.update();
if (typeof(stats) !== 'undefined') stats.update();
};
// state
var pressed = {};
var clock = new THREE.Clock();
// globals
var scene = getScene();
var camera = getCamera();
var light = getLight(scene);
var renderer = getRenderer();
var world = getPhysics();
var physicsMaterial = getPhysicsMaterial();
//var stats = getStats();
//var controls = getControls(camera, renderer);
// global body references
var sphereBody, floorBody;
// add meshes
var loader = new THREE.TextureLoader();
var floors = getPlanes(scene, loader);
var background = getBackground(scene, loader);
var sphereData = getSphere(scene);
var sphere = sphereData[0];
var sphereGroup = sphereData[1];
addObjectPhysics();
addListeners();
render();
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.js'></script>
Answers to comment questions
#jparimaa I think the most intuitive implementation would make W add forward momentum, S add backward momentum, and A and D rotate the camera around the ball. Is that possible?
#HariV The controls you link to are the ones I used in the demo without physics above. Is it possible to get that logic working with physics?
I think it's most intuitive for users if the W key always moves the ball "forward" relative to the camera
One option would be to calculate the direction between the ball and the camera and add velocity to that direction. In this case if you push the ball forward then you could rotate the camera without it affecting the velocity of the ball. Only after you press W/S after the rotation it would change the direction. I'm not sure if that is what you want but maybe this will give you some ideas.
I tried the following code (rotation is global variable initialized to 0)
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var dir = new THREE.Vector3(sphereBody.position.x, sphereBody.position.y, sphereBody.position.z);
dir.sub(camera.position).normalize(); // direction vector between the camera and the ball
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.x += moveDistance * dir.x;
sphereBody.velocity.z += moveDistance * dir.z;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.x -= moveDistance * dir.x;
sphereBody.velocity.z -= moveDistance * dir.z;
}
}
function moveCamera() {
var delta = clock.getDelta();
var sensitivity = 150;
var rotateAngle = Math.PI / 2 * delta * sensitivity;
if (pressed['A'] || pressed['ARROWLEFT']) {
rotation -= rotateAngle;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
rotation += rotateAngle;
}
var rotZ = Math.cos(rotation)
var rotX = Math.sin(rotation)
var distance = 200;
camera.position.x = sphereBody.position.x - (distance * rotX);
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z - (distance * rotZ);
camera.lookAt(sphereGroup.position);
}
I'm trying to rotate multiple objects around the same Vector3 point but in different directions - so they effectively 'swarm' around the point.
I'm using the trig approach rather simply wrapping each object in a Container and applying some random rotations as I'm projecting their 3D vector to 2D positions to attach label DIVs above the canvas, and the container approach messes with with project class.
Here's my current code which makes all objects rotate around the point along the same orbit path:
for(var i = 0; i<objectsArr.length; i++){
var obj = objectsArr[i];
var radius = obj.angle * (Math.PI / 180);
obj.position.x = obj.radius * Math.cos(radius);
obj.position.y = obj.radius * Math.sin(radius);
obj.angle += obj.orbitSpeed;
}
Does anyone know how I can make them orbit in random directions along X, Y, Z axis?
First, see this answer about rotating objects about a point.
From your code, it looks like you're storing each object's orbit speed, and their current angle of rotation (which represents the vector to the position). Rather than storing a 2D angle, store it as a Vector3 which represents the normal of (perpendicular to) the object's orbital plane. (This will let you really get a "swarm" going later, when you can define different orbital planes.) I also recommend storing the orbit speed in radians per step, so you don't have to perform the conversion every pass.
The rest of problem actually becomes very simple with the Vector3.applyAxisAngle method.
Some pseudo-code:
Subtract the rotation point position from the object's position.
Use the object's orbit speed, and angle to update the temp position.
Add the rotation point position back to the object's position.
To see it in your code:
var obj;
for(var i = 0; i< objectsArr.length; i++){
obj = objectsArr[i];
obj.position.sub(rotationPoint); // rotationPoint is a Vector3
obj.position.applyAxisAngle(obj.angle, obj.orbitSpeed);
obj.add(rotationPoint);
}
And here's a live demo of a few objects orbiting randomly about a "nucleus" at (10, 10, 10).
var renderer, scene, camera, controls, stats, nucleus;
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 60,
NEAR = 1,
FAR = 1000;
var electrons = [],
numElectrons = 100; // more electrons = slower updating
function populateScene() {
var geo = new THREE.SphereBufferGeometry(10, 16, 16);
var mat = new THREE.MeshPhongMaterial({color:"blue"});
nucleus = new THREE.Mesh(geo, mat);
nucleus.position.set(10, 10, 10); // you can change these values
scene.add(nucleus);
var electron = null,
plane = new THREE.Plane(),
point = new THREE.Vector3();
geo = new THREE.SphereBufferGeometry(1, 16, 16);
mat = new THREE.MeshPhongMaterial({color:"red"});
for(var i = 0; i < numElectrons; ++i){
electron = new THREE.Mesh(geo, mat);
electrons.push(electron);
electron.angle = new THREE.Vector3(
Math.random(),
Math.random(),
Math.random()
).normalize();
electron.orbitSpeed = (Math.random() * 0.05) + 0.05;
if(Math.random() > 0.5) electron.orbitSpeed *= -1;
plane.normal.copy(electron.angle);
point.set(Math.random(), Math.random(), Math.random());
plane.projectPoint(point, electron.position);
electron.position.setLength(Math.floor(Math.random() * 20) + 15);
electron.position.applyAxisAngle(electron.angle, Math.random() / 10);
electron.position.add(nucleus.position);
scene.add(electron);
}
}
function updateElectrons(){
var obj = null;
for(var i = 0; i < numElectrons; ++i){
obj = electrons[i]
obj.position.sub(nucleus.position);
obj.position.applyAxisAngle(obj.angle, obj.orbitSpeed);
obj.position.add(nucleus.position);
}
}
function init() {
document.body.style.backgroundColor = "slateGray";
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
document.body.style.overflow = "hidden";
document.body.style.margin = "0";
document.body.style.padding = "0";
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 100;
scene.add(camera);
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.dynamicDampingFactor = 0.5;
controls.rotateSpeed = 3;
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
resize();
window.onresize = resize;
populateScene();
animate();
}
function resize() {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
if (renderer && camera && controls) {
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
controls.handleResize();
}
}
function render() {
renderer.render(scene, camera);
}
function animate() {
requestAnimationFrame(animate);
updateElectrons();
render();
controls.update();
stats.update();
}
function threeReady() {
init();
}
(function() {
function addScript(url, callback) {
callback = callback || function() {};
var script = document.createElement("script");
script.addEventListener("load", callback);
script.setAttribute("src", url);
document.head.appendChild(script);
}
addScript("https://threejs.org/build/three.js", function() {
addScript("https://threejs.org/examples/js/controls/TrackballControls.js", function() {
addScript("https://threejs.org/examples/js/libs/stats.min.js", function() {
threeReady();
})
})
})
})();
three.js r86
I've got a double click function to allow the user to double click on a car model and it displays which objects have been intersected; e.g. wipers, grille, tyres and so on, and this function displays them in a list with the number of items the double click intersected with.
However, I am now trying to get it so that when a certain part of the car is clicked, for example, the tyres, it will display a paragraph with information on them. I can see how this is just a case of checking the name of the intersecting object and then displaying the relevant text if it intersects it, but every time I go to do what I think is right, it just breaks the already existing function to the point where the whole thing won't run.
I'm not exactly a JavaScript or Three.js pro at all, but trying to progress my function further is proving to be rather difficult.
Any suggestions? I've included the entire double click function, however it's when it's checking if there has been intersections near the bottom that is where the alterations need to be.
// On Mouse double click event function
function onDoubleClick(event) {
// Set the mouse down flag to false
mouseDown = false;
// Canvas x (left) and y (top) position
var canvasLeft = 0;
var canvasTop = 0;
// "event.clientX" is the mouse x position. "event.clientY" is the mouse y position
var tempX = event.clientX - canvasLeft;
var tempY = event.clientY - canvasTop;
// Create a normalised vector in 2d space
var vector = new THREE.Vector3((tempX / window.innerWidth) * 2 - 1, - (tempY / innerHeight) * 2 + 1, 0.5);
// Unproject a 2D point into the 3D word
// Use the camera projection matrix to transform the vector to the 3D world space
vector.unproject(camera);
// Send a ray in the direction the user has clicked from the cameras position
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
// Check if the ray has intersected with any objects and get intersections
var intersects = raycaster.intersectObjects(objects, true);
// Check if intersected with objects
if (intersects.length > 0) {
var tempStr = "Number of items: " + intersects.length + " ";
// List the items that were hit
for(var i=0; i < intersects.length; i++){
if(intersects[i].object.name != ""){
// The mesh name set above
tempStr += " | Name: " + intersects[i].object.name;
} else {
// The names inside the model
tempStr += " | Name: " + intersects[i].object.parent.name;
}
}
//Debug information
document.getElementById("debugInfo").innerHTML = tempStr + ".<br>";
//END
}
}
EDIT:
This is the entire code for the javascript file, as altering elements of the double click function seems to stop the page from loading.
window.onload = init;
// declare variables
var scene,camera,renderer, container;
var controls, guiControls, datGUI;
var grid, color;
var cube, cubeGeometry, cubeMaterial;
var plane, planeGeometry, planeMaterial;
var skyBoxMesh, texture_placeholder;
var spotLight;
var stats;
// Handles the mouse events.
var mouseOverCanvas;
var mouseDown;
// An array of objects that can be clicked on
var objects = [];
//DAE models
var showroom ,carOld, carNew;
var daeObject;
var animations;
var kfAnimations = [];
var kfAnimationsLength = 0;
var lastFrameCurrentTime = [];
var clock = new THREE.Clock();
var mouseOverCanvas, mouseDown;
var objectsClick=[];
function init() {
container = document.createElement( 'div' );
document.body.appendChild( container );
//creates empty scene
scene = new THREE.Scene();
//camera
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, .1, 500);
camera.position.x = 40;
camera.position.y = 40;
camera.position.z = 40;
camera.lookAt(scene.position);
//renderer
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setClearColor(0xe6f2ff);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMapSoft = true;
container.appendChild( renderer.domElement );
// Add an event to set if the mouse is over our canvas
renderer.domElement.onmouseover=function(e){ mouseOverCanvas = true; }
renderer.domElement.onmousemove=function(e){ mouseOverCanvas = true; }
renderer.domElement.onmouseout=function(e){ mouseOverCanvas = false; }
renderer.domElement.onmousedown=function(e){ mouseDown = true; }
renderer.domElement.onmouseup=function(e){ mouseDown = false; }
// Double Click Event. Set a function called "onDoubleClick"
renderer.domElement.ondblclick=onDoubleClick;
// stats
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
container.appendChild( stats.domElement );
//adds controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', render);
var ambient = new THREE.AmbientLight( 0xadad85 );
scene.add( ambient );
//---------- creates grid ---------------
grid = new THREE.GridHelper(50,5);
color= new THREE.Color("rgb(255,0,0)");
grid.setColors( 0x000000);
scene.add(grid);
//----------- creates cube --------------
cubeGeometry = new THREE.BoxGeometry(5,5,5);
cubeMaterial = new THREE.MeshPhongMaterial({color: 0xff3300});
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.x = 0;
cube.position.y = 6;
cube.position.z = 2.5;
cube.castShadow = true;
scene.add(cube);
//----------- creates plane ---------------
planeGeomenty= new THREE.PlaneGeometry(100,100,100);
planeMaterial = new THREE.MeshLambertMaterial({color: 0x00cc00});
plane = new THREE.Mesh(planeGeomenty, planeMaterial);
//position the add objects to the scene
plane.rotation.x = -.5*Math.PI;
plane.receiveShadow = true;
scene.add(plane);
//------------- skyBox --------------
texture_placeholder = document.createElement('canvas');
texture_placeholder.width = 128;
texture_placeholder.height = 128;
var context = texture_placeholder.getContext('2d');
context.fillStyle = 'rgb(200,200, 200)';
context.fillRect(0, 0,texture_placeholder.width, texture_placeholder.height);
var materials = [
loadTexture('images/skybox/posX.jpg'),
loadTexture('images/skybox/negX.jpg'),
loadTexture('images/skybox/posY.jpg'),
loadTexture('images/skybox/negY.jpg'),
loadTexture('images/skybox/posZ.jpg'),
loadTexture('images/skybox/negZ.jpg')
];
skyBoxMesh = new THREE.Mesh(new THREE.BoxGeometry(500,500,500,7,7,7),
new THREE.MeshFaceMaterial(materials));
skyBoxMesh.scale.x = -1;
scene.add(skyBoxMesh);
//---------- loads collada files -----------
loadCollada();
daeObject = cube;
// initialise datGUI controls values
guiControls = new function() {
this.rotationY = 0.0;
this.positionX = 0.0;
this.positionY = 0.0;
this.positionZ = -10;
this.lightX = 20;
this.lightY = 35;
this.lightZ = 40;
this.intensity = 1;
this.distance = 0;
this.angle = 1.570;
this.target = cube;
}
//add spotLight with starting parameters
spotLight = new THREE.SpotLight(0xffffff);
spotLight.castShadow = true;
spotLight.position.set(20,35,40);
spotLight.intensity = guiControls.intensity;
spotLight.distance = guiControls.distance;
spotLight.angle = guiControls.angle;
scene.add(spotLight);
//adds controls on the scene
datGUI = new dat.GUI();
// datGUI.add(guiControls, 'positionZ', 0, 1);
datGUI.add(guiControls, 'positionZ', -10, 25, 0.5). name("Move the car");
datGUI.add(guiControls, 'rotationY', 0, 1).name('Rotate the car');
datGUI.add(guiControls, 'lightX', -60, 180);
datGUI.add(guiControls, 'lightY', 0, 180);
datGUI.add(guiControls, 'lightZ', -60, 180);
datGUI.add(guiControls, 'target',[ 'cube','Modern Mini', 'Classic Mini']).onChange(function() {
if(guiControls.target == 'cube'){
spotLight.target = cube;
daeObject = cube;
}
else if(guiControls.target == 'Classic Mini'){
spotLight.target = carOld;
daeObject = carOld;
}
else if(guiControls.target = 'Modern Mini'){
spotLight.target = carNew;
daeObject = carNew;
}
});
datGUI.add(guiControls, 'intensity', 0.01, 5).onChange(function (value){
spotLight.intensity = value;
});
datGUI.add(guiControls, 'distance', 0, 1000).onChange(function (value){
spotLight.distance = value;
});
datGUI.add(guiControls, 'angle', 0.001, 1.570).onChange(function (value){
spotLight.angle = value;
});
datGUI.close();
container.appendChild(renderer.domElement);
window.addEventListener( 'resize', onWindowResize, false );
}
//------------------------- END INIT() ----------------------------
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function loadCollada() {
//--- Loads the Classic Mini ---
colladaLoader = new THREE.ColladaLoader();
colladaLoader.options.convertUpAxis = true;
colladaLoader.load( 'dae_files/ClassicMini.dae', function ( collada ) {
carOld = collada.scene; // stores dae file to a global variable
carOld.position.set( 14.5, 1.8, -10 );
carOld.scale.set( 0.04, 0.04, 0.04 );
carOld.traverse(function (child) {
child.castShadow = true;
child.receiveShadow = true;
});
carOld.updateMatrix();
carOld.name = "Classic";
scene.add( carOld );
objects.push( carOld );
} );
//--- loads Modern Mini ---
colladaLoader = new THREE.ColladaLoader();
colladaLoader.options.convertUpAxis = true;
colladaLoader.load( 'dae_files/ModernMini.dae', function ( collada ) {
carNew = collada.scene;
carNew.position.set( -14.5, 6.3, -10 );
carNew.scale.set( 0.06, 0.06, 0.06 );
// creates shadow
carNew.traverse(function (child) {
child.castShadow = true;
child.receiveShadow = true;
});
carNew.updateMatrix();
carNew.name = "Modern";
scene.add( carNew );
objects.push( carNew );
} );
//--- loads the Showroom ---
colladaLoader = new THREE.ColladaLoader();
colladaLoader.options.convertUpAxis = true;
colladaLoader.load( 'dae_files/roomAnim2.dae', function collada( collada ) {
showroom = collada.scene;
animations = collada.animations;
kfAnimationsLength = animations.length;
// Initialise last frame current time.
for ( var i = 0; i < kfAnimationsLength; i++ ) {
lastFrameCurrentTime[i] = 0;
}
// Get all the key frame animations.
for ( var i = 0; i < kfAnimationsLength; i++ ) {
var anim = animations[ i ];
var keyFrameAnim = new THREE.KeyFrameAnimation( anim );
keyFrameAnim.timeScale = 1;
keyFrameAnim.loop = false;
kfAnimations.push( keyFrameAnim );
anim = kfAnimations[i];
anim.play();
}
showroom.position.set(0, 0, -20);
showroom.scale.set(0.06, 0.06, 0.06);
showroom.traverse(function (child) {
child.castShadow = true;
child.receiveShadow = true;
});
showroom.updateMatrix();
scene.add( showroom );
animate();
} );
}
// On Mouse double click event function
function onDoubleClick(event) {
// Set the mouse down flag to false
mouseDown = false;
// Canvas x (left) and y (top) position
var canvasLeft = 0;
var canvasTop = 0;
// "event.clientX" is the mouse x position. "event.clientY" is the mouse y position
var tempX = event.clientX - canvasLeft;
var tempY = event.clientY - canvasTop;
// Create a normalised vector in 2d space
var vector = new THREE.Vector3((tempX / window.innerWidth) * 2 - 1, - (tempY / innerHeight) * 2 + 1, 0.5);
// Unproject a 2D point into the 3D word
// Use the camera projection matrix to transform the vector to the 3D world space
vector.unproject(camera);
// Send a ray in the direction the user has clicked from the cameras position
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
// Check if the ray has intersected with any objects and get intersections
var intersects = raycaster.intersectObjects(objects, true);
// Check if intersected with objects
if (intersects.length > 0) {
var tempStr = "Number of items: " + intersects.length + " ";
// List the items that were hit
for(var i=0; i < intersects.length; i++){
if(intersects[i].object.name != ""){
// The mesh name set above
tempStr += " | Name: " + intersects[i].object.name;
} else {
// The names inside the model
tempStr += " | Name: " + intersects[i].object.parent.name;
}
}
//Debug information
document.getElementById("debugInfo").innerHTML = tempStr + ".<br>";
//END
}
}
function loopAnimations(){
// Loop through all animations
for ( var i = 0; i < kfAnimationsLength; i++ ) {
// Check if the animation is player and not paused.
if(kfAnimations[i].isPlaying && !kfAnimations[i].isPaused){
if(kfAnimations[i].currentTime == lastFrameCurrentTime[i]) {
kfAnimations[i].stop();
//kfAnimations[i].play();
lastFrameCurrentTime[i] = 0;
}
}
}
}
function play_pauseAnim() {
//checks is there animation and is it paused
if(kfAnimationsLength > 0) {
if(kfAnimations[0].isPlaying) {
for(i = 0; i < kfAnimationsLength; i++){
kfAnimations[i].stop();
}
}else {
for(i = 0; i < kfAnimationsLength; i++) {
lastFrameCurrentTime[i] = 0;
//kfAnimations[i].play(kfAnimations[i].currentTime);
kfAnimations[i].play(0);
}
}
}
}
function checkTime(){
if(kfAnimationsLength > 0) {
if(kfAnimations[0].isPlaying) {
if(kfAnimations[0].currentTime > 3){
play_pauseAnim();
}
}
}
}
// create a render loop to draw the scene 60 times per second
function render() {
//checkTime();
daeObject.rotation.y += guiControls.rotationY;
//if (daeObject.position.z < 25) {
daeObject.position.z = guiControls.positionZ;
//}
spotLight.rotation.x += guiControls.rotationX;
spotLight.rotation.y += guiControls.rotationY;
spotLight.rotation.z += guiControls.rotationZ;
stats.update();
}
function animate () {
var deltaTime = clock.getDelta();
for ( var i = 0; i < kfAnimationsLength; i++ ) {
// Get a key frame animation.
var anim = kfAnimations[i];
anim.update( deltaTime );
}
loopAnimations();
requestAnimationFrame(animate);
// Update last frame current time.
for ( var i = 0; i < kfAnimationsLength; i++ ) {
lastFrameCurrentTime[i] = kfAnimations[i].currentTime;
}
render();
renderer.render(scene, camera);
}
// Loads skybox texture
function loadTexture(path) {
var texture = new THREE.Texture(texture_placeholder);
var material = new THREE.MeshBasicMaterial({
map: texture,
overdraw: 0.5
});
var image = new Image();
image.onload = function() {
texture.image = this;
texture.needsUpdate = true;
};
image.src = path;
return material;
}
Macast,
Please, check if you haven't forgotten in your code:
var objets = [];
var raycaster = new THREE.Raycaster();
And for each part of the car this line :
objects.push( mesh );
Ex:
var geometry = new THREE.RingGeometry( 1, 5, 32 );
var material = new THREE.MeshBasicMaterial( { color: 0xff0000, side: THREE.DoubleSide } );
var simpleTire = new THREE.Mesh( geometry, material );
simpleTire.name = 'tire';
objects.push( simpleTire );
scene.add( simpleTire );
Then, it's simple :
if ( intersects.length > 0 ) {
switch(intersects[0].object.name){
case 'tire':
console.log('A pretty red tire');
break;
case 'motor':
console.log('An electric motor');
break;
}
}
I am trying to work with a three.js environment with multiple objects that change mass when they are clicked on. Unfortunately the documentation hasn't been helpful and I haven't been able to find any examples of the best way to do this.
Here is the portion in which I am adding a random number of objects to the scene.
var random = getRandomArbitrary(4, 50);
for (var i = 0; i <= random; i++) {
var geometry = new THREE.IcosahedronGeometry( 5, 0 );
var physijsMaterial = Physijs.createMaterial(
new THREE.MeshLambertMaterial( { color: 0xffffff, emissive: 0x333333, shading: THREE.FlatShading } ),
0, // high friction
0 // medium restitution /bouciness
);
var smallSphere = new Physijs.SphereMesh(
geometry,
physijsMaterial,
0
);
smallSphere.position.y = getRandomArbitrary(-50, 50);
smallSphere.position.z = getRandomArbitrary(-50, 50);
smallSphere.position.x = getRandomArbitrary(-50, 50);
smallSphere.name = "Crystals";
scene.add(smallSphere);
}
Here is the portion in which I am trying to update the mass of an object.
function generateGravity(event)
{
event.preventDefault();
var vector = new THREE.Vector3();
vector.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1, 0.5 );
vector.unproject( camera );
raycaster.ray.set( camera.position, vector.sub( camera.position ).normalize() );
var intersects = raycaster.intersectObjects( scene.children, true );
console.log(intersects);
// intersects.object
length = intersects.length;
for (var x = 0; x < length; x++) {
if (intersects[x].object.name == "Crystals") {
console.log(intersects[x].object.mass);
intersects[x].object._physijs.mass = 50;
// intersects[x].object.remove();
}
}
}
What I'm curious to know is the best approach to updating the mass of an object during an event. Right now this function generateGravity is being called in the "render" method.
I realized this was happening because I placed scene.simulate() after render().