THREEJS - Indexed BufferGeometry with 2 materials - three.js

I want to create ONE single buffer geometry that can hold many materials.
I have read that in order to achieve this in BufferGeometry, I need to use groups. So I created the following "floor" mesh:
var gg=new THREE.BufferGeometry(),vtx=[],fc=[[],[]],mm=[
new THREE.MeshLambertMaterial({ color:0xff0000 }),
new THREE.MeshLambertMaterial({ color:0x0000ff })
];
for(var y=0 ; y<11 ; y++)
for(var x=0 ; x<11 ; x++) {
vtx.push(x-5,0,y-5);
if(x&&y) {
var p=(vtx.length/3)-1;
fc[(x%2)^(y%2)].push(
p,p-11,p-1,
p-1,p-11,p-12
);
}
}
gg.addAttribute('position',new THREE.Float32BufferAttribute(vtx,3));
Array.prototype.push.apply(fc[0],fc[1]); gg.setIndex(fc[0]);
gg.computeVertexNormals();
gg.addGroup(0,100,0);
gg.addGroup(100,100,1);
scene.add(new THREE.Mesh(gg,mm));
THE ISSUE:
looking at the example in https://www.crazygao.com/vc/tst2.htm can see that the BLUE material looks weird.
Single material showup OK.
2 materials with group as above, in any case show the BLUE really strage.
Changing the 1st group to start=0, count=200 (for all triangles) and removing the 2nd group, will show MORE squares of RED (obviously) but still NOT in the way I would like it to show.
Changing the 1st group count to any value greater than 200 will cause a crash (obviously) of attempting to access vertex out of range...
Is anyone know clearly what shall I do?
I am using THREE.js v.101 and I prefer to not create special custom shader for that, or add another vertex buffer to duplicate those I already have, and I prefer to not create 2 meshes as this may get much more complicated with advanced models.

Check out this: https://jsfiddle.net/mmalex/zebos3va/
fix #1 - don't define group 0
fix #2 - 2nd parameter in .addGroup is buffer length, it must be multiple of 3 (100 was wrong)
var gg = new THREE.BufferGeometry(),
vtx = [],
fc = [[],[]],
mm = [
new THREE.MeshLambertMaterial({
color: 0xff0000
}),
new THREE.MeshLambertMaterial({
color: 0x0000ff
})
];
for (var y = 0; y < 11; y++)
for (var x = 0; x < 11; x++) {
vtx.push(x - 5, 0, y - 5);
if (x && y) {
var p = (vtx.length / 3) - 1;
fc[(x % 2) ^ (y % 2)].push(
p, p - 11, p - 1,
p - 1, p - 11, p - 12
);
}
}
gg.addAttribute('position', new THREE.Float32BufferAttribute(vtx, 3));
fc[0].push.apply(fc[1]);
gg.setIndex(fc[0]);
gg.computeVertexNormals();
// group 0 is everything, unless you define group 1
// fix #1 - don't define group 0
// fix #2 - 2nd parameter is buffer length, it must be multiple of 3 (100 was wrong)
gg.addGroup(0, 102, 1);
scene.add(new THREE.Mesh(gg, mm));

Related

Three.js: how to combine several indices & vector arrays to one

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 );
}

Three.js memory allocation & workflow question

Let’s say I want to make 100 objects - for example cars, like the one you see here:
This car is currently comprised of 5 meshes: one yellow Cube and four blue Spheres
What I’d like to know is what would be the most efficient/correct way to make 100 of these cars - or maybe 500 - in terms of memory management/ CPU performance, etc.
The way I’m currently going about doing this is as follows:
Make an empty THREE.Group called “newCarGroup” -
Create the yellow rectangular Mesh for the body of the car - called “carBodyMesh”
Create four blue Sphere Meshes for the Tires called “tire1Mesh”, “tire2Mesh”, “tire3Mesh”, and “tire4Mesh”
Add the Body and the four Tires to the “newCarGroup”
And finally, in a FOR loop, create/instantiate 100 “newCarGroup” objects, adding each one to the SCENE at a random position
The code is below.
It's working perfectly well right now, but I’d like to know if this is the “proper”/best way to do this?
Consider it’s possible I might end up needing 1,000 cars - or 5,000 cars. So will this scale properly?
Also, I need to add more objects to the car: like 4 windows - actually make that 6 windows, to also include the front and back windshields, then four headlights, etc.
So the final Car Object alone may end up being comprised of 20 meshes - or more.
Being that I’m kinda new to THREE.JS I wanna make sure I develop good habits and go about this sort of thing the right way.
Here’s my code:
function makeOneCar() {
var newCarGroup = new THREE.Group();
// 1. CAR-Body:
const bodyGeometry = new THREE.BoxGeometry(30, 10, 10);
const bodyMaterial = new THREE.MeshPhongMaterial({ color: "yellow" } );
const carBodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
// 2. TIRES:
const tireGeometry = new THREE.SphereGeometry(2, 16, 16);;
const tireMaterial = new THREE.MeshPhongMaterial( { color: "blue" } );
const tire1Mesh = new THREE.Mesh(tireGeometry, tireMaterial);
const tire2Mesh = new THREE.Mesh(tireGeometry, tireMaterial);
const tire3Mesh = new THREE.Mesh(tireGeometry, tireMaterial);
const tire4Mesh = new THREE.Mesh(tireGeometry, tireMaterial);
// TIRE 1 Position:
tire1Mesh.position.x = carBodyMesh.position.x - 11;
tire1Mesh.position.y = carBodyMesh.position.y - 4.15;
tire1Mesh.position.z = carBodyMesh.position.z + 4.5;
// TIRE 2 Position:
tire2Mesh.position.x = carBodyMesh.position.x + 11;
tire2Mesh.position.y = carBodyMesh.position.y - 4.15;
tire2Mesh.position.z = carBodyMesh.position.z + 4.5;
// TIRE 3 Position:
tire3Mesh.position.x = carBodyMesh.position.x - 11;
tire3Mesh.position.y = carBodyMesh.position.y - 4.15;
tire3Mesh.position.z = carBodyMesh.position.z - 4.5;
// TIRE 4 Position:
tire4Mesh.position.x = carBodyMesh.position.x + 11;
tire4Mesh.position.y = carBodyMesh.position.y - 4.15;
tire4Mesh.position.z = carBodyMesh.position.z - 4.5;
// Putting it all together:
newCarGroup.add(carBodyMesh);
newCarGroup.add(tire1Mesh);
newCarGroup.add(tire2Mesh);
newCarGroup.add(tire3Mesh);
newCarGroup.add(tire4Mesh);
// Setting (x, y, z) Coordinates - RANDOMLY
let randy = Math.floor(Math.random() * 10);
let newCarGroupX = randy % 2 == 0 ? Math.random() * 250 : Math.random() * -250;
let newCarGroupY = 0.0;
let newCarGroupZ = randy % 2 == 0 ? Math.random() * 250 : Math.random() * -250;
newCarGroup.position.set(newCarGroupX, newCarGroupY, newCarGroupZ)
scene.add(newCarGroup);
}
function makeCars() {
for(var carCount = 0; carCount < 100; carCount ++) {
makeOneCar();
}
}
I’d like to know if this is the “proper”/best way to do this?
This is subjective. You say the method works great for your current use-case, so for that use-case, it is fine.
So will this scale properly?
The simple answer is: No. The more complex answer is: ...not really.
You're re-using the geometry and materials, which is good. But every Mesh you create has meta information surrounding it, which adds to your overall memory footprint.
Also, every standard Mesh you add incurs what is known as a "draw call", which is the GPU drawing that particular shape. Instead, take a look at InstancedMesh. This allows the GPU to be given instructions on how to draw the shape throughout the scene once. Yes, rather than drawing each cube individually, the GPU can draw all the cubes at the same time, and they can even have different colors and transformations. There are limitations to this class, but it's a good starting point to understanding how instancing works.

ThreeJS - THREE.BufferGeometry.computeBoundingSphere() Gives Error: NaN Position Values

I am creating a simple THREE.PlaneBufferGeometry using Threejs. The surface is a geologic surface in the earth.
This surface has local gaps or 'holes' in it represented by NaN's. I have read another similar, but older, post where the suggestion was to fill the position Z component with 'undefined' rather than NaN. I tried that but get this error:
THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.
PlaneBufferGeometry {uuid: "8D8EFFBF-7F10-4ED5-956D-5AE1EAD4DD41", name: "", type: "PlaneBufferGeometry", index: Uint16BufferAttribute, attributes: Object, …}
Here is the TypeScript function that builds the surface:
AddSurfaces(result) {
let surfaces: Surface[] = result;
if (this.surfaceGroup == null) {
this.surfaceGroup = new THREE.Group();
this.globalGroup.add(this.surfaceGroup);
}
surfaces.forEach(surface => {
var material = new THREE.MeshPhongMaterial({ color: 'blue', side: THREE.DoubleSide });
let mesh: Mesh2D = surface.arealMesh;
let values: number[][] = surface.values;
let geometry: PlaneBufferGeometry = new THREE.PlaneBufferGeometry(mesh.width, mesh.height, mesh.nx - 1, mesh.ny - 1);
var positions = geometry.getAttribute('position');
let node: number = 0;
// Surfaces in Three JS are ordered from top left corner x going fastest left to right
// and then Y ('j') going from top to bottom. This is backwards in Y from how we do the
// modelling in the backend.
for (let j = mesh.ny - 1; j >= 0; j--) {
for (let i = 0; i < mesh.nx; i++) {
let value: number = values[i][j];
if(!isNaN(values[i][j])) {
positions.setZ(node, -values[i][j]);
}
else {
positions.setZ(node, undefined); /// This does not work? Any ideas?
}
node++;
}
}
geometry.computeVertexNormals();
var plane = new THREE.Mesh(geometry, material);
plane.receiveShadow = true;
plane.castShadow = true;
let xOrigin: number = mesh.xOrigin;
let yOrigin: number = mesh.yOrigin;
let cx: number = xOrigin + (mesh.width / 2.0);
let cy: number = yOrigin + (mesh.height / 2.0);
// translate point to origin
let tempX: number = xOrigin - cx;
let tempY: number = yOrigin - cy;
let azi: number = mesh.azimuth;
let aziRad = azi * Math.PI / 180.0;
// now apply rotation
let rotatedX: number = tempX * Math.cos(aziRad) - tempY * Math.sin(aziRad);
let rotatedY: number = tempX * Math.sin(aziRad) + tempY * Math.cos(aziRad);
cx += (tempX - rotatedX);
cy += (tempY - rotatedY);
plane.position.set(cx, cy, 0.0);
plane.rotateZ(aziRad);
this.surfaceGroup.add(plane);
});
this.UpdateCamera();
this.animate();
}
Thanks!
I have read another similar, but older, post where the suggestion was to fill the position Z component with 'undefined' rather than NaN.
Using undefined will fail in the same way like using NaN. BufferGeometry.computeBoundingSphere() computes the radius based on Vector3.distanceToSquared(). If you call this method with a vector that contains no valid numerical data, NaN will be returned.
Hence, you can't represent the gaps in a geometry with NaN or undefined position data. The better way is to generate a geometry which actually represents the geometry of your geologic surface. Using ShapeBufferGeometry might be a better candidate since shapes do support the concept of holes.
three.js r117
THREE.PlaneBufferGeometry:: parameters: {
width: number;
height: number;
widthSegments: number;
heightSegments: number;
};
widthSegments or heightSegments should be greater 1 ,if widthSegments < 1 ,widthSegments may be equal 0 or nan.
In my case, it was happening when I tried to create a beveled shape based on a single vector or a bunch of identical vectors - so there was only a single point. Filtering out such shapes solved the issue.

How to morphTarget of an .obj file (BufferGeometry)

I'm trying to morph the vertices of a loaded .obj file like in this example: https://threejs.org/docs/#api/materials/MeshDepthMaterial - when 'wireframe' and 'morphTargets' are activated in THREE.MeshDepthMaterial.
But I can't reach the desired effect. From the above example the geometry can be morphed via geometry.morphTargets.push( { name: 'target1', vertices: vertices } ); however it seems that morphTargets is not available for my loaded 3D object as it is a BufferGeometry.
Instead I tried to change independently each vertices point from myMesh.child.child.geometry.attributes.position.array[i], it kind of works (the vertices of my mesh are moving) but not as good as the above example.
Here is a Codepen of what I could do.
How can I reach the desired effect on my loaded .obj file?
Adding morph targets to THREE.BufferGeometry is a bit different than THREE.Geometry. Example:
// after loading the mesh:
var morphAttributes = mesh.geometry.morphAttributes;
morphAttributes.position = [];
mesh.material.morphTargets = true;
var position = mesh.geometry.attributes.position.clone();
for ( var j = 0, jl = position.count; j < jl; j ++ ) {
position.setXYZ(
j,
position.getX( j ) * 2 * Math.random(),
position.getY( j ) * 2 * Math.random(),
position.getZ( j ) * 2 * Math.random()
);
}
morphAttributes.position.push(position); // I forgot this earlier.
mesh.updateMorphTargets();
mesh.morphTargetInfluences[ 0 ] = 0;
// later, in your render() loop:
mesh.morphTargetInfluences[ 0 ] += 0.001;
three.js r90

Can a skybox be a single image, and if not why?

When I search for some skybox images (e.g. google images) I am getting hits showing single images in a sideways cross pattern. But all the three.js examples (for example) I've managed to find show loading 6 images.
It feels strange that I have to cut up a single image, and then have the extra load of 6 images instead of one image.
The documentation is a bit vague (i.e. as to whether 6 images is an option, or the only way to do it).
Here is a question that seems to be using a single image, but it is one row, and the answer uses a 2x3 grid; neither of them are the cross shape!
(BTW, bonus question: I tried working this out from the source code, but where is it? The THREE.CubeTextureLoader().load() code is a loop to load however many URLs it is given (NB. no checking that it is 6), then calls THREE.Texture, which seems very generic.)
Answer: yes, it can definitely be a single image. You have the proof in the stackoverflow question you provided.
Why the cross images exists: Sometimes (I have done this in OpenGl), you can specify coordinates in you images, for each one of the 6 faces of your cube. It doesn't look like the three.js library offers this functionnality.
What they offer is the .repeat and .offset attributes. This is what is being used in the single image jsfiddle.
for ( var i = 0; i < 6; i ++ ) {
t[i] = THREE.ImageUtils.loadTexture( imgData ); //2048x256
t[i].repeat.x = 1 / 8;
t[i].offset.x = i / 8;
// Could also use repeat.y and offset.y, which are probably used in the 2x3 image
....
You can experiment with the fiddle to see what happens if you modify those values. i.e.
t[i].repeat.x = 1 / 4;
t[i].offset.x = i / 4;
Good luck, hope this helped.
Bonus question edit : Also, in the THREE.CubeTextureLoader().load() code, it does in fact do an automatic update once 6 images have been loaded :
...
if ( loaded === 6 ) {
texture.needsUpdate = true;
if ( onLoad ) onLoad( texture );
}
FYI, correct mapping after tried it out:
x_offset = [3, 1, 2, 2, 2, 0];
y_offset = [1, 1, 2, 0, 1, 1];
for ( var i = 0; i < 6; i ++ ) {
t[i] = loader.load( imgData, render ); //2330 x 1740 // cubemap
t[i].repeat.x = 1 / 4;
t[i].offset.x = x_offset[i] / 4;
t[i].repeat.y = 1 / 3;
t[i].offset.y = y_offset[i] / 3;
//t[i].magFilter = THREE.NearestFilter;
t[i].minFilter = THREE.NearestFilter;
t[i].generateMipmaps = false;
materials.push( new THREE.MeshBasicMaterial( { map: t[i] } ) );
}

Resources