I am using the following library: google-maps-api-threejs-layer
Now I want to add, a picking feature.
So, this is my modified code:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Three.js Layer - Google Maps API</title>
<style>
html, body, #map-div {
margin: 0;
padding: 0;
height: 100%;
}
</style>
<script src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
<script src="data.js"></script>
<script src="styles.js"></script>
<script src="../lib/detector.js"></script>
<script src="../lib/dat.gui.js"></script>
<script src="../lib/three.js"></script>
<script src="../threejs-layer.js"></script>
<script>
var vector = new THREE.Vector2();
var raycaster = new THREE.Raycaster();
var myLayer;
function init() {
var container = document.getElementById('map-div');
var map = new google.maps.Map(container, {
zoom: 3,
mapTypeControl: false,
center: new google.maps.LatLng(10, 0),
mapTypeId: google.maps.MapTypeId.TERRAIN,
styles: styles
});
// if you add renderertype:'Canvas' to the options for ThreejsLayer, you can force the usage of CanvasRenderer
myLayer = new ThreejsLayer({ map: map }, function(layer){
if (layer.renderertype=='Canvas' || !Detector.webgl) {
texture = new THREE.Texture(generateSprite());
particles = new THREE.Object3D();
material = new THREE.SpriteMaterial({
size: 20,
opacity: 1,
depthTest: false,
transparent: false
});
photos.forEach(function (photo) {
var particle = new THREE.Sprite(material);
var location = new google.maps.LatLng(photo[0], photo[1]),
vertex = layer.fromLatLngToVertex(location);
particle.position.set(vertex.x, vertex.y, 0);
particle.scale.x = particle.scale.y = 20;
particles.add(particle);
material.size = 20;
});
} else {
var geometry = new THREE.Geometry(),
texture = new THREE.Texture(generateSprite()),
material, particles;
photos.forEach(function(photo){
var location = new google.maps.LatLng(photo[0], photo[1]),
vertex = layer.fromLatLngToVertex(location);
geometry.vertices.push( vertex );
});
texture.needsUpdate = true;
material = new THREE.PointCloudMaterial({
size: 20,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: false
});
particles = new THREE.PointCloud( geometry, material );
}
layer.add( particles );
gui = new dat.GUI();
function update(){
if (layer.renderertype=='Canvas' || !Detector.webgl) material.map = new THREE.Texture(generateSprite(material.size));
layer.render();
}
gui.add(material, 'size', 2, 100).onChange(update);
gui.add(material, 'opacity', 0.1, 1).onChange(update);
});
}
function onClick( event ) {
vector.x = ( (event.screenX - myLayer.renderer.domElement.offsetLeft) / myLayer.renderer.domElement.clientWidth ) * 2 - 1;
vector.y = - ( (event.screenY - myLayer.renderer.domElement.offsetTop) / myLayer.renderer.domElement.clientHeight ) * 2 + 1;
raycaster.setFromCamera(vector, myLayer.camera);
var intersects = raycaster.intersectObjects(myLayer.scene.children, true);
if(intersects.length > 0){
console.log(vector.x, vector.y);
}
}document.addEventListener('click', onClick, false);
function generateSprite(size) {
var canvas = document.createElement('canvas'),
context = canvas.getContext('2d'),
gradient;
size = size || 20;
canvas.width = size;
canvas.height = size;
gradient = context.createRadialGradient(
canvas.width / 2, canvas.height / 2, 0,
canvas.width / 2, canvas.height / 2, canvas.width / 2
);
gradient.addColorStop(1.0, 'rgba(255,255,255,0)');
gradient.addColorStop(0.0, 'rgba(255,255,255,1)');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
return canvas;
}
document.addEventListener('DOMContentLoaded', init, false);
</script>
</head>
<body>
<div id="map-div"></div>
</body>
</html>
As you can see, I am using the raycaster. So, maybe you want an updated three js library: link.
OK, so the problem is when I click, near the geometries (outside the boxes) I can intersect elements, so intersects.length is greater than zero. I only want to intersect pixels inside the geometries in the map. How can I fix it?
EDIT:
I created a repo: https://github.com/FacundoGFlores/google-maps-api-threejs-layer
In your example you are using a PointCloud to render the points on the map. As you cannot reasonably expect any raycasting to precisely hit any single point, three.js uses a treshold-value for the raycaster that might need to get adjusted depending on your use-case.
Related
I've been working on putting together a very simple program that will create a height map from an image and texture that image in three.js
After a bit of hacking around I can generate the height map/mesh from an image quite easily but I cannot map a texture to this mesh object. I'm not sure where I'm going wrong although I'm pretty sure it is in the addMesh(); function. This is my first attempt with three.js so it may well be something very obvious.
The error I am thrown is:
THREE.Material: 'map' parameter is undefined.
This is what is rendered:
This is the code I'm using:
// Define variables
var camera, scene, renderer, geometry, material, mesh;
var img = document.getElementById("landscape-image");
var texture = new THREE.TextureLoader().load( "assets/texture.png" )
// Get the pixels by draw the image onto a canvas. From the canvas get the Pixel (R,G,B,A)
function getTerrainPixelData(){
var canvas = document.getElementById("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var data = canvas.getContext('2d').getImageData(0,0, img.height, img.width).data;
var normPixels = []
for (var i = 0, n = data.length; i < n; i += 4) {
// get the average value of R, G and B.
normPixels.push((data[i] + data[i+1] + data[i+2]) / 3); //Essentially render image black and white
}
return normPixels;
}
// Create a mesh from pixel data
function addMesh() {
var numSegments = img.width-1; //Equal to number of image pixels - 1
geometry = new THREE.PlaneGeometry(2400, 2400, numSegments, numSegments);
material = new THREE.MeshBasicMaterial({
wireframe: true,
map: texture
});
terrain = getTerrainPixelData();
for (var i = 0, l = geometry.vertices.length; i < l; i++) // For each vertex
{
var terrainValue = terrain[i] / 255;
geometry.vertices[i].z = geometry.vertices[i].z + terrainValue * 800 ; // Define height of vertices against image pixel data
}
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh)
}
// Function to render scene and camera
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 10000); //Field-of-View, Aspect Ratio, Near Render, Far Render
camera.position.z = 3000; // Camera distance
renderer = new THREE.WebGLRenderer();
controls = new THREE.OrbitControls( camera, renderer.domElement );
addMesh();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
render();
body {
margin: 0;
}
#canvas {
display: none;
}
#assets {
display: none;
}
<html>
<head>
<title>Landscape Test</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="assets"><img id="landscape-image" src="assets/Heightmap2.png"/></div>
<canvas id="canvas"></canvas>
<script src="js/three.min.js"></script>
<script src="js/OrbitControls.js"></script> <!-- added so user can rotate the scene with the mouse -->
<script src="js/app.js"></script>
</body>
</html>
I am attempting to create a model of the ISS orbiting the earth. I am using threeJS to accomplish this. So far I have everything working perfectly other than I am having trouble with updating the position of the imported ISS model. I have a sphere that orbits the earth getting its position from an AJAX call every 5 seconds that is then converted to coordinates on my earth sphere model. I essentially want to replace the sphere (posSphere in code) that orbits, with the model of the ISS. Having trouble as the model is loaded asynchronously. What I have currently tried is including parameter in the animate function for the model, and on callback send it to the animate function, but this causes the entire scene to freeze.
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - OBJLoader + MTLLoader</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
font-family: Monospace;
background-color: #000;
color: #fff;
margin: 0px;
overflow: hidden;
}
</style>
</head>
<body>
<script src="Resources/three.js"></script>
<script src="Resources/TDSLoader.js"></script>
<script src="Resources/FBXLoader.js"></script>
<script src="Resources/GLTFLoader.js"></script>
<script src="Resources/inflate.min.js"></script>
<script src="Resources/TrackballControls.js"></script>
<script>
var lat, long, issPosition;
//*********************PRELIM FUNCTIONS BEGIN**********************************
//AJAX request for current position of the ISS
function GetValue() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var requestResponse = xhr.responseText;
var issInfo = JSON.parse(requestResponse);
var Lat = issInfo.iss_position.latitude;
var Long = issInfo.iss_position.longitude;
callback(Lat, Long); //callback function with lat and long info
}
};
xhr.open("GET", "http://api.open-notify.org/iss-now.json", true);
xhr.send();
}
function callback(Lat, Long) {
lat = Lat; //set global variables equal to lat and long so animate function has access
long = Long;
}
GetValue(); //function call to get iss location
setInterval(GetValue, 5000); //interval for iss location, updates every 5 seconds
//convert long & lat to 3D coordinate
function latLongToVector3(lat, lon, radius, heigth) {
var phi = (lat)*Math.PI/180;
var theta = (lon-180)*Math.PI/180;
var x = -(radius+heigth) * Math.cos(phi) * Math.cos(theta);
var y = (radius+heigth) * Math.sin(phi);
var z = (radius+heigth) * Math.cos(phi) * Math.sin(theta);
return new THREE.Vector3(x,y,z);
}
//******************PRELIM FUNCTIONS END********************************************
//******************THREE JS ENVIRONMENT BEGIN**************************************
var width = window.innerWidth;
var height = window.innerHeight;
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, width/height, 0.01, 1000);
camera.position.z = 400;
var controls = new THREE.TrackballControls( camera );
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [ 65, 83, 68 ];
var renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
var direcLight = new THREE.DirectionalLight(0xffffff, 0.3);
direcLight.position.set(-3,3,1.5);
direcLight.castShadow = true;
scene.add(direcLight);
var ambientLight = new THREE.AmbientLight(0xc9c9c9, 1.5);
scene.add(ambientLight);
var geometry1 = new THREE.SphereGeometry(200,32,32);
var geometry2 = new THREE.SphereGeometry(202.5,32,32);
var geometry3 = new THREE.SphereGeometry(3, 32, 32);
var material1 = new THREE.MeshBasicMaterial ({
color: 0xff0000
});
var material2 = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('Resources/Earth3/earthmapoftwo.jpg'),
bumpMap: new THREE.TextureLoader().load('Resources/Earth3/Bump2.jpg'),
bumpScale: 1,
specularMap: new THREE.TextureLoader().load('Resources/Earth3/oceanmaskbytwo.png'),
specular: new THREE.Color('grey'),
shininess: 40
});
var material3 = new THREE.MeshPhongMaterial({
alphaMap: new THREE.TextureLoader().load('Resources/Earth3/Clouds.png'),
transparent: true,
});
var issModel;
var loaderGLTF = new THREE.GLTFLoader();
loaderGLTF.load(
"Resources/Earth3/iss.gltf",
function(gltf) { //I want to be able to access this gltf outside of this callback function
gltf.scene.scale.x = 0.1;
gltf.scene.scale.y = 0.1;
gltf.scene.scale.z = 0.1;
console.log(gltf.scene.position);
scene.add(gltf.scene);
animate(gltf); //send gltf to animate function, this does not work
}
)
var sphere = new THREE.Mesh(geometry1, material2);
var clouds = new THREE.Mesh(geometry2, material3);
var posSphere = new THREE.Mesh(geometry3, material1);
sphere.receiveShadow = true;
clouds.receiveShadow = true;
posSphere.receiveShadow = true;
scene.add(sphere);
scene.add(clouds);
scene.add(posSphere);
function animate(gltf) {
clouds.rotation.x += 0.0001;
clouds.rotation.y += 0.0001;
clouds.rotation.z += 0.0001;
issPosition = latLongToVector3(lat, long, 200, 10);
posSphere.position.x = issPosition.x; //sphere's location is updated with the issPosition & works
posSphere.position.y = issPosition.y;
posSphere.position.z = issPosition.z;
gltf.scene.position.x = issPosition.x; //Part I am struggling with here, how to access the gltf's position from within this function. Essentially I want to replace the sphere with the gltf
gltf.scene.position.y = issPosition.y;
gltf.scene.position.z = issPosition.z;
controls.update();
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
//**************************THREE JS ENVIRONMENT END*************************************
</script>
</body>
</html>
You're defining a parameter to the animate function but then immediately calling it with no parameter.. (down at the last line of your code). that's why your app is "freezing". (its not freezing.. it's probably throwing an exception.. You can see that in the debugger console.. Ctrl+Shift+J in chrome or More Tools->Developer Tools from the ... menu in the upper right.. Exceptions should show up in red at the bottom.)
Take out the line of code where you do 'animate(gltf)' and instead do "issModel = gltf.scene;"
Then in your animate function instead of doing "gltf.scene.position.x = " etc.
Just do "issModel.position.copy(issPosition);"
If you're still stuck, try hosting it online somewhere and we can take a deeper look.
I have created a globe in three.js and am using an image mapped to the sphere.
On top of this, I'm using the ThreeGeoJSON library to render geojson data.
However the geographies don't match up.
I need to rotate the globe with the mapped image so that they align, but I can't figure out how to do so. I tried setting a quaternion variable and rotating based on that, but can't get it working. Any help or pointers would be very much appreciated.
Here you can see a working version of what I've done so far:
http://bl.ocks.org/jhubley/8450d7b0df0a4a9fd8ce52d1775515d5
All of the code, images, data here:
https://gist.github.com/jhubley/8450d7b0df0a4a9fd8ce52d1775515d5
I've also pasted the index.html below.
<html>
<head>
<title>ThreeGeoJSON</title>
<script src="threeGeoJSON.js"></script>
<!-- Three.js library, movement controls, and jquery for the geojson-->
<script src="three.min.js"></script>
<script src="TrackballControls.js"></script>
<script src="jquery-1.10.2.min.js"></script>
</head>
<body>
<script type="text/JavaScript">
var width = window.innerWidth,
height = window.innerHeight;
// Earth params
var radius = 9.99,
segments = 32,
rotation = 0 ;
//New scene and camera
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(55, width / height, 0.01, 1000);
camera.position.z = 1;
camera.position.x = -.2;
camera.position.y = .5;
//New Renderer
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
//Add lighting
scene.add(new THREE.AmbientLight(0x333333));
var light = new THREE.DirectionalLight(0xe4eef9, .7);
light.position.set(12,12,8);
scene.add(light);
var quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 2 );
var sphere = createSphere(radius, segments);
//sphere.rotation.y = rotation;
sphere.rotation = new THREE.Euler().setFromQuaternion( quaternion );
scene.add(sphere)
//Create a sphere to make visualization easier.
var geometry = new THREE.SphereGeometry(10, 32, 32);
var material = new THREE.MeshPhongMaterial({
//wireframe: true,
//transparent: true
});
function createSphere(radius, segments) {
return new THREE.Mesh(
new THREE.SphereGeometry(radius, segments, segments),
new THREE.MeshPhongMaterial({
map: THREE.ImageUtils.loadTexture('relief.jpg'),
bumpMap: THREE.ImageUtils.loadTexture('elev_bump_4k.jpg'),
bumpScale: 0.005,
specularMap: THREE.ImageUtils.loadTexture('wateretopo.png'),
specular: new THREE.Color('grey')
})
);
}
var clouds = createClouds(radius, segments);
clouds.rotation.y = rotation;
scene.add(clouds)
function createClouds(radius, segments) {
return new THREE.Mesh(
new THREE.SphereGeometry(radius + .003, segments, segments),
new THREE.MeshPhongMaterial({
map: THREE.ImageUtils.loadTexture('n_amer_clouds.png'),
transparent: true
})
);
}
//Draw the GeoJSON
var test_json = $.getJSON("countries_states.geojson", function(data) {
drawThreeGeo(data, 10, 'sphere', {
color: 'red'
})
});
//Draw the GeoJSON loggerhead data
var test_json = $.getJSON("loggerhead-distro-cec-any.json", function(data) {
drawThreeGeo(data, 10, 'sphere', {
color: 'blue'
})
});
//Set the camera position
camera.position.z = 30;
//Enable controls
var controls = new THREE.TrackballControls(camera);
//Render the image
function render() {
controls.update();
requestAnimationFrame(render);
renderer.render(scene, camera);
}
render();
</script>
</body>
</html>
Instead of ancient r66, I used r81 (just replaced three.min.js). I modified your createSphere() function a bit, and seems it's working.
function createSphere(radius, segments) {
var sphGeom = new THREE.SphereGeometry(radius, segments, segments);
sphGeom.rotateY(THREE.Math.degToRad(-90));
return new THREE.Mesh(
sphGeom,
new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('relief.jpg'),
bumpMap: new THREE.TextureLoader().load('elev_bump_4k.jpg'),
bumpScale: 0.005,
specularMap: new THREE.TextureLoader().load('wateretopo.png'),
specular: new THREE.Color('grey')
})
);
}
The only thing I did was to rotate the sphere's geometry around Y-axis at -90 degrees. The result is here
I am trying to create spikes on earth(sphere geometry). Though everything works fines, but spikes dont align with globe. I want spike to align something like below image. But my spikes dont lookAt(new THREE.Vector3(0,0,0)) despite mentioned. Please help me out.
I purposefully mentioned code required for debugging. Let me know if you need more code for this. Below image is how i want my spikes to align with sphere.
But this is how it looks
My Main JS initialization file.
$(document).ready(function () {
// Initializing Camera
Influx.Camera = new Influx.Camera({
fov: 60,
aspectRatio: window.innerWidth / window.innerHeight,
near: 1,
far: 1000,
position: {
x: 0,
y: 0,
z: 750
}
});
//Initializing Scene
Influx.Scene = new Influx.Scene();
// Initializing renderer
Influx.Renderer = new Influx.Renderer({
clearColor: 0x000000,
size: {
width: window.innerWidth,
height: window.innerHeight
}
});
Influx.Globe = new Influx.Globe({
radius: 300,
width: 50,
height: 50
});
//
Influx.Stars = new Influx.Stars({
particleCount: 15000,
particle: {
color: 0xFFFFFF,
size: 1
}
});
Influx.moveTracker = new Influx.moveTracker();
Influx.EventListener = new Influx.EventListener();
(function animate() {
requestAnimationFrame( animate );
render();
controls.update();
})();
function render() {
camera.lookAt(scene.position);
group.rotation.y -= 0.001;
renderer.render( scene, camera );
};
});
Below is code responsible for generating spikes on Globe.
Influx.Spikes = function (lat, long) {
// convert the positions from a lat, lon to a position on a sphere.
var latLongToVector3 = function(lat, lon, RADIUS, heigth) {
var phi = (lat) * Math.PI/180,
theta = (lon-180) * Math.PI/180;
var x = -(RADIUS+heigth) * Math.cos(phi) * Math.cos(theta),
y = (RADIUS+heigth) * Math.sin(phi),
z = (RADIUS+heigth) * Math.cos(phi) * Math.sin(theta);
return new THREE.Vector3(x, y, z);
};
var geom = new THREE.Geometry();
var BoxGeometry = new THREE.BoxGeometry(1, 100, 1);
//iterates through the data points and makes boxes with the coordinates
var position = latLongToVector3(lat, long, 300, 2);
var box = new THREE.Mesh( BoxGeometry );
//each position axis needs to be set separately, otherwise the box
//will instantiate at (0,0,0)
box.position.x = position.x;
box.position.y = position.y;
box.position.z = position.z;
box.lookAt(new THREE.Vector3(0, 0, 0));
box.updateMatrix();
//merges the geometry to speed up rendering time, don't use THREE.GeometryUtils.merge because it's deprecated
geom.merge(box.geometry, box.matrix);
var total = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({
color: getRandomColor(),
morphTargets: true
}));
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++ ) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
//add boxes to the group
group.add(total);
scene.add(group);
};
Influx.Camera = function(params = {}) {
if ( !$.isEmptyObject(params) ) {
window.camera = new THREE.PerspectiveCamera(params.fov, params.aspectRatio, params.near, params.far);
camera.position.set(params.position.x, params.position.y, params.position.z);
camera.lookAt(new THREE.Vector3(0,0,0));
} else {
console.log("Trouble with Initializing Camera");
return;
}
};
Remember that lookAt takes a direction vector, you give to this method the vector (0, 0, 0), this is actually not a normalized direction vector. So you must calculate the direction:
from your box position to the center of the sphere AND normalize it.
var dir = box.position.sub(world.position).normalize();
box.lookAt(dir);
And now just a set of code good conventions that may help you:
var BoxGeometry = new THREE.BoxGeometry(1, 100, 1);
Here I would rather use another var name for the box geometry, not to mix up with the "class" definition from THREE and to follow naming conventions:
var boxGeometry = new THREE.BoxGeometry(1, 100, 1);
And here:
box.position.x = position.x;
box.position.y = position.y;
box.position.z = position.z;
You can just set:
box.position.copy(position);
I also meet this problem, and I fixed it, the solution is: box.lookAt(new THREE.Vector3(0, 0, 0)) must after box.scale.z = xxxx
I am able to display a THREE.TubeGeometry figure as follows
Code below, link to jsbin
<html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.js"></script>
<script>
// global variables
var renderer;
var scene;
var camera;
var geometry;
var control;
var count = 0;
var animationTracker;
init();
drawSpline();
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('lightgray', 1.0);
renderer.setSize(window.innerWidth, window.innerHeight);
// position and point the camera to the center of the scene
camera.position.x = 0;
camera.position.y = 40;
camera.position.z = 40;
camera.lookAt(scene.position);
// add the output of the renderer to the html element
document.body.appendChild(renderer.domElement);
}
function drawSpline(numPoints)
{
var numPoints = 100;
// var start = new THREE.Vector3(-5, 0, 20);
var start = new THREE.Vector3(-5, 0, 20);
var middle = new THREE.Vector3(0, 35, 0);
var end = new THREE.Vector3(5, 0, -20);
var curveQuad = new THREE.QuadraticBezierCurve3(start, middle, end);
var tube = new THREE.TubeGeometry(curveQuad, numPoints, 0.5, 20, false);
var mesh = new THREE.Mesh(tube, new THREE.MeshNormalMaterial({
opacity: 0.9,
transparent: true
}));
scene.add(mesh);
renderer.render(scene, camera);
}
</script>
</body>
</html>
However, I would like to display incrementally, as in, like an arc that is loading, such that it starts as the start point, draws incrementally and finally looks the below arc upon completion.
I have been putting in some effort, and was able to do this by storing all the points/coordinates covered by the arc, and drawing lines between the consecutive coordinates, such that I get the 'arc loading incrementally' feel. However, is there a better way to achieve this? This is the link to jsbin
Adding the code here as well
<!DOCTYPE html>
<html>
<head>
<title>Incremental Spline Curve</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<script>
// global variables
var renderer;
var scene;
var camera;
var splineGeometry;
var control;
var count = 0;
var animationTracker;
// var sphereCamera;
var sphere;
var light;
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(0x000000, 1.0);
renderer.setClearColor( 0xffffff, 1 );
renderer.setSize(window.innerWidth, window.innerHeight);
// position and point the camera to the center of the scene
camera.position.x = 0;
camera.position.y = 40;
camera.position.z = 40;
camera.lookAt(scene.position);
// add the output of the renderer to the html element
document.body.appendChild(renderer.domElement);
// //init for sphere
// sphereCamera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
// sphereCamera.position.y = -400;
// sphereCamera.position.z = 400;
// sphereCamera.rotation.x = .70;
sphere = new THREE.Mesh(new THREE.SphereGeometry(0.8,31,31), new THREE.MeshLambertMaterial({
color: 'yellow',
}));
light = new THREE.DirectionalLight('white', 1);
// light.position.set(0,-400,400).normalize();
light.position.set(0,10,10).normalize();
//get points covered by Spline
getSplineData();
}
//save points in geometry.vertices
function getSplineData() {
var curve = new THREE.CubicBezierCurve3(
new THREE.Vector3( -5, 0, 10 ),
new THREE.Vector3(0, 20, 0 ),
new THREE.Vector3(0, 20, 0 ),
new THREE.Vector3( 2, 0, -25 )
);
splineGeometry = new THREE.Geometry();
splineGeometry.vertices = curve.getPoints( 50 );
animate();
}
//scheduler loop
function animate() {
if(count == 50)
{
cancelAnimationFrame(animationTracker);
return;
}
//add line to the scene
drawLine();
renderer.render(scene, camera);
// renderer.render(scene, sphereCamera);
count += 1;
// camera.position.z -= 0.25;
// camera.position.y -= 0.25;
animationTracker = requestAnimationFrame(animate);
}
function drawLine() {
var lineGeometry = new THREE.Geometry();
var lineMaterial = new THREE.LineBasicMaterial({
color: 0x0000ff
});
console.log(splineGeometry.vertices[count]);
console.log(splineGeometry.vertices[count+1]);
lineGeometry.vertices.push(
splineGeometry.vertices[count],
splineGeometry.vertices[count+1]
);
var line = new THREE.Line( lineGeometry, lineMaterial );
scene.add( line );
}
// calls the init function when the window is done loading.
window.onload = init;
</script>
<body>
</body>
</html>
Drawback : The drawback of doing it the above way is that, end of the day, I'm drawing a line between consecutive points, and so I lose out on a lot of the effects possible in TubeGeometry such as, thickness, transparency etc.
Please suggest me an alternative way to get a smooth incremental load for the TubeGeometry.
THREE.TubeGeometry returns a THREE.BufferGeometry.
With THREE.BufferGeometry, you have access to a property drawRange that you can set to animate the drawing of the mesh:
let nEnd = 0, nMax, nStep = 90; // 30 faces * 3 vertices/face
...
const geometry = new THREE.TubeGeometry( path, pathSegments, tubeRadius, radiusSegments, closed );
nMax = geometry.attributes.position.count;
...
function animate() {
requestAnimationFrame( animate );
nEnd = ( nEnd + nStep ) % nMax;
mesh.geometry.setDrawRange( 0, nEnd );
renderer.render( scene, camera );
}
EDIT: For another approach, see this SO answer.
three.js r.144
Normally you would be able to use the method .getPointAt() to "get a vector for point at relative position in curve according to arc length" to get a point at a certain percentage of the length of the curve.
So normally if you want to draw 70% of the curve and a full curve is drawn in 100 segments. Then you could do:
var percentage = 70;
var curvePath = new THREE.CurvePath();
var end, start = curveQuad.getPointAt( 0 );
for(var i = 1; i < percentage; i++){
end = curveQuad.getPointAt( percentage / 100 );
lineCurve = new THREE.LineCurve( start, end );
curvePath.add( lineCurve );
start = end;
}
But I think this is not working for your curveQuad since the getPointAt method is not implemented for this type. A work around is to get a 100 points for your curve in an array like this:
points = curve.getPoints(100);
And then you can do almost the same:
var percentage = 70;
var curvePath = new THREE.CurvePath();
var end, start = points[ 0 ];
for(var i = 1; i < percentage; i++){
end = points[ percentage ]
lineCurve = new THREE.LineCurve( start, end );
curvePath.add( lineCurve );
start = end;
}
now your curvePath holds the line segments you want to use for drawing the tube:
// draw the geometry
var radius = 5, radiusSegments = 8, closed = false;
var geometry = new THREE.TubeGeometry(curvePath, percentage, radius, radiusSegments, closed);
Here a fiddle with a demonstration on how to use this dynamically
I'm not really that familiar with three.js. But I think I can be of assistance. I have two solutions for you. Both based on the same principle: build a new TubeGeometry or rebuild the current one, around a new curve.
Solution 1 (Simple):
var CurveSection = THREE.Curve.create(function(base, from, to) {
this.base = base;
this.from = from;
this.to = to;
}, function(t) {
return this.base.getPoint((1 - t) * this.from + t * this.to);
});
You define a new type of curve which just selects a segment out of a given curve. Usage:
var curve = new CurveSection(yourCurve, 0, .76); // Where .76 is your percentage
Now you can build a new tube.
Solution 2 (Mathematics!):
You are using for your arc a quadratic bezier curve, that's awesome! This curve is a parabola. You want just a segment of that parabola and that is again a parabola, just with other bounds.
What we need is a section of the bezier curve. Let's say the curve is defined by A (start), B (direction), C (end). If we want to change the start to a point D and the end to a point F we need the point E that is the direction of the curve in D and F. So the tangents to our parabola in D and F have to intersect in E. So the following code will give us the desired result:
// Calculates the instersection point of Line3 l1 and Line3 l2.
function intersection(l1, l2) {
var A = l1.start;
var P = l2.closestPointToPoint(A);
var Q = l1.closestPointToPoint(P);
var l = P.distanceToSquared(A) / Q.distanceTo(A);
var d = (new THREE.Vector3()).subVectors(Q, A);
return d.multiplyScalar(l / d.length()).add(A);
}
// Calculate the tangentVector of the bezier-curve
function tangentQuadraticBezier(bezier, t) {
var s = bezier.v0,
m = bezier.v1,
e = bezier.v2;
return new THREE.Vector3(
THREE.CurveUtils.tangentQuadraticBezier(t, s.x, m.x, e.x),
THREE.CurveUtils.tangentQuadraticBezier(t, s.y, m.y, e.y),
THREE.CurveUtils.tangentQuadraticBezier(t, s.z, m.z, e.z)
);
}
// Returns a new QuadraticBezierCurve3 with the new bounds.
function sectionInQuadraticBezier(bezier, from, to) {
var s = bezier.v0,
m = bezier.v1,
e = bezier.v2;
var ns = bezier.getPoint(from),
ne = bezier.getPoint(to);
var nm = intersection(
new THREE.Line3(ns, tangentQuadraticBezier(bezier, from).add(ns)),
new THREE.Line3(ne, tangentQuadraticBezier(bezier, to).add(ne))
);
return new THREE.QuadraticBezierCurve3(ns, nm, ne);
}
This is a very mathematical way, but if you should need the special properties of a Bezier curve, this is the way to go.
Note: The first solution is the simplest. I am not familiar with Three.js so I wouldn't know what the most efficient way to implement the animation is. Three.js doesn't seem to use the special properties of a bezier curve so maybe solution 2 isn't that useful.
I hope you have gotten something useful out of this.