Repository: three.js
Issue: Layers: Feature request to not render children of non-matching object or mark an object as an endpoint for testing layers
Labels: Enhancement

## Description of the problem

If I have the following object hierarchy:
```
Scene (layer = default)
 - Camera (layer = 0,1)
 - Other camera (layer = default)
 - Group (layer = default)
  - Group (layer = default)
  - Group (layer = 1)            <-- this object doesn't match "Other camera"
   - Object3D (layer = default)  <-- but its children will still be rendered
    - Object3D (layer = default)
   - Object3D (layer = default)
   - Object3D (layer = default)
    - Object3D (layer = default)
    - Object3D (layer = default)
    - Object3D (layer = default)
   - Object3D (layer = default)
...more objects...
   - Object3D (layer = default)
    - Object3D (layer = default)
    - Object3D (layer = default)

(default layer, as defined by Three.js = 0)
```
I would expect "Other camera" to render any of the Object3Ds in the non-matching Group. However, unless I specifically set each of them to layer 1, they are still rendered. I would like to be able to mark the layer 1 Group to a endpoint in the layer calculation:
```
default = 0

Scene (layer = default)
 - Camera (layer = 0,1)
 - Other camera (layer = default)
 - Group (layer = default)
  - Group (layer = 1, layers.endpoint = true) <-- additional property of Layers called "endpoint"
   - Object3D (layer = default)
    - Object3D (layer = default)
   - Object3D (layer = default)
   - Object3D (layer = default)
    - Object3D (layer = default)
    - Object3D (layer = default)
    - Object3D (layer = default)
   - Object3D (layer = default)
...more objects...
   - Object3D (layer = default)
    - Object3D (layer = default)
    - Object3D (layer = default)
```
As I understand it, this could be easily implemented in the recursive rendering function:
```
function renderObject(object, ..., ignoreLayers) {
  if (!object.visible) return;
  
  // Since ignoreLayers means that one of this object's parents
  // was both shown and a layer endpoint, it is safe to assume
  // this object should be shown too
  if (ignoreLayers || object.layers.test(camera.layers)) {
    ...do the actual rendering...

    for (const child of object.children) {
      renderObject(child, ..., ignoreLayers || object.layers.endpoint)
    }
  }
}
```
This solution is very flexible. If the user doesn't set `Layers.endpoint`, child objects of a non-matching parent will still be rendered. However, this solution allows for layers to be managed at an arbitrarily high level. This is great for object oriented purposes, since before the change, the following hierarchy would be problematic

```javascript
// Tools.js
import Hammer from './Hammer.js';
import Shovel from './Shovel.js'

class Tools extends THREE.Group {
  constructor() {
    super();
    this.layers.set(1);

    // This would be rendered by a camera set to layer 0 only :(
    this.hammer = new Hammer();
    this.shovel = new Shovel();
  }
}

// Hammer.js
class Hammer extends THREE.Mesh {
  constructor() {
    // Load complex GLTF model with many individual objects for each part and each material
    super(geo, mat)
    this.add(other part of mesh) // Each of these would have to have its layers set separately
    this.add(other part of mesh)
    this.add(other part of mesh)
  }
}

// Shovel.js
class Shovel extends THREE.Mesh {
  constructor() {
    // Load complex GLTF model with many individual objects for each part and each material
    super(geo, mat)
    this.add(other part of mesh) // Each of these would have to have its layers set separately
    this.add(other part of mesh)
    this.add(other part of mesh)
  }
}
```
It is not reasonable to expect the user to traverse through each child object of the Hammer, Shovel, and all other child classes, setting each `Layers` to a specific value when it could be done at a high level:
```javascript
// Tools.js
import Hammer from './Hammer.js';
import Shovel from './Shovel.js'

class Tools THREE.Group {
  constructor() {
    super();
    this.layers.set(_LAYER);
    this.layers.endpoint = true;

    // This would **not** be rendered by a camera set to layer 0 only :)
    this.hammer = new Hammer();
    this.shovel = new Shovel();
  }

  static get layer() {
    return _LAYER; // Cameras can use this.layer.enable(tools.layer) to show all tools
  }
}

const _LAYER = 1;

// Hammer.js and Shovel.js don't need to know what layer was assigned in Tools.js. It would be hard for them to know even if they wanted to, because importing Tools.js would create a circular reference.
```
To explain for clarity, the `Renderer` would see the `endpoint` set to `true` and then use the `ToolBelt`'s layers object for all of its children

## Other solution

Make `Layers` behave like `object.visible` does. If a parent's layers don't match, don't render its children. This is simple but less flexible.

## Other other solution

`THREE.Object3D.layers` works differently than `THREE.Group.layers`. Children of a `THREE.Group` would not be rendered if the group's layer didn't match whereas children of a `THREE.Object3D` (or `THREE.Mesh`, etc.) would have their own layers tested.

## Note

I'm not 100% sure if the word `Layers.endpoint` is the best property name. Here are some other ideas:
- `Layers.isEndpoint` (Boolean)
- `Layers.final` (Boolean)
- `Layers.override` (Boolean)
- `Layers.mode` (Constant enum)
- `Layers.grouped` (Boolean)
- `Object3D.renderChildrenIfInvisible` (Boolean, default true)


##### Three.js version

- [ ] Dev
- [x] r114
- [ ] ...

All versions since `Layers` added, all browsers, all OSes.