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

Detach unused pool elements from THREE.js scene #5186

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
54 changes: 54 additions & 0 deletions docs/components/attached.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: attached
type: components
layout: docs
parent_section: components
source_code: src/components/attached.js
examples: []
---

[visible]: ./visible.md
[pool]: ./pool.md

The attached component determines whether an entity is attached to the THREE.js scene graph at all.

All entities are attached by default. Detaching an entity from the scene means that the entity and its descendants will have no interactions in the 3D scene at all: it is not rendered, it will not interact with raycasters.

This is similar to the [visible][visible] component, but places even more limitations on the entity.

Invisible entities are not rendered, but their position in space is still updated every frame, and they can interact with raycasters, be checked for collisions etc.

In contrast, when an entity is detached from the scene, by setting `attached="false"`, even these interactions do not occur, which will improve performance even further vs. just making an entity invisible. For example, entity pools implemented by the [pool][pool] component detach entities in the pool when they are not in use.

It's a common pattern to create container entities that contain an entire group of entities that you can flip on an off with `attached`.


## Example

```html
<a-entity attached="false"></a-entity>
```

## Value

| Value | Description |
|-------|----------------------------------------------------------------------------------------|
| true | The entity will be rendered and visible; the default value. |
| false | The entity will be detached from the THREE.js scene. It will not be rendered nor
visible, and will not interact with anything else in the scene. |

## Updating Attachment

It is slightly faster to control attachment to the THREE.js scene using direct calls to [`attachToScene()`](../core/entity.md#attachtoscene-) and [`detachFromScene()`](../core/entity.md#detachfromscene-):

```js
// direct use of entity interface
el.detachFromScene()

// with setAttribute.
e.setAttribute('attached', false)

```

Updates at the three.js level will still be reflected when doing
`entityEl.getAttribute('attached');`.
4 changes: 4 additions & 0 deletions docs/components/pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ source_code: src/components/scene/pool.js
examples: []
---

[attached]: ./attached.md

The pool component allows for [object
pooling](https://en.wikipedia.org/wiki/Object_pool_pattern). This gives us a
reusable pool of entities to avoid creating and destroying the same kind of
Expand All @@ -15,6 +17,8 @@ entities in dynamic scenes. Object pooling helps reduce garbage collection pause
Note that entities requested from the pool are paused by default and you need
to call `.play()` in order to activate their components' tick functions.

For performance reasons, unused entities in the pool are [detached from the THREE.js scene][attached].

## Example

For example, we may have a game with enemy entities that we want to reuse.
Expand Down
12 changes: 9 additions & 3 deletions docs/components/visible.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ parent_section: components
source_code: src/components/visible.js
examples: []
---
[attached]: ./attached.md

The visible component determines whether to render an entity. If set to
`false`, then the entity will not be visible nor drawn.

Visibility effectively applies to all children. If an entity's parent or
ancestor entity has visibility set to false, then the entity will also not be
visible nor draw. It's a common pattern to create container entities that
contain an entire group of entities that you can flip on an off with `visible`.
visible nor draw. However, the entity's position in space is still maintained,
so it can still iuinteract with raycasters, be checked for collisions etc.

When you want an entity or group of entities to have no interactions at all, it
is usually preferable (for performance reasons) to detach them from the THREE.js scene
entirely by toggling the [`attached`][attached] component.


## Example

Expand All @@ -30,7 +36,7 @@ contain an entire group of entities that you can flip on an off with `visible`.

## Updating Visibility

[update]: ../introduction/javascript-events-and-dom-apis.md#updating-a-component-with-setattribute
[update]: ../introduction/javascript-events-dom-apis.md#updating-a-component-with-setattribute-

It is slightly faster to update visibility at the three.js level versus [via
`.setAttribute`][update].
Expand Down
30 changes: 30 additions & 0 deletions docs/core/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,38 @@ entity.addState('selected');
entity.is('selected'); // >> true
```

[attach]: #attachtoscene-
[detach]: #detachfromscene-

### `attachToScene ()`

`attachToScene` can be used to re-attach an entity to the THREE.js scene that has been detached
using `detachFromScene`.

See [`detachFromScene`][detach] for more explanation.

### `destroy ()`

Clean up memory related to the entity such as clearing all components and their data.

### `detachFromScene ()`

[pool]: ../components/pool.md
[attached]: ../components/attached.md

`detachFromScene` will detach the element from the THREE.js scene, typically for performance reasons.
Detaching an entity from the scene means that the entity and its descendants will have no interactions
in the 3D scene at all: it is not rendered, it will not interact with raycasters.

The main reason for detaching an entity from the scene, rather than destroying it are:
- to maintain state on the entity
- for performance reasons, as it is typically faster to re-attach an entity to the scene, than to recreate it and all its descendants.

For performance reasons, entity pools implemented by the [pool][pool] component detach entities in the pool when they are not in use.

Attachment to the THREE.js scene can also be controlled through the [attached][attached] component.


### `emit (name, detail, bubbles)`

[animation]: ../components/animation.md
Expand Down Expand Up @@ -452,6 +480,8 @@ entity.is('selected'); // >> false
|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| child-attached | A child entity was attached to the entity. |
| child-detached | A child entity was detached from the entity. |
| attached-to-scene | The entity was attached to the THREE.js scene. This occurs on initial entity creation, and following subsequent calls to [`attachToScene()`][attach] |
| detached-from-scene | The entity was detached from the THREE.js scene. This occurs a call to to [`detachFromScene()`][detach] (unless the call is made durign entity creation, prior to the entity being attached to the THREE.js scene). |
| componentchanged | One of the entity's components was modified. This event is throttled. Do not use this for reading position and rotation changes, rather [use a tick handler](../camera.md#reading-position-or-rotation-of-the-camera). |
| componentinitialized | One of the entity's components was initialized. |
| componentremoved | One of the entity's components was removed. |
Expand Down
16 changes: 16 additions & 0 deletions src/components/attached.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var registerComponent = require('../core/component').registerComponent;

/**
* Attached component.
*/
module.exports.Component = registerComponent('attached', {
schema: {default: true},

update: function () {
if (this.data) {
this.el.attachToScene();
} else {
this.el.detachFromScene();
}
}
});
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require('./animation');
require('./attached');
require('./camera');
require('./cursor');
require('./daydream-controls');
Expand Down
20 changes: 17 additions & 3 deletions src/components/raycaster.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,16 @@ module.exports.Component = registerComponent('raycaster', {
this.observer.observe(this.el.sceneEl, OBSERVER_CONFIG);
this.el.sceneEl.addEventListener('object3dset', this.setDirty);
this.el.sceneEl.addEventListener('object3dremove', this.setDirty);
this.el.sceneEl.addEventListener('attached-to-scene', this.setDirty);
this.el.sceneEl.addEventListener('detached-from-scene', this.setDirty);
},

removeEventListeners: function () {
this.observer.disconnect();
this.el.sceneEl.removeEventListener('object3dset', this.setDirty);
this.el.sceneEl.removeEventListener('object3dremove', this.setDirty);
this.el.sceneEl.removeEventListener('attached-to-scene', this.setDirty);
this.el.sceneEl.removeEventListener('detached-from-scene', this.setDirty);
},

/**
Expand Down Expand Up @@ -409,13 +413,23 @@ module.exports.Component = registerComponent('raycaster', {
var key;
var i;
var objects = this.objects;
var scene = this.el.sceneEl.object3D;

function isAttachedToScene (object) {
if (object.parent) {
return isAttachedToScene(object.parent);
} else {
return (object === scene);
}
}

// Push meshes and other attachments onto list of objects to intersect.
objects.length = 0;
for (i = 0; i < els.length; i++) {
if (els[i].isEntity && els[i].object3D) {
for (key in els[i].object3DMap) {
objects.push(els[i].getObject3D(key));
var el = els[i];
if (el.isEntity && el.object3D && isAttachedToScene(el.object3D)) {
for (key in el.object3DMap) {
objects.push(el.getObject3D(key));
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/scene/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ module.exports.Component = registerComponent('pool', {
el.setAttribute('mixin', this.data.mixin);
el.object3D.visible = false;
el.pause();
el.detachFromScene();
this.container.appendChild(el);
this.availableEls.push(el);
},
Expand Down Expand Up @@ -93,6 +94,7 @@ module.exports.Component = registerComponent('pool', {
this.createEntity();
}
el = this.availableEls.shift();
el.attachToScene();
this.usedEls.push(el);
el.object3D.visible = true;
return el;
Expand All @@ -110,6 +112,7 @@ module.exports.Component = registerComponent('pool', {
this.usedEls.splice(index, 1);
this.availableEls.push(el);
el.object3D.visible = false;
el.detachFromScene();
el.pause();
return el;
}
Expand Down
43 changes: 42 additions & 1 deletion src/core/a-entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class AEntity extends ANode {
this.parentEl = null;
this.rotationObj = {};
this.states = [];
this.attachedToParent = false;
// tracks attach to / detach from THREE.js Scene graph (independent of DOM attachment)
this.attachedToScene = true;
// Used to preserve object3D's parent when detached from THREE.js scene graph
this.object3DParent = null;
}

/**
Expand Down Expand Up @@ -203,7 +208,14 @@ class AEntity extends ANode {
if (!el.object3D) {
throw new Error("Trying to add an element that doesn't have an `object3D`");
}
this.object3D.add(el.object3D);
if (el.attachedToScene) {
this.object3D.add(el.object3D);
el.emit('attached-to-scene');
} else {
// store off parent, for a later call to 'attachToScene()'
el.object3DParent = this.object3D;
}

this.emit('child-attached', {el: el});
}

Expand Down Expand Up @@ -253,11 +265,39 @@ class AEntity extends ANode {
remove (el) {
if (el) {
this.object3D.remove(el.object3D);
el.object3DParent = null;
} else {
this.parentNode.removeChild(this);
}
}

/**
* Attach el to THREE.js scene graph
*/
attachToScene () {
this.attachedToScene = true;

if (this.attachedToParent) {
if (!this.object3DParent) {
this.object3DParent = this.parentNode.object3D;
}
this.object3DParent.add(this.object3D);
this.emit('attached-to-scene');
}
}

/**
* Detach el from THREE.js scene graph
*/
detachFromScene () {
this.attachedToScene = false;
this.object3DParent = this.object3D.parent;
if (this.object3DParent) {
this.object3DParent.remove(this.object3D);
this.emit('detached-from-scene');
}
}

/**
* @returns {array} Direct children that are entities.
*/
Expand Down Expand Up @@ -732,6 +772,7 @@ class AEntity extends ANode {
if (attr === 'rotation') { return getRotation(this); }
if (attr === 'scale') { return this.object3D.scale; }
if (attr === 'visible') { return this.object3D.visible; }
if (attr === 'attached') { return this.attachedToScene; }
component = this.components[attr];
if (component) { return component.data; }
return window.HTMLElement.prototype.getAttribute.call(this, attr);
Expand Down
53 changes: 53 additions & 0 deletions tests/components/attached.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* global assert, process, setup, suite, test, THREE */
var elFactory = require('../helpers').elFactory;

suite('attached', function () {
var el;

setup(function (done) {
elFactory().then(_el => {
el = _el;
done();
});
});

suite('update', function () {
test('treats empty as true', function () {
el.setAttribute('attached', '');
assert.equal(el.object3D.parent, el.parentNode.object3D);
assert.ok(el.attachedToScene);
});

test('can set to attached', function () {
el.setAttribute('attached', true);
assert.equal(el.object3D.parent, el.parentNode.object3D);
assert.ok(el.attachedToScene);
});

test('can set to not attached', function () {
el.setAttribute('attached', false);
assert.equal(el.object3D.parent, null);
assert.notOk(el.attachedToScene);
});

test('Non-default object3D parent maintained when detached & re-attached', function () {
var alternateParent = new THREE.Group();
alternateParent.add(el.object3D);
el.setAttribute('attached', false);
assert.equal(el.object3D.parent, null);
assert.notOk(el.attachedToScene);
el.setAttribute('attached', true);
assert.equal(el.object3D.parent, alternateParent);
assert.ok(el.attachedToScene);
});

test('getAttribute is affected by changes made direct to entity', function () {
el.setAttribute('attached', true);
assert.ok(el.getAttribute('attached'));
el.detachFromScene();
assert.notOk(el.getAttribute('attached'));
el.attachToScene();
assert.ok(el.getAttribute('attached'));
});
});
});
Loading