THREE.Geometry will be removed from core with r125

Hmm… yikes. Ok thank you.

Did you have any particular concern with that method? I think there could be better APIs for this (e.g. that avoid an extra pass on the vertices) but I suspect it is still more efficient than using the old method from Geometry.

IMO, the method mergeMesh() was problematic since its name implies that you can use the method only with an instance of THREE.Mesh which is not true. Geometry can also represent points and lines. Internally, it does this:

this.merge( mesh.geometry, mesh.matrix );

which seems not correct either since it should use the world matrix.

Here’s the idea I’d been thinking about: BufferGeometry.merge or BufferGeometryUtils.mergeBufferGeometries: Add support for transformation matrix · Issue #18918 · mrdoob/three.js · GitHub it’s probably independent of these other methods that do a simple merge, but would be a nice way to create (and update) a draw batch representing multiple objects.

1 Like

Yea I’ve used that method for a while no issues. I will work to redo it using the tools available for buffer geometries. I have one other use case dependent on Geometry involving a custom function that West Langley helped me with a while back but I’ll save that for later.

A little bummed that the legacy Geometry is for module use only as not everyone wants to use that method. On the other hand I’ll be glad to get rid of Geometry all together, I’ve been weeding it’s use cases out in my app for years.

1 Like

The announcement that Geometry is now being taken out of the core reminded me that there is still an outstanding issue.

Why is the Geometry faster than BufferGeometry? - #10 by hofk

I have brought the old test example to r124 and simplified it even more and structured it better. In addition comments in the source code, which clarify the few absolutely necessary differences.

See SpeedTest_r124

In the meantime I had another performance problem.

Jerky camera movement with many objects

The solution was:
“The new approach is to do completely without groups and instead create a separate geometry and mesh for each material used. This only makes sense if the parts are immobile after creation.”

Thereupon the selection in the test example.
Material Index,
multiMaterial array or single material

It can be seen that here, too, the many groups obviously influence the performance.

 // ***** write groups for multi material *****

for ( let f = 0, p = 0; f < faceCount; f ++, p += 3 ) {
	
	g.addGroup( p, 3, 0 );
	
}

Since I am not able to trace the transformation from Geometry to the actual rendering via the GPU, I can only guess.

Is the concrete way from Geometry via the intermediate BufferGeometry for (material)groups more efficient than the way from the self-defined Buffergeometry?

// set material index:   Geometry - faces /  BufferGeometry - groups 
if ( multimat )	{

	if ( g.isGeometry ) {
		
		g.faces[ fIdx ].materialIndex = materialSegment;
		g.faces[ fIdx + 1 ].materialIndex = materialSegment;
		
	}
	
	if ( g.isBufferGeometry ) {
		
		g.groups[ fIdx ].materialIndex = materialSegment;
		g.groups[ fIdx + 1 ].materialIndex = materialSegment;
		
	}
}

How do I get the same performance with BufferGeometry ?

For me this is just a test out of curiosity, but may be relevant for migration of some application.

I myself have been working only with BufferGeometry for a long time now.


source code
(// for BufferGeometry: docs: .groupsNeedUpdate ?)

<!DOCTYPE html>
<!--  https://discourse.threejs.org/t/three-geometry-will-be-removed-from-core-with-r125/22401/25 -->
<head>
	<title>  SpeedTest_r124  </title>
	<meta charset="utf-8" />
<style>
	body{
	overflow: hidden;
	margin: 0;
	}  
 </style>
</head>
<body> 

	_______ ..... segments 
	<input type="text" size="5" id="hs" value="100" >  x  
 	<input type="text" size="5" id="rs" value="100" > 
	 .......
	<input type="checkbox" name="color" id="matindex" checked="checked" > Mat.index
	</br>_______ ..... 	
	<input type="radio" name="geom" id="Geometry" checked="checked" > Geometry
	<input type="radio" name="geom" id="BufferGeometry" > ind. BufferGeo. 
	|
	<input type="radio" name="mat" id="multimat" checked="checked" > multi Mat. [ ]
	<input type="radio" name="mat" id="singlemat"  > single Mat. |
	</br>.................... 
	<button type="button" id="show"> >>>>> show new mesh <<<<< </button>
	
	
</body>

	<script src="three.min.124.js"></script>
	<script src="OrbitControls.124.js"></script>
	<script src="stats.min.124.js"></script>
	
<script>

'use strict'

// @author hofk

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 200000 );
camera.position.set( 400, 600, 1000 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0xeeeeee, 1 );
const container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement ); 

const controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableZoom = true;
const clock  = new THREE.Clock( true );
let t; // time
let	g; // Geometry or BufferGeometry
let mesh;
let hs; // height segment count ( y direction )
let rs; // radial segment count ( here in test:  x direction )
let hss; // hs + 1
let rss; // rs + 1
let vertexCount;
let vIdx;	// vertex Index
let idxCount;
let faceCount;
let fIdx;	// face index
let j0, j1; // j index 
let a, b1, c1, c2; // vertex indices,  b2 equals c1
let ni, nj; // relative counter variable
let posIdx; // position Index
let x, y, z; // position coordinates
let materialSegment;
let matindex = null;
let multimat = null;;

let showGeo = false;

// materials
 
const side = THREE.DoubleSide;

// material or multi materials

const material = new THREE.MeshBasicMaterial( { color: 0x880088, side: side, wireframe: false } );

/*
const materials = [

	new THREE.MeshBasicMaterial( { color: 0x228822, side: side, wireframe: false } ),
	new THREE.MeshBasicMaterial( { color: 0x000000, side: side, wireframe: true } )	
	
];
*/
 
const materials = [
																	// material index 
    new THREE.MeshBasicMaterial( { color: 0xc4a013, transparent: true, opacity: 0.3, side: side } ), //  0 transparent
	new THREE.MeshBasicMaterial( { color: 0xff0000, side: side } ),	//  1 red
	new THREE.MeshBasicMaterial( { color: 0x00ff00, side: side } ),	//  2 green
	new THREE.MeshBasicMaterial( { color: 0x0000ff, side: side } ),	//  3 blue
	new THREE.MeshBasicMaterial( { color: 0xffff00, side: side } ),	//  4 yellow
	new THREE.MeshBasicMaterial( { color: 0xff00ff, side: side } ),	//  5 mgenta
	new THREE.MeshBasicMaterial( { color: 0x00ffff, side: side } ),	//  6 cyan	
	new THREE.MeshBasicMaterial( { color: 0x7755ff, side: side } ),	//  7 color
	new THREE.MeshBasicMaterial( { color: 0x000000, side: side, wireframe: true } )	//  8 grey
	
];
 
const stats = new Stats();
container.appendChild( stats.dom );

document.getElementById( "show" ).onclick = showNewMesh;

animate();

// ......................

function showNewMesh() {

	if ( mesh ) {
	
		scene.remove( mesh );
		g.dispose();
		showGeo = false;
		
		matindex = null;
		multimat = null;
				
	}
	
	hs = Math.floor( document.getElementById( "hs" ).value );
	rs = Math.floor( document.getElementById( "rs" ).value );
	
	hss = hs + 1;
	rss = rs + 1;
	
	vertexCount = hss * rss;
	faceCount =  hs * rs * 2;
	
	matindex = document.getElementById( "matindex" ).checked;
	multimat = document.getElementById( "multimat" ).checked;
	
	//  ....................... Geometry or BufferGeometry ...................................
	
	if ( document.getElementById( "Geometry" ).checked ) g = new THREE.Geometry();
	if ( document.getElementById( "BufferGeometry" ).checked ) g = new THREE.BufferGeometry();
		
	// mesh
	
	if ( multimat ) {
		
		mesh = new THREE.Mesh( g, materials ); // multi materials
		
	} else {
	
		mesh = new THREE.Mesh( g, material ); // single material
			
	}		

	scene.add( mesh );
	
	// configure Geometry or BufferGeometry
	
	if ( g.isGeometry ) {
		
		for ( let i = 0; i < vertexCount; i ++ ) { 
			
			g.vertices.push( new THREE.Vector3( 0, 0, 0 ) ); 
		
		}
		
	}	
		
	if ( g.isBufferGeometry ) {
	
		idxCount = 0;
		
		g.faceIndices = new Uint32Array( faceCount * 3 );
		g.vertices = new Float32Array( vertexCount * 3 );  
				
		g.setIndex( new THREE.BufferAttribute( g.faceIndices, 1 ) );
		//g.addAttribute( 'position', new THREE.BufferAttribute( g.vertices, 3 ).setDynamic( true ) ); // older version
		g.setAttribute( 'position', new THREE.BufferAttribute( g.vertices, 3 ).setUsage(THREE.DynamicDrawUsage ) );
		
		 // ***** write groups for multi material *****
		
		for ( let f = 0, p = 0; f < faceCount; f ++, p += 3 ) {
			
			g.addGroup( p, 3, 0 );
			
		}
 		
	}
	
	// faces Geometry or BufferGeometry	
	
	for ( let j = 0; j < rs; j ++ ) {
	
		j0 = hss * j; 
		j1 = hss * ( j + 1 );
		
		for ( let i = 0; i < hs; i ++ ) {
			
			// 2 faces / segment,  3 vertex indices
			a =  j0 + i;
			b1 = j1 + i;			// right-bottom
			c1 = j1 + 1 + i;
			// b2 = j1 + 1 + i; =c1// left-top 
			c2 = j0 + 1 + i;
			
			if ( g.isGeometry ) {
				
				g.faces.push( new THREE.Face3( a, b1, c1 ) ); // right-bottom
				g.faces.push( new THREE.Face3( a, c1, c2 ) ); // left-top
				
			}
			
			if ( g.isBufferGeometry ) {
			
				g.faceIndices[ idxCount     ] = a; // right-bottom
				g.faceIndices[ idxCount + 1 ] = b1;
				g.faceIndices[ idxCount + 2 ] = c1; 
				
				g.faceIndices[ idxCount + 3 ] = a; // left-top
				g.faceIndices[ idxCount + 4 ] = c1,
				g.faceIndices[ idxCount + 5 ] = c2; 
				
				idxCount += 6;
				
			}
						
		}
		
	}
	
	showGeo = true;	 // start animation
	
}

function move( t ) {
	
	for ( let j = 0; j < rss; j ++ ) {
		
		nj = j / rs;
		
		y = 400 * nj;							// calculate y
		
		for ( let i = 0; i < hss; i ++ ) {
			
			ni   = i / hs;
			
			x = 400 * ni;						// calculate x
			
			z =  400 * Math.sin( t + ni + nj );	// calculate z
			
			vIdx = hss * j + i;
			
			// set vertices Geometry or BufferGeometry
				
			if ( g.isGeometry ) g.vertices[ vIdx ].set( x, y, z );
			
			if ( g.isBufferGeometry ) {
			
				posIdx = vIdx * 3;
				
				g.vertices[ posIdx ]  = x;		
				g.vertices[ posIdx + 1 ]  = y;
				g.vertices[ posIdx + 2 ]  = z;
				
			}	
			
		}
		
	}
	   
	if ( matindex ) {
		
		for ( let j = 0; j < rs ; j ++ ) {
			
			for ( let i = 0; i < hs; i ++ ) {
				
				materialSegment = Math.floor( materials.length * ( 1 + Math.cos( 0.2 * t + i * i + 2 * j ) ) / 2 ); // calculate material
				
				fIdx = 2 * hs * j + 2 * i;
				
				// set material index:   Geometry - faces /  BufferGeometry - groups 
				if ( multimat )	{
				
					if ( g.isGeometry ) {
						
						g.faces[ fIdx ].materialIndex = materialSegment;
						g.faces[ fIdx + 1 ].materialIndex = materialSegment;
						
					}
					
					if ( g.isBufferGeometry ) {
						
						g.groups[ fIdx ].materialIndex = materialSegment;
						g.groups[ fIdx + 1 ].materialIndex = materialSegment;
						
					}
				}
			}			
		}		
	}		

	// dynamic update:  Geometry - vertices  /  BufferGeometry - attributes.position
	
	if ( g.isGeometry ) g.verticesNeedUpdate  = true;	
	if ( g.isBufferGeometry ) g.attributes.position.needsUpdate = true;
	
	// for Geometry:		docs: .groupsNeedUpdate : Boolean | Set to true if a face3 materialIndex has been updated.
	// for BufferGeometry:	docs: ?
	g.groupsNeedUpdate = true;
	
}

function animate() {
	
	requestAnimationFrame( animate );
	t = clock.getElapsedTime();
	
	if ( showGeo ) move( t );
	 
	renderer.render( scene, camera );
	controls.update();
	
	stats.update();
	
}	
</script>
</html>

I’ve never written an answer to the original topic but the performance difference happens because of different number of draw calls in your demo.

Geometry: approx 8600
BufferGeometry: 20000

I don’t know how you generate your geometry data but I recommend to group all buffer geometry groups which share the same material index.

1 Like

There is also a related PR at GitHub which was unfortunately never merged. I have actually approved it so we have something to start with.

This PR could be revisited.

I still think a helper method that optimizes groups should work with both indexed and non-indexed geometries (and not just add an index if not present). Besides, the method can assume that all vertices are assigned to a group.

3 Likes

The groups are actually the problem.

I created a separate group for each triangle, because I wanted to dynamically assign a variable material to all triangles individually.

But since rectangles are often needed, I always assigned the same material index to two triangles in a row. But each triangle still has its own group.

It is interesting now, however, that when converting geometry to BufferGeometry this is obviously taken into account somehow. Because also there I have assigned the material index to the two faces individually.

g.faces[ fIdx ].materialIndex = materialSegment;
g.faces[ fIdx + 1 ].materialIndex = materialSegment;

If you put the two triangles of BufferGeometry into a material group, you get the same performance as with Geometry.

replaced:  */
for ( let f = 0, p = 0; f < faceCount / 2; f ++, p += 6 ) { 
	
	g.addGroup( p, 6, 0 ); // group for two faces >> one square
			
}

log.innerHTML = log.innerHTML + g.groups.length;

replaced:  */
g.groups[ fIdx / 2 ].materialIndex = materialSegment; // one group for two faces (as defined above)

See SpeedTest_r124_1 _1

So much drama over a non-event. I for one, welcome our BufferGeometry overlords.

In all seriousness. If you think you have a use-case where Geometry is better than BufferGeometry in a significant way - you don’t. You may have a use-case, but it’s not in the scope of three.js. If you’re very upset by it - fork the library, make a copy of the deprecated class or do one of a thousand other things to not be affected by this. Your discomfort is not a good argument to keep three.js from evolving and ditching support for 2 distinct geometry formats.

This change will make three.js easier to maintain and to develop in the future as well as cutting down the code base in a significant way. It also makes three.js less ambiguous and easier to learn (there’s less to learn now).

6 Likes

From a professional user’s perspective, the removal of Geometry is a win. :slightly_smiling_face:

But for beginners who first want to see how three.js works and what can be achieved quickly, it looks different. :roll_eyes: :thinking:

This also becomes clear, for example, in the above test example, if you compare the necessary code.

It needs well explained beginner examples with BufferGeometry as compensation.

The beginners find with the search still longer time mainly old geometry examples. :slightly_frowning_face:

4 Likes

I understand your point, and it is valid in the void. However, nothing is static, if “old” tutorials are outdated, they are just that - outdated. If you’re studying modern graphics, and you read a book on OpenGL from early 2000s, that’s not something that should be then used as an argument against OpenGL evolution.

As far as necessary code goes, it’s the same for basic examples. If we go into advanced usage with assigning different materials per triangle - that’s by no means a beginner’s use-case. For that matter - if it’s a common usecase, there may be an abstraction written for it within or without three.js project, so it would boil down to 1-2 lines of code for the user regardless.

This is a constant theme in the world, you have a thing you’re used to, a new thing that’s objectively better in most ways comes along, and people lament the few areas in which new thing is less good. Often this notion of “less” is very subjective too.

I remember friends commenting on how a laser mouse was worse than a ball mouse because “you can’t feel it move” since there’s no physical rolling ball inside it.

3 Likes

So if I wanted to add mesh like this THREE.BoxGeometry( 1, 1, 1 ) it wont work anymore isn’t it?

@playbyan1453

It’ll work.

4 Likes

What’ll happen to methods such as mergeVertices() or computeVertexNormals() ? I certainly wouldn’t mind you lot rolling them into THREE.BufferGeometry, rewriting them myself for Buffergeo would be nigh impossible I think

1 Like

What’ll happen to methods such as mergeVertices() or computeVertexNormals()

A mergeVertices function is available in the BufferGeometryUtils script in examples. Once vertices are merged you can generate vertex normals – they might not wind up being identical to what you see with Geometry.mergeVertices because the BufferGeometryUtils.mergeVertices function cannot merge vertices that do not share exact buffer attributes but it should give a similar result in most cases.

I just had a premonition that THREE.Face3 may meet the same fate.
“♫ oh no!, oh no!, oh no no no no no no no!”

See Raycaster: BufferGeometry with new Face3 - #4 by Mugen87

2 Likes

In 2016-2017 I had programmed my addon with Geometry. After a hint from @Mugen87 I realized the addon with indexed and non-indexed BufferGeometry. With such a somewhat extensive project one must make then nevertheless some adjustments, e.g. with the normals. I documented this at the time.

See Addon. Produces almost infinite many time-varying geometries with functions - #12 by hofk
Some more pictures on page 5 of the german forum.
PHP, HTML & JavaScript- Forum/3D Grafik - WebGL mit three.js XProfan

In the source code on Github you can compare the variants.

Does it possibly make sense to build a helper function that allows to emulate the behavior of Geometry at BufferGeometry 1 to 1?

May I ask how you are using THREE.Face3 in your code?

Like mentioned in the other thread, three.js still uses THREE.Face3 as a data structure in context of raycasting.