mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
102.73k stars 35.38k forks source link

Object3D, Mesh, Scene, Group ambiguity #8711

Closed pailhead closed 8 years ago

pailhead commented 8 years ago
Description of the problem

I'm a little bit confused with the concept of creating a scene graph with three.js, not sure if this should be posted to stack overflow.

Object3D

Base class for scene graph objects.

Scene

Scenes allow you to set up what and where is to be rendered by three.js. This is where you place objects, lights and cameras.

These two make sense for constructing scene graphs, but where does Group fit in, or how is it different than Object3D?

But then Mesh also extends object3d and adds flavor to the mix.

So a minimal graph needed to draw something is

[Scene]->[Mesh]->[Geometry,Material]

But where do object and group fit in

[Scene]->[Group]->[Object3D]->[Mesh]

If object3D has only one child mesh, then that seems reduntant, since mesh is already an object3d and can be positioned. If it has multiple children then it should be a group, if it doesnt have any what is the use case scenario?

Then this concept makes constructing a graph a bit confusing when it comes to loading meshes.

Lets say you want to "draw a box", and look at the documentation:

var geometry = new THREE.BoxGeometry( 1, 1, 1 ); //geometry instantiated and filled by Box

var material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );//material instantiated

var cube = new THREE.Mesh( geometry, material ); //graph node created
scene.add( cube ); //branch created 

All nice and synchronous, geometry and material declared ahead of a mesh, and then a mesh is constructed with them.

But then if you want to load geometry, and you look at the documentation:


// instantiate a loader

var loader = new THREE.JSONLoader();

// load a resource
loader.load(
    // resource URL
    'models/animated/monster/monster.js',
    // Function when resource is loaded
    function ( geometry, materials ) {

        var material = new THREE.MultiMaterial( materials );
        var object = new THREE.Mesh( geometry, material );
        scene.add( object );
    }
);

Since scene already exists, the graph exists. But some time in the future, depending on when a Geometry class gets it's data, we continue creating the graph.

I'm not sure if it was always possible to instantiate a Mesh without providing an instance of Geometry, it is now so it is possible to add this to a mesh that has already been created.

I'm not sure exactly how to explain the ambiguity here but i think this example illustrates it well. I'm also trying to relate to how Unity handles the scene graph. Their GameObject can have a mesh/rendering component attached, but this entity is not a 3d object.

[Scene]->[Object3D]->[Object3D]->[Objec3D]->[Mesh]

does the mesh here really need to have TRS, and it's own matrix, i.e. be an object3d? ...

Three.js version
jonnenauha commented 8 years ago

You are comparing Unitys Entity-Component architecture to three.js inheritance. In the EC model each component add something to the base entity, the entity itself usually does nothing (except maybe a unique id). This is not the approach three.js took and it would be fairly hard to go to it now without breaking the world. I'm not sure if it would be any better in JS land either, more objects in mem and more maps/iteration to get your component.

If you have Scene -> Mesh you should not think of it as scene Scene -> Object3D -> Mesh or you are inventing that middle thing into there, its really Scene -> Mesh (inherits Object3D). When the scene is iterated, there is only one child at the root, not a Object3D that in turn has a Mesh. Your JsonLoader example does exactly this, one mesh in the scene root.

You can of course build it like that if you wish. If you do [Scene]->[Object3D]->[Object3D]->[Objec3D]->[Mesh] without those in the middle objects having any transforms, it is indeed adding nothing. But if its useful to do those parent to achieve some offsetting etc. then go ahead.

All the different formats and loaders can behave differently in what they output. If you use e.g. Obj or Collada loaders, they will give you multiple objects back (depending on the parsed file) and try to represent the parsed file best they can.

Afaik Group is a fairly new addition. A while back when it arrived I'm pretty sure I read the code and understood that it should be used if you dont need pos/rot/scale, but just a container of children. Back then I think it could skip Group typed objects in the frustum culling steps in projectObject, looking at the code now though, it is no longer there. Seems that THREE.Group is not utilized at all essentially in the current build. I dont know what the current purpose of it is then (?!). At least loaders use it, but I dont really see the point as its still inheriting Object3D (e.g. https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/OBJLoader.js#L576)

pailhead commented 8 years ago

What about:


var wheelParts = [ 'tire.js','rim.js', 'bolt.js','logo.js', 'tireValve.js' ]:

var car = new THREE.Object3D();

var wheels = []; //is this where Group should go?

var frontRightWheel = new THREE.Object3D();
frontRightWheel.set( + X , 0 , +Z );
wheels.push( frontRightWheel );
var frontLeftWheel = ...
//...

for( var i = 0 ; i < wheelParts.length ; i ++ ) {
  if(wheelParts[i] !== 'bolt.js') //special case
  loader.load( wheelParts[ i ] , function( geometry ) {
      //just flat mesh added to a "wheel" Object3D 
      for( var j = 0 ; j < wheels.length ; j ++ ){
         wheels[j].add( new THREE.Mesh( geometry ) ); //mesh?, no positioning no scaling no rotation
      }
   }
   else //bolt.js needs positioning
   loader.load( wheelParts[ i ] , function( geometry ){
      for( var j = 0 ; j < wheels.length ; j ++ ){
         for ( var radial = 0 ; radial < 5 ; radial ++ ){
            var m = new THREE.Mesh( geometry ); //objec3d?
            m.rotation.x = Math.PI * 2 * radial / 5;
            wheels[j].add(m);
      }
}

//what if i just wanted to say

 for( var j = 0 ; j < wheels.length ; j ++ ){
   for ( var radial = 0 ; radial < 5 ; radial ++ ){
            var myBolt = new THREE.Object3D();
            m.rotation.x = Math.PI * 2 * radial / 5;
            wheels[j].add(myBolt);
   }
}

//but can't say it with a mesh because i don't have a geometry, unless i set up some proxy system

The most common case of Mesh use i come across is as just a wrapper for geometry and material, never positioned on it's own but rather through some hierarchy of object3ds.

I need to get more familiar with what graph loaders do. I thought obj also doesn't load a graph, only geometry, with subgeometries? The case above is for a "single mesh". I can see it making sense with it should be used if you dont need pos/rot/scale, for grouping sub meshes?

mrdoob commented 8 years ago

THREE.Group (which extends THREE.Object3D) is just a nicer API and it also hints the renderer to not look for geometries or materials inside.

When dealing with a THREE.Object3D, the renderer doesn't know if it has a geometry or material, as the object could be, in fact, a THREE.Mesh.

pailhead commented 8 years ago

but then you need to assume that your graph should never look like this?

+Scene
   |
   +-Object3D //cant be group
   |   |    
   |   +-Mesh  //because of this guy
   |   |   |
   |   |   +-Mesh
   |   |   |
   |   |   +-Object3D
   |   |       |
   |   |       +-Mesh
   |   |       +-Mesh
   |   |
   |   +-Object3D //even though it has many more of these without geometry
   |   +-Object3D
   |   +-Object3D
   |   +-Object3D
   |
   |
   ...

But this is valid:


   +Scene
   |
   +-Group
   |   |    
   |   +-OBJECT3D
   |   |   |
   |   |   +-FIRSTMESH
   |   |       +-Mesh
   |   |       |
   |   |       +-Object3D
   |   |           |
   |   |           +-Mesh
   |   |           +-Mesh
   |   |
   |   +-Object3D
   |   +-Object3D
   |   +-Object3D
   |   +-Object3D
   |
   |
   ...
mrdoob commented 8 years ago
+Scene
   |
   +-Object3D //cant be group
   |   |    
   |   +-Mesh  //because of this guy

That's incorrect. The renderer will continue parsing the object's children. Geometries and Materials are not children.

pailhead commented 8 years ago

Should it be group in that case?

Group.children[ Mesh, Object3D, Object3D... Object3D]

mrdoob commented 8 years ago

Both would work. It's just nicer looking (code reading wise) and faster for the WebGLRenderer to use THREE.Group.

pailhead commented 8 years ago

But do you see my point? I may be confused over nothing, but if it's faster, why ever use Object3D for any kind of grouping? I.e. why should any other type of Object3D other than Scene and Group have .children.length > 0?

mrdoob commented 8 years ago

Well, I think Mesh should be able to have children, and *Light, and *Camera. There are many cases where you want to add a Mesh as children of the camera, or the other way around. So it was easier to let Object3D have children and anything that extends it.

pailhead commented 8 years ago

If im understanding correctly, Object3D should never find itself in a chain in its "raw" form. Group should replace abstract positioning and offseting, while anything else should be specialized Light, Camera Mesh Scene etc.?

mrdoob commented 8 years ago

How would you do this otherwise? If a Mesh couldn't have children, that example wouldn't be possible.

mrdoob commented 8 years ago

If im understanding correctly, Object3D should never find itself in a chain in its "raw" form. Group should replace abstract positioning and offseting,

Yes, but it's also fine if you want to use Object3D for some reason. Group it's more of a syntactical sugar.

pailhead commented 8 years ago

It would completely overcomplicate this example, but this is the thing that is possible, but tricky to do in my first example, with loaders.

Any async operation creating a Mesh chops up the graph creation of a branch.

example from documentation, more geometries means N async points in time where Meshes get appended to a graph.


var boxCluster = new THREE.Group();

loader.load( 'fancyBox' , function( geometry ) {

  for ( var i = 0 ; i < 100 ; i ++ ){

   var m = new THREE.Mesh( geometry );
   applyRandomPosition( m ); //set up spatial property on mesh
   boxCluster.add(m) //set up graph relation of nodes
  }
}

/********
for( var i = 0 ; i < boxTypes.length ; i ++){
  loader.load( boxType[i] ... 
  // many chopped up callbacks appending to "boxCluster" branch

Constructing a mesh like this i believe wasn't possible in older versions? This is now not async as far as the graph is concerned, but a delegation is a bit complicated?

var boxCluster = new THREE.Group();

for ( var i = 0 ; i < 100 ; i ++ ){

   var m = new THREE.Mesh();
   applyRandomPosition( m ); //set up spatial property on mesh

   boxCluster.add(m) //set up graph relation of nodes
}

loader.load( 'fancyBox' , function( geometry ) {
    //delegate single geometry to some branch 
    //which can make sense since the loader creates this instance when parsing
    //but this loader also has to be aware of that branch, and a graph existance alltogether
    for( var i = 0 ; i < boxCluster.children.length ; i ++ ){
      boxCluster.children[i].geometry = geometry;
    }
)

/*******************
//loading geometry without any awarness of any graphs 
boxCluster.userData.boxOptions = [];

loader.load('fancyBoxA' , function(geometry){
  boxCluster.userData.boxOptions.push(geometry);

Vs a proxy:

var boxCluster = new THREE.Group();
var fancyBoxProxy = new THREE.Geometry();

for ( var i = 0 ; i < 100 ; i ++ ){

   var m = new THREE.Mesh( fancyBoxProxy  );
   applyRandomPosition( m ); //set up spatial property on mesh

   boxCluster.add(m) //set up graph relation of nodes
}

//box cluster is created

loader.load( 'fancyBox' , function( geometry ) {
  //loader not aware of graph and delegation, only updates geometry
  fancyBox.merge( geometry); 
)

@mrdoob

mrdoob commented 8 years ago

Any async operation creating a Mesh chops up the graph creation of a branch.

I don't see how...?

pailhead commented 8 years ago

the example's complicated im updating the previous answer :)

jonnenauha commented 8 years ago

@pailhead The first two examples of boxCluster share the same geometry and are essentially identical. Its not automatically cloned inside new THREE.Mesh(geom).

Afaik the renderer is smart enough to upload it once to the GPU and reference it in each mesh draw call. (correct me if I'm wrong @mrdoob).

I all your examples the loader is not aware of your scene graph (like your comment suggest), the anon function you pass it does. Its not part of the loader, that is part of your app, that should know about the scene graph and where to stick the objects/geometries.

All the examples are identical other than how youd prefer to write it. If I would write that code, I would pick the first one and only put meshes into the parent once they have the desired geometry and materials setup. I would be dubious reading the code that tries to do the 2nd or 3rd option :)

pailhead commented 8 years ago

All the examples are identical other than how youd prefer to write it. If I would write that code, I would pick the first one and only put meshes into the parent once they have the desired geometry and materials setup.

.

Any async operation creating a Mesh chops up the graph creation of a branch.

.

I don't see how...?

Conflict :)

So we agree that it "chops up" the graph creation. I'm not sure how else to call the multiple points when a graph is appended.

pailhead commented 8 years ago

Afaik the renderer is smart enough to upload it once to the GPU and reference it in each mesh draw call. (correct me if I'm wrong @mrdoob).

From what i remember, Meshes just reference a geometry, that has a uuid. Internally WebGLRenderer holds another structure that it uses for GPU communication? That's why the merge needs to be more elaborate, setting on a bunch of flags etc. While i'm not 100% sure what happens when you just switch the reference, i think the render() figures out that it's a different geometry, a uuid it has not uploaded to the GPU and has a pointer to hence creates it.

In effect, not at all different then just calling new Mesh( downloadedGeom ), your app will stutter.

I think this is what has the final say before the gpu takes over?:

https://github.com/mrdoob/three.js/blob/dev/src/renderers/webgl/WebGLGeometries.js#L7

If it fails to get a geometry, this is where Geometry gets converted to BufferGeometry.

jonnenauha commented 8 years ago

I guess that depends what you mean by chopping up, I would not call it that :) It's just normal three.js code to us.

I'm not sure what you are arguing for and want me to agree on. You wish to change the core mechanics of three.js with this issue?

pailhead commented 8 years ago

What about the people who don't consider this normal three.js code?

I.e you have to give your object chain to someone ( a branch of the graph) that is set up to pop in at many different times, and they cant figure out where to look for it?

The common question "why is this undefined"?

Which then goes on to, "i need to wait for N geometries, in order to work on [O3d]->[mesh]->[o3d]-[MYMESH]->[anotherMesh]..."

jonnenauha commented 8 years ago

Nothing gets uploaded on the stop in Mesh constructor. Your just passing the same JS object (geometry) reference to all of the meshes. The upload happens on next render() if the mesh is to be rendered (part of the scene, not hidden or frustum culled out).

You app will stutter exactly as much in all of your examples. There is no difference.

pailhead commented 8 years ago

the stutter is besides the point :)

i was trying to illustrate that the same thing happens on

existingMesh.geometry = someOtherGeometry

and

new Mesh( someOtherGeometry ) which gets added where existingMesh would already been

jonnenauha commented 8 years ago

Those people should then get familiar with the library and learn how it should be used via examples etc. or pick another lib I guess :)

I can't follow your line of thought anymore. I'm sure there is question somewhere in there :)

(I don't represent three.js, just a random guy on the internet trying to help)

pailhead commented 8 years ago

uh huh, i disagree,

There is already so much abstraction going on, this is a great example , the stutter. If you had "uploadToGPU" instead of renderer.render taking care of that, it would be much more obvious that you can control this in a different way, as simple as making sure that it's only one thing added at a time with a distance of a few frames etc.

My lines of thought are that you couple loading logic to a branch creation, and that it's "chopped up", meaning you have to rely on that logic to make sure that your entire branch is created before you can go on doing something with a parent.child[a].child[b].child[....

This further means that when creating a Mesh from THREE.BoxGeometry and using a JSONLoader you have to have a completely different concept of creating that branch. Asynchronous, relying on a loader etc.

pailhead commented 8 years ago

wheel = new THREE.Object3D();

for( var i = 0 ; i < 5 ; i++)
wheel.add( new THREE.Mesh() );

//why does this need to happen asynchronously?
wheel.userData.wheelOption = 0;

wheel.toggleOptions = function(){
 var o = this.userData.wheelOption;
 o = ++ o < this.children.length ? o : 0;
 for( var i = 0 ; i < this.children.length ; i ++){
  this.children.visible = i == o;
 }
}.bind(wheel);
jonnenauha commented 8 years ago

The abstraction and ease of use is by design. This is the first line in the readme:

The aim of the project is to create a lightweight 3D library with a very low level of complexity — in other words, for dummies.

Low level of complexity means for the end user, not necessarily the internals and how its written. If you want to manage and have strict control over GPU uploads and all that yourself I think you need to write your own renderer or check at other alternatives. three.js also has DirectGeometry and friends that might get you close but I think you cant control the GPU uploads, but can reduce the work it needs to do before uploads.

pailhead commented 8 years ago

again, the gpu upload is not the issue, the graph construction, which is pretty high level and basic can get confusing, that is the point. In the example above, it's a pretty high level concern - toggle some meshes visible. These meshes can be declared ahead of any asset management, or after. This logic can be known, i.e. "there are 5 options" "switch current on, others off". No need for loaders nor procedural geometry anywhere.

mrdoob commented 8 years ago

My lines of thought are that you couple loading logic to a branch creation, and that it's "chopped up", meaning you have to rely on that logic to make sure that your entire branch is created before you can go on doing something with a parent.child[a].child[b].child[....

If you just want to have a scene graph that doesn't change, why don't you just wait until the loading is done before rendering anything?

jonnenauha commented 8 years ago

Well, it was fully the point/issue of your previous post, which I was replying on. But I guess you changed your mind already what we are disagreeing on :) Now its back to scene graph construction being confusing.

pailhead commented 8 years ago

But you mentioned the gpu first, i didnt think it was relevant. Apologies for the digression. I think we all agree that there are a few ways to do the same thing.

mrdoob commented 8 years ago

Can we close this then? 😛

pailhead commented 8 years ago

the chopped up graph is not a concern, and the ambigouity between mesh and just objectd?

pailhead commented 8 years ago
wheel = new THREE.Group();

for( var i = 0 ; i < 5 ; i++)
wheel.add( new THREE.Group() );

wheel.userData.wheelOption = 0;

wheel.toggleOptions = function(){
 var o = this.userData.wheelOption;
 o = ++ o < this.children.length ? o : 0;
 for( var i = 0 ; i < this.children.length ; i ++){
  this.children[0].children[0].visible = i == o;
 }
}.bind(wheel);

would be the correct way to set up a graph with some logic, and later attach a mesh to it?

mrdoob commented 8 years ago

I don't understand what that code is supposed to do...

pailhead commented 8 years ago

Set up a branch synchronously, that expects 5 meshes to be added, could be THREE.BoxGeometry, or JSONLoader that give Geometry to them, but this code block doesn't care. It knows it can set visibility on a Mesh, doesn't care what the mesh contains or when it obtained it.

pailhead commented 8 years ago

maybe visibility is confusing, lets say you just want to move one child along some axis on some event, you don't care what geometry it has.

You would be using the Object3D methods of a Mesh. But you don't have a Mesh if you are loading some geometry, but you can have it if you construct procedural geometry.

But if instead of a mesh you say, "on event, parent moves child[ E ] up" and use Object3D you can define that logic within that branch (parent) , completely agnostic of the geometry, texture, material etc.

mrdoob commented 8 years ago

Well, create a container for it then (THREE.Group). Move the container and add the meshes to it as they load.

jonnenauha commented 8 years ago

I think toggleOptions should not be tied into the three.js scene/objects. That is application logic and belongs to you app (if I were to write that). I would keep app state and rendering separate.

But thats one way of writing it of course, it's up to you as a developer how you design it.

var car = new THREE.Object3D();
car.position.set(0, 20, 0);
scene.add(car);

for( var i = 0 ; i < 5 ; i++) {
    var wheel = new THREE.Mesh(wheelGeom, wheelMaterial);
    wheel.position.set(math here to offset wheels from car center);
    car.add(wheel);
}

var setWheelVisible = function(index, visible) {
    if (car.children[index])
        car.children[index].visible = visible;
};

var rotateWheels = function(deg) {
    for( var i = 0 ; i < 5 ; i++)
        car.children[i].rotation.x += THREE.Math.degToRad(deg);
}

var update = function() {
    rotateWheels(45 * frametimeAndStuff);
    requestAnimationFrame(update);
};
requestAnimationFrame(update);

Not sure if I understood you right but setWheelVisible or rotateWheels do not touch the geomery or need to know about it. You app does and you do.

jonnenauha commented 8 years ago

But I think this is better suited for stack overflow help or philosophical discussion :) Not a bug report.

pailhead commented 8 years ago

I realize visible is a bad example. Material also has the same interface, and is involving rendering on many levels. Let's say it is something that is entirely in the realm of THREE and an exclusively Object3D. Like .translateX( value ).

I'm trying to say that this, in addition to this:

Well, create a container for it then (THREE.Group). Move the container and add the meshes to it as they load.

Makes the Mesh pretty redundant. I.e. if it's most likely always at the end of that chain, you dont need to apply any TRS to it.

pailhead commented 8 years ago

Well i just learned that an undocumented class is supposed to be used as the backbone for graph creation in three.js :) , is it still a stack overflow thing? Maybe the issue is "document Group ASAP"

mrdoob commented 8 years ago

Well i just learned that an undocumented class is supposed to be used as the backbone for graph creation in three.js, is it still a stack overflow thing?

What undocumented class? Oh! Group is not documented? We should fix that.

pailhead commented 8 years ago

"use for positioning things" could actually be the solution?

mrdoob commented 8 years ago

Makes the Mesh pretty redundant. I.e. if it's most likely always at the end of that chain, you dont need to apply any TRS to it.

In that case, set mesh.matrixAutoUpdate to false.

jonnenauha commented 8 years ago

I think Object3D is the back bone still. Group is a slight optimization as said. You probably need a huge scenes before you see the perf hit of Object3D and Group. But do agree it should be documented (better).

For big scenes matrixAutoUpdate is a good idea.

@mrdoob Maybe put the pros and cons (if any) perf wise of using Group vs. Object3D in the docs. Would be nice to have the steps you save in render explicitly stated there.

pailhead commented 8 years ago

Not sure if I understood you right but setWheelVisible or rotateWheels do not touch the geomery or need to know about it. You app does and you do. @jonnenauha

I'm trying to write everything in es6 and class as much as it is convenient. In this particular case i would argue that the setWheelVisible or rotateWheels should not be part of the app either, but part of the car.

Which could be built from MeshToggler and ObjectRotator etc.

This use case becomes pretty complicated when you have to consider wiring callbacks at this level.

pailhead commented 8 years ago

But designing a class:


class MultiWheel extends THREE.Group{
 constructor( geometryArray){
   super();
   //logic
  }
}

can be over-engineering at some point when you just need a 3d scope, to group some things and set up some spatial logic.

mrdoob commented 8 years ago

/me leaves conversation

pailhead commented 8 years ago

:(

http://threejs.org/examples/#webgl_geometry_hierarchy2

how much does this change if instead of one box, it's the stanford bunny, the blender monkey, and a few other async options.

pailhead commented 8 years ago

close it as not an issue