I am creating a game where players can move around from first-person perspective, where the ground is generated with Perlin noise and therefore uneven. I would like to simulate gravity in the game. Hence, a raycasting thing has been implemented, which is supposed to find the player's distance from the ground and stop them from falling when they hit the ground. Here is my code (if the snipper is unclear visit https://3d.211368e.repl.co):
const scene = new THREE.Scene(), camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000000000000), renderer = new THREE.WebGLRenderer(), canvas = renderer.domElement;
camera.rotation.order = "YXZ";
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMapType = THREE.PCFSoftShadowMap;
document.body.appendChild(canvas);
const light = new THREE.DirectionalLight( 0xffffff, 1);
light.position.set(0, 10000, 0);
light.castShadow = true;
light.shadow.camera.top = 10000;
light.shadow.camera.right = 10000;
light.shadow.camera.bottom = -10000;
light.shadow.camera.left = -10000;
light.shadow.camera.far = 100000;
wwwww
scene.add(light);
var sky = new THREE.Mesh(new THREE.SphereGeometry(100000, 3, 3, 0, Math.PI, 0, Math.PI), new THREE.MeshBasicMaterial({color: 0x579ebb}));
sky.material.side = THREE.BackSide;
sky.rotateX(-Math.PI / 2);
scene.add(sky);
class Vector2{
constructor(x, y){
this.x = x;
this.y = y;
}
dot(other){
return this.x * other.x + this.y * other.y;
}
}
function Shuffle(tab){
for(let e = tab.length-1; e > 0; e--){
let index = Math.round(Math.random() * (e-1)),
temp = tab[e];
tab[e] = tab[index];
tab[index] = temp;
}
}
function MakePermutation(){
let P = [];
for(let i = 0; i < 256; i++){
P.push(i);
}
Shuffle(P);
for(let i = 0; i < 256; i++){
P.push(P[i]);
}
return P;
}
let P = MakePermutation();
function GetConstantVector(v){
let h = v & 3;
if(h == 0) return new Vector2(1.0, 1.0);
if(h == 1) return new Vector2(-1.0, 1.0);
if(h == 2) return new Vector2(-1.0, -1.0);
return new Vector2(1.0, -1.0);
}
function Fade(t){
return ((6 * t - 15) * t + 10) * t ** 3;
}
function Lerp(t, a1, a2){
return a1 + t*(a2-a1);
}
function Noise2D(x, y){
let X = Math.floor(x) & 255;
let Y = Math.floor(y) & 255;
let xf = x - Math.floor(x);
let yf = y - Math.floor(y);
let topRight = new Vector2(xf - 1, yf - 1);
let topLeft = new Vector2(xf, yf - 1);
let bottomRight = new Vector2(xf - 1, yf);
let bottomLeft = new Vector2(xf, yf);
let valueTopRight = P[P[X+1]+Y+1];
let valueTopLeft = P[P[X]+Y+1];
let valueBottomRight = P[P[X+1]+Y];
let valueBottomLeft = P[P[X]+Y];
let dotTopRight = topRight.dot(GetConstantVector(valueTopRight));
let dotTopLeft = topLeft.dot(GetConstantVector(valueTopLeft));
let dotBottomRight = bottomRight.dot(GetConstantVector(valueBottomRight));
let dotBottomLeft = bottomLeft.dot(GetConstantVector(valueBottomLeft));
let u = Fade(xf);
let v = Fade(yf);
return Lerp(u, Lerp(v, dotBottomLeft, dotTopLeft), Lerp(v, dotBottomRight, dotTopRight));
}
const plane = new THREE.Mesh(new THREE.PlaneGeometry(10000, 10000, 500, 500), new THREE.MeshPhongMaterial({color: 0x00aa00}));
plane.rotateX(-Math.PI / 2 + 0.00001);
plane.receiveShadow = true;
for (let y = 0, i = 0; y < 501; y++){
for(let x = 0; x < 501; x++, i++){
let n = 0.0, a = 1.0, f = 0.005;
for (let o = 0; o < 3; o++){
let v = a*Noise2D(x*f, y*f);
n += v;
a *= 0.5;
f *= 2.0;
}
n += 1;
n /= 2;
plane.geometry.vertices[i].z = n * 1000;
}
}
scene.add(plane);
const point = plane.geometry.vertices[Math.floor(Math.random() * 1000)];
camera.position.set(point.x, point.z + 2, point.y);
const geo = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshBasicMaterial({color: 0xff0000}));
geo.castShadow = true;
scene.add(geo);
const render = () => {
requestAnimationFrame(render);
const below = new THREE.Vector3(camera.position.x, -1000000, camera.position.y), cast = new THREE.Raycaster(camera.position, below), intersect = cast.intersectObject(plane);
if (intersect.length > 0){
if (intersect[0].distance < 3) camera.translateY(-1);
}else{
camera.translateY(-1);
}
renderer.render(scene, camera);
}
render();
onmousemove = () => {
if (camera.rotation._x > -0.8 || camera.rotation._y > -0.8){
camera.rotateX(-Math.atan(event.movementY / 300));
camera.rotateY(-Math.atan(event.movementX / 300));
}else{
if (Math.atan(event.movementY / 300) < 0) camera.rotateX(-Math.atan(event.movementY / 300));
if (Math.atan(event.movementX / 300) < 0) camera.rotateY(-Math.atan(event.movementX / 300));
}
camera.rotation.z = 0;
}
onresize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
onkeydown = (event) => {
if (event.key == "w") camera.translateZ(-10);
if (event.key == "a") camera.translateX(-1);
if (event.key == "s") camera.translateZ(1);
if (event.key == "d") camera.translateX(1);
if (event.key == "ArrowUp") camera.translateY(1);
if (event.key == "ArrowDown") camera.translateY(-1);
}
body{
margin: 0;
background-color: black;
overflow: hidden;
}
canvas{
border: none;
}
<meta name="viewport" content="width=device-width">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/94/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/utils/SceneUtils.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/libs/dat.gui.min.js"></script>
If the ground is not detected at least 3 units below the camera, the player will continue falling. However, sometimes nothing is spotted below the camera, while the player is clearly hovering over the ground. This is extremely frustrating. Is there any reliable alternative method to solve this problem, such as using something other than raycasting? Or is there a bug in the code? TIA
See the Raycaster documentation. The constructor takes the origin, the direction and near and far parameters. So you could do:
const gravityDirection = new THREE.Vector3(0, -1, 0);
cast = new THREE.Raycaster(camera.position, gravityDirection, 0, 3);
and this also makes the distance check redundant, as the far parameter already filters out hits further away than 3 units.
Related
I have the function CreateChunk(x,z) that creates a "chunk" of terrain in the specified coordinates x and z that is a plane, whose vertex heights are modified with Perlin noise and then painted based on their height (a layer of water is added too) as you see below :
A single chunk
Everything works fine until I try to make more chunks:
Many chunks
I know this is how it should work and there is nothing wrong, but, what can I do to "synchronize" them so where one ends, the other starts? While keeping a procedural generation.
If you need the code tell me, but I was just asking for an idea to follow.
You need to know what tile you want to build and what density of noise you want to have on the tiles.
For some ideas, have a look at this forum post: https://discourse.threejs.org/t/help-getting-the-actual-position-of-a-vertices-in-a-buffer-geometry/29649/4
And I'll leave the snippet here. Maybe it will be helpful for other seekers :)
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.135.0";
import {OrbitControls} from "https://cdn.skypack.dev/three#0.135.0/examples/jsm/controls/OrbitControls";
import {ImprovedNoise} from "https://cdn.skypack.dev/three#0.135.0/examples/jsm/math/ImprovedNoise";
THREE.BufferGeometry.prototype.toQuads = ToQuads;
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 8, 13);
let renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x404040);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
})
const perlin = new ImprovedNoise();
let controls = new OrbitControls(camera, renderer.domElement);
let step = 20;
for(let z = -4; z <= 4; z ++){
for(let x = -4; x <= 4; x++){
let p = createPlane(step, Math.random() * 0x7f7f7f + 0x7f7f7f);
setNoise(p.geometry, new THREE.Vector2(x, z), 2, 5);
p.geometry.rotateX(Math.PI * 0.5);
p.position.set(x, 0, z).multiplyScalar(step);
scene.add(p);
}
}
renderer.setAnimationLoop( _ => {
renderer.render(scene, camera);
})
function createPlane( step, color){
let g = new THREE.PlaneGeometry(step, step, 25, 25).toQuads();
let m = new THREE.LineBasicMaterial({color: color});
let l = new THREE.LineSegments(g, m);
return l;
}
function setNoise(g, uvShift, multiplier, amplitude){
let pos = g.attributes.position;
let uv = g.attributes.uv;
let vec2 = new THREE.Vector2();
for(let i = 0; i < pos.count; i++){
vec2.fromBufferAttribute(uv, i).add(uvShift).multiplyScalar(multiplier);
pos.setZ(i, perlin.noise(vec2.x, vec2.y, 0) * amplitude );
}
}
function ToQuads() {
let g = this;
let p = g.parameters;
let segmentsX = (g.type == "TorusBufferGeometry" ? p.tubularSegments : p.radialSegments) || p.widthSegments || p.thetaSegments || (p.points.length - 1) || 1;
let segmentsY = (g.type == "TorusBufferGeometry" ? p.radialSegments : p.tubularSegments) || p.heightSegments || p.phiSegments || p.segments || 1;
let indices = [];
for (let i = 0; i < segmentsY + 1; i++) {
let index11 = 0;
let index12 = 0;
for (let j = 0; j < segmentsX; j++) {
index11 = (segmentsX + 1) * i + j;
index12 = index11 + 1;
let index21 = index11;
let index22 = index11 + (segmentsX + 1);
indices.push(index11, index12);
if (index22 < ((segmentsX + 1) * (segmentsY + 1) - 1)) {
indices.push(index21, index22);
}
}
if ((index12 + segmentsX + 1) <= ((segmentsX + 1) * (segmentsY + 1) - 1)) {
indices.push(index12, index12 + segmentsX + 1);
}
}
g.setIndex(indices);
return g;
}
</script>
I looked at this example with three js to draw particles with images and works perfectly but i want to change the image with a switch when click a button (calls this function):
const changeImg = function(num) {
switch (num)
{
case 0:
imgData ="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA....";
break;
case 1:
imgData = "data:image/png;base64,iVBORw0KGgoAAAAN..."
break;
}
img.src = imgData;
}
And works but when you click multiple times website becomes slow.
How can I update just the image without slowing down the website?
EDIT 1
I change the code like this:
var renderer, scene, camera, ww, wh, particles, mw, mh, mz, numState;
numState = 0;
mz = 6; // Matrerial size
ww = document.getElementById('map-container').offsetWidth,
wh = 450;
mw = ww * 2;
mh = wh * 2;
var centerVector = new THREE.Vector3(0, 0, 0);
var previousTime = 0
speed = 10
isMouseDown = false;
// Render
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("map"),
antialias: true
});
renderer.setSize(mw, mh);
renderer.setClearColor(0x12347C);
// Scence
scene = new THREE.Scene();
// Camera
camera = new THREE.OrthographicCamera( ww / - 2, ww / 2, wh / 2, wh / - 2, 1, 1000 );
camera.position.set(7, 0, 4);
camera.lookAt(centerVector);
scene.add(camera);
camera.zoom = 4;
camera.updateProjectionMatrix();
// Geometry
var geometry = new THREE.Geometry();
var material = new THREE.PointsMaterial({
size: mz,
color: 0xFFFFFF,
sizeAttenuation: false
});
// Particle
particles = new THREE.Points();
var getImageData = function(image) {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
return ctx.getImageData(0, 0, image.width, image.height);
}
var drawTheMap = function() {
geometry.dispose();
particles.material.dispose();
particles.geometry.dispose();
for (var y = 0, y2 = imagedata.height; y < y2; y += 2) {
for (var x = 0, x2 = imagedata.width; x < x2; x += 2) {
if (imagedata.data[(x * 4 + y * 4 * imagedata.width)] < 128) {
var vertex = new THREE.Vector3();
vertex.x = x - imagedata.width / 2;
vertex.y = -y + imagedata.height / 2;
vertex.z = -Math.random()*500;
vertex.speed = Math.random() / speed + 0.015;
geometry.vertices.push(vertex);
}
}
}
particles.material = material;
particles.geometry = geometry;
scene.add(particles);
requestAnimationFrame(render);
};
var init = function() {
imagedata = getImageData(image);
drawTheMap();
onResize();
window.addEventListener('mousemove', onMousemove, false);
window.addEventListener('mousedown', onMousedown, false);
window.addEventListener('mouseup', onMouseup, false);
window.addEventListener('resize', onResize, false);
};
var onResize = function(){
var mov1, mov2;
ww = document.getElementById('map-container').offsetWidth;
wh = 450;
if (window.innerWidth > 850) {
mw = ww * 2;
mh = wh * 2;
mz = 6;
mov1 = 2.2;
mov2 = 1.9;
particles.material.size = mz;
} else {
mw = ww;
mh = wh;
mz = 3;
mov1 = 2;
mov2 = 2;
particles.material.size = mz;
}
renderer.setSize(mw, mh);
camera.left = ww / - mov1;
camera.right = ww / 2;
camera.top = wh / mov2;
camera.bottom = wh / - 2;
camera.updateProjectionMatrix();
};
var onMouseup = function(){
isMouseDown = false;
}
var onMousedown = function(e){
isMouseDown = true;
lastMousePos = {x:e.clientX, y:e.clientY};
};
var onMousemove = function(e){
if(isMouseDown){
camera.position.x += (e.clientX-lastMousePos.x)/100;
camera.position.y -= (e.clientY-lastMousePos.y)/100;
camera.lookAt(centerVector);
lastMousePos = {x:e.clientX, y:e.clientY};
}
};
var render = function(a) {
requestAnimationFrame(render);
particles.geometry.verticesNeedUpdate = true;
if(!isMouseDown){
camera.position.x += (0-camera.position.x)*0.06;
camera.position.y += (0-camera.position.y)*0.06;
camera.lookAt(centerVector);
}
renderer.render(scene, camera);
};
var imgData;
var image;
imgData ="data:image/png;base64,iVBORw0KGgoAAA...";
const changeState = function(state, num) {
document.getElementById('dropbox-choose').innerHTML = state;
numState = num;
switch (numState)
{
case 0:
imgData ="data:image/png;base64,iVBORw0KGgoAAA...";
break;
case 1:
imgData = "data:image/png;base64,iVBORw0KGgoI..."
break;
}
image.src = imgData;
}
image = document.createElement("img");
image.onload = init;
image.src = imgData;
And the THREE.WebGLRenderer is only applied once but when I click to change the image, it does not update and also I still have the problem that the website slows down
it's my first time using three js and i don't know if i'm applying well what it says in the documentation
EDIT 2
var renderer, scene, camera, ww, wh, particles, mw, mh, mz, numState;
numState = 0;
mz = 6;
ww = document.getElementById('map-container').offsetWidth,
wh = 450;
mw = ww * 2;
mh = wh * 2;
var centerVector = new THREE.Vector3(0, 0, 0);
var previousTime = 0
speed = 10
isMouseDown = false;
// Render
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("map"),
antialias: true
});
renderer.setSize(mw, mh);
renderer.setClearColor(0x12347C);
// Scence
scene = new THREE.Scene();
// Camera
camera = new THREE.OrthographicCamera( ww / - 2, ww / 2, wh / 2, wh / - 2, 1, 1000 );
camera.position.set(7, 0, 4);
camera.lookAt(centerVector);
scene.add(camera);
camera.zoom = 4;
camera.updateProjectionMatrix();
// Geometry
//var geometry = new THREE.Geometry();
var material = new THREE.PointsMaterial({
size: mz,
color: 0xFFFFFF,
sizeAttenuation: false
});
// Particle
particles = new THREE.Points();
particles.material = material
scene.add(particles);
var getImageData = function(image) {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
return ctx.getImageData(0, 0, image.width, image.height);
}
var drawTheMap = function() {
let vertices = particles.geometry; // this acts as a REFERENCE!
vertices.length = 0; // clears the vertices array
for (var y = 0, y2 = imagedata.height; y < y2; y += 2) {
for (var x = 0, x2 = imagedata.width; x < x2; x += 2) {
if (imagedata.data[(x * 4 + y * 4 * imagedata.width)] < 128) {
var vertex = new THREE.Vector3();
vertex.x = x - imagedata.width / 2;
vertex.y = -y + imagedata.height / 2;
vertex.z = -Math.random()*500;
vertex.speed = Math.random() / speed + 0.015;
vertices.vertices.push(vertex);
}
}
}
particles.geometry.verticesNeedUpdate = true; // Inform three.js of the update
requestAnimationFrame(render);
};
var init = function() {
imagedata = getImageData(image);
drawTheMap();
onResize();
window.addEventListener('mousemove', onMousemove, false);
window.addEventListener('mousedown', onMousedown, false);
window.addEventListener('mouseup', onMouseup, false);
window.addEventListener('resize', onResize, false);
};
var onResize = function(){
var mov1, mov2;
ww = document.getElementById('map-container').offsetWidth;
wh = 450;
if (window.innerWidth > 850) {
mw = ww * 2;
mh = wh * 2;
mz = 6;
mov1 = 2.2;
mov2 = 1.9;
particles.material.size = mz;
} else {
mw = ww;
mh = wh;
mz = 3;
mov1 = 2;
mov2 = 2;
particles.material.size = mz;
}
renderer.setSize(mw, mh);
camera.left = ww / - mov1;
camera.right = ww / 2;
camera.top = wh / mov2;
camera.bottom = wh / - 2;
camera.updateProjectionMatrix();
};
var onMouseup = function(){
isMouseDown = false;
}
var onMousedown = function(e){
isMouseDown = true;
lastMousePos = {x:e.clientX, y:e.clientY};
};
var onMousemove = function(e){
if(isMouseDown){
camera.position.x += (e.clientX-lastMousePos.x)/100;
camera.position.y -= (e.clientY-lastMousePos.y)/100;
camera.lookAt(centerVector);
lastMousePos = {x:e.clientX, y:e.clientY};
}
};
var render = function(a) {
requestAnimationFrame(render);
particles.geometry.verticesNeedUpdate = true;
if(!isMouseDown){
camera.position.x += (0-camera.position.x)*0.06;
camera.position.y += (0-camera.position.y)*0.06;
camera.lookAt(centerVector);
}
renderer.render(scene, camera);
};
var imgData;
var image;
imgData ="data:image/png;base64,iVBORw0KGgoAAA...";
const changeState = function(state, num) {
document.getElementById('dropbox-choose').innerHTML = state;
numState = num;
switch (numState)
{
case 0:
imgData ="data:image/png;base64,iVBORw0KGgoAAA...";
break;
case 1:
imgData = "data:image/png;base64,iVBORw0KGgoAAA..."
break;
}
image.src = imgData;
}
image = document.createElement("img");
image.onload = init;
image.src = imgData;
When I click to change the image, it does not update and also I still have the problem that the website slows down. I cahaged vertcies.push to vertices.vertices.push()
I know I mentioned disposal in a previous version of my answer, but let's instead consider re-using all of your objects.
particles - Add it to the scene immediately after creation.
material - Assign it to particles immediately; No need to re-assign it every time.
geometry - Don't create it globally, we'll let it work from within particles.
Now what we're going to do is replace the vertices and tell three.js that there are new vertices to upload to the GPU.
var drawTheMap = function() {
let vertices = particles.geometry; // this acts as a REFERENCE!
vertices.length = 0; // clears the vertices array
for (var y = 0, y2 = imagedata.height; y < y2; y += 2) {
for (var x = 0, x2 = imagedata.width; x < x2; x += 2) {
if (imagedata.data[(x * 4 + y * 4 * imagedata.width)] < 128) {
var vertex = new THREE.Vector3();
vertex.x = x - imagedata.width / 2;
vertex.y = -y + imagedata.height / 2;
vertex.z = -Math.random()*500;
vertex.speed = Math.random() / speed + 0.015;
vertices.push(vertex);
}
}
}
particles.geometry.verticesNeedUpdate = true; // Inform three.js of the update
requestAnimationFrame(render);
};
The important part here (other than replacing the contents of the vertices array) is setting particles.geometry.verticesNeedUpdate = true;. This is what triggers three.js to replace the vertices on the GPU. Everything else is re-used, not recreated, so it should run fairly smooth.
The solution is change THREE.geometry to THREE.BufferGeometry
var drawTheMap = function() {
particles.geometry = new THREE.BufferGeometry();
var positions = [];
for (var y = 0, y2 = imagedata.height; y < y2; y += 2) {
for (var x = 0, x2 = imagedata.width; x < x2; x += 2) {
if (imagedata.data[(x * 4 + y * 4 * imagedata.width)] < 128) {
positions.push(x - imagedata.width / 2);
positions.push(-y + imagedata.height / 2);
positions.push(-Math.random()*500);
particles.geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
}
}
}
particles.geometry.verticesNeedUpdate = true;
requestAnimationFrame(render);
};
I'm trying to make a database of words where the most important words are closer to the top of the sphere and the less important are further away. So I created a sphere with enough vertices for each word, created a list of those vertices in order of distance from the top of the sphere, and placed the text sprites at the positions of the vertices in order of that sorted list.
Video version: https://i.gyazo.com/aabaf0b4a26f4413dc6a0ebafab2b4bd.mp4
Sounded like a good plan in my head, but clearly the geometry of a sphere causes the words to be further spread out the further away from the top they are. I need a result that looks like a somewhat even distribution across the surface. It doesn't have to be perfect, just visually closer than this.
How can I achieve the desired effect?
Here are the relevant methods:
positionDb(db) {
console.log("mostRelated", db.mostRelated);
console.log("depthList", this.depthList);
let mostRelated = db.mostRelated;
let depthList = this.depthList;
for (let i = 0; i < mostRelated.length; i++) {
this.addTextNode(mostRelated[i].data, this.depthList[i].vertice, this.depthList[i].depth);
}
}
addTextNode(text, vert, distance) {
let fontSize = 0.5 * (600 / distance);
let sprite = new THREE.TextSprite({
fillStyle: '#000000',
fontFamily: '"Arial", san-serif',
fontSize: fontSize,
fontWeight: 'bold',
text: text
});
this.scene.add(sprite);
sprite.position.set(vert.x, vert.y, vert.z);
setTimeout(() => {
sprite.fontFamily = '"Roboto", san-serif';
}, 1000)
}
this.scene = scene;
this.geometry = new THREE.SphereGeometry(420, 50, 550);
var material = new THREE.MeshBasicMaterial({
color: 0x0011ff
});
var sphere = new THREE.Mesh(this.geometry, wireframe);
var wireframe = new THREE.WireframeGeometry(this.geometry);
let frontVert = {
x: 0,
y: 100,
z: 0
}
let depthList = [];
this.geometry.vertices.forEach(vertice => {
let depth = getDistance(frontVert, vertice);
if (depthList.length === 0) {
depthList.push({
depth,
vertice
});
} else {
let flag = false;
for (let i = 0; i < depthList.length; i++) {
let item = depthList[i];
if (depth < item.depth) {
flag = true;
depthList.splice(i, 0, {
depth,
vertice
});
break;
}
}
if (!flag) depthList.push({
depth,
vertice
});
}
});
Maybe a fibonacci sphere
function fibonacciSphere(numPoints, point) {
const rnd = 1;
const offset = 2 / numPoints;
const increment = Math.PI * (3 - Math.sqrt(5));
const y = ((point * offset) - 1) + (offset / 2);
const r = Math.sqrt(1 - Math.pow(y, 2));
const phi = (point + rnd) % numPoints * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return new THREE.Vector3(x, y, z);
}
Example:
function fibonacciSphere(numPoints, point) {
const rnd = 1;
const offset = 2 / numPoints;
const increment = Math.PI * (3 - Math.sqrt(5));
const y = ((point * offset) - 1) + (offset / 2);
const r = Math.sqrt(1 - Math.pow(y, 2));
const phi = (point + rnd) % numPoints * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return new THREE.Vector3(x, y, z);
}
function main() {
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
function addTextNode(text, vert) {
const div = document.createElement('div');
div.className = 'label';
div.textContent = text;
div.style.marginTop = '-1em';
const label = new THREE.CSS2DObject(div);
label.position.copy(vert);
scene.add(label);
}
const renderer = new THREE.CSS2DRenderer();
const container = document.querySelector('#c');
container.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
const numPoints = 50;
for (let i = 0; i < numPoints; ++i) {
addTextNode(`p${i}`, fibonacciSphere(numPoints, i));
}
function render(time) {
time *= 0.001;
// three's poor choice of how to hanlde size strikes again :(
renderer.setSize(container.clientWidth, container.clientHeight);
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body {
margin: 0;
overflow: hidden;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
.label {
color: red;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/examples/js/renderers/CSS2DRenderer.js"></script>
<div id="c"></div>
I'm looking for a better/faster way to find the neighbor faces (that share the same edge) in my PlaneBufferGeometry. Currently my THREE.Raycaster intersects fine with my object (using intersectObject). But I need to find all surrounding faces.
At this moment I use the following dirty way to find the 'next door' faces. It works well in my scenario, but doesn't feel right:
let rc = new THREE.Raycaster();
let intersects = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
let v = new THREE.Vector3(x + i, y, z + j);
rc.set(v, new THREE.Vector3(0, -1, 0));
rc.near = 0;
rc.far = 2;
let subIntersects = rc.intersectObject(mesh);
for (let n = 0; n < subIntersects.length; n++) {
intersects.push(subIntersects[n]);
}
}
}
Is there for instance a way to quickly find these faces in the mesh.geometry.attributes.position.array?
Any suggestions are welcome. Thanks in advance!
You mentioned 50k items in the position array. That doens't seem too slow to me. This example is only 10x10 so 100 items in the array so it's easy to see it's working, but I had it set to 200x200 which is 40k items and it seemed fast enough?
'use strict';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 1;
const scene = new THREE.Scene();
scene.background = new THREE.Color('#444');
scene.add(camera);
const planeGeometry = new THREE.PlaneBufferGeometry(1, 1, 20, 20);
const material = new THREE.MeshBasicMaterial({color: 'blue'});
const plane = new THREE.Mesh(planeGeometry, material);
scene.add(plane);
const edgeGeometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
edgeGeometry.setAttribute('position', planeGeometry.getAttribute('position'));
edgeGeometry.setIndex([]);
const edgeMaterial = new THREE.MeshBasicMaterial({
color: 'yellow',
wireframe: true,
depthTest: false,
});
const edges = new THREE.Mesh(edgeGeometry, edgeMaterial);
scene.add(edges);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
class PickHelper {
constructor() {
this.raycaster = new THREE.Raycaster();
}
pick(normalizedPosition, scene, camera, time) {
// cast a ray through the frustum
this.raycaster.setFromCamera(normalizedPosition, camera);
// get the list of objects the ray intersected
const intersectedObjects = this.raycaster.intersectObjects(scene.children, [plane]);
if (intersectedObjects.length) {
// pick the first object. It's the closest one
const intersection = intersectedObjects[0];
const faceIndex = intersection.faceIndex;
const indexAttribute = planeGeometry.getIndex();
const indices = indexAttribute.array;
const vertIds = indices.slice(faceIndex * 3, faceIndex * 3 + 3);
const neighbors = []; // note: self will be added to list
for (let i = 0; i < indices.length; i += 3) {
for (let j = 0; j < 3; ++j) {
const p0Ndx = indices[i + j];
const p1Ndx = indices[i + (j + 1) % 3];
if ((p0Ndx === vertIds[0] && p1Ndx === vertIds[1]) ||
(p0Ndx === vertIds[1] && p1Ndx === vertIds[0]) ||
(p0Ndx === vertIds[1] && p1Ndx === vertIds[2]) ||
(p0Ndx === vertIds[2] && p1Ndx === vertIds[1]) ||
(p0Ndx === vertIds[2] && p1Ndx === vertIds[0]) ||
(p0Ndx === vertIds[0] && p1Ndx === vertIds[2])) {
neighbors.push(...indices.slice(i, i + 3));
break;
}
}
}
const edgeIndices = edgeGeometry.getIndex();
edgeIndices.array = new Uint16Array(neighbors);
edgeIndices.count = neighbors.length;
edgeIndices.needsUpdate = true;
}
}
}
const pickPosition = {x: 0, y: 0};
const pickHelper = new PickHelper();
clearPickPosition();
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
pickHelper.pick(pickPosition, scene, camera, time);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
}
function setPickPosition(event) {
const pos = getCanvasRelativePosition(event);
pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
}
function clearPickPosition() {
// unlike the mouse which always has a position
// if the user stops touching the screen we want
// to stop picking. For now we just pick a value
// unlikely to pick something
pickPosition.x = -100000;
pickPosition.y = -100000;
}
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
window.addEventListener('touchstart', (event) => {
// prevent the window from scrolling
event.preventDefault();
setPickPosition(event.touches[0]);
}, {passive: false});
window.addEventListener('touchmove', (event) => {
setPickPosition(event.touches[0]);
});
window.addEventListener('touchend', clearPickPosition);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.js"></script>
<canvas id="c"></canvas>
Like I mentioned in the comment. If you know it's PlaneBufferGeometry then you can look in the three.js code and see the exact layout of faces so given a faceIndex you can just compute the neighbors directly. The code above is generic, at least for BufferGeometry with an index.
Looking at the code I'm pretty sure it's
// it looks like this is the grid order for PlaneBufferGeometry
//
// b --c
// |\\1|
// |0\\|
// a-- d
const facesAcrossRow = planeGeometry.parameters.widthSegments * 2;
const col = faceIndex % facesAcrossRow
const row = faceIndex / facesAcrossRow | 0;
const neighboringFaceIndices = [];
// check left face
if (col > 0) {
neighboringFaceIndices.push(row * facesAcrossRow + col - 1);
}
// check right face
if (col < facesAcrossRow - 1) {
neighboringFaceIndices.push(row * facesAcrossRow + col + 1);
}
// check up. there can only be one up if we're in an odd triangle (b,c,d)
if (col % 2 && row < planeGeometry.parameters.heightSegments) {
// add the even neighbor in the next row
neighboringFaceIndices.push((row + 1) * facesAcrossRow + col - 1);
}
// check down. there can only be one down if we're in an even triangle (a,b,d)
if (col % 2 === 0 && row > 0) {
// add the odd neighbor in the previous row
neighboringFaceIndices.push((row - 1) * facesAcrossRow + col + 1);
}
Trying that out
'use strict';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 1;
const scene = new THREE.Scene();
scene.background = new THREE.Color('#444');
scene.add(camera);
const planeGeometry = new THREE.PlaneBufferGeometry(1, 1, 20, 20);
const material = new THREE.MeshBasicMaterial({color: 'blue'});
const plane = new THREE.Mesh(planeGeometry, material);
scene.add(plane);
const edgeGeometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
edgeGeometry.setAttribute('position', planeGeometry.getAttribute('position'));
edgeGeometry.setIndex([]);
const edgeMaterial = new THREE.MeshBasicMaterial({
color: 'yellow',
wireframe: true,
depthTest: false,
});
const edges = new THREE.Mesh(edgeGeometry, edgeMaterial);
scene.add(edges);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
class PickHelper {
constructor() {
this.raycaster = new THREE.Raycaster();
}
pick(normalizedPosition, scene, camera, time) {
// cast a ray through the frustum
this.raycaster.setFromCamera(normalizedPosition, camera);
// get the list of objects the ray intersected
const intersectedObjects = this.raycaster.intersectObjects(scene.children, [plane]);
if (intersectedObjects.length) {
// pick the first object. It's the closest one
const intersection = intersectedObjects[0];
const faceIndex = intersection.faceIndex;
const indexAttribute = planeGeometry.getIndex();
const indices = indexAttribute.array;
// it looks like this is the grid order for PlaneBufferGeometry
//
// b --c
// |\\1|
// |0\\|
// a-- d
const facesAcrossRow = planeGeometry.parameters.widthSegments * 2;
const col = faceIndex % facesAcrossRow
const row = faceIndex / facesAcrossRow | 0;
const neighboringFaceIndices = [];
// check left face
if (col > 0) {
neighboringFaceIndices.push(row * facesAcrossRow + col - 1);
}
// check right face
if (col < facesAcrossRow - 1) {
neighboringFaceIndices.push(row * facesAcrossRow + col + 1);
}
// check up. there can only be one up if we're in an odd triangle (b,c,d)
if (col % 2 && row < planeGeometry.parameters.heightSegments) {
// add the even neighbor in the next row
neighboringFaceIndices.push((row + 1) * facesAcrossRow + col - 1);
}
// check down. there can only be one down if we're in an even triangle (a,b,d)
if (col % 2 === 0 && row > 0) {
// add the odd neighbor in the previous row
neighboringFaceIndices.push((row - 1) * facesAcrossRow + col + 1);
}
const neighbors = [];
for (const faceIndex of neighboringFaceIndices) {
neighbors.push(...indices.slice(faceIndex * 3, faceIndex * 3 + 3));
}
const edgeIndices = edgeGeometry.getIndex();
edgeIndices.array = new Uint16Array(neighbors);
edgeIndices.count = neighbors.length;
edgeIndices.needsUpdate = true;
}
}
}
const pickPosition = {x: 0, y: 0};
const pickHelper = new PickHelper();
clearPickPosition();
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
pickHelper.pick(pickPosition, scene, camera, time);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
}
function setPickPosition(event) {
const pos = getCanvasRelativePosition(event);
pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
}
function clearPickPosition() {
// unlike the mouse which always has a position
// if the user stops touching the screen we want
// to stop picking. For now we just pick a value
// unlikely to pick something
pickPosition.x = -100000;
pickPosition.y = -100000;
}
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
window.addEventListener('touchstart', (event) => {
// prevent the window from scrolling
event.preventDefault();
setPickPosition(event.touches[0]);
}, {passive: false});
window.addEventListener('touchmove', (event) => {
setPickPosition(event.touches[0]);
});
window.addEventListener('touchend', clearPickPosition);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.js"></script>
<canvas id="c"></canvas>
For something more complex than a PlaneBufferGeometry
you could also pre-generate a map of faceIndexs to neighbors if the code at the top is too slow.
Is it real to fill all polygons? Codepen. As I get it ThreeGeoJSON can not fill polygons, outlines only. Also I've tried Earcut for triangulation.
drawThreeGeo(data, radius, 'sphere', {color: 'yellow' // I want to edit fill color of lands, not outline color})
I suggest you to use better map: countries.geojson
The solution consists of following steps, for each shape:
Put vertices inside of shape, so that when triangulated, it could bend around the globe,
Run https://github.com/mapbox/delaunator to build triangulated mesh,
Step 2 will create triangles outside the shape too, we need to remove them by looking into each triangle, and deciding if it belongs to shape or not,
Bend the triangulated mesh with convertCoordinates
You can test my jsfiddle: http://jsfiddle.net/mmalex/pg5a4132/
Warning: it is quite slow because of high level of detail of input.
The complete solution:
/* Draw GeoJSON
Iterates through the latitude and longitude values, converts the values to XYZ coordinates, and draws the geoJSON geometries.
*/
let TRIANGULATION_DENSITY = 5; // make it smaller for more dense mesh
function verts2array(coords) {
let flat = [];
for (let k = 0; k < coords.length; k++) {
flat.push(coords[k][0], coords[k][1]);
}
return flat;
}
function array2verts(arr) {
let coords = [];
for (let k = 0; k < arr.length; k += 2) {
coords.push([arr[k], arr[k + 1]]);
}
return coords;
}
function findBBox(points) {
let min = {
x: 1e99,
y: 1e99
};
let max = {
x: -1e99,
y: -1e99
};
for (var point_num = 0; point_num < points.length; point_num++) {
if (points[point_num][0] < min.x) {
min.x = points[point_num][0];
}
if (points[point_num][0] > max.x) {
max.x = points[point_num][0];
}
if (points[point_num][1] < min.y) {
min.y = points[point_num][1];
}
if (points[point_num][1] > max.y) {
max.y = points[point_num][1];
}
}
return {
min: min,
max: max
};
}
function isInside(point, vs) {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
var x = point[0],
y = point[1];
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0],
yi = vs[i][1];
var xj = vs[j][0],
yj = vs[j][1];
var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
function genInnerVerts(points) {
let res = [];
for (let k = 0; k < points.length; k++) {
res.push(points[k]);
}
let bbox = findBBox(points);
let step = TRIANGULATION_DENSITY;
let k = 0;
for (let x = bbox.min.x + step / 2; x < bbox.max.x; x += step) {
for (let y = bbox.min.y + step / 2; y < bbox.max.y; y += step) {
let newp = [x, y];
if (isInside(newp, points)) {
res.push(newp);
}
k++;
}
}
return res;
}
function removeOuterTriangles(delaunator, points) {
let newTriangles = [];
for (let k = 0; k < delaunator.triangles.length; k += 3) {
let t0 = delaunator.triangles[k];
let t1 = delaunator.triangles[k + 1];
let t2 = delaunator.triangles[k + 2];
let x0 = delaunator.coords[2 * t0];
let y0 = delaunator.coords[2 * t0 + 1];
let x1 = delaunator.coords[2 * t1];
let y1 = delaunator.coords[2 * t1 + 1];
let x2 = delaunator.coords[2 * t2];
let y2 = delaunator.coords[2 * t2 + 1];
let midx = (x0 + x1 + x2) / 3;
let midy = (y0 + y1 + y2) / 3;
let midp = [midx, midy];
if (isInside(midp, points)) {
newTriangles.push(t0, t1, t2);
}
}
delaunator.triangles = newTriangles;
}
var x_values = [];
var y_values = [];
var z_values = [];
var progressEl = $("#progress");
var clickableObjects = [];
var someColors = [0x909090, 0x808080, 0xa0a0a0, 0x929292, 0x858585, 0xa9a9a9];
function drawThreeGeo(json, radius, shape, options) {
var json_geom = createGeometryArray(json);
var convertCoordinates = getConversionFunctionName(shape);
for (var geom_num = 0; geom_num < json_geom.length; geom_num++) {
console.log("Processing " + geom_num + " of " + json_geom.length + " shapes");
// if (geom_num !== 17) continue;
// if (geom_num > 10) break;
if (json_geom[geom_num].type == 'Point') {
convertCoordinates(json_geom[geom_num].coordinates, radius);
drawParticle(y_values[0], z_values[0], x_values[0], options);
} else if (json_geom[geom_num].type == 'MultiPoint') {
for (let point_num = 0; point_num < json_geom[geom_num].coordinates.length; point_num++) {
convertCoordinates(json_geom[geom_num].coordinates[point_num], radius);
drawParticle(y_values[0], z_values[0], x_values[0], options);
}
} else if (json_geom[geom_num].type == 'LineString') {
for (let point_num = 0; point_num < json_geom[geom_num].coordinates.length; point_num++) {
convertCoordinates(json_geom[geom_num].coordinates[point_num], radius);
}
drawLine(y_values, z_values, x_values, options);
} else if (json_geom[geom_num].type == 'Polygon') {
let group = createGroup(geom_num);
let randomColor = someColors[Math.floor(someColors.length * Math.random())];
for (let segment_num = 0; segment_num < json_geom[geom_num].coordinates.length; segment_num++) {
let coords = json_geom[geom_num].coordinates[segment_num];
let refined = genInnerVerts(coords);
let flat = verts2array(refined);
let d = new Delaunator(flat);
removeOuterTriangles(d, coords);
let delaunayVerts = array2verts(d.coords);
for (let point_num = 0; point_num < delaunayVerts.length; point_num++) {
// convertCoordinates(refined[point_num], radius);
convertCoordinates(delaunayVerts[point_num], radius);
}
// drawLine(y_values, z_values, x_values, options);
drawMesh(group, y_values, z_values, x_values, d.triangles, randomColor);
}
} else if (json_geom[geom_num].type == 'MultiLineString') {
for (let segment_num = 0; segment_num < json_geom[geom_num].coordinates.length; segment_num++) {
let coords = json_geom[geom_num].coordinates[segment_num];
for (let point_num = 0; point_num < coords.length; point_num++) {
convertCoordinates(json_geom[geom_num].coordinates[segment_num][point_num], radius);
}
drawLine(y_values, z_values, x_values);
}
} else if (json_geom[geom_num].type == 'MultiPolygon') {
let group = createGroup(geom_num);
let randomColor = someColors[Math.floor(someColors.length * Math.random())];
for (let polygon_num = 0; polygon_num < json_geom[geom_num].coordinates.length; polygon_num++) {
for (let segment_num = 0; segment_num < json_geom[geom_num].coordinates[polygon_num].length; segment_num++) {
let coords = json_geom[geom_num].coordinates[polygon_num][segment_num];
let refined = genInnerVerts(coords);
let flat = verts2array(refined);
let d = new Delaunator(flat);
removeOuterTriangles(d, coords);
let delaunayVerts = array2verts(d.coords);
for (let point_num = 0; point_num < delaunayVerts.length; point_num++) {
// convertCoordinates(refined[point_num], radius);
convertCoordinates(delaunayVerts[point_num], radius);
}
// drawLine(y_values, z_values, x_values, options);
drawMesh(group, y_values, z_values, x_values, d.triangles, randomColor)
}
}
} else {
throw new Error('The geoJSON is not valid.');
}
}
progressEl.text("Complete!");
}
function createGeometryArray(json) {
var geometry_array = [];
if (json.type == 'Feature') {
geometry_array.push(json.geometry);
} else if (json.type == 'FeatureCollection') {
for (var feature_num = 0; feature_num < json.features.length; feature_num++) {
geometry_array.push(json.features[feature_num].geometry);
}
} else if (json.type == 'GeometryCollection') {
for (var geom_num = 0; geom_num < json.geometries.length; geom_num++) {
geometry_array.push(json.geometries[geom_num]);
}
} else {
throw new Error('The geoJSON is not valid.');
}
//alert(geometry_array.length);
return geometry_array;
}
function getConversionFunctionName(shape) {
var conversionFunctionName;
if (shape == 'sphere') {
conversionFunctionName = convertToSphereCoords;
} else if (shape == 'plane') {
conversionFunctionName = convertToPlaneCoords;
} else {
throw new Error('The shape that you specified is not valid.');
}
return conversionFunctionName;
}
function convertToSphereCoords(coordinates_array, sphere_radius) {
var lon = coordinates_array[0];
var lat = coordinates_array[1];
x_values.push(Math.cos(lat * Math.PI / 180) * Math.cos(lon * Math.PI / 180) * sphere_radius);
y_values.push(Math.cos(lat * Math.PI / 180) * Math.sin(lon * Math.PI / 180) * sphere_radius);
z_values.push(Math.sin(lat * Math.PI / 180) * sphere_radius);
}
function convertToPlaneCoords(coordinates_array, radius) {
var lon = coordinates_array[0];
var lat = coordinates_array[1];
var plane_offset = radius / 2;
z_values.push((lat / 180) * radius);
y_values.push((lon / 180) * radius);
}
function drawParticle(x, y, z, options) {
var particle_geom = new THREE.Geometry();
particle_geom.vertices.push(new THREE.Vector3(x, y, z));
var particle_material = new THREE.ParticleSystemMaterial(options);
var particle = new THREE.ParticleSystem(particle_geom, particle_material);
scene.add(particle);
clearArrays();
}
function drawLine(x_values, y_values, z_values, options) {
var line_geom = new THREE.Geometry();
createVertexForEachPoint(line_geom, x_values, y_values, z_values);
var line_material = new THREE.LineBasicMaterial(options);
var line = new THREE.Line(line_geom, line_material);
scene.add(line);
clearArrays();
}
function createGroup(idx) {
var group = new THREE.Group();
group.userData.userText = "_" + idx;
scene.add(group);
return group;
}
function drawMesh(group, x_values, y_values, z_values, triangles, color) {
var geometry = new THREE.Geometry();
for (let k = 0; k < x_values.length; k++) {
geometry.vertices.push(
new THREE.Vector3(x_values[k], y_values[k], z_values[k])
);
}
for (let k = 0; k < triangles.length; k += 3) {
geometry.faces.push(new THREE.Face3(triangles[k], triangles[k + 1], triangles[k + 2]));
}
geometry.computeVertexNormals()
var mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({
side: THREE.DoubleSide,
color: color,
wireframe: true
}));
clickableObjects.push(mesh);
group.add(mesh);
clearArrays();
}
function createVertexForEachPoint(object_geometry, values_axis1, values_axis2, values_axis3) {
for (var i = 0; i < values_axis1.length; i++) {
object_geometry.vertices.push(new THREE.Vector3(values_axis1[i],
values_axis2[i], values_axis3[i]));
}
}
function clearArrays() {
x_values.length = 0;
y_values.length = 0;
z_values.length = 0;
}
var scene = new THREE.Scene();
var raycaster = new THREE.Raycaster();
var camera = new THREE.PerspectiveCamera(32, window.innerWidth / window.innerHeight, 0.5, 1000);
var radius = 200;
camera.position.x = 140.7744005681177;
camera.position.y = 160.30950538100814;
camera.position.z = 131.8637122564268;
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
var light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
scene.add(light);
var light = new THREE.AmbientLight(0x505050); // soft white light
scene.add(light);
var geometry = new THREE.SphereGeometry(radius, 32, 32);
var material = new THREE.MeshPhongMaterial({
color: 0x1e90ff
});
var sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
var test_json = $.getJSON("https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson", function(data) {
drawThreeGeo(data, radius + 1, 'sphere', {
color: 'yellow'
})
});
var controls = new THREE.TrackballControls(camera);
controls.rotateSpeed *= 0.5;
controls.zoomSpeed *= 0.5;
controls.panSpeed *= 0.5;
controls.minDistance = 10;
controls.maxDistance = 5000;
function render() {
controls.update();
requestAnimationFrame(render);
renderer.setClearColor(0x1e90ff, 1);
renderer.render(scene, camera);
}
render()
function convert_lat_lng(lat, lng, radius) {
var phi = (90 - lat) * Math.PI / 180,
theta = (180 - lng) * Math.PI / 180,
position = new THREE.Vector3();
position.x = radius * Math.sin(phi) * Math.cos(theta);
position.y = radius * Math.cos(phi);
position.z = radius * Math.sin(phi) * Math.sin(theta);
return position;
}
// this will be 2D coordinates of the current mouse position, [0,0] is middle of the screen.
var mouse = new THREE.Vector2();
var hoveredObj; // this objects is hovered at the moment
// Following two functions will convert mouse coordinates
// from screen to three.js system (where [0,0] is in the middle of the screen)
function updateMouseCoords(event, coordsObj) {
coordsObj.x = ((event.clientX - renderer.domElement.offsetLeft + 0.5) / window.innerWidth) * 2 - 1;
coordsObj.y = -((event.clientY - renderer.domElement.offsetTop + 0.5) / window.innerHeight) * 2 + 1;
}
function onMouseMove(event) {
updateMouseCoords(event, mouse);
latestMouseProjection = undefined;
clickedObj = undefined;
raycaster.setFromCamera(mouse, camera); {
var intersects = raycaster.intersectObjects(clickableObjects);
let setGroupColor = function(group, colorHex) {
for (let i = 0; i < group.children.length; i++) {
if (!group.children[i].userData.color) {
group.children[i].userData.color = hoveredObj.parent.children[i].material.color.clone();
group.children[i].material.color.set(colorHex);
group.children[i].material.needsUpdate = true;
}
}
}
let resetGroupColor = function(group) {
// set all shapes of the group to initial color
for (let i = 0; i < group.children.length; i++) {
if (group.children[i].userData.color) {
group.children[i].material.color = group.children[i].userData.color;
delete group.children[i].userData.color;
group.children[i].material.needsUpdate = true;
}
}
}
if (intersects.length > 0) {
latestMouseProjection = intersects[0].point;
// reset colors for previously hovered group
if (hoveredObj) {
resetGroupColor(hoveredObj.parent);
}
hoveredObj = intersects[0].object;
if (!hoveredObj.parent) return;
// set colors for hovered group
setGroupColor(hoveredObj.parent, 0xff0000);
} else {
if (!hoveredObj || !hoveredObj.parent) return;
// nothing is hovered => just reset colors on the last group
resetGroupColor(hoveredObj.parent);
hoveredObj = undefined;
console.log("<deselected>");
}
}
}
window.addEventListener('mousemove', onMouseMove, false);
You'd need to split each country into a separate geometry, use a raycaster to find out which country the mouse is over, then change its material.color. You can see raycasting in action in this example with source code available on the bottom-right corner. The key lines in that example are:
function onDocumentMouseMove( event ) {
event.preventDefault();
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function render() {
// find intersections
raycaster.setFromCamera( mouse, camera );
var intersects = raycaster.intersectObjects( scene.children );
if ( intersects.length > 0 ) {
if ( INTERSECTED != intersects[ 0 ].object ) {
if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
INTERSECTED = intersects[ 0 ].object;
INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
INTERSECTED.material.emissive.setHex( 0xff0000 );
}
} else {
if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
INTERSECTED = null;
}
renderer.render( scene, camera );
}