I am trying to visualize a grand strategy (EU4, CK3, HOI) like map in Three.js. I started creating meshes for every cell. the results are fine (screenshot 1 & 2).
Separate mesh approach - simple land / water differentiation :
Separate mesh approach - random cell color :
however, with a lot of cells, performance becomes an issue (I am getting 15fps with 10k cells).
In order to improve performance I would like to combine all these separate indices & vertex arrays into 2 big arrays, which will then be used to create a single mesh.
I am looping through all my cells to push their indices, vertices & colors into the big arrays like so:
addCellGeometryToMapGeometry(cell) {
let startIndex = this.mapVertices.length;
let cellIndices = cell.indices.length;
let cellVertices = cell.vertices.length;
let color = new THREE.Color( Math.random(), Math.random(), Math.random() );
for (let i = 0; i < cellIndices; i++) {
this.mapIndices.push(startIndex + cell.indices[i]);
}
for (let i = 0; i < cellVertices; i++) {
this.mapVertices.push(cell.vertices[i]);
this.mapColors.push (color);
}
}
I then generate the combined mesh:
generateMapMesh() {
let geometry = new THREE.BufferGeometry();
const material = new THREE.MeshPhongMaterial( {
side: THREE.DoubleSide,
flatShading: true,
vertexColors: true,
shininess: 0
} );
geometry.setIndex( this.mapIndices );
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( this.mapVertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( new Float32Array(this.mapColors.length), 3 ) );
for ( let i = 0; i < this.mapColors.length; i ++ ) {
geometry.attributes.color.setXYZ(i, this.mapColors[i].r, this.mapColors[i].g, this.mapColors[i].b);
}
return new THREE.Mesh( geometry, material );
}
Unfortunately the results are underwhelming:
While the data in the combined arrays look okay, only every third cell is rendered. In some cases the indices seem to get mixed up too.
Combined approach - random cell colors :
In other similar topics it is recommended to merge existing meshes. However, I figured that my approach should allow me to better understand what is actually happening & potentially save on performance as well.
Has my code obvious flaws that I cannot see?
Or am I generally on a wrong path, if so, how should it be done instead?
I actually found the issue in my code. wrong:
let startIndex = this.mapVertices.length;
The issue here is that the values in the indices array always reference a vertex (which consists of 3 consecutive array entries in the vertices array). correct:
let startIndex = this.mapVertices.length / 3;
Additionally I should only push one color per vertex instead of one per vertex array entry (= 1 per coordinate) but make sure that the arraylength of the geometry.color attribute stays at it is.
With these 2 changes, the result for the combined mesh looks exactly the same as when creating a separate mesh for every cell. The performance improvement is impressive.
separate meshes:
60 - 65 ms needed to render a frame
144 mb allocated memory
combined mesh:
0 - 1 ms needed to render a frame
58 mb allocated memory
Here are the fixed snippets:
addCellGeometryToMapGeometry(cell) {
let startIndex = this.mapVertices.length / 3;
let cellIndices = cell.indices.length;
let cellVertices = cell.vertices.length;
console.log('Vertex-- maplength: ' + startIndex + ' celllength: ' + cellVertices);
console.log('Indices -- maplength: ' + this.mapIndices.length + ' celllength: ' + cellIndices);
console.log({cell});
let color = new THREE.Color( Math.random(), Math.random(), Math.random() );
for (let i = 0; i < cellIndices; i++) {
this.mapIndices.push(startIndex + cell.indices[i]);
}
for (let i = 0; i < cellVertices; i++) {
this.mapVertices.push(cell.vertices[i]);
if (i % 3 === 0) { this.mapColors.push (color); }
}
}
generateMapMesh() {
let geometry = new THREE.BufferGeometry();
const material = new THREE.MeshPhongMaterial( {
side: THREE.DoubleSide,
flatShading: true,
vertexColors: true,
shininess: 0
} );
geometry.setIndex( this.mapIndices );
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( this.mapVertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( new Float32Array(this.mapVertices.length), 3 ) );
for ( let i = 0; i < this.mapColors.length; i ++ ) {
geometry.attributes.color.setXYZ(i, this.mapColors[i].r, this.mapColors[i].g, this.mapColors[i].b);
}
return new THREE.Mesh( geometry, material );
}
Related
I am working on an arcade style Everest Flight Simulator.
In my debugger where I am building this, I have a terrain and helicopter class which generate the BufferGeometry terrain mesh, the Groups for the helipad Geometries, and the group for the helicopter Camera and Geometry.
My issue is that currently I can't seem to get any collision to detect. I imagine it may not support BufferGeometries so that is an issue for me because I need the terrain to be a Buffer since it's far too expansive... as a standard geometry it causes a memory crash in the browser.
However, testing the helipad geometries alone it still does not trigger. They are in a group so I add the groups to a global window array and set the collision check to be recursive but to no avail.
Ultimately, I am open to other forms of collision detection and may need two types as I have to use buffer geometries. Any ideas on how to fix this or a better solution?
The Helicopter Object Itself
// Rect to Simulate Helicopter
const geometry = new THREE.BoxGeometry( 2, 1, 4 ),
material = new THREE.MeshBasicMaterial(),
rect = new THREE.Mesh( geometry, material );
rect.position.x = 0;
rect.position.y = terrain.returnCameraStartPosY();
rect.position.z = 0;
rect.rotation.order = "YXZ";
rect.name = "heli";
// Link Camera and Helicopter
const heliCam = new THREE.Group(),
player = new Helicopter(heliCam, "OH-58 Kiowa", 14000);
heliCam.add(camera);
heliCam.add(rect);
heliCam.position.set( 0, 2040, -2000 );
heliCam.name = "heliCam";
scene.add(heliCam);
Adding Objects to Global Collision Array
// Add Terrain
const terrain = new Terrain.ProceduralTerrain(),
terrainObj = terrain.returnTerrainObj(),
helipadEnd = new Terrain.Helipad( 0, 1200, -3600, "Finish", true ),
helipadStart = new Terrain.Helipad( 0, 2000, -2000, "Start", false ),
helipadObjStart = helipadStart.returnHelipadObj(),
helipadObjEnd = helipadEnd.returnHelipadObj();
window.collidableMeshList.push(terrainObj);
window.collidableMeshList.push(helipadObjStart);
window.collidableMeshList.push(helipadObjEnd);
Collision Detection Function Run Every Frame
collisionDetection(){
const playerOrigin = this.heli.children[1].clone(); // Get Box Mesh from Player Group
for (let i = playerOrigin.geometry.vertices.length - 1; i >= 0; i--) {
const localVertex = playerOrigin.geometry.vertices[i].clone(),
globalVertex = localVertex.applyMatrix4( playerOrigin.matrix ),
directionVector = globalVertex.sub( playerOrigin.position ),
ray = new THREE.Raycaster( playerOrigin, directionVector.clone().normalize() ),
collisionResults = ray.intersectObjects( window.collidableMeshList, true ); // Recursive Boolean for children
if ( collisionResults.length > 0 ){
this.landed = true;
console.log("Collision");
}
// if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() ){
// this.landed = true;
// console.log("Collision with vectorLength")
// }
}
}
It's hard to tell what's going on inside your custom classes, but it looks like you're using an Object3D as the first argument of the raycaster, instead of a Vector3 when you use this.heli.children[1].clone(). Why don't you try something like:
var raycaster = new THREE.Raycaster();
var origin = this.heli.children[1].position;
raycaster.set(origin, direction);
Also, are you sure you're using a BufferGeometry? Because when you access a vertex value like this: playerOrigin.geometry.vertices[i], it should give you an error. There is no vertices attribute in a BufferGeometry so I don't know how you're determining the direction vector.
How I can fill a loaded STL mesh ( like suzane NOT SIMPLE SHAPES LIKE CUBE etc) with random particles and animate it inside this geometry bounds with three.js ?
I see many examples but all of it for simple shapes with geometrical bounds like cube or sphere with limit by coordinates around center
https://threejs.org/examples/?q=points#webgl_custom_attributes_points3
TNX
A concept, using a ray, that counts intersections of the ray with faces of a mesh, and if the number is odd, it means that the point is inside of the mesh:
Codepen
function fillWithPoints(geometry, count) {
var ray = new THREE.Ray()
var size = new THREE.Vector3();
geometry.computeBoundingBox();
let bbox = geometry.boundingBox;
let points = [];
var dir = new THREE.Vector3(1, 1, 1).normalize();
for (let i = 0; i < count; i++) {
let p = setRandomVector(bbox.min, bbox.max);
points.push(p);
}
function setRandomVector(min, max){
let v = new THREE.Vector3(
THREE.Math.randFloat(min.x, max.x),
THREE.Math.randFloat(min.y, max.y),
THREE.Math.randFloat(min.z, max.z)
);
if (!isInside(v)){return setRandomVector(min, max);}
return v;
}
function isInside(v){
ray.set(v, dir);
let counter = 0;
let pos = geometry.attributes.position;
let faces = pos.count / 3;
let vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
for(let i = 0; i < faces; i++){
vA.fromBufferAttribute(pos, i * 3 + 0);
vB.fromBufferAttribute(pos, i * 3 + 1);
vC.fromBufferAttribute(pos, i * 3 + 2);
if (ray.intersectTriangle(vA, vB, vC)) counter++;
}
return counter % 2 == 1;
}
return new THREE.BufferGeometry().setFromPoints(points);
}
The concepts from the previous answer is very good, but it has some performance limitations:
the whole geometry is tested with every ray
the recursion on points outside can lead to stack overflow
Moreover, it's incompatible with indexed geometry.
It can be improved by creating a spatial hashmap storing the geometry triangles and limiting the intersection test to only some part of the mesh.
Demonstration
I'm having this error while adding my own geometry's attribute.
I've already read this WebGL GL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to access out of range vertices in attribute 1 , and I understand what is the problem, but I can't figure out why.
I'm building a BufferGeometry, a tree, starting from 1000 object. 300 objects are using a LeafGeometry, 700 objects are using a BoxGeometry.
I want to fulfill a buffer, containing a value that tells if a vertex belongs to the trunk or to the foliage. What I'm doing is the follow:
1) First I calculate the dimension of the buffer (and here I think I'm doing it wrong) calling : getTotNumVertices(LeafGeometry.new(options), BoxGeometry.new(options), 1000, 3000)
function getTotNumVertices(foliage_geometry, trunk_geometry, tot_objects, foliage_start_at){
let n_vertices_in_leaf = foliage_geometry.vertices.length * 3;
let n_vertices_in_trunk = trunk_geometry.vertices.length * 3;
let n_vertices_in_leafs = foliage_start_at * n_vertices_in_leaf;
let n_vertices_in_stam = (tot_objects - foliage_start_at) * n_vertices_in_trunk;
return{
tot_vertices: (n_vertices_in_stam + n_vertices_in_leafs),
n_vertices_leaf: n_vertices_in_leaf,
n_vertices_trunk: n_vertices_in_trunk
};
}
2)Once I've got the total number of vertex, I create the buffer
function createBuffers(n_vert){
// I'm returnin an array becuase in my real code I'm returning
// more than one buffer
return {
isLeafBuffer: new Float32Array(n_vert)
};
}
3) Then I build my BufferGeometry, merging together the 1000 objects:
let hash_vertex_info = getTotNumVertices(leafGeom, geometries["box"], 1000, 300);
let buffers = createBuffers(hash_vertex_info.tot_vertices);
let geometry = new THREE.Geometry();
let objs = buildTheTree(1000, 300);
for (let i = 0; i < objs.length; i++){
// here code that fullfills the buffers
let mesh = objs[i];
mesh.updateMatrix();
geometry.merge(mesh.geometry, mesh.matrix);
}
let bufGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
console.log(bufGeometry.attributes.position.count);
console.log(hash_vertex_info.tot_vertices);
And here the problem, the value of is bufGeometry.attributes.position.count is 623616, the value of hash_vertex_info.tot_vertices is 308940.
When drawing, WebGL try do access a value bigger than 308940 and then the error.
What am I doing wrong?
///////////EDIT AFTER A WHILE
Basically, I'm having facing the same problem explained in this question
Does converting a Geometry to a BufferGeometry in Three.js increase the number of vertices?
I need to calculate the total number of vertices in order to create a buffer that will contain values for my shader. This is my code, the number of vertices it is still different between the merged geometry and the buffer geometry obtained from it.
let tot_objects = 100;
let material = new THREE.MeshStandardMaterial( {color: 0x00ff00} );
let geometry = new THREE.BoxGeometry(5, 5, 5, 4, 4, 4);
let objs = populateGroup(geometry, material, tot_objects);
//let's merge all the objects in one geometry
let mergedGeometry = new THREE.Geometry();
for (let i = 0; i < objs.length; i++){
let mesh = objs[i];
mesh.updateMatrix();
mergedGeometry.merge(mesh.geometry, mesh.matrix);
}
let bufGeometry = new THREE.BufferGeometry().fromGeometry(mergedGeometry);
let totVerticesMergedGeometry = (mergedGeometry.vertices.length ) + (mergedGeometry.faces.length * 3);
console.log(bufGeometry.attributes.position.count); // 57600
console.log(totVerticesMergedGeometry); // 67400 !!!
scene.add(new THREE.Mesh(bufGeometry, material));
function populateGroup(selected_geometry, selected_material, tot_objects) {
let objects = [];
for (var i = 0; i< tot_objects; i++) {
let coord = {x:i, y:i, z:i};
let object = new THREE.Mesh(selected_geometry, selected_material);
object.position.set(coord.x, coord.y, coord.z);
object.rotateY( (90 + 40 + i * 100/tot_objects) * -Math.PI/180.0 );
objects.push(object);
}
return objects;
}
The number of totVerticesMergedGeometry and bufGeometry.attributes.position.count should be the same, but is is still different.
Is my way of counting vertices wrong? actually it is the same used here https://github.com/mrdoob/three.js/blob/master/src/core/DirectGeometry.js#L166, meaning (geometry.vertices.length) + (geometry.faces.length * 3).
What I was doing wrong was the way to calculate the number of vertices.
The number of vertices used for the buffer is calculate with MyObjectGeometry.faces.lenght * 3 * NumberOfObjectThatWillBeMerged
A more detailed answer is here Why the number of vertices in a merged Geometry differs from the number of vertices in the BufferedGeometry obtained from it?
I had this error, because I was calling the constructor with the values instead of an array of values:
- var colors = new Float32Array(
+ var colors = new Float32Array( [
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 1.0,
0.0, 1.0, 1.0,
1.0, 1.0, 1.0,
- );
+ ] );
I'm generating a random plane that animates movement in the vertices to give a crystalline effect. When I use regular PlaneGeometry, shading is not a problem: http://codepen.io/shshaw/pen/GJppEX
However, I tried to switch to PlaneBufferGeometry to see if I could get better performance, but the shading disappeared.
http://codepen.io/shshaw/pen/oXjyJL?editors=001
var planeGeometry = new THREE.PlaneBufferGeometry(opts.planeSize, opts.planeSize, opts.planeDefinition, opts.planeDefinition),
planeMaterial = new THREE.MeshLambertMaterial({
color: 0x555555,
emissive: 0xdddddd,
shading: THREE.NoShading
}),
plane = new THREE.Mesh(planeGeometry, planeMaterial),
defaultVertices = planeGeometry.attributes.position.clone().array;
function randomVertices() {
var vertices = planeGeometry.attributes.position.clone().array;
for (var i = 0; i <= vertices.length; i += 3) {
// x
vertices[i] = defaultVertices[i] + (rand(-opts.variance.x, opts.variance.x));
// y
vertices[i + 1] = defaultVertices[i + 1] + (rand(-opts.variance.y, opts.variance.y));
// z
vertices[i + 2] = rand(-opts.variance.z, -opts.variance.z);
}
return vertices;
}
plane.geometry.attributes.position.array = randomVertices();
As I saw suggested in this answer to 'Shading on a plane', I tried:
plane.geometry.computeVertexNormals();
On render, I have tried all of the following attributes for the geometry to make sure it's updating the normals & vertices, like I've done on the working example with PlaneGeometry:
plane.geometry.verticesNeedUpdate = true;
plane.geometry.normalsNeedUpdate = true;
plane.geometry.computeVertexNormals();
plane.geometry.computeFaceNormals();
plane.geometry.normalizeNormals();
What has happened to the shading? Can I bring it back on a PlaneBufferGeometry mesh, or do I need to stick with PlaneGeometry?
Thanks!
I have a 3D scene in three.js in which I need to get an array of objects that are within X range of a source object. At the moment, the example I'm using is utilizing raycasting inside of a for loop that iterates an array of "collidable objects" that exist in the scene. I feel like there must be a better way to handle this because this approach is exponentially more complex if every object in the array has to raycast from itself to every other object in the array. This has massive performance impacts as the array of collidable objects grows.
//hold collidable objects
var collidableObjects = [];
var scene = new THREE.Scene();
var cubeGeo = new THREE.CubeGeometry( 10 , 10 , 10 );
var materialA = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
var materialB = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
var cubeA = new THREE.Mesh( cubeGeo , materialA );
collidableObjects.push( cubeA );
scene.add( cubeA );
//Change this variable to a larger number to see the processing time explode
var range = 100;
for( var x = 0 ; x < range ; x += 20 ) {
for( var z = 0; z < range ; z += 20 ) {
if( x === 0 && z === 0 ) continue;
var cube = new THREE.Mesh( cubeGeo , materialB );
scene.add( cube );
cube.position.x = x;
cube.position.z = z;
collidableObjects.push( cube );
var cube = cube.clone();
scene.add( cube );
cube.position.x = x * -1;
cube.position.z = z;
collidableObjects.push( cube );
var cube = cube.clone();
scene.add( cube );
cube.position.x = x;
cube.position.z = z * -1;
collidableObjects.push( cube );
var cube = cube.clone();
scene.add( cube );
cube.position.x = x * -1;
cube.position.z = z * -1;
collidableObjects.push( cube );
}
}
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
camera.position.y = 200;
camera.lookAt( scene.position );
function render() {
//requestAnimationFrame(render);
renderer.render(scene, camera);
console.log( getObjectsWithinRange( cubeA , 30 ) );
}
function getObjectsWithinRange( source , range ) {
var startTime = new Date().getTime();
var inRange = [];
for( var i = 0; i < collidableObjects.length ; ++i ) {
var ray = new THREE.Raycaster( source.position , collidableObjects[i].position , 0 , range );
if( ( obj = ray.intersectObject( collidableObjects[i] ) ) && obj.length ) {
inRange.push( obj[0] );
}
}
var endTime = new Date().getTime();
console.log( 'Processing Time: ' , endTime - startTime );
return inRange;
}
render();
You can see the JSfiddle of this here.
If you change the indicated variable to a larger number say 200, then you'll see the processing time start to get out of control. I feel like there has to be a simpler way to reduce down the array of doing this so I looked at the documentation for the Raycaster of three.js and I noticed that both the near and far attributes say "This value indicates which objects can be discarded based on the distance." so I presume there's some internal function that is used to refine the results down based on distance before casting all the rays.
I did a little digging on this and came up with a single function inside of Ray.js.
distanceToPoint: function () {
var v1 = new THREE.Vector3();
return function ( point ) {
var directionDistance = v1.subVectors( point, this.origin ).dot( this.direction );
// point behind the ray
if ( directionDistance < 0 ) {
return this.origin.distanceTo( point );
}
v1.copy( this.direction ).multiplyScalar( directionDistance ).add( this.origin );
return v1.distanceTo( point );
};
}(),
I guess what I'm looking for is a better way to get all of the objects in the scene that are within X radius of a source object. I don't even need to use the Raycasting because I'm not interested in mesh collision, rather just a list of the objects within X radius of the source object. I don't even need to recurse into the children of those objects because of the way the scene is set up. So I feel like there must be some internal function or something that simply uses the THREE.Vector3 objects and math to refine them by distance. That has to be a lot cheaper math to run than Raycasting in this case. If there's already a function that handles this somewhere in three.js, I don't want to recreate one from scratch. I also realize this may be a very long-winded question for what could very well be a single line answer, but I wanted to make sure I have all the details and whatnot here in case someone else looking to do this searches for it later.
Collision checking is a more general problem and I think you'll have more success if you think about it in a context outside of Three.js. There are a number of methods for managing large numbers of objects that need to check for collision with each other. Here are a few optimizations that might be relevant to you here:
The first optimization is for each object to have a boolean property indicating whether it moved since the last physics update. If both objects you're comparing haven't moved, you don't need to recalculate collision. This is mostly relevant if you have a large number of objects in a steady state (like crates you can push around). There are a number of other optimizations you can build on top of this; for example, often if two objects haven't moved, they won't be colliding, because if they were colliding they would be recoiling (moving apart).
The second optimization is that you usually only need to check collision within a certain distance. For example, if you know that all of your objects are smaller than 100 units, then you can just check whether (x1-x2)^2 + (y1-y2)^2 + (z1-z2)^2 > 100^2. If the check is true (indicating the distance between the two objects is large) then you don't need to calculate detailed collisions. In fact this is more or less the near/far optimization that Raycaster provides for you, but you are not making use of it in your code, since you are always calling the intersectObject method.
The third optimization is that you are allocating a bunch of new Raycaster and related objects in every physics update. Instead, you can keep a pool of Raycasters (or even a single Raycaster) and just update their properties. This will avoid a lot of garbage collecting.
Finally, the most common generalized approach to dealing with a large number of collideable objects is called spatial partitioning. The idea is basically that you divide your world into a given number of spaces and keep track of which space objects are in. Then, when you need to calculate collision, you only need to check other objects that are in the same space. The most common approach for doing this is to use an Octree (an 8-ary tree). As WestLangley mentioned, Three.js has an Octree implementation starting in r59, along with an example (source). Here is a reasonable introduction to the concept of spatial partitioning using 2D examples.
Outside of these optimizations, if you need to do anything particularly complicated, you may want to consider using an external physics library, which will manage optimizations like these for you. The most popular ones for use with Three.js at the moment are Physijs, Cannon.js, and Ammo.js.