Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: Update BatchedMesh with addInstance docs #28456

Merged
merged 4 commits into from
Jun 2, 2024

Conversation

gkjohnson
Copy link
Collaborator

@gkjohnson gkjohnson commented May 21, 2024

Related issue: #28404, #28462

Description

Updates the BatchedMesh docs with the new functionality from #28404. Separate PR so it's easier to take a look at. It would be good to get some eyes on it that are less familiar with the concepts here, as well. I've also removed the instancedMultiDraw "getInstancedCountAt" and "setInstanceCountAt" functions to avoid naming confusion.

cc @donmccurdy @RenaudRohlinger @CodyJasonBennett

@gkjohnson gkjohnson added this to the r165 milestone May 21, 2024
docs/api/en/objects/BatchedMesh.html Outdated Show resolved Hide resolved
[page:Integer maxIndexCount] - the max number of indices to be used by all geometries.<br />
[page:Material material] - an instance of [page:Material]. Default is a
new [page:MeshBasicMaterial].<br />
[page:Integer maxItemCount] - the max number of individual items planned to be added and rendered.<br />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 'items' will need to be defined, maybe something like:

Suggested change
[page:Integer maxItemCount] - the max number of individual items planned to be added and rendered.<br />
[page:Integer maxItemCount] - the max number of individual items (total instances of any geometry) planned to be added and rendered.<br />

I lean slightly toward the name, maxInstanceCount, with clarification that the max instance count is shared, not per-geometry.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially given the requirement of instantiating each geometry, I think the term "instances" over "items" makes most sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops - thanks I thought I got rid of "items" everywhere!

Comment on lines +68 to +69
[page:Integer maxVertexCount] - the max number of vertices to be used by all unique geometries.<br />
[page:Integer maxIndexCount] - the max number of indices to be used by all unique geometries.<br />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize it's implied by the word "unique", but just to spell it out:

Suggested change
[page:Integer maxVertexCount] - the max number of vertices to be used by all unique geometries.<br />
[page:Integer maxIndexCount] - the max number of indices to be used by all unique geometries.<br />
[page:Integer maxVertexCount] - the max number of vertices to be used by all unique geometries. Additional instances of a geometry do not contribute toward the vertex limit.<br />
[page:Integer maxIndexCount] - the max number of indices to be used by all unique geometries. Additional instances of a geometry do not contribute toward the index limit.<br />
docs/api/en/objects/BatchedMesh.html Outdated Show resolved Hide resolved
</p>

<h3>
[method:Integer setGeometryAt]( [param:Integer index], [param:BufferGeometry geometry] )
</h3>
<p>
[page:Integer index]: Which geometry index to replace with this geometry.
[page:Integer index]: Which item index to replace with this geometry.
Copy link
Collaborator

@donmccurdy donmccurdy May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't "geometry" be the right term in this case? Questions:

  1. We're using the terms "index" and "id" interchangeably in this section, which do we prefer?
  2. When calling setGeometryAt for an ID previously allocated with addGeometry, are instances of that geometry affected?
  3. When calling setGeometryAt for an ID previously allocated with addInstance, is any other instance affected?
  4. Is an "instance of an instance" different in any way from an instance created from the same base geometry?

We could consider not supporting creation of "instances of instances" quite yet, if the preferred answers to these questions are non-obvious.

</p>
<p>
[page:BufferGeometry geometry]: The geometry to substitute at the given geometry index.
</p>
<p>
Replaces the geometry at `index` with the provided geometry. Throws an error if there is not enough space reserved for geometry at the index.
Replaces the geometry used by the item at `index` with the provided geometry. Throws an error if there is not enough space reserved for geometry
at the index. Calling this will change all instances that are rendering that geometry.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line answers some of my questions above, but now I'm wondering. Is this by design, by technical requirement, or undecided?

Suppose a BatchedMesh represents a forest with instances of various tree species, each having a high-res and low-res geometry available. Trees within 100m use a high-poly geometry, and those further away use low-res geometries. When an instance crosses the 100m radius I might want to swap out the geometry of that instance alone, which may be difficult within this API. I expect it would be less common to want to the shared geometry of all instances at once, but I'm not sure.

Copy link
Collaborator Author

@gkjohnson gkjohnson May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this was one of the reasons for the other notes in #28404:

Another API option might be to enabling managing of geometry vs instances separately ...

There are technically two types of ids at being used here. A "geometry id", which identifies a range in the BatchedMesh geometry to draw, and an "instance id", which internally refers to a geometry to draw via geometry id. Instance ids are, of course, used to set matrices colors, etc of the given id, as well. The current API obscures geometry ids by referencing the geometries via instance id in the interest of making the API easier to understand but it may just make it more confusing in some cases.

In the LoD case you're referring to users could just hide and show associated with different LoDs but I agree that's not ideal if only because of memory usage (unnecessary matrix data storage). And adding / deleting instances will have a performance cost to "fix up" all ids & matrix textures once the "optimize" function is eventually added. We could add a "swap geometry" function to change the underlying geometry pointer but this will lead to other issues like losing the underlying geometry id once all the associated instances lose it.

I'm starting to feel that we should expose the underlying geometry ids (via addGeometry) in addition to instances (via addInstance) so the user can be aware of and control both, which I think may be more explainable, too. And then other class abstractions can simplify things further if needed. The downsides are that this will be a bit more verbose and be a breaking change. Using the class would now look like this (copied from the other PR):

const mesh = new BatchedMesh( ... );

// initialize geometries
const geometryId0 = mesh.addGeometry( sphereGeometry );
const geometryId1 = mesh.addGeometry( boxGeometry );

// initialize instances
const instancedId0 = mesh.addInstance( geometryId0 );
mesh.setMatrixAt( instanceId0, ... );

const instancedId1 = mesh.addInstance( geometryId1 );
mesh.setMatrixAt( instanceId1, ... );

const instancedId2 = mesh.addInstance( geometryId0 );
mesh.setMatrixAt( instanceId2, ... );

const instancedId3 = mesh.addInstance( geometryId1 );
mesh.setMatrixAt( instanceId3, ... );

And then we can swap the underlying geometry like so:

mesh.setGeometryForInstance( instanceId0, geometryId1 );

Thanks for thinking about some other use cases!

Copy link
Collaborator

@donmccurdy donmccurdy May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the LoD case you're referring to users could just hide and show associated with different LoDs but I agree that's not ideal if only because of memory usage ...

Yes, it's currently a similar API InstancedMesh. To support a similar use case there we need multiple InstancedMesh objects, and move the instance matrix between them to 'change' that instance. I think it can be confusing that users must move the instance matrix around to change the geometry of the instance, and this feels similar.

I like the API direction above. Maybe in the interest of getting a first version out sooner, what do you think of saying...

  1. addGeometry returns a geometry ID
  2. addInstance takes a geometry ID and returns an instance ID
  3. instance IDs and geometry IDs are not interchangeable (at least for now)

.... and perhaps we do a separate PR for setGeometryForInstance to keep things simpler. I think the API setGeometryAt( id, geometry ) could also work, we'd just need to keep a WeakMap<BufferGeometry, number> for the lookups, maybe a little overhead involved.

The downsides are that this will be a bit more verbose and be a breaking change.

What is the breaking change compared to today? That calling addGeometry alone doesn't cause anything to render?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also keep the API as you first proposed, say that all IDs are instance IDs, and change the behavior of setGeometryAt to affect only one instance. Either:

// (A) geometry must already be in BatchedMesh, with or without instances
batch.setGeometryAt( id, geometry )

// (B) geometry must already be in BatchedMesh, with >=1 instance 
batch.setGeometryAt( targetId, sourceId );

If the last instance of a geometry is removed, all references are lost, and it's eligible for some hypothetical future cleanup process.

Copy link
Collaborator Author

@gkjohnson gkjohnson May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • addGeometry returns a geometry ID
  • addInstance takes a geometry ID and returns an instance ID
  • instance IDs and geometry IDs are not interchangeable (at least for now)

Yes this is what I would propose for now if we're going to change the API. We can add more functions later.

I think the API setGeometryAt( id, geometry ) could also work

Perhaps as an option? But it shouldn't be required since once the geometry is in the batched mesh you should want to be able to discard / GC the original geometry to save memory. This can also be done by a user abstraction.

What is the breaking change compared to today? That calling addGeometry alone doesn't cause anything to render?

Yeah - just this. The user must now call "addInstance" after "addGeometry".

If the last instance of a geometry is removed, all references are lost, and it's eligible for some hypothetical future cleanup process.

This is basically what's happening now. But I don't think I love the idea of requiring that users keep geometry around once it's been added to the BatchedMesh. As I mention above I think one expected benefit is that you can GC it after.

I'm inclined to change the API to require calling addInstance if that sounds good to you?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the docs to reflect the separation of instanceId and geometryId.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on all points! 👍

Copy link
Collaborator

@donmccurdy donmccurdy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All remaining comments are suggestions only, thanks!

Comment on lines +37 to +45
const boxGeometryId = batchedMesh.addGeometry( box );
const sphereGeometryId = batchedMesh.addGeometry( sphere );

// create instances of those geometries
const boxInstancedId1 = batchedMesh.addInstance( boxGeometryId );
const boxInstancedId2 = batchedMesh.addInstance( boxGeometryId );

const sphereInstancedId1 = batchedMesh.addInstance( sphereGeometryId );
const sphereInstancedId2 = batchedMesh.addInstance( sphereGeometryId );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea, certainly not necessary in this release. We could consider having addInstance take a Matrix4 as a second argument, so that users creating static geometry do not need to explicitly save the instance ID and call setMatrixAt later.

[page:Integer maxIndexCount] - the max number of indices to be used by all geometries.<br />
[page:Material material] - an instance of [page:Material]. Default is a
new [page:MeshBasicMaterial].<br />
[page:Integer maxItemCount] - the max number of individual items planned to be added and rendered.<br />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially given the requirement of instantiating each geometry, I think the term "instances" over "items" makes most sense.

Comment on lines +242 to +252
[method:Integer setGeometryAt]( [param:Integer geometryId], [param:BufferGeometry geometry] )
</h3>
<p>
[page:Integer index]: Which geometry index to replace with this geometry.
[page:Integer geometryId]: Which geometry id to replace with this geometry.
</p>
<p>
[page:BufferGeometry geometry]: The geometry to substitute at the given geometry index.
[page:BufferGeometry geometry]: The geometry to substitute at the given geometry id.
</p>
<p>
Replaces the geometry at `index` with the provided geometry. Throws an error if there is not enough space reserved for geometry at the index.
Replaces the geometry at `geometryId` with the provided geometry. Throws an error if there is not enough space reserved for geometry.
Calling this will change all instances that are rendering that geometry.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes here, but perhaps the future API is:

  • setGeometryAt( geometryId, geometry ) overwrites a geometry (up to the allocated vertex count), and affects all instances of that geometry
  • setInstanceAt( instanceId, geometryId ) changes the geometry instantiated by a specific instance ID.
@mrdoob mrdoob modified the milestones: r165, r166 May 31, 2024
@gkjohnson gkjohnson merged commit d7d25d3 into mrdoob:dev Jun 2, 2024
3 checks passed
@gkjohnson gkjohnson deleted the batched-docs branch June 2, 2024 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
3 participants