Repository: vue-gl/vue-gl
Pull request #700 (2019-11-12 00:52:07)

Simplify namespace management and rendering lifecycles
* Use `Namespace` class to detect named objects.
  * `Namespace` listens object edit events and execute callbacks.
  * Named components emit events when updated.
  * Detecting named objects immediately occur, not in `beforeRender` hook.
* Documentation for `VglNamespace` component.


Patch:
diff --git a/docs/_layouts/component.html b/docs/_layouts/component.html
index e2dee99..2fda0c8 100644
--- a/docs/_layouts/component.html
+++ b/docs/_layouts/component.html
@@ -96,7 +96,7 @@
             {% endif %}
           {% endfor %}
           {% for p in page_list %}
-            {% if p.title %}
+            {% if p.title and p.title != 'Components' %}
               <h1{% if p.url == page.url %} class="cur"{% endif %}><a href="{{ p.url | relative_url }}">{{ p.title }}</a></h1>
               {% if p.toc %}
                 {% for topic in p.toc %}
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 91b46e7..a52ae9d 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -62,7 +62,7 @@
             {% endif %}
           {% endfor %}
           {% for p in page_list %}
-            {% if p.title %}
+            {% if p.title and p.title != 'Components' %}
               <h1{% if p.url == page.url %} class="cur"{% endif %}><a href="{{ p.url | relative_url }}">{{ p.title }}</a></h1>
               {% if p.toc %}
                 {% for topic in p.toc %}
diff --git a/src/cameras/vgl-camera.js b/src/cameras/vgl-camera.js
index 6bbb9f8..f08b6b2 100644
--- a/src/cameras/vgl-camera.js
+++ b/src/cameras/vgl-camera.js
@@ -10,7 +10,7 @@ import { vector3, spherical } from '../validators';
  * You probably want a [VglPerspectiveCamera](vgl-perspective-camera)
  * and [VglOrthographicCamera](vgl-orthographic-camera).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -29,12 +29,23 @@ export default {
     orbitPosition: spherical,
   },
   computed: {
+    /** The THREE.Camera instance. */
     inst: () => new Camera(),
   },
+  methods: {
+    /** Emit an event in the `cameras` namespace. */
+    emitAsCamera() { this.vglNamespace.cameras.emit(this.name, this.inst); },
+  },
+  created() {
+    this.vglObject3d.listen(this.emitAsCamera);
+  },
+  beforeDestroy() {
+    this.vglObject3d.unlisten(this.emitAsCamera);
+    this.vglNamespace.cameras.delete(this.name, this.inst);
+  },
   watch: {
     inst: {
       handler(inst) {
-        this.vglNamespace.cameras[this.name] = inst;
         if (this.orbitPosition || this.orbitTarget) {
           let target;
           if (this.orbitTarget) target = parseVector3(this.orbitTarget);
@@ -44,13 +55,13 @@ export default {
           }
           inst.lookAt(target || new Vector3());
         }
+        this.vglNamespace.cameras.set(this.name, inst);
       },
       immediate: true,
     },
     name(name, oldName) {
-      const { vglNamespace: { cameras }, inst } = this;
-      if (cameras[oldName] === inst) delete cameras[oldName];
-      cameras[name] = inst;
+      this.vglNamespace.cameras.delete(oldName, this.inst);
+      this.vglNamespace.cameras.set(name, this.inst);
     },
     orbitTarget(orbitTarget) {
       const target = parseVector3(orbitTarget);
@@ -58,6 +69,7 @@ export default {
         this.inst.position.setFromSpherical(parseSpherical(this.orbitPosition)).add(target);
       }
       this.inst.lookAt(target);
+      this.vglObject3d.emit();
     },
     orbitPosition(orbitPosition) {
       this.inst.position.setFromSpherical(parseSpherical(orbitPosition));
@@ -68,10 +80,7 @@ export default {
       } else {
         this.inst.lookAt(new Vector3());
       }
+      this.vglObject3d.emit();
     },
   },
-  beforeDestroy() {
-    const { vglNamespace: { cameras }, inst } = this;
-    if (cameras[this.name] === inst) delete cameras[this.name];
-  },
 };
diff --git a/src/cameras/vgl-orthographic-camera.js b/src/cameras/vgl-orthographic-camera.js
index 45e1a46..ad6ff19 100644
--- a/src/cameras/vgl-orthographic-camera.js
+++ b/src/cameras/vgl-orthographic-camera.js
@@ -21,6 +21,7 @@ export default {
     far: { type: number, default: 2000 },
   },
   computed: {
+    /** The THREE.OrthographicCamera instance. */
     inst: () => new OrthographicCamera(),
   },
   watch: {
@@ -31,12 +32,20 @@ export default {
           near: parseFloat(this.near),
           far: parseFloat(this.far),
         });
-        inst.updateProjectionMatrix();
       },
       immediate: true,
     },
-    zoom(zoom) { this.inst.zoom = parseFloat(zoom); this.inst.updateProjectionMatrix(); },
-    near(near) { this.inst.near = parseFloat(near); this.inst.updateProjectionMatrix(); },
-    far(far) { this.inst.far = parseFloat(far); this.inst.updateProjectionMatrix(); },
+    zoom(zoom) {
+      this.inst.zoom = parseFloat(zoom);
+      this.vglObject3d.emit();
+    },
+    near(near) {
+      this.inst.near = parseFloat(near);
+      this.vglObject3d.emit();
+    },
+    far(far) {
+      this.inst.far = parseFloat(far);
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/cameras/vgl-perspective-camera.js b/src/cameras/vgl-perspective-camera.js
index 55be7d5..163c490 100644
--- a/src/cameras/vgl-perspective-camera.js
+++ b/src/cameras/vgl-perspective-camera.js
@@ -23,6 +23,7 @@ export default {
     fov: { type: number, default: 50 },
   },
   computed: {
+    /** The THREE.PerspectiveCamera instance. */
     inst: () => new PerspectiveCamera(),
   },
   watch: {
@@ -34,13 +35,24 @@ export default {
           far: parseFloat(this.far),
           fov: parseFloat(this.fov),
         });
-        inst.updateProjectionMatrix();
       },
       immediate: true,
     },
-    zoom(zoom) { this.inst.zoom = parseFloat(zoom); this.inst.updateProjectionMatrix(); },
-    near(near) { this.inst.near = parseFloat(near); this.inst.updateProjectionMatrix(); },
-    far(far) { this.inst.far = parseFloat(far); this.inst.updateProjectionMatrix(); },
-    fov(fov) { this.inst.fov = parseFloat(fov); this.inst.updateProjectionMatrix(); },
+    zoom(zoom) {
+      this.inst.zoom = parseFloat(zoom);
+      this.vglObject3d.emit();
+    },
+    near(near) {
+      this.inst.near = parseFloat(near);
+      this.vglObject3d.emit();
+    },
+    far(far) {
+      this.inst.far = parseFloat(far);
+      this.vglObject3d.emit();
+    },
+    fov(fov) {
+      this.inst.fov = parseFloat(fov);
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/core/namespace.js b/src/core/namespace.js
new file mode 100644
index 0000000..e8ec0c9
--- /dev/null
+++ b/src/core/namespace.js
@@ -0,0 +1,76 @@
+export default class Namespace {
+  constructor(parent) {
+    this.parent = parent;
+    this.children = [];
+    this.hash = Object.create(null);
+    this.listeners = Object.create(null);
+    this.globalListeners = [];
+    if (parent) parent.children.push(this);
+  }
+
+  listen(key, fn) {
+    if (typeof key === 'function') {
+      if (!this.globalListeners.includes(key)) this.globalListeners.push(key);
+    } else if (key in this.listeners) {
+      if (!this.listeners[key].includes(fn)) this.listeners[key].push(fn);
+    } else {
+      this.listeners[key] = [fn];
+    }
+  }
+
+  unlisten(key, fn) {
+    if (typeof key === 'function') {
+      const index = this.globalListeners.indexOf(key);
+      if (index >= 0) this.globalListeners.splice(index, 1);
+    } else {
+      const listeners = this.listeners[key];
+      if (listeners) {
+        if (listeners.length > 1) {
+          const index = listeners.indexOf(fn);
+          if (index >= 0) listeners.splice(index, 1);
+        } else if (listeners[0] === fn) {
+          delete this.listeners[key];
+        }
+      }
+    }
+  }
+
+  get(key) {
+    if (key in this.hash) return this.hash[key];
+    return this.parent && this.parent.get(key);
+  }
+
+  set(key, inst) {
+    if (this.hash[key] === inst) return;
+    this.hash[key] = inst;
+    this.emit(key, inst);
+  }
+
+  keys() {
+    const keys = Object.keys(this.hash);
+    if (!this.parent) return keys;
+    const parentKeys = this.parent.keys().filter((key) => !(key in this.hash));
+    return [...parentKeys, ...keys];
+  }
+
+  delete(key, inst) {
+    if (this.hash[key] !== inst) return;
+    delete this.hash[key];
+    this.emit(key, this.parent && this.parent.get(key));
+  }
+
+  emit(key, inst) {
+    if (key in this.listeners) this.listeners[key].forEach((fn) => fn(inst));
+    this.children.forEach((child) => { if (!(key in child.hash)) child.emit(key, inst); });
+    this.globalListeners.forEach((fn) => fn(this));
+  }
+
+  destroy() {
+    if (this.parent) {
+      const { children } = this.parent;
+      children.splice(children.indexOf(this), 1);
+    }
+  }
+
+  fork() { return new Namespace(this); }
+}
diff --git a/src/core/tree.js b/src/core/tree.js
new file mode 100644
index 0000000..c60d397
--- /dev/null
+++ b/src/core/tree.js
@@ -0,0 +1,19 @@
+export default class {
+  constructor(parent, getter) {
+    this.listeners = parent ? parent.listeners : [];
+    this.inst = getter || (() => {});
+  }
+
+  listen(fn) {
+    if (!this.listeners.includes(fn)) this.listeners.push(fn);
+  }
+
+  unlisten(fn) {
+    const index = this.listeners.indexOf(fn);
+    if (index >= 0) this.listeners.splice(index, 1);
+  }
+
+  emit() {
+    this.listeners.forEach((fn) => fn());
+  }
+}
diff --git a/src/core/vgl-geometry.js b/src/core/vgl-geometry.js
index aff9c54..a6fe8bd 100644
--- a/src/core/vgl-geometry.js
+++ b/src/core/vgl-geometry.js
@@ -9,7 +9,11 @@ import { parseArray } from '../parsers';
  */
 
 export default {
-  inject: ['vglNamespace'],
+  inject: {
+    vglNamespace: {
+      default() { throw new Error('VueGL components must be wraped by VglNamespace component.'); },
+    },
+  },
   props: {
     /** Name of the component. */
     name: string,
@@ -21,8 +25,22 @@ export default {
     normalAttribute: floatArray,
   },
   computed: {
+    /** The THREE.BufferGeometry instance. */
     inst: () => new BufferGeometry(),
   },
+  methods: {
+    /**
+     * Emit an event in `geometries` namespace. Call this method after editing instance's
+     * properties.
+     */
+    update() {
+      if (this.name !== undefined) this.vglNamespace.geometries.emit(this.name, this.inst);
+    },
+  },
+  beforeDestroy() {
+    if (this.name !== undefined) this.vglNamespace.geometries.delete(this.name, this.inst);
+    this.inst.dispose();
+  },
   watch: {
     inst: {
       handler(inst, oldInst) {
@@ -45,14 +63,13 @@ export default {
           inst.setAttribute('normal', normalAttribute);
         }
         if (oldInst) oldInst.dispose();
-        this.vglNamespace.geometries[this.name] = inst;
+        this.vglNamespace.geometries.set(this.name, inst);
       },
       immediate: true,
     },
     name(name, oldName) {
-      const { vglNamespace: { geometries }, inst } = this;
-      if (geometries[oldName] === inst) delete geometries[oldName];
-      geometries[name] = inst;
+      if (oldName !== undefined) this.vglNamespace.geometries.delete(oldName, this.inst);
+      if (name !== undefined) this.vglNamespace.geometries.set(name, this.inst);
     },
     positionAttribute(positionAttribute) {
       const positionArray = parseArray(positionAttribute);
@@ -68,6 +85,7 @@ export default {
         this.inst.setAttribute('position', attributeObject);
       }
       attributeObject.needsUpdate = true;
+      this.update();
     },
     colorAttribute(colorAttribute) {
       const colorArray = parseArray(colorAttribute);
@@ -83,6 +101,7 @@ export default {
         this.inst.setAttribute('color', attributeObject);
       }
       attributeObject.needsUpdate = true;
+      this.update();
     },
     normalAttribute(normalAttribute) {
       const normalArray = parseArray(normalAttribute);
@@ -98,14 +117,10 @@ export default {
         this.inst.setAttribute('normal', attributeObject);
       }
       attributeObject.needsUpdate = true;
+      this.update();
     },
   },
-  beforeDestroy() {
-    const { vglNamespace: { geometries }, inst } = this;
-    if (geometries[this.name] === inst) delete geometries[this.name];
-    inst.dispose();
+  render(h) {
+    return this.$slots.default ? h('div', this.$slots.default) : undefined;
   },
-  created() { this.vglNamespace.update(); },
-  beforeUpdate() { this.vglNamespace.update(); },
-  render(h) { return this.$slots.default ? h('div', this.$slots.default) : undefined; },
 };
diff --git a/src/core/vgl-namespace.js b/src/core/vgl-namespace.js
index 26cad2a..0a08803 100644
--- a/src/core/vgl-namespace.js
+++ b/src/core/vgl-namespace.js
@@ -1,42 +1,66 @@
+import Namespace from './namespace';
+
+/**
+ * This component provides maps for managing objects by name (string), and also provides utility
+ * methods. `vglNamespace` object can be injected to descendant components for using it.
+ * It has following namespaces:
+ *
+ * - `vglNamespace.cameras`
+ * - `vglNamespace.scenes`
+ * - `vglNamespace.geometries`
+ * - `vglNamespace.materials`
+ * - `vglNamespace.textures`
+ * - `vglNamespace.object3ds`
+ *
+ * Each namespace internally has a map of strings and objects, and has methods to access them.
+ * Each namespace has following methods:
+ *
+ * - `Namespace.prototype.get(key: string)`
+ *   Get an instance corresponding passed `key`.
+ * - `Namespace.prototype.set(key: string, inst: any)`
+ *   Set an instance corresponding passed `key`.
+ * - `Namespace.prototype.delete(key: string, inst: any)`
+ *   Delete an instance corresponding passed `key`, only if the instance exactly equals passed
+ *   `inst`.
+ * - `Namespace.prototype.listen(key: string, callback: func)`
+ *   Add a listener function that will be called when the instance corresponding passed key changes.
+ * - `Namespace.prototype.unlisten(key: string, callback: func)`
+ *   Remove a listener function registered to passed key.
+ *
+ * When `VglNamespace` components are nested, they create nested namespaces except for `cameras` and
+ * `scenes`. Since a nested namespace search instances only in its ancestors, you can use same key
+ * string in sibling namespaces.
+ */
+
 export default {
   inject: {
     vglNamespace: {
-      default() {
-        const renderers = [];
-        const beforeRender = [];
-        let updated;
-        return {
-          renderers,
-          cameras: Object.create(null),
-          scenes: Object.create(null),
-          update: () => {
-            if (!updated) {
-              this.$nextTick(() => {
-                beforeRender.forEach((hook) => { hook(); });
-                renderers.forEach((vm) => { vm.render(); });
-                updated = false;
-              });
-              updated = true;
-            }
-          },
-          beforeRender,
-          geometries: Object.create(null),
-          materials: Object.create(null),
-          textures: Object.create(null),
-          object3ds: Object.create(null),
-        };
-      },
+      default: () => ({ cameras: new Namespace(), scenes: new Namespace() }),
     },
   },
   provide() {
+    const {
+      geometries, materials, textures, object3ds,
+    } = this.vglNamespace;
     return {
       vglNamespace: Object.create(this.vglNamespace, {
-        geometries: { value: Object.create(this.vglNamespace.geometries) },
-        materials: { value: Object.create(this.vglNamespace.materials) },
-        textures: { value: Object.create(this.vglNamespace.textures) },
-        object3ds: { value: Object.create(this.vglNamespace.object3ds) },
+        geometries: { value: geometries ? geometries.fork() : new Namespace() },
+        materials: { value: materials ? materials.fork() : new Namespace() },
+        textures: { value: textures ? textures.fork() : new Namespace() },
+        object3ds: { value: object3ds ? object3ds.fork() : new Namespace() },
       }),
     };
   },
-  render(h) { return this.$slots.default ? h('div', this.$slots.default) : undefined; },
+  beforeDestroy() {
+    const {
+      geometries, materials, textures, object3ds,
+    } = this.vglNamespace;
+    if (geometries) geometries.destroy();
+    if (materials) materials.destroy();
+    if (textures) textures.destroy();
+    if (object3ds) object3ds.destroy();
+  },
+  render(h) {
+    return this.$slots.default ? h('div', this.$slots.default) : undefined;
+  },
 };
diff --git a/src/core/vgl-object3d.js b/src/core/vgl-object3d.js
index 14e6195..b681d70 100644
--- a/src/core/vgl-object3d.js
+++ b/src/core/vgl-object3d.js
@@ -1,4 +1,5 @@
 import { Object3D } from 'three';
+import Tree from './tree';
 import { parseVector3, parseEuler, parseQuaternion } from '../parsers';
 import {
   vector3,
@@ -38,35 +39,43 @@ export default {
     /** Optional name of the object. */
     name: string,
     /** Whether the object is visible. */
-    visible: {
-      type: boolean,
-      default: true,
+    hidden: boolean,
+  },
+  inject: {
+    vglObject3d: {
+      default: () => new Tree(),
     },
+    vglNamespace: {
+      default() { throw new Error('VueGL components must be wraped by VglNamespace component.'); },
+    },
+  },
+  provide() {
+    return {
+      vglObject3d: new Tree(this.vglObject3d, () => this.inst),
+    };
   },
   computed: {
+    /** The THREE.Object3D instance. */
     inst: () => new Object3D(),
+    /** The parent THREE.Object3D instance.  */
+    parent() { return this.vglObject3d.inst(); },
   },
-  inject: {
-    vglObject3d: { default: {} },
-    vglNamespace: 'vglNamespace',
-  },
-  provide() {
-    const vm = this;
-    return { vglObject3d: { get inst() { return vm.inst; } } };
+  methods: {
+    /** Emit an event in the `object3ds` namespace. */
+    emitAsObject3d() { this.vglNamespace.object3ds.emit(this.name, this.inst); },
   },
-  created() { this.vglNamespace.update(); },
-  beforeUpdate() { this.vglNamespace.update(); },
   beforeDestroy() {
-    const { vglNamespace, inst, name } = this;
-    if (inst.parent) inst.parent.remove(inst);
-    if (vglNamespace.object3ds[name] === inst) this.$delete(vglNamespace.object3ds, name);
-    vglNamespace.update();
+    if (this.inst.parent) this.inst.parent.remove(this.inst);
+    if (this.name !== undefined) {
+      this.vglObject3d.unlisten(this.emitAsObject3d);
+      this.vglNamespace.object3ds.delete(this.name, this.inst);
+    }
   },
   watch: {
     inst: {
       handler(inst, oldInst) {
         if (oldInst && oldInst.parent) oldInst.parent.remove(oldInst);
-        if (this.vglObject3d.inst) this.vglObject3d.inst.add(inst);
+        if (this.parent) this.parent.add(inst);
         if (this.position) inst.position.copy(parseVector3(this.position));
         if (this.rotation) inst.rotation.copy(parseEuler(this.rotation));
         if (this.rotationQuaternion) inst.quaternion.copy(parseQuaternion(this.rotationQuaternion));
@@ -74,32 +83,61 @@ export default {
         Object.assign(inst, {
           castShadow: this.castShadow,
           receiveShadow: this.receiveShadow,
-          visible: this.visible,
+          visible: !this.hidden,
+          name: this.name,
         });
-        if (this.name !== undefined) {
-          // eslint-disable-next-line no-param-reassign
-          inst.name = this.name;
-          this.$set(this.vglNamespace.object3ds, this.name, inst);
-        }
+        if (this.name !== undefined) this.vglNamespace.object3ds.set(this.name, inst);
+        if (oldInst) this.vglObject3d.emit();
       },
       immediate: true,
     },
-    'vglObject3d.inst': function parentInst(inst) { inst.add(this.inst); },
-    position(position) { this.inst.position.copy(parseVector3(position)); },
-    rotation(rotation) { this.inst.rotation.copy(parseEuler(rotation)); },
+    parent(parent) { parent.add(this.inst); },
+    name: [
+      function handler(name) { this.inst.name = name; },
+      {
+        handler(name, oldName) {
+          if (oldName !== undefined) {
+            this.vglNamespace.object3ds.delete(oldName, this.inst);
+            if (name === undefined) this.vglObject3d.unlisten(this.emitAsObject3d);
+          }
+          if (name !== undefined) {
+            this.vglNamespace.object3ds.set(name, this.inst);
+            if (oldName === undefined) this.vglObject3d.listen(this.emitAsObject3d);
+          }
+        },
+        immediate: true,
+      },
+    ],
+    position(position) {
+      this.inst.position.copy(parseVector3(position));
+      this.vglObject3d.emit();
+    },
+    rotation(rotation) {
+      this.inst.rotation.copy(parseEuler(rotation));
+      this.vglObject3d.emit();
+    },
     rotationQuaternion(rotationQuaternion) {
       this.inst.quaternion.copy(parseQuaternion(rotationQuaternion));
+      this.vglObject3d.emit();
+    },
+    scale(scale) {
+      this.inst.scale.copy(parseVector3(scale));
+      this.vglObject3d.emit();
     },
-    scale(scale) { this.inst.scale.copy(parseVector3(scale)); },
-    castShadow(castShadow) { this.inst.castShadow = castShadow; },
-    receiveShadow(receiveShadow) { this.inst.receiveShadow = receiveShadow; },
-    name(name, oldName) {
-      const { vglNamespace: { object3ds }, inst } = this;
-      if (object3ds[oldName] === inst) this.$delete(object3ds, oldName);
-      this.inst.name = name;
-      this.$set(object3ds, name, inst);
+    castShadow(castShadow) {
+      this.inst.castShadow = castShadow;
+      this.vglObject3d.emit();
     },
-    visible(visible) { this.inst.visible = visible; },
+    receiveShadow(receiveShadow) {
+      this.inst.receiveShadow = receiveShadow;
+      this.vglObject3d.emit();
+    },
+    hidden(hidden) {
+      this.inst.visible = !hidden;
+      this.vglObject3d.emit();
+    },
+  },
+  render(h) {
+    return this.$slots.default ? h('div', this.$slots.default) : undefined;
   },
-  render(h) { return this.$slots.default ? h('div', this.$slots.default) : undefined; },
 };
diff --git a/src/extras/vgl-curve.js b/src/extras/vgl-curve.js
index 95b932b..7cc5347 100644
--- a/src/extras/vgl-curve.js
+++ b/src/extras/vgl-curve.js
@@ -1,7 +1,11 @@
 import { Curve } from 'three';
 
 export default {
-  inject: ['vglNamespace'],
+  inject: {
+    vglNamespace: {
+      default() { throw new Error('VueGL components must be wraped by VglNamespace component.'); },
+    },
+  },
   computed: {
     inst: () => new Curve(),
   },
diff --git a/src/geometries/vgl-box-geometry.js b/src/geometries/vgl-box-geometry.js
index a17c890..951895e 100644
--- a/src/geometries/vgl-box-geometry.js
+++ b/src/geometries/vgl-box-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * This is the quadrilateral primitive geometry component,
  * corresponding [THREE.BoxGeometry](https://threejs.org/docs/index.html#api/geometries/BoxGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-circle-geometry.js b/src/geometries/vgl-circle-geometry.js
index c1ec51e..802474c 100644
--- a/src/geometries/vgl-circle-geometry.js
+++ b/src/geometries/vgl-circle-geometry.js
@@ -11,7 +11,7 @@ import { number } from '../validators';
  * It can also be used to create regular polygons,
  * where the number of segments determines the number of sides.
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-cylinder-geometry.js b/src/geometries/vgl-cylinder-geometry.js
index fb9c616..9cbc6e7 100644
--- a/src/geometries/vgl-cylinder-geometry.js
+++ b/src/geometries/vgl-cylinder-geometry.js
@@ -6,7 +6,7 @@ import { number, boolean } from '../validators';
  * This is a component for generating cylinder geometries,
  * corresponding [THREE.CylinderGeometry](https://threejs.org/docs/index.html#api/geometries/CylinderGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-dodecahedron-geometry.js b/src/geometries/vgl-dodecahedron-geometry.js
index 85065d0..941c7ff 100644
--- a/src/geometries/vgl-dodecahedron-geometry.js
+++ b/src/geometries/vgl-dodecahedron-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating a dodecahedron geometries,
  * corresponding [THREE.DodecahedronGeometry](https://threejs.org/docs/index.html#api/geometries/DodecahedronGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-extrude-geometry.js b/src/geometries/vgl-extrude-geometry.js
index 24106b1..9af41f7 100644
--- a/src/geometries/vgl-extrude-geometry.js
+++ b/src/geometries/vgl-extrude-geometry.js
@@ -7,7 +7,7 @@ import { parseShape } from '../parsers';
  * A component for creating extruded geometry from a path shape,
  * corresponding [THREE.ExtrudeGeometry](https://threejs.org/docs/index.html#api/geometries/ExtrudeGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-icosahedron-geometry.js b/src/geometries/vgl-icosahedron-geometry.js
index aeb8268..c8e5a84 100644
--- a/src/geometries/vgl-icosahedron-geometry.js
+++ b/src/geometries/vgl-icosahedron-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating a icosahedron geometries,
  * corresponding [THREE.IcosahedronGeometry](https://threejs.org/docs/index.html#api/geometries/IcosahedronGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-lathe-geometry.js b/src/geometries/vgl-lathe-geometry.js
index a4b66d5..002d29e 100644
--- a/src/geometries/vgl-lathe-geometry.js
+++ b/src/geometries/vgl-lathe-geometry.js
@@ -7,7 +7,7 @@ import { vector2Array, number } from '../validators';
  * A component for creating extruded geometry from a path shape,
  * corresponding [THREE.ExtrudeGeometry](https://threejs.org/docs/index.html#api/geometries/ExtrudeGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-octahedron-geometry.js b/src/geometries/vgl-octahedron-geometry.js
index b7280b5..1bc1ace 100644
--- a/src/geometries/vgl-octahedron-geometry.js
+++ b/src/geometries/vgl-octahedron-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating a octahedron geometries,
  * corresponding [THREE.OctahedronGeometry](https://threejs.org/docs/index.html#api/geometries/OctahedronGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-plane-geometry.js b/src/geometries/vgl-plane-geometry.js
index 25feae8..eaa0a1b 100644
--- a/src/geometries/vgl-plane-geometry.js
+++ b/src/geometries/vgl-plane-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating plane geometries,
  * corresponding [THREE.PlaneGeometry](https://threejs.org/docs/index.html#api/geometries/PlaneGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-ring-geometry.js b/src/geometries/vgl-ring-geometry.js
index 3b4e2ad..bc4f1f6 100644
--- a/src/geometries/vgl-ring-geometry.js
+++ b/src/geometries/vgl-ring-geometry.js
@@ -11,7 +11,7 @@ import { number } from '../validators';
  * It can also be used to create regular polygons,
  * where the number of segments determines the number of sides.
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-sphere-geometry.js b/src/geometries/vgl-sphere-geometry.js
index e5142d0..108bdf5 100644
--- a/src/geometries/vgl-sphere-geometry.js
+++ b/src/geometries/vgl-sphere-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * This is a component for generating sphere geometries,
  * corresponding [THREE.SphereGeometry](https://threejs.org/docs/index.html#api/geometries/SphereGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-tetrahedron-geometry.js b/src/geometries/vgl-tetrahedron-geometry.js
index f52bed0..2891bb8 100644
--- a/src/geometries/vgl-tetrahedron-geometry.js
+++ b/src/geometries/vgl-tetrahedron-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating a tetrahedron geometries,
  * corresponding [THREE.TetrohedronGeometry](https://threejs.org/docs/index.html#api/geometries/TetrohedronGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-text-geometry.js b/src/geometries/vgl-text-geometry.js
index 0126ee4..15e6ebf 100644
--- a/src/geometries/vgl-text-geometry.js
+++ b/src/geometries/vgl-text-geometry.js
@@ -8,7 +8,7 @@ const fonts = Object.create(null);
  * A component for generating text as a single geometry,
  * corresponding [THREE.TextGeometry](https://threejs.org/docs/index.html#api/geometries/TextGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
@@ -54,18 +54,19 @@ export default {
         if (!fonts[src]) {
           fonts[src] = [() => {
             if (src === this.font) this.f = src;
-            this.vglNamespace.update();
           }];
           new FontLoader().load(src, (font) => {
             const queue = fonts[src];
             fonts[src] = font;
             queue.forEach((f) => { f(); });
+            this.vglNamespace.geometries.emit(this.name, this.inst);
           });
         } else if (fonts[src].isFont) {
           this.f = src;
         } else {
           fonts[src].push(() => { if (src === this.font) this.f = src; });
         }
+        this.vglNamespace.geometries.emit(this.name, this.inst);
       },
       immediate: true,
     },
diff --git a/src/geometries/vgl-torus-geometry.js b/src/geometries/vgl-torus-geometry.js
index be44fd9..4c52f13 100644
--- a/src/geometries/vgl-torus-geometry.js
+++ b/src/geometries/vgl-torus-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating torus geometries,
  * corresponding [THREE.TorusGeometry](https://threejs.org/docs/index.html#api/geometries/TorusGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/geometries/vgl-torus-knot-geometry.js b/src/geometries/vgl-torus-knot-geometry.js
index 523e52a..7acd498 100644
--- a/src/geometries/vgl-torus-knot-geometry.js
+++ b/src/geometries/vgl-torus-knot-geometry.js
@@ -6,7 +6,7 @@ import { number } from '../validators';
  * A component for generating torus knot geometries,
  * corresponding [THREE.TorusKnotGeometry](https://threejs.org/docs/index.html#api/geometries/TorusKnotGeometry).
  *
- * Properties of [VglGeometry](vgl-geometry) are also available as mixin.
+ * Properties of [VglGeometry](../core/vgl-geometry) are also available as mixin.
  */
 
 export default {
diff --git a/src/helpers/vgl-arrow-helper.js b/src/helpers/vgl-arrow-helper.js
index cf604d9..e566f15 100644
--- a/src/helpers/vgl-arrow-helper.js
+++ b/src/helpers/vgl-arrow-helper.js
@@ -7,7 +7,7 @@ import { number, string, vector3 } from '../validators';
  * An 3D arrow object for visualizing directions,
  * corresponding [THREE.ArrowHelper](https://threejs.org/docs/index.html#api/helpers/ArrowHelper).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -25,7 +25,9 @@ export default {
     headWidth: number,
   },
   computed: {
+    /** The THREE.ArrowHelper instance. */
     inst: () => new ArrowHelper(new Vector3(0, 1, 0), new Vector3()),
+    /** Array(3) of helper properties. Arrow length, head length, and head width. */
     len() {
       return [
         parseFloat(this.length),
@@ -43,8 +45,17 @@ export default {
       },
       immediate: true,
     },
-    dir(dir) { this.inst.setDirection(parseVector3(dir).normalize()); },
-    len(len) { this.inst.setLength(...len); },
-    color(color) { this.inst.setColor(new Color(color)); },
+    dir(dir) {
+      this.inst.setDirection(parseVector3(dir).normalize());
+      this.vglObject3d.emit();
+    },
+    len(len) {
+      this.inst.setLength(...len);
+      this.vglObject3d.emit();
+    },
+    color(color) {
+      this.inst.setColor(new Color(color));
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/helpers/vgl-axes-helper.js b/src/helpers/vgl-axes-helper.js
index f379542..69295b6 100644
--- a/src/helpers/vgl-axes-helper.js
+++ b/src/helpers/vgl-axes-helper.js
@@ -7,7 +7,7 @@ import { number } from '../validators';
  * corresponding [THREE.AxesHelper](https://threejs.org/docs/index.html#api/helpers/AxesHelper).
  * The X axis is red. The Y axis is green. The Z axis is blue.
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -17,6 +17,7 @@ export default {
     size: { type: number, default: 1 },
   },
   computed: {
+    /** The THREE.AxesHelper instance. */
     inst() { return new AxesHelper(parseFloat(this.size)); },
   },
 };
diff --git a/src/helpers/vgl-box-helper.js b/src/helpers/vgl-box-helper.js
index 48f547d..c98784f 100644
--- a/src/helpers/vgl-box-helper.js
+++ b/src/helpers/vgl-box-helper.js
@@ -6,7 +6,7 @@ import { string } from '../validators';
  * A helper component to show the world-axis-aligned bounding box around its parent,
  * corresponding [THREE.BoxHelper](https://threejs.org/docs/index.html#api/helpers/BoxHelper).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -18,14 +18,34 @@ export default {
     object: string,
   },
   computed: {
+    /** The THREE.BoxHelper instance. */
     inst() { return new BoxHelper(undefined, this.color); },
   },
   methods: {
-    setFromObject() { this.inst.setFromObject(this.vglNamespace.object3ds[this.object]); },
+    /** Set the geometry of the helper box from given object. */
+    setFromObject(obj) { this.inst.setFromObject(obj); },
   },
-  created() { this.vglNamespace.beforeRender.push(this.setFromObject); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setFromObject } = this;
-    beforeRender.splice(beforeRender.indexOf(setFromObject), 1);
+    if (this.object !== undefined) {
+      this.vglNamespace.object3ds.unlisten(this.object, this.setFromObject);
+    }
+  },
+  watch: {
+    inst() {
+      if (this.object !== undefined) {
+        this.setFromObject(this.vglNamespace.object3ds.get(this.object));
+      }
+    },
+    object: {
+      handler(name, oldName) {
+        const { vglNamespace: { object3ds }, setFromObject } = this;
+        if (oldName !== undefined) object3ds.unlisten(oldName, setFromObject);
+        if (name !== undefined) {
+          object3ds.listen(name, setFromObject);
+          setFromObject(this.vglNamespace.object3ds.get(name));
+        }
+      },
+      immediate: true,
+    },
   },
 };
diff --git a/src/helpers/vgl-camera-helper.js b/src/helpers/vgl-camera-helper.js
index 2f4ed48..6f320ee 100644
--- a/src/helpers/vgl-camera-helper.js
+++ b/src/helpers/vgl-camera-helper.js
@@ -1,4 +1,4 @@
-import { CameraHelper } from 'three';
+import { CameraHelper, Object3D } from 'three';
 import VglObject3d from '../core/vgl-object3d';
 import { string } from '../validators';
 
@@ -7,30 +7,56 @@ import { string } from '../validators';
  * corresponding [THREE.CameraHelper](https://threejs.org/docs/index.html#api/helpers/CameraHelper).
  * It visualizes the frustum of a camera using a LineSegments.
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
   mixins: [VglObject3d],
   props: {
     /** Name of the camera to visualize. */
-    camera: string,
+    camera: { type: string, required: true },
+  },
+  data: () => ({
+    /** If camera specified by the name exists or not. Do not set this data manually. */
+    exist: false,
+  }),
+  computed: {
+    /**
+     * The THREE.CameraHelper instance. If any cameras specified by the name, it returns a
+     * THREE.Object3D instance.
+     */
+    inst() {
+      if (!this.exist) return new Object3D();
+      const camera = this.vglNamespace.cameras.get(this.camera);
+      camera.updateProjectionMatrix();
+      return new CameraHelper(camera);
+    },
   },
   methods: {
-    setHelper() {
-      if (!this.inst.children.length) {
-        this.inst.add(new CameraHelper(this.vglNamespace.cameras[this.camera]));
-      } else {
-        const [helper] = this.inst.children;
-        helper.camera = this.vglNamespace.cameras[this.camera];
-        helper.camera.updateProjectionMatrix();
-        helper.update();
+    /** Update the helper geometry for given camera object. */
+    update(camera) {
+      if (!camera) {
+        this.exist = false;
+        return;
       }
+      this.exist = true;
+      camera.updateProjectionMatrix();
+      this.inst.camera = camera;
+      this.inst.update();
+      this.vglObject3d.emit();
     },
   },
-  created() { this.vglNamespace.beforeRender.push(this.setHelper); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setHelper } = this;
-    beforeRender.splice(beforeRender.indexOf(setHelper), 1);
+    this.vglNamespace.cameras.unlisten(this.camera, this.update);
+  },
+  watch: {
+    camera: {
+      handler(name, oldName) {
+        if (oldName !== undefined) this.vglNamespace.cameras.unlisten(oldName, this.update);
+        this.vglNamespace.cameras.listen(name, this.update);
+        this.exist = !!this.vglNamespace.cameras.get(name);
+      },
+      immediate: true,
+    },
   },
 };
diff --git a/src/helpers/vgl-directional-light-helper.js b/src/helpers/vgl-directional-light-helper.js
index fe3557c..4a3f929 100644
--- a/src/helpers/vgl-directional-light-helper.js
+++ b/src/helpers/vgl-directional-light-helper.js
@@ -1,4 +1,4 @@
-import { DirectionalLightHelper } from 'three';
+import { DirectionalLightHelper, Object3D } from 'three';
 import VglObject3d from '../core/vgl-object3d';
 import { string, number } from '../validators';
 
@@ -6,7 +6,7 @@ import { string, number } from '../validators';
  * A helper component to assist with visualizing a DirectionalLight's effect on the scene,
  * corresponding [THREE.DirectionalLightHelper](https://threejs.org/docs/index.html#api/helpers/DirectionalLightHelper).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -19,30 +19,60 @@ export default {
     /** Name of the directional light being visualized. */
     light: string,
   },
-  data() { return { s: undefined }; },
+  data: () => ({
+    /**
+     * Light object's UUID. This would be null if light object is not specified by `light` prop. Do
+     * not set this data manually.
+     */
+    lightUuid: null,
+  }),
+  computed: {
+    /**
+     * The THREE.DirectionalLightHelper instance. If any cameras specified by the name, it returns
+     * a THREE.Object3D instance.
+     */
+    inst() {
+      if (!this.lightUuid) return new Object3D();
+      const light = this.vglNamespace.object3ds.get(this.light);
+      return new DirectionalLightHelper(light, parseFloat(this.size));
+    },
+  },
   methods: {
-    setHelper() {
-      const light = this.vglNamespace.object3ds[this.light];
-      if (this.inst.children.length) {
-        const [helper] = this.inst.children;
-        if (helper.light === light && this.s === this.size) {
-          helper.color = this.color;
-          helper.update();
-          return;
-        }
-        this.inst.remove(helper);
-      }
-      this.s = this.size;
-      this.inst.add(new DirectionalLightHelper(
-        light,
-        parseFloat(this.size),
-        this.color,
-      ));
+    /** Set `lightUuid` data to given object's UUID. */
+    setLightUuid(light) {
+      this.lightUuid = light ? light.uuid : null;
+      this.inst.update();
     },
   },
-  created() { this.vglNamespace.beforeRender.push(this.setHelper); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setHelper } = this;
-    beforeRender.splice(beforeRender.indexOf(setHelper), 1);
+    if (this.light !== undefined) {
+      this.vglNamespace.object3ds.unlisten(this.light, this.setLightUuid);
+    }
+  },
+  watch: {
+    inst(inst) {
+      if (this.lightUuid && this.color !== undefined) {
+        Object.assign(inst, { color: this.color }).update();
+      }
+    },
+    light: {
+      handler(name, oldName) {
+        if (oldName !== undefined) this.vglNamespace.object3ds.unlisten(oldName, this.setLightUuid);
+        if (name !== undefined) {
+          this.vglNamespace.object3ds.listen(name, this.setLightUuid);
+          const light = this.vglNamespace.object3ds.get(this.light);
+          this.setLightUuid(light);
+        }
+      },
+      immediate: true,
+    },
+    color: {
+      handler(color) {
+        if (!this.lightUuid) return;
+        this.inst.color = color;
+        this.inst.update();
+      },
+      immediate: true,
+    },
   },
 };
diff --git a/src/helpers/vgl-grid-helper.js b/src/helpers/vgl-grid-helper.js
index 56e6e7d..05dfd78 100644
--- a/src/helpers/vgl-grid-helper.js
+++ b/src/helpers/vgl-grid-helper.js
@@ -7,7 +7,7 @@ import { number, string } from '../validators';
  * corresponding [THREE.GridHelper](https://threejs.org/docs/index.html#api/helpers/GridHelper).
  * Grids are two-dimensional arrays of lines.
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -23,6 +23,7 @@ export default {
     colorGrid: { type: string, default: '#888888' },
   },
   computed: {
+    /** The THREE.GridHelper instance. */
     inst() {
       return new GridHelper(
         parseFloat(this.size),
diff --git a/src/helpers/vgl-polar-grid-helper.js b/src/helpers/vgl-polar-grid-helper.js
index 32439d6..646938b 100644
--- a/src/helpers/vgl-polar-grid-helper.js
+++ b/src/helpers/vgl-polar-grid-helper.js
@@ -7,7 +7,7 @@ import { number, string } from '../validators';
  * correcponding [THREE.PolarGridHelper](https://threejs.org/docs/index.html#api/helpers/PolarGridHelper).
  * Grids are two-dimensional arrays of lines.
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -30,6 +30,7 @@ export default {
     color2: { type: string, default: '#888888' },
   },
   computed: {
+    /** The THREE.PolarGridHelper instance. */
     inst() {
       return new PolarGridHelper(
         parseFloat(this.radius),
diff --git a/src/helpers/vgl-rect-area-light-helper.js b/src/helpers/vgl-rect-area-light-helper.js
index ddbed6b..dbeb50f 100644
--- a/src/helpers/vgl-rect-area-light-helper.js
+++ b/src/helpers/vgl-rect-area-light-helper.js
@@ -1,11 +1,11 @@
-import { RectAreaLightHelper } from 'three';
+import { RectAreaLightHelper, Object3D } from 'three';
 import VglObject3d from '../core/vgl-object3d';
 import { string } from '../validators';
 
 /**
  * Creates a visual aid for a RectAreaLight, corresponding [THREE.RectAreaLightHelper](https://threejs.org/docs/#api/en/helpers/RectAreaLightHelper).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -14,29 +14,62 @@ export default {
     /** If this is not the set the helper will take the color of the light. */
     color: { type: string },
     /** Name of the RectAreaLight being visualized. */
-    light: { type: string, required: true },
+    light: { type: string },
+  },
+  data: () => ({
+    /**
+     * Light object's UUID. This would be null if light object is not specified by `light` prop. Do
+     * not set this data manually.
+     */
+    lightUuid: null,
+  }),
+  computed: {
+    /**
+     * The THREE.RectAreaLightHelper instance. If any cameras specified by the name, it returns a
+     * THREE.Object3D instance.
+     */
+    inst() {
+      if (!this.lightUuid) return new Object3D();
+      const light = this.vglNamespace.object3ds.get(this.light);
+      return new RectAreaLightHelper(light);
+    },
   },
   methods: {
-    setHelper() {
-      const light = this.vglNamespace.object3ds[this.light];
-      if (this.inst.children.length) {
-        const [helper] = this.inst.children;
-        if (helper.light === light) {
-          helper.color = this.color;
-          helper.update();
-          return;
-        }
-        this.inst.remove(helper);
-      }
-      this.inst.add(new RectAreaLightHelper(
-        light,
-        this.color,
-      ));
+    /** Set `lightUuid` data to given object's UUID. */
+    setLightUuid(light) {
+      this.lightUuid = light ? light.uuid : null;
+      this.inst.update();
     },
   },
-  created() { this.vglNamespace.beforeRender.push(this.setHelper); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setHelper } = this;
-    beforeRender.splice(beforeRender.indexOf(setHelper), 1);
+    if (this.light !== undefined) {
+      this.vglNamespace.object3ds.unlisten(this.light, this.setLightUuid);
+    }
+  },
+  watch: {
+    inst(inst) {
+      if (this.lightUuid && this.color !== undefined) {
+        Object.assign(inst, { color: this.color }).update();
+      }
+    },
+    light: {
+      handler(name, oldName) {
+        if (oldName !== undefined) this.vglNamespace.object3ds.unlisten(oldName, this.setLightUuid);
+        if (name !== undefined) {
+          this.vglNamespace.object3ds.listen(name, this.setLightUuid);
+          const light = this.vglNamespace.object3ds.get(this.light);
+          this.setLightUuid(light);
+        }
+      },
+      immediate: true,
+    },
+    color: {
+      handler(color) {
+        if (!this.lightUuid) return;
+        this.inst.color = color;
+        this.inst.update();
+      },
+      immediate: true,
+    },
   },
 };
diff --git a/src/helpers/vgl-spot-light-helper.js b/src/helpers/vgl-spot-light-helper.js
index 13f8c48..4893d82 100644
--- a/src/helpers/vgl-spot-light-helper.js
+++ b/src/helpers/vgl-spot-light-helper.js
@@ -1,4 +1,4 @@
-import { SpotLightHelper } from 'three';
+import { SpotLightHelper, Object3D } from 'three';
 import VglObject3d from '../core/vgl-object3d';
 import { string } from '../validators';
 
@@ -6,7 +6,7 @@ import { string } from '../validators';
  * This component displays a cone shaped helper object for a SpotLight,
  * corresponding [THREE.SpotLightHelper](https://threejs.org/docs/index.html#api/helpers/SpotLightHelper).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -17,27 +17,60 @@ export default {
     /** Name of the spot light being visualized. */
     light: string,
   },
+  data: () => ({
+    /**
+     * Light object's UUID. This would be null if light object is not specified by `light` prop. Do
+     * not set this data manually.
+     */
+    lightUuid: null,
+  }),
+  computed: {
+    /**
+     * The THREE.SpotLightHelper instance. If any cameras specified by the name, it returns a
+     * THREE.Object3D instance.
+     */
+    inst() {
+      if (!this.lightUuid) return new Object3D();
+      const light = this.vglNamespace.object3ds.get(this.light);
+      return new SpotLightHelper(light);
+    },
+  },
   methods: {
-    setHelper() {
-      if (this.inst.children.length) {
-        const [helper] = this.inst.children;
-        if (helper.light === this.vglNamespace.object3ds[this.light]) {
-          helper.color = this.color;
-          helper.update();
-        } else {
-          this.inst.remove(helper);
-          helper.dispose();
-          this.inst.add(new SpotLightHelper(this.vglNamespace.object3ds[this.light], this.color));
-        }
-      } else {
-        this.inst.add(new SpotLightHelper(this.vglNamespace.object3ds[this.light], this.color));
-      }
+    /** Set `lightUuid` data to given object's UUID. */
+    setLightUuid(light) {
+      this.lightUuid = light ? light.uuid : null;
+      this.inst.update();
     },
   },
-  created() { this.vglNamespace.beforeRender.push(this.setHelper); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setHelper, inst: { children } } = this;
-    beforeRender.splice(beforeRender.indexOf(setHelper), 1);
-    if (children.length) children[0].dispose();
+    if (this.light !== undefined) {
+      this.vglNamespace.object3ds.unlisten(this.light, this.setLightUuid);
+    }
+  },
+  watch: {
+    inst(inst) {
+      if (this.lightUuid && this.color !== undefined) {
+        Object.assign(inst, { color: this.color }).update();
+      }
+    },
+    light: {
+      handler(name, oldName) {
+        if (oldName !== undefined) this.vglNamespace.object3ds.unlisten(oldName, this.setLightUuid);
+        if (name !== undefined) {
+          this.vglNamespace.object3ds.listen(name, this.setLightUuid);
+          const light = this.vglNamespace.object3ds.get(this.light);
+          this.setLightUuid(light);
+        }
+      },
+      immediate: true,
+    },
+    color: {
+      handler(color) {
+        if (!this.lightUuid) return;
+        this.inst.color = color;
+        this.inst.update();
+      },
+      immediate: true,
+    },
   },
 };
diff --git a/src/lights/vgl-ambient-light.js b/src/lights/vgl-ambient-light.js
index 36f718b..ddab83b 100644
--- a/src/lights/vgl-ambient-light.js
+++ b/src/lights/vgl-ambient-light.js
@@ -12,6 +12,7 @@ import VglLight from './vgl-light';
 export default {
   mixins: [VglLight],
   computed: {
+    /** The THREE.AmbientLight instance. */
     inst: () => new AmbientLight(),
   },
 };
diff --git a/src/lights/vgl-directional-light.js b/src/lights/vgl-directional-light.js
index 2ae376e..37faba2 100644
--- a/src/lights/vgl-directional-light.js
+++ b/src/lights/vgl-directional-light.js
@@ -10,6 +10,7 @@ import VglLight from './vgl-light';
 export default {
   mixins: [VglLight],
   computed: {
+    /** The THREE.DirectionalLight instance. */
     inst: () => new DirectionalLight(),
   },
 };
diff --git a/src/lights/vgl-hemisphere-light.js b/src/lights/vgl-hemisphere-light.js
index 02ffe72..bb74d86 100644
--- a/src/lights/vgl-hemisphere-light.js
+++ b/src/lights/vgl-hemisphere-light.js
@@ -13,9 +13,11 @@ import { string } from '../validators';
 export default {
   mixins: [VglLight],
   props: {
+    /** The light's ground color */
     groundColor: { type: string, default: '#fff' },
   },
   computed: {
+    /** The THREE.HemisphereLight instance. */
     inst: () => new HemisphereLight(),
   },
   watch: {
@@ -23,6 +25,9 @@ export default {
       handler(inst) { inst.groundColor.setStyle(this.groundColor); },
       immediate: true,
     },
-    groundColor(groundColor) { this.inst.groundColor.setStyle(groundColor); },
+    groundColor(groundColor) {
+      this.inst.groundColor.setStyle(groundColor);
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/lights/vgl-light.js b/src/lights/vgl-light.js
index 41b3973..3a78a3b 100644
--- a/src/lights/vgl-light.js
+++ b/src/lights/vgl-light.js
@@ -6,7 +6,7 @@ import { string, number } from '../validators';
  * Abstract mixin component for lights,
  * corresponding [THREE.Light](https://threejs.org/docs/index.html#api/lights/Light).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -18,6 +18,7 @@ export default {
     intensity: { type: number, default: 1 },
   },
   computed: {
+    /** The THREE.Light instance. */
     inst: () => new Light(),
   },
   watch: {
@@ -28,7 +29,13 @@ export default {
       },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
-    intensity(intensity) { this.inst.intensity = parseFloat(intensity); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.vglObject3d.emit();
+    },
+    intensity(intensity) {
+      this.inst.intensity = parseFloat(intensity);
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/lights/vgl-point-light.js b/src/lights/vgl-point-light.js
index 2512615..ea3161a 100644
--- a/src/lights/vgl-point-light.js
+++ b/src/lights/vgl-point-light.js
@@ -23,6 +23,7 @@ export default {
     decay: { type: number, default: 1 },
   },
   computed: {
+    /** The THREE.PointLight instance. */
     inst: () => new PointLight(),
   },
   watch: {
@@ -35,7 +36,13 @@ export default {
       },
       immediate: true,
     },
-    distance(distance) { this.inst.distance = parseFloat(distance); },
-    decay(decay) { this.inst.decay = parseFloat(decay); },
+    distance(distance) {
+      this.inst.distance = parseFloat(distance);
+      this.vglObject3d.emit();
+    },
+    decay(decay) {
+      this.inst.decay = parseFloat(decay);
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/lights/vgl-rect-area-light.js b/src/lights/vgl-rect-area-light.js
index c8413b0..7eac830 100644
--- a/src/lights/vgl-rect-area-light.js
+++ b/src/lights/vgl-rect-area-light.js
@@ -20,6 +20,7 @@ export default {
     height: { type: number, default: 10 },
   },
   computed: {
+    /** The THREE.RectAreaLight instance. */
     inst: () => new RectAreaLight(),
   },
   watch: {
@@ -32,7 +33,13 @@ export default {
       },
       immediate: true,
     },
-    width(width) { this.inst.width = parseFloat(width); },
-    height(height) { this.inst.height = parseFloat(height); },
+    width(width) {
+      this.inst.width = parseFloat(width);
+      this.vglObject3d.emit();
+    },
+    height(height) {
+      this.inst.height = parseFloat(height);
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/lights/vgl-spot-light.js b/src/lights/vgl-spot-light.js
index 1386602..2474903 100644
--- a/src/lights/vgl-spot-light.js
+++ b/src/lights/vgl-spot-light.js
@@ -36,6 +36,7 @@ export default {
     target: vector3,
   },
   computed: {
+    /** The THREE.SpotLight instance. */
     inst: () => new SpotLight(),
   },
   beforeDestroy() {
@@ -51,15 +52,32 @@ export default {
           penumbra: parseFloat(this.penumbra),
         });
         if (this.target) inst.target.position.copy(parseVector3(this.target));
-        if (this.vglObject3d.inst) this.vglObject3d.inst.add(inst.target);
+        if (this.parent) this.parent.add(inst.target);
       },
       immediate: true,
     },
-    'vglObject3d.inst': function parentInst(inst) { inst.add(this.inst.target); },
-    distance(distance) { this.inst.distance = parseFloat(distance); },
-    decay(decay) { this.inst.decay = parseFloat(decay); },
-    angle(angle) { this.inst.angle = parseFloat(angle); },
-    penumbra(penumbra) { this.inst.penumbra = parseFloat(penumbra); },
-    target(target) { this.inst.target.position.copy(parseVector3(target)); },
+    parent(parent) {
+      parent.add(this.inst.target);
+    },
+    distance(distance) {
+      this.inst.distance = parseFloat(distance);
+      this.vglObject3d.emit();
+    },
+    decay(decay) {
+      this.inst.decay = parseFloat(decay);
+      this.vglObject3d.emit();
+    },
+    angle(angle) {
+      this.inst.angle = parseFloat(angle);
+      this.vglObject3d.emit();
+    },
+    penumbra(penumbra) {
+      this.inst.penumbra = parseFloat(penumbra);
+      this.vglObject3d.emit();
+    },
+    target(target) {
+      this.inst.target.position.copy(parseVector3(target));
+      this.vglObject3d.emit();
+    },
   },
 };
diff --git a/src/materials/vgl-line-basic-material.js b/src/materials/vgl-line-basic-material.js
index 1e0835b..40a368f 100644
--- a/src/materials/vgl-line-basic-material.js
+++ b/src/materials/vgl-line-basic-material.js
@@ -22,6 +22,7 @@ export default {
     linejoin: { type: string, default: 'round' },
   },
   computed: {
+    /** The THREE.LineBasicMaterial instance. */
     inst: () => new LineBasicMaterial(),
   },
   watch: {
@@ -36,9 +37,21 @@ export default {
       },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
-    linewidth(width) { this.inst.linewidth = parseFloat(width); },
-    linecap(cap) { this.inst.linecap = cap; },
-    linejoin(join) { this.inst.linejoin = join; },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
+    linewidth(width) {
+      this.inst.linewidth = parseFloat(width);
+      this.update();
+    },
+    linecap(cap) {
+      this.inst.linecap = cap;
+      this.update();
+    },
+    linejoin(join) {
+      this.inst.linejoin = join;
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-line-dashed-material.js b/src/materials/vgl-line-dashed-material.js
index cca5832..c9d9800 100644
--- a/src/materials/vgl-line-dashed-material.js
+++ b/src/materials/vgl-line-dashed-material.js
@@ -22,6 +22,7 @@ export default {
     gapSize: { type: number, default: 1 },
   },
   computed: {
+    /** The THREE.LineDashedMaterial instance. */
     inst: () => new LineDashedMaterial(),
   },
   watch: {
@@ -36,9 +37,21 @@ export default {
       },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
-    linewidth(width) { this.inst.linewidth = parseFloat(width); },
-    dashSize(dashSize) { this.inst.dashSize = parseFloat(dashSize); },
-    gapSize(gapSize) { this.inst.gapSize = parseFloat(gapSize); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
+    linewidth(width) {
+      this.inst.linewidth = parseFloat(width);
+      this.update();
+    },
+    dashSize(dashSize) {
+      this.inst.dashSize = parseFloat(dashSize);
+      this.update();
+    },
+    gapSize(gapSize) {
+      this.inst.gapSize = parseFloat(gapSize);
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-material.js b/src/materials/vgl-material.js
index 4a69bdc..011c01b 100644
--- a/src/materials/vgl-material.js
+++ b/src/materials/vgl-material.js
@@ -27,7 +27,11 @@ const sides = {
  */
 
 export default {
-  inject: ['vglNamespace'],
+  inject: {
+    vglNamespace: {
+      default() { throw new Error('VueGL components must be wraped by VglNamespace component.'); },
+    },
+  },
   props: {
     /** Name of the material. */
     name: string,
@@ -37,8 +41,18 @@ export default {
     vertexColors: { type: string, default: 'no' },
   },
   computed: {
+    /** The THREE.Material instance. */
     inst: () => new Material(),
   },
+  methods: {
+    /**
+     * Emit an event in `materials` namespace. Call this method after editing instance's
+     * properties.
+     */
+    update() {
+      if (this.name !== undefined) this.vglNamespace.materials.emit(this.name, this.inst);
+    },
+  },
   watch: {
     inst: {
       handler(inst) {
@@ -46,23 +60,27 @@ export default {
           side: sides[this.side],
           vertexColors: vertexColors[this.vertexColors],
         });
-        this.vglNamespace.materials[this.name] = inst;
+        this.vglNamespace.materials.set(this.name, inst);
       },
       immediate: true,
     },
     name(name, oldName) {
-      const { vglNamespace: { materials }, inst } = this;
-      if (materials[oldName] === inst) delete materials[oldName];
-      materials[name] = inst;
+      if (oldName !== undefined) this.vglNamespace.materials.delete(oldName, this.inst);
+      if (name !== undefined) this.vglNamespace.materials.set(name, this.inst);
+    },
+    side(side) {
+      this.inst.side = sides[side];
+      this.update();
+    },
+    vertexColors(colors) {
+      this.inst.vertexColors = vertexColors[colors];
+      this.update();
     },
-    side(side) { this.inst.side = sides[side]; },
-    vertexColors(colors) { this.inst.vertexColors = vertexColors[colors]; },
   },
   beforeDestroy() {
-    const { vglNamespace: { materials }, inst } = this;
-    if (materials[this.name] === inst) delete materials[this.name];
+    if (this.name !== undefined) this.vglNamespace.materials.delete(this.name, this.inst);
+  },
+  render(h) {
+    return this.$slots.default ? h('div', this.$slots.default) : undefined;
   },
-  created() { this.vglNamespace.update(); },
-  beforeUpdate() { this.vglNamespace.update(); },
-  render(h) { return this.$slots.default ? h('div', this.$slots.default) : undefined; },
 };
diff --git a/src/materials/vgl-mesh-basic-material.js b/src/materials/vgl-mesh-basic-material.js
index 946d7de..fa91b8c 100644
--- a/src/materials/vgl-mesh-basic-material.js
+++ b/src/materials/vgl-mesh-basic-material.js
@@ -19,6 +19,7 @@ export default {
     map: string,
   },
   computed: {
+    /** The THREE.MeshBasicMaterial instance. */
     inst: () => new MeshBasicMaterial(),
   },
   watch: {
@@ -26,6 +27,9 @@ export default {
       handler(inst) { inst.color.setStyle(this.color); },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-depth-material.js b/src/materials/vgl-mesh-depth-material.js
index 2c50764..458a365 100644
--- a/src/materials/vgl-mesh-depth-material.js
+++ b/src/materials/vgl-mesh-depth-material.js
@@ -19,6 +19,7 @@ export default {
     map: string,
   },
   computed: {
+    /** The THREE.MeshDepthMaterial instance. */
     inst: () => new MeshDepthMaterial(),
   },
   watch: {
@@ -26,6 +27,9 @@ export default {
       handler(inst) { Object.assign(inst, { fog: this.fog }); },
       immediate: true,
     },
-    fog(fog) { this.inst.fog = fog; },
+    fog(fog) {
+      this.inst.fog = fog;
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-lambert-material.js b/src/materials/vgl-mesh-lambert-material.js
index 60db27b..74e5f46 100644
--- a/src/materials/vgl-mesh-lambert-material.js
+++ b/src/materials/vgl-mesh-lambert-material.js
@@ -22,6 +22,7 @@ export default {
     map: string,
   },
   computed: {
+    /** The THREE.MeshLambertMaterial instance. */
     inst: () => new MeshLambertMaterial(),
   },
   watch: {
@@ -29,6 +30,9 @@ export default {
       handler(inst) { inst.color.setStyle(this.color); },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-normal-material.js b/src/materials/vgl-mesh-normal-material.js
index 4bc116e..c97dd81 100644
--- a/src/materials/vgl-mesh-normal-material.js
+++ b/src/materials/vgl-mesh-normal-material.js
@@ -16,6 +16,7 @@ export default {
     fog: boolean,
   },
   computed: {
+    /** The THREE.MeshNormalMaterial instance. */
     inst: () => new MeshNormalMaterial(),
   },
   watch: {
@@ -23,6 +24,9 @@ export default {
       handler(inst) { Object.assign(inst, { fog: this.fog }); },
       immediate: true,
     },
-    fog(fog) { this.inst.fog = fog; },
+    fog(fog) {
+      this.inst.fog = fog;
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-phong-material.js b/src/materials/vgl-mesh-phong-material.js
index c293348..43c21e3 100644
--- a/src/materials/vgl-mesh-phong-material.js
+++ b/src/materials/vgl-mesh-phong-material.js
@@ -22,6 +22,7 @@ export default {
     shininess: { type: number, default: 30 },
   },
   computed: {
+    /** The THREE.MeshPhongMaterial instance. */
     inst: () => new MeshPhongMaterial(),
   },
   watch: {
@@ -35,8 +36,17 @@ export default {
       },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
-    specular(specular) { this.inst.specular.setStyle(specular); },
-    shininess(shininess) { this.inst.shininess = parseFloat(shininess); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
+    specular(specular) {
+      this.inst.specular.setStyle(specular);
+      this.update();
+    },
+    shininess(shininess) {
+      this.inst.shininess = parseFloat(shininess);
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-physical-material.js b/src/materials/vgl-mesh-physical-material.js
index 4fdc6a8..652a157 100644
--- a/src/materials/vgl-mesh-physical-material.js
+++ b/src/materials/vgl-mesh-physical-material.js
@@ -20,6 +20,7 @@ export default {
     reflectivity: { type: number, default: 0.5 },
   },
   computed: {
+    /** The THREE.MeshPhysicalMaterial instance. */
     inst: () => new MeshPhysicalMaterial(),
   },
   watch: {
@@ -33,8 +34,17 @@ export default {
       },
       immediate: true,
     },
-    clearcoat(clearcoat) { this.inst.clearcoat = parseFloat(clearcoat); },
-    clearcoatRoughness(roughness) { this.inst.clearcoatRoughness = parseFloat(roughness); },
-    reflectivity(reflectivity) { this.inst.reflectivity = parseFloat(reflectivity); },
+    clearcoat(clearcoat) {
+      this.inst.clearcoat = parseFloat(clearcoat);
+      this.update();
+    },
+    clearcoatRoughness(roughness) {
+      this.inst.clearcoatRoughness = parseFloat(roughness);
+      this.update();
+    },
+    reflectivity(reflectivity) {
+      this.inst.reflectivity = parseFloat(reflectivity);
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-standard-material.js b/src/materials/vgl-mesh-standard-material.js
index eaa7eec..9befaa8 100644
--- a/src/materials/vgl-mesh-standard-material.js
+++ b/src/materials/vgl-mesh-standard-material.js
@@ -19,6 +19,7 @@ export default {
     map: string,
   },
   computed: {
+    /** The THREE.MeshStandardMaterial instance. */
     inst: () => new MeshStandardMaterial(),
   },
   watch: {
@@ -26,6 +27,9 @@ export default {
       handler(inst) { inst.color.setStyle(this.color); },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-mesh-toon-material.js b/src/materials/vgl-mesh-toon-material.js
index 179968f..8ed65a7 100644
--- a/src/materials/vgl-mesh-toon-material.js
+++ b/src/materials/vgl-mesh-toon-material.js
@@ -11,6 +11,7 @@ import VglMeshPhongMaterial from './vgl-mesh-phong-material';
 export default {
   mixins: [VglMeshPhongMaterial],
   computed: {
+    /** The THREE.MeshToonMaterial instance. */
     inst: () => new MeshToonMaterial(),
   },
 };
diff --git a/src/materials/vgl-points-material.js b/src/materials/vgl-points-material.js
index b52cb18..36d7692 100644
--- a/src/materials/vgl-points-material.js
+++ b/src/materials/vgl-points-material.js
@@ -20,6 +20,7 @@ export default {
     disableSizeAttenuation: boolean,
   },
   computed: {
+    /** The THREE.PointsMaterial instance. */
     inst: () => new PointsMaterial(),
   },
   watch: {
@@ -33,8 +34,17 @@ export default {
       },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
-    size(size) { this.inst.size = parseFloat(size); },
-    disableSizeAttenuation(disabled) { this.inst.sizeAttenuation = !disabled; },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
+    size(size) {
+      this.inst.size = parseFloat(size);
+      this.update();
+    },
+    disableSizeAttenuation(disabled) {
+      this.inst.sizeAttenuation = !disabled;
+      this.update();
+    },
   },
 };
diff --git a/src/materials/vgl-shader-material.js b/src/materials/vgl-shader-material.js
index 20d9156..9c755f9 100644
--- a/src/materials/vgl-shader-material.js
+++ b/src/materials/vgl-shader-material.js
@@ -106,6 +106,7 @@ export default {
   },
 
   computed: {
+    /** The THREE.ShaderMaterial instance. */
     inst() {
       return new ShaderMaterial({
         fog: this.fog,
@@ -135,36 +136,40 @@ export default {
     defines: {
       handler(defines) {
         this.inst.defines = defines;
-        this.inst.needsUpdate = true;
+        this.update();
       },
       deep: true,
     },
     fragmentShader(fragmentShader) {
       this.inst.fragmentShader = fragmentShader;
-      this.inst.needsUpdate = true;
+      this.update();
     },
     linewidth(linewidth) {
       this.inst.linewidth = parseFloat(linewidth);
+      this.update();
     },
     flatShading(flatShading) {
       this.inst.flatShading = flatShading;
-      this.inst.needsUpdate = true;
+      this.update();
     },
     uniforms: {
       handler(uniforms) {
         this.inst.uniforms = uniforms;
+        this.update();
       },
       deep: true,
     },
     vertexShader(vertexShader) {
       this.inst.vertexShader = vertexShader;
-      this.inst.needsUpdate = true;
+      this.update();
     },
     wireframe(wireframe) {
       this.inst.wireframe = wireframe;
+      this.update();
     },
     wireframeLinewidth(wireframeLinewidth) {
       this.inst.wireframeLinewidth = parseFloat(wireframeLinewidth);
+      this.update();
     },
   },
 };
diff --git a/src/materials/vgl-shadow-material.js b/src/materials/vgl-shadow-material.js
index 900d2f4..fc8a526 100644
--- a/src/materials/vgl-shadow-material.js
+++ b/src/materials/vgl-shadow-material.js
@@ -11,6 +11,7 @@ import VglMaterial from './vgl-material';
 export default {
   mixins: [VglMaterial],
   computed: {
+    /** The THREE.ShadowMaterial instance. */
     inst: () => new ShadowMaterial(),
   },
 };
diff --git a/src/materials/vgl-sprite-material.js b/src/materials/vgl-sprite-material.js
index a93d913..a723f75 100644
--- a/src/materials/vgl-sprite-material.js
+++ b/src/materials/vgl-sprite-material.js
@@ -18,6 +18,7 @@ export default {
     map: string,
   },
   computed: {
+    /** The THREE.SpriteMaterial instance. */
     inst: () => new SpriteMaterial(),
   },
   watch: {
@@ -25,6 +26,9 @@ export default {
       handler(inst) { inst.color.setStyle(this.color); },
       immediate: true,
     },
-    color(color) { this.inst.color.setStyle(color); },
+    color(color) {
+      this.inst.color.setStyle(color);
+      this.update();
+    },
   },
 };
diff --git a/src/messages.js b/src/messages.js
deleted file mode 100644
index d6f0c83..0000000
--- a/src/messages.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const cameraPropRequiredMessage = 'Cannot identify the camera. camera prop must be set when multiple cameras are defined.';
-export const scenePropRequiredMessage = 'Cannot identify the scene. scene prop must be set when multiple scenes are defined.';
-export const cameraTypeUnknownMessage = 'Unknown camera type.';
diff --git a/src/mixins.js b/src/mixins.js
index b34ace5..a00afe8 100644
--- a/src/mixins.js
+++ b/src/mixins.js
@@ -4,35 +4,85 @@ import VglObject3d from './core/vgl-object3d';
 export const VglObject3dWithMatarial = {
   mixins: [VglObject3d],
   methods: {
-    setMaterial() {
-      const { vglNamespace: { materials }, material, inst } = this;
-      if (Array.isArray(material)) {
-        inst.material = material.reduce(
-          (acc, current) => (materials[current] ? [...acc, materials[current]] : acc), [],
+    setMaterial(material) {
+      const { vglNamespace: { materials }, inst } = this;
+      if (Array.isArray(this.material)) {
+        inst.material = this.material.reduce(
+          (acc, current) => (materials.get(current) ? [...acc, materials.get(current)] : acc), [],
         );
-      } else if (materials[material]) inst.material = materials[material];
+      } else if (material) inst.material = material;
+      this.vglObject3d.emit();
+    },
+  },
+  watch: {
+    inst() {
+      if (this.material !== undefined) {
+        this.setMaterial(this.vglNamespace.materials.get(this.material));
+      }
+    },
+    material: {
+      handler(material, oldMaterial) {
+        const { vglNamespace: { materials }, setMaterial } = this;
+        if (oldMaterial !== undefined) {
+          if (Array.isArray(oldMaterial)) {
+            oldMaterial.forEach((oldName) => materials.unlisten(oldName, setMaterial));
+          } else {
+            materials.unlisten(oldMaterial, setMaterial);
+          }
+        }
+        if (material !== undefined) {
+          if (Array.isArray(material)) {
+            material.forEach((name) => materials.listen(name, setMaterial));
+            setMaterial();
+          } else {
+            materials.listen(material, setMaterial);
+            setMaterial(materials.get(material));
+          }
+        }
+      },
+      immediate: true,
     },
   },
-  created() { this.vglNamespace.beforeRender.unshift(this.setMaterial); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setMaterial } = this;
-    beforeRender.splice(beforeRender.indexOf(setMaterial), 1);
+    const { vglNamespace: { materials }, material, setMaterial } = this;
+    if (Array.isArray(material)) {
+      material.forEach((name) => materials.unlisten(name, setMaterial));
+    } else {
+      materials.unlisten(material, setMaterial);
+    }
   },
 };
 
 export const VglObject3dWithMatarialAndGeometry = {
   mixins: [VglObject3dWithMatarial],
   methods: {
-    setGeometry() {
-      const { vglNamespace: { geometries }, geometry, inst } = this;
-      if (geometries[geometry]) inst.geometry = geometries[geometry];
+    setGeometry(geometry) {
+      if (geometry) {
+        this.inst.geometry = geometry;
+        this.vglObject3d.emit();
+        if (this.name !== undefined) this.vglNamespace.object3ds.emit(this.name, this.inst);
+      }
     },
   },
-  created() { this.vglNamespace.beforeRender.unshift(this.setGeometry); },
-  beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setGeometry } = this;
-    beforeRender.splice(beforeRender.indexOf(setGeometry), 1);
+  watch: {
+    inst() {
+      if (this.geometry !== undefined) {
+        this.setGeometry(this.vglNamespace.geometries.get(this.geometry));
+      }
+    },
+    geometry: {
+      handler(geometry, oldGeometry) {
+        const { vglNamespace: { geometries }, setGeometry } = this;
+        if (oldGeometry !== undefined) geometries.unlisten(oldGeometry, setGeometry);
+        if (geometry !== undefined) {
+          geometries.listen(geometry, setGeometry);
+          setGeometry(geometries.get(geometry));
+        }
+      },
+      immediate: true,
+    },
   },
+  beforeDestroy() { this.vglNamespace.geometries.unlisten(this.geometry, this.setGeometry); },
 };
 
 export const VglMaterialWithMap = {
@@ -40,12 +90,25 @@ export const VglMaterialWithMap = {
   methods: {
     setMap() {
       const { vglNamespace: { textures }, inst, map } = this;
-      if (map in textures) inst.map = textures[map];
+      if (textures.keys().includes(map)) inst.map = textures.get(map);
+      this.vglNamespace.materials.emit(this.name, inst);
     },
   },
-  created() { this.vglNamespace.beforeRender.unshift(this.setMap); },
-  beforeDestroy() {
-    const { vglNamespace: { beforeRender }, setMap } = this;
-    beforeRender.splice(beforeRender.indexOf(setMap), 1);
+  watch: {
+    inst() {
+      if (this.map !== undefined) this.setMap(this.vglNamespace.textures.get(this.map));
+    },
+    map: {
+      handler(map, oldMap) {
+        const { vglNamespace: { textures }, setMap } = this;
+        if (oldMap !== undefined) textures.unlisten(oldMap, setMap);
+        if (map !== undefined) {
+          textures.listen(map, setMap);
+          setMap(textures.get(map));
+        }
+      },
+      immediate: true,
+    },
   },
+  beforeDestroy() { this.vglNamespace.textures.unlisten(this.map, this.setMap); },
 };
diff --git a/src/objects/vgl-group.js b/src/objects/vgl-group.js
index d2ae7a9..2b4bc85 100644
--- a/src/objects/vgl-group.js
+++ b/src/objects/vgl-group.js
@@ -6,12 +6,13 @@ import VglObject3d from '../core/vgl-object3d';
  * corresponding [THREE.Group](https://threejs.org/docs/index.html#api/objects/Group).
  * Its purpose is to make working with groups of objects syntactically clearer.
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
   mixins: [VglObject3d],
   computed: {
+    /** The THREE.Group instance. */
     inst: () => new Group(),
   },
 };
diff --git a/src/objects/vgl-line-loop.js b/src/objects/vgl-line-loop.js
index 2a40a24..b1e63c8 100644
--- a/src/objects/vgl-line-loop.js
+++ b/src/objects/vgl-line-loop.js
@@ -11,6 +11,7 @@ import VglLine from './vgl-line';
 export default {
   mixins: [VglLine],
   computed: {
+    /** The THREE.LineLoop instance. */
     inst: () => new LineLoop(),
   },
 };
diff --git a/src/objects/vgl-line-segments.js b/src/objects/vgl-line-segments.js
index 5fadb54..db18aba 100644
--- a/src/objects/vgl-line-segments.js
+++ b/src/objects/vgl-line-segments.js
@@ -11,6 +11,7 @@ import VglLine from './vgl-line';
 export default {
   mixins: [VglLine],
   computed: {
+    /** The THREE.LineSegments instance. */
     inst: () => new LineSegments(),
   },
 };
diff --git a/src/objects/vgl-line.js b/src/objects/vgl-line.js
index 05d1043..069a09f 100644
--- a/src/objects/vgl-line.js
+++ b/src/objects/vgl-line.js
@@ -6,7 +6,7 @@ import { string } from '../validators';
  * A continuous line component,
  * corresponding [THREE.Line](https://threejs.org/docs/index.html#api/objects/Line).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -18,6 +18,7 @@ export default {
     material: string,
   },
   computed: {
+    /** The THREE.Line instance. */
     inst: () => new Line(),
   },
   methods: {
@@ -25,9 +26,38 @@ export default {
       if (this.inst.material.isLineDashedMaterial) this.inst.computeLineDistances();
     },
   },
-  created() { this.vglNamespace.beforeRender.push(this.computeLineDistances); },
   beforeDestroy() {
-    const { vglNamespace: { beforeRender }, computeLineDistances } = this;
-    beforeRender.splice(beforeRender.indexOf(computeLineDistances), 1);
+    if (this.geometry !== undefined) {
+      this.vglNamespace.geometries.unlisten(this.geometry, this.computeLineDistances);
+    }
+    if (this.material !== undefined) {
+      this.vglNamespace.materials.unlisten(this.material, this.computeLineDistances);
+    }
+  },
+  watch: {
+    geometry: {
+      handler(geometry, oldGeometry) {
+        if (oldGeometry !== undefined) {
+          this.vglNamespace.geometries.unlisten(oldGeometry, this.computeLineDistances);
+        }
+        if (geometry !== undefined) {
+          this.vglNamespace.geometries.listen(geometry, this.computeLineDistances);
+          this.computeLineDistances();
+        }
+      },
+      immediate: true,
+    },
+    material: {
+      handler(material, oldMaterial) {
+        if (oldMaterial !== undefined) {
+          this.vglNamespace.materials.unlisten(oldMaterial, this.computeLineDistances);
+        }
+        if (material !== undefined) {
+          this.vglNamespace.materials.listen(material, this.computeLineDistances);
+          this.computeLineDistances();
+        }
+      },
+      immediate: true,
+    },
   },
 };
diff --git a/src/objects/vgl-mesh.js b/src/objects/vgl-mesh.js
index b3e3d4c..8a30001 100644
--- a/src/objects/vgl-mesh.js
+++ b/src/objects/vgl-mesh.js
@@ -6,7 +6,7 @@ import { string, names } from '../validators';
  * A component representing triangular polygon mesh based objects,
  * corresponding [THREE.Mesh](https://threejs.org/docs/index.html#api/objects/Mesh).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -23,6 +23,7 @@ export default {
     material: names,
   },
   computed: {
+    /** The THREE.Mesh instance. */
     inst: () => new Mesh(),
   },
 };
diff --git a/src/objects/vgl-points.js b/src/objects/vgl-points.js
index ca9d6fc..fd2998a 100644
--- a/src/objects/vgl-points.js
+++ b/src/objects/vgl-points.js
@@ -6,7 +6,7 @@ import { string } from '../validators';
  * A component for displaying points,
  * corresponding [THREE.Points](https://threejs.org/docs/index.html#api/objects/Points).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -18,6 +18,7 @@ export default {
     material: string,
   },
   computed: {
+    /** The THREE.Points instance. */
     inst: () => new Points(),
   },
 };
diff --git a/src/objects/vgl-sprite.js b/src/objects/vgl-sprite.js
index 7b9b5f6..b4dad92 100644
--- a/src/objects/vgl-sprite.js
+++ b/src/objects/vgl-sprite.js
@@ -6,7 +6,7 @@ import { string } from '../validators';
  * A sprite component corresponding [THREE.Sprite](https://threejs.org/docs/index.html#api/objects/Sprite).
  * It is a plane that always faces towards the camera.
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
@@ -16,6 +16,7 @@ export default {
     material: string,
   },
   computed: {
+    /** The THREE.Sprite instance. */
     inst: () => new Sprite(),
   },
 };
diff --git a/src/renderers/utilities.js b/src/renderers/utilities.js
new file mode 100644
index 0000000..0a2fbb9
--- /dev/null
+++ b/src/renderers/utilities.js
@@ -0,0 +1,16 @@
+// eslint-disable-next-line import/prefer-default-export
+export function setCameraSize(camera, width, height) {
+  if (camera.isPerspectiveCamera) {
+    Object.assign(camera, { aspect: width / height });
+  } else if (camera.isOrthographicCamera) {
+    Object.assign(camera, {
+      left: -width / 2,
+      right: width / 2,
+      top: height / 2,
+      bottom: -height / 2,
+    });
+  } else {
+    throw new TypeError('Unknown camera type.');
+  }
+  camera.updateProjectionMatrix();
+}
diff --git a/src/renderers/vgl-renderer.js b/src/renderers/vgl-renderer.js
index e38df7a..a14b1c6 100644
--- a/src/renderers/vgl-renderer.js
+++ b/src/renderers/vgl-renderer.js
@@ -1,13 +1,13 @@
 import { WebGLRenderer } from 'three';
 import VglNamespace from '../core/vgl-namespace';
 import { boolean, string } from '../validators';
-import { cameraPropRequiredMessage, scenePropRequiredMessage, cameraTypeUnknownMessage } from '../messages';
+import { setCameraSize } from './utilities';
 
 /**
  * This component creates a canvas that have WebGL context.
  * Options are corresponding [THREE.WebGLRenderer](https://threejs.org/docs/index.html#api/core/Object3D).
  *
- * Properties of [VglNamespace](vgl-namespace) are also available as mixin.
+ * Properties of [VglNamespace](../core/vgl-namespace) are also available as mixin.
  */
 
 export default {
@@ -42,6 +42,7 @@ export default {
     shadowMapEnabled: boolean,
   },
   computed: {
+    /** The THREE.WebGLRenderer instance. */
     inst() {
       const inst = new WebGLRenderer({
         precision: this.precision,
@@ -57,75 +58,134 @@ export default {
       inst.shadowMap.enabled = this.shadowMapEnabled;
       return inst;
     },
-    cameraInst() {
-      if (this.camera !== undefined) return this.vglNamespace.cameras[this.camera];
-      let camera;
-      // eslint-disable-next-line guard-for-in, no-restricted-syntax
-      for (const key in this.vglNamespace.cameras) {
-        if (camera) throw new ReferenceError(cameraPropRequiredMessage);
-        camera = this.vglNamespace.cameras[key];
+  },
+  methods: {
+    /**
+     * Set camera for rendering the scene. If canvas is already mounted, also calculates view size
+     * or aspect ratio.
+    */
+    setCameraRef(camera) {
+      this.cameraRef = camera;
+      if (this.$el) {
+        if (camera) setCameraSize(camera, this.$el.clientWidth, this.$el.clientHeight);
+        this.requestRender(camera && this.sceneRef);
       }
-      return camera;
     },
-    sceneInst() {
-      if (this.scene !== undefined) return this.vglNamespace.scenes[this.scene];
-      let scene;
-      // eslint-disable-next-line guard-for-in, no-restricted-syntax
-      for (const key in this.vglNamespace.scenes) {
-        if (scene) throw new ReferenceError(scenePropRequiredMessage);
-        scene = this.vglNamespace.scenes[key];
-      }
-      return scene;
+    /** Set scene to be rendered. */
+    setSceneRef(scene) {
+      this.sceneRef = scene;
+      if (this.$el) this.requestRender(scene && this.cameraRef);
     },
-  },
-  methods: {
-    render() {
-      const { inst, cameraInst, sceneInst } = this;
-      if (cameraInst && sceneInst) {
-        if (cameraInst.isPerspectiveCamera) {
-          const aspect = this.$el.clientWidth / this.$el.clientHeight;
-          if (cameraInst.aspect !== aspect) {
-            cameraInst.aspect = aspect;
-            cameraInst.updateProjectionMatrix();
-          }
-        } else if (cameraInst.isOrthographicCamera) {
-          const width = this.$el.clientWidth / 2;
-          const height = this.$el.clientHeight / 2;
-          if (cameraInst.right !== width || cameraInst.top !== height) {
-            cameraInst.left = -width;
-            cameraInst.right = width;
-            cameraInst.top = height;
-            cameraInst.bottom = -height;
-            cameraInst.updateProjectionMatrix();
+    /** Set camera for rendering the scene when `camera` prop is undefined. */
+    setFallbackCamera(cameras) {
+      const keys = cameras.keys();
+      this.setCameraRef(keys.length === 1 ? cameras.get(keys[0]) : undefined);
+    },
+    /** Set scene to be rendered when `scene` prop is undefined. */
+    setFallbackScene(scenes) {
+      const keys = scenes.keys();
+      this.setSceneRef(keys.length === 1 ? scenes.get(keys[0]) : undefined);
+    },
+    /**
+     * Call render function at next tick. Even if this method was called multiple times, it will be
+     * render just once.
+     */
+    requestRender(...args) {
+      if (!this.reservation) {
+        this.$nextTick(() => {
+          if (this.reservation > 0) {
+            this.inst.render(this.sceneRef, this.cameraRef);
+          } else if (!this.cameraRef && this.vglNamespace.cameras.keys().length) {
+            if (this.camera === undefined) {
+              const { length } = this.vglNamespace.cameras.keys();
+              throw new ReferenceError(
+                `Cannot identify the camera. Multiple(${length}) cameras are defined but camera prop is not given.`,
+              );
+            } else {
+              throw new ReferenceError(
+                `Cannot identify the camera. The camera named ${this.camera} is not defined.`,
+              );
+            }
+          } else if (!this.sceneRef && this.vglNamespace.scenes.keys().length) {
+            if (this.scene === undefined) {
+              const { length } = this.vglNamespace.cameras.keys();
+              throw new ReferenceError(
+                `Cannot identify the scene. Multiple(${length}) scenes are defined but scene prop is not given.`,
+              );
+            } else {
+              throw new ReferenceError(
+                `Cannot identify the scene. The scene named ${this.scene} is not defined.`,
+              );
+            }
           }
-        } else {
-          throw new TypeError(cameraTypeUnknownMessage);
-        }
-        inst.render(sceneInst, cameraInst);
+          this.reservation = 0;
+        });
       }
+      this.reservation = !args.length || args[0] ? 1 : -1;
     },
   },
+  mounted() {
+    this.inst.setSize(this.$el.clientWidth, this.$el.clientHeight);
+    this.$el.appendChild(this.inst.domElement);
+    if (this.cameraRef) setCameraSize(this.cameraRef, this.$el.clientWidth, this.$el.clientHeight);
+    this.requestRender(this.cameraRef && this.sceneRef);
+  },
+  beforeDestroy() {
+    if (this.camera === undefined) {
+      this.vglNamespace.cameras.unlisten(this.setFallbackCamera);
+    } else {
+      this.vglNamespace.cameras.unlisten(this.camera, this.setCameraRef);
+    }
+    if (this.scene === undefined) {
+      this.vglNamespace.scenes.unlisten(this.setFallbackScene);
+    } else {
+      this.vglNamespace.scenes.unlisten(this.scene, this.setSceneRef);
+    }
+    this.inst.dispose();
+  },
   watch: {
     inst(inst, oldInst) {
       if (this.$el) {
         inst.setSize(this.$el.clientWidth, this.$el.clientHeight);
         this.$el.replaceChild(inst.domElement, oldInst.domElement);
-        this.vglNamespace.update();
+        if (this.cameraRef && this.sceneRef) this.requestRender();
       }
       oldInst.dispose();
     },
-  },
-  created() {
-    this.vglNamespace.renderers.push(this);
-  },
-  mounted() {
-    this.inst.setSize(this.$el.clientWidth, this.$el.clientHeight);
-    this.$el.appendChild(this.inst.domElement);
-    this.vglNamespace.update();
-  },
-  beforeDestroy() {
-    this.vglNamespace.renderers.splice(this.vglNamespace.renderers.indexOf(this), 1);
-    this.inst.dispose();
+    camera: {
+      handler(camera, oldCamera) {
+        if (oldCamera === undefined) {
+          this.vglNamespace.cameras.unlisten(this.setFallbackCamera);
+        } else {
+          this.vglNamespace.cameras.unlisten(oldCamera, this.setCameraRef);
+        }
+        if (camera === undefined) {
+          this.vglNamespace.cameras.listen(this.setFallbackCamera);
+          this.setFallbackCamera(this.vglNamespace.cameras);
+        } else {
+          this.vglNamespace.cameras.listen(camera, this.setCameraRef);
+          this.setCameraRef(this.vglNamespace.cameras.get(camera));
+        }
+      },
+      immediate: true,
+    },
+    scene: {
+      handler(scene, oldScene) {
+        if (oldScene === undefined) {
+          this.vglNamespace.scenes.unlisten(this.setFallbackScene);
+        } else {
+          this.vglNamespace.scenes.unlisten(oldScene, this.setSceneRef);
+        }
+        if (scene === undefined) {
+          this.vglNamespace.scenes.listen(this.setFallbackScene);
+          this.setFallbackScene(this.vglNamespace.scenes);
+        } else {
+          this.vglNamespace.scenes.listen(scene, this.setSceneRef);
+          this.setSceneRef(this.vglNamespace.scenes.get(scene));
+        }
+      },
+      immediate: true,
+    },
   },
   render(h) {
     return h('div', [h('iframe', {
@@ -140,7 +200,10 @@ export default {
         load: (event) => {
           event.target.contentWindow.addEventListener('resize', () => {
             this.inst.setSize(this.$el.clientWidth, this.$el.clientHeight);
-            this.vglNamespace.update();
+            if (this.cameraRef) {
+              setCameraSize(this.cameraRef, this.$el.clientWidth, this.$el.clientHeight);
+              if (this.sceneRef) this.requestRender();
+            }
           }, false);
         },
       },
diff --git a/src/scenes/vgl-scene.js b/src/scenes/vgl-scene.js
index a8b1845..58ed264 100644
--- a/src/scenes/vgl-scene.js
+++ b/src/scenes/vgl-scene.js
@@ -7,16 +7,14 @@ import { string, fog } from '../validators';
  * This is where you place objects,
  * corresponding [THREE.Scene](https://threejs.org/docs/index.html#api/scenes/Scene).
  *
- * Properties of [VglObject3d](vgl-object3d) are also available as mixin.
+ * Properties of [VglObject3d](../core/vgl-object3d) are also available as mixin.
  */
 
 export default {
   mixins: [VglObject3d],
   props: {
     /** the color, near and far parameters of the scene's fog */
-    fog: {
-      type: fog,
-    },
+    fog,
     /**
      * Expecting to accept a string representing a color.
      * Will be overwrited by backgroundTexture prop if both props are set
@@ -26,48 +24,65 @@ export default {
     backgroundTexture: string,
   },
   computed: {
+    /** The THREE.Scene instance. */
     inst: () => new Scene(),
   },
   methods: {
-    setBackgroundTexture() {
-      const { vglNamespace: { textures }, inst, backgroundTexture } = this;
-      if (backgroundTexture in textures) inst.background = textures[backgroundTexture];
+    /** Set scene's background property to given texture. */
+    setBackgroundTexture(texture) {
+      this.inst.background = texture || null;
+      this.vglObject3d.emit();
     },
+    /** Emit an event in the `scenes` namespace. */
+    emitAsScene() { this.vglNamespace.scenes.emit(this.name, this.inst); },
   },
   created() {
-    const { vglNamespace: { beforeRender }, setBackgroundTexture } = this;
-    beforeRender.unshift(setBackgroundTexture);
+    this.vglObject3d.listen(this.emitAsScene);
+  },
+  beforeDestroy() {
+    if (this.backgroundTexture !== undefined) {
+      this.vglNamespace.textures.unlisten(this.backgroundTexture, this.setBackgroundTexture);
+    }
+    this.vglNamespace.scenes.delete(this.name, this.inst);
+    this.vglObject3d.unlisten(this.emitAsScene);
   },
   watch: {
     inst: {
       handler(inst) {
-        this.vglNamespace.scenes[this.name] = inst;
-        if (this.fog) this.inst.fog = parseFog(this.fog);
-        if (this.backgroundColor) this.inst.background = parseColor(this.backgroundColor);
-        if (this.backgroundTexture) {
-          this.inst.background = this.vglNamespace.textures[this.backgroundTexture] || null;
+        if (this.fog !== undefined) this.inst.fog = parseFog(this.fog);
+        if (this.backgroundColor !== undefined) {
+          this.inst.background = parseColor(this.backgroundColor);
         }
+        if (this.backgroundTexture !== undefined) {
+          this.setBackgroundTexture(this.vglNamespace.textures.get(this.backgroundTexture));
+        }
+        this.vglNamespace.scenes.set(this.name, inst);
       },
       immediate: true,
     },
     name(name, oldName) {
-      const { vglNamespace: { scenes }, inst } = this;
-      if (scenes[oldName] === inst) delete scenes[oldName];
-      scenes[name] = inst;
+      this.vglNamespace.scenes.delete(oldName, this.inst);
+      this.vglNamespace.scenes.set(name, this.inst);
     },
     fog(newFog) {
       this.inst.fog = parseFog(newFog);
+      this.vglObject3d.emit();
     },
     backgroundColor(color) {
       this.inst.background = parseColor(color);
+      this.vglObject3d.emit();
     },
-    backgroundTexture(name) {
-      this.inst.background = this.vglNamespace.textures[name] || null;
+    backgroundTexture: {
+      handler(name, oldName) {
+        if (oldName !== undefined) {
+          this.vglNamespace.textures.unlisten(oldName, this.setBackgroundTexture);
+        }
+        if (name !== undefined) {
+          this.vglNamespace.textures.listen(name, this.setBackgroundTexture);
+          this.setBackgroundTexture(this.vglNamespace.textures.get(name));
+        }
+      },
+      immediate: true,
     },
   },
-  beforeDestroy() {
-    const { vglNamespace: { scenes, beforeRender }, inst, setBackgroundTexture } = this;
-    if (scenes[this.name] === inst) delete scenes[this.name];
-    beforeRender.splice(beforeRender.indexOf(setBackgroundTexture), 1);
-  },
 };
diff --git a/src/textures/vgl-texture.js b/src/textures/vgl-texture.js
index ae27719..9eeef55 100644
--- a/src/textures/vgl-texture.js
+++ b/src/textures/vgl-texture.js
@@ -124,7 +124,11 @@ const encoding = {
  */
 
 export default {
-  inject: ['vglNamespace'],
+  inject: {
+    vglNamespace: {
+      default() { throw new Error('VueGL components must be wraped by VglNamespace component.'); },
+    },
+  },
   props: {
     /** The path or URL to the file. This can also be a Data URI. */
     src: string,
@@ -146,17 +150,26 @@ export default {
     encoding: { type: string, default: 'linear' },
   },
   computed: {
+    /** The THREE.Texture instance. */
     inst() {
       return new TextureLoader().load(this.src, (texture) => {
         if (this.format) Object.assign(texture, { format: format[this.format] });
-        this.vglNamespace.update();
+        this.update();
       });
     },
   },
+  methods: {
+    /** Emit an event in the `textures` namespace. */
+    update() {
+      if (this.name !== undefined) this.vglNamespace.textures.emit(this.name, this.inst);
+    },
+  },
+  beforeDestroy() {
+    if (this.name !== undefined) this.vglNamespace.textures.delete(this.name, this.inst);
+  },
   watch: {
     inst: {
       handler(inst) {
-        this.vglNamespace.textures[this.name] = inst;
         Object.assign(inst, {
           mapping: mapping[this.mapping],
           wrapS: wrapping[this.wrapS],
@@ -173,37 +186,74 @@ export default {
         if (this.offset) inst.offset.copy(parseVector2(this.offset));
         if (this.repeat) inst.repeat.copy(parseVector2(this.repeat));
         if (this.center) inst.center.copy(parseVector2(this.center));
+        if (this.name !== undefined) this.vglNamespace.textures.set(this.name, inst);
       },
       immediate: true,
     },
     name(name, oldName) {
-      const { vglNamespace: { textures }, inst } = this;
-      if (textures[oldName] === inst) delete textures[oldName];
-      textures[name] = inst;
-    },
-    mapping(mode) { this.inst.mapping = mapping[mode]; },
-    wrapS(mode) { this.inst.wrapS = wrapping[mode]; },
-    wrapT(mode) { this.inst.wrapT = wrapping[mode]; },
-    magFilter(mode) { this.inst.magFilter = filter[mode]; },
-    minFilter(mode) { this.inst.minFilter = filter[mode]; },
-    anisotropy(anisotropy) { this.inst.anisotropy = parseInt(anisotropy, 10); },
-    format(mode) { this.inst.format = format[mode]; },
-    type(mode) { this.inst.type = type[mode]; },
-    offset(offset) { this.inst.offset.copy(parseVector2(offset)); },
-    repeat(repeat) { this.inst.repeat.copy(parseVector2(repeat)); },
-    rotation(rotation) { this.inst.rotation = parseFloat(rotation); },
-    center(center) { this.inst.center.copy(parseVector2(center)); },
-    premultiplyAlpha(premultiplyAlpha) { this.inst.premultiplyAlpha = premultiplyAlpha; },
-    unpackAlignment(unpackAlignment) { this.inst.unpackAlignment = parseInt(unpackAlignment, 10); },
-    encoding(mode) { this.inst.encoding = encoding[mode]; },
-  },
-  beforeDestroy() {
-    const { vglNamespace: { textures }, inst, name } = this;
-    if (textures[name] === inst) delete textures[name];
-  },
-  beforeUpdate() {
-    this.inst.needsUpdate = true;
-    this.vglNamespace.update();
+      if (oldName !== undefined) this.vglNamespace.textures.delete(oldName, this.inst);
+      if (name !== undefined) this.vglNamespace.textures.set(name, this.inst);
+    },
+    mapping(mode) {
+      this.inst.mapping = mapping[mode];
+      this.update();
+    },
+    wrapS(mode) {
+      this.inst.wrapS = wrapping[mode];
+      this.update();
+    },
+    wrapT(mode) {
+      this.inst.wrapT = wrapping[mode];
+      this.update();
+    },
+    magFilter(mode) {
+      this.inst.magFilter = filter[mode];
+      this.update();
+    },
+    minFilter(mode) {
+      this.inst.minFilter = filter[mode];
+      this.update();
+    },
+    anisotropy(anisotropy) {
+      this.inst.anisotropy = parseInt(anisotropy, 10);
+      this.update();
+    },
+    format(mode) {
+      this.inst.format = format[mode];
+      this.update();
+    },
+    type(mode) {
+      this.inst.type = type[mode];
+      this.update();
+    },
+    offset(offset) {
+      this.inst.offset.copy(parseVector2(offset));
+      this.update();
+    },
+    repeat(repeat) {
+      this.inst.repeat.copy(parseVector2(repeat));
+      this.update();
+    },
+    rotation(rotation) {
+      this.inst.rotation = parseFloat(rotation);
+      this.update();
+    },
+    center(center) {
+      this.inst.center.copy(parseVector2(center));
+      this.update();
+    },
+    premultiplyAlpha(premultiplyAlpha) {
+      this.inst.premultiplyAlpha = premultiplyAlpha;
+      this.update();
+    },
+    unpackAlignment(unpackAlignment) {
+      this.inst.unpackAlignment = parseInt(unpackAlignment, 10);
+      this.update();
+    },
+    encoding(mode) {
+      this.inst.encoding = encoding[mode];
+      this.update();
+    },
   },
   render(h) {
     return this.$slots.default ? h('div', this.$slots.default) : undefined;


Test patch:
diff --git a/test/core/namespace.spec.js b/test/core/namespace.spec.js
new file mode 100644
index 0000000..8cb2ac9
--- /dev/null
+++ b/test/core/namespace.spec.js
@@ -0,0 +1,347 @@
+import Namespace from '../../src/core/namespace';
+
+describe('Namespace class', () => {
+  let ns;
+  describe('get method', () => {
+    const testValue1 = { a: 1 };
+    const testValue2 = { b: 2 };
+    const testValue3 = { c: 3 };
+    const testValue4 = { d: 4 };
+    const testValue5 = { e: 5 };
+    const testValue6 = { f: 6 };
+    const testValue7 = { g: 7 };
+    const testValue8 = { h: 8 };
+    const testValue9 = { i: 9 };
+    beforeEach(() => {
+      const parent = new Namespace();
+      ns = parent.fork();
+      const child = ns.fork();
+      parent.set('testKey1', testValue1);
+      parent.set('testKey2', testValue2);
+      parent.set('testKey6', testValue7);
+      ns.set('testKey1', testValue3);
+      ns.set('testKey3', testValue4);
+      ns.set('testKey5', testValue5);
+      child.set('testKey5', testValue6);
+      child.set('testKey6', testValue8);
+      child.set('testKey7', testValue9);
+    });
+    test('should return the object', () => {
+      expect(ns.get('testKey3')).toBe(testValue4);
+    });
+    test('should return the self hosting when parent namespace has same key', () => {
+      expect(ns.get('testKey1')).toBe(testValue3);
+    });
+    test('should return the parent hosting object when only parent namespace has the key', () => {
+      expect(ns.get('testKey2')).toBe(testValue2);
+    });
+    test('should return undefined if the key does not exist', () => {
+      expect(ns.get('testKey4')).toBeUndefined();
+    });
+    test('should return undefined if only child namespace has the key', () => {
+      expect(ns.get('testKey7')).toBeUndefined();
+    });
+    test('should return the self hosting object when child namespace has same key', () => {
+      expect(ns.get('testKey5')).toBe(testValue5);
+    });
+    test('should return the parent hosting object when child and parent has same key', () => {
+      expect(ns.get('testKey6')).toBe(testValue7);
+    });
+  });
+  describe('set method', () => {
+    let listener1;
+    let listener2;
+    let listener3;
+    let listener4;
+    let listener5;
+    let listener6;
+    let listener7;
+    let listener8;
+    let listener9;
+    let listener10;
+    let testValue1;
+    let testValue2;
+    beforeEach(() => {
+      listener1 = jest.fn();
+      listener2 = jest.fn();
+      listener3 = jest.fn();
+      listener4 = jest.fn();
+      listener5 = jest.fn();
+      listener6 = jest.fn();
+      listener7 = jest.fn();
+      listener8 = jest.fn();
+      listener9 = jest.fn();
+      listener10 = jest.fn();
+      testValue1 = { j: 10 };
+      testValue2 = { k: 11 };
+      const parent = new Namespace();
+      ns = parent.fork();
+      const child = ns.fork();
+      parent.listen('testKey1', listener1);
+      parent.listen('testKey2', listener2);
+      parent.listen('testKey6', listener8);
+      ns.listen('testKey3', listener3);
+      ns.listen('testKey2', listener4);
+      ns.listen('testKey4', listener5);
+      child.listen('testKey5', listener6);
+      child.listen('testKey4', listener7);
+      child.listen('testKey6', listener9);
+      child.set('testKey7', testValue2);
+      child.listen('testKey7', listener10);
+    });
+    test('should call the listener with value', () => {
+      ns.set('testKey3', testValue1);
+      expect(listener3).toHaveBeenCalledWith(testValue1);
+    });
+    test('should call the self hosting listener even if parent has listener for same key', () => {
+      ns.set('testKey2', testValue1);
+      expect(listener4).toHaveBeenCalledWith(testValue1);
+    });
+    test('should not call the parent hosting listener', () => {
+      ns.set('testKey1', testValue1);
+      expect(listener1).not.toHaveBeenCalled();
+    });
+    test('should not call the parent hosting listener even if the listener exists', () => {
+      ns.set('testKey2', testValue1);
+      expect(listener2).not.toHaveBeenCalled();
+    });
+    test('should call the self hosting listener even if child has listener for same key', () => {
+      ns.set('testKey4', testValue1);
+      expect(listener5).toHaveBeenCalledWith(testValue1);
+    });
+    test('should call the child hosting listener if child has listener for the key', () => {
+      ns.set('testKey5', testValue1);
+      expect(listener6).toHaveBeenCalledWith(testValue1);
+    });
+    test('should call the child hosting listener even if child has listener for same key', () => {
+      ns.set('testKey4', testValue1);
+      expect(listener7).toHaveBeenCalledWith(testValue1);
+    });
+    test('should not call the child hosting listener when child has value for the key', () => {
+      ns.set('testKey7', testValue1);
+      expect(listener10).not.toHaveBeenCalled();
+    });
+    test('should call only child hosting listener when child and parent have listners', () => {
+      ns.set('testKey6', testValue1);
+      expect(listener8).not.toHaveBeenCalled();
+      expect(listener9).toHaveBeenCalledWith(testValue1);
+    });
+  });
+  describe('delete method', () => {
+    let listener1;
+    let listener2;
+    let listener3;
+    let listener4;
+    let listener5;
+    let listener6;
+    let testValue1;
+    let testValue2;
+    let testValue3;
+    let testValue4;
+    let testValue5;
+    let testValue6;
+    let testValue7;
+    let testValue8;
+    let testValue9;
+    beforeEach(() => {
+      listener1 = jest.fn();
+      listener2 = jest.fn();
+      listener3 = jest.fn();
+      listener4 = jest.fn();
+      listener5 = jest.fn();
+      listener6 = jest.fn();
+      testValue1 = { j: 10 };
+      testValue2 = { k: 11 };
+      testValue3 = { l: 12 };
+      testValue4 = { m: 13 };
+      testValue5 = { n: 14 };
+      testValue6 = { o: 15 };
+      testValue7 = { p: 16 };
+      testValue8 = { q: 17 };
+      testValue9 = { r: 18 };
+      const parent = new Namespace();
+      ns = parent.fork();
+      const child = ns.fork();
+      ns.set('testKey1', testValue1);
+      ns.set('testKey2', testValue2);
+      ns.listen('testKey2', listener1);
+      parent.set('testKey3', testValue4);
+      ns.set('testKey3', testValue3);
+      parent.set('testKey4', testValue6);
+      ns.set('testKey4', testValue5);
+      parent.listen('testKey4', listener3);
+      ns.listen('testKey4', listener2);
+      ns.set('testKey5', testValue8);
+      child.set('testKey5', testValue7);
+      ns.listen('testKey5', listener4);
+      child.listen('testKey5', listener5);
+      ns.set('testKey6', testValue9);
+      child.listen('testKey6', listener6);
+    });
+    test('should delete the self hosting object', () => {
+      ns.delete('testKey1', testValue1);
+      expect(ns.get('testKey1')).toBeUndefined();
+    });
+    test('should not delete if passed value does not match existing one', () => {
+      ns.delete('testKey1', { j: 10 });
+      expect(ns.get('testKey1')).toBe(testValue1);
+    });
+    test('should call the hosting listener with undefined', () => {
+      ns.delete('testKey2', testValue2);
+      expect(listener1).toHaveBeenCalledWith(undefined);
+    });
+    test('should delete only the self hosting object when parent namespace has same key', () => {
+      ns.delete('testKey3', testValue3);
+      expect(ns.get('testKey3')).toBe(testValue4);
+    });
+    test('should call the listener with parent value when parent has same key', () => {
+      ns.delete('testKey4', testValue5);
+      expect(listener2).toHaveBeenCalledWith(testValue6);
+    });
+    test('should not call the parent hosting listener', () => {
+      ns.delete('testKey4', testValue5);
+      expect(listener3).not.toHaveBeenCalled();
+    });
+    test('should not call the child hosting listener when child has an object for the key', () => {
+      ns.delete('testKey5', testValue8);
+      expect(listener5).not.toHaveBeenCalled();
+    });
+    test('should call the self hosting listener even if child has an object for the key', () => {
+      ns.delete('testKey5', testValue8);
+      expect(listener4).toHaveBeenCalledWith(undefined);
+    });
+    test('should call the child hosting listener when child has no objects for the key', () => {
+      ns.delete('testKey6', testValue9);
+      expect(listener6).toHaveBeenCalledWith(undefined);
+    });
+  });
+  describe('listen method', () => {
+    let listener1;
+    let listener2;
+    let listener3;
+    let listener4;
+    let listener5;
+    let listener6;
+    let testValue1;
+    beforeEach(() => {
+      ns = new Namespace();
+      listener1 = jest.fn();
+      listener2 = jest.fn();
+      listener3 = jest.fn();
+      listener4 = jest.fn();
+      listener5 = jest.fn();
+      listener6 = jest.fn();
+      testValue1 = { s: 19 };
+    });
+    test('should register multiple callbacks for same key', () => {
+      ns.listen('testKey', listener1);
+      ns.listen('testKey', listener2);
+      ns.listen('testKey', listener3);
+      ns.set('testKey', testValue1);
+      expect(listener1).toHaveBeenCalledWith(testValue1);
+      expect(listener2).toHaveBeenCalledWith(testValue1);
+      expect(listener3).toHaveBeenCalledWith(testValue1);
+    });
+    test('should not register duplicate callbacks for same key', () => {
+      ns.listen('testKey', listener1);
+      ns.listen('testKey', listener1);
+      ns.set('testKey', testValue1);
+      expect(listener1).toHaveBeenNthCalledWith(1, testValue1);
+    });
+    test('should register a callback as global listener', () => {
+      ns.listen(listener4);
+      ns.listen(listener6);
+      ns.set('testKey', testValue1);
+      expect(listener4).toHaveBeenNthCalledWith(1, ns);
+      expect(listener6).toHaveBeenNthCalledWith(1, ns);
+    });
+    test('should not register duplicate callbacks as global listener', () => {
+      ns.listen(listener5);
+      ns.listen(listener5);
+      ns.set('testKey', testValue1);
+      expect(listener5).toHaveBeenNthCalledWith(1, ns);
+    });
+  });
+  describe('unlisten method', () => {
+    let listener1;
+    let listener2;
+    let listener3;
+    let listener4;
+    let listener5;
+    let testValue1;
+    beforeEach(() => {
+      listener1 = jest.fn();
+      listener2 = jest.fn();
+      listener3 = jest.fn();
+      listener4 = jest.fn();
+      listener5 = jest.fn();
+      testValue1 = { t: 20 };
+      ns = new Namespace();
+      ns.listen('testKey1', listener1);
+      ns.listen('testKey1', listener2);
+      ns.listen('testKey1', listener3);
+      ns.listen(listener4);
+      ns.listen(listener5);
+    });
+    test('should unregister a callback', () => {
+      ns.unlisten('testKey1', listener1);
+      ns.set('testKey1', testValue1);
+      expect(listener1).not.toHaveBeenCalled();
+      expect(listener2).toHaveBeenCalledWith(testValue1);
+      expect(listener3).toHaveBeenCalledWith(testValue1);
+    });
+    test('should unregister callbacks', () => {
+      ns.unlisten('testKey1', listener1);
+      ns.unlisten('testKey1', listener2);
+      ns.unlisten('testKey1', listener3);
+      ns.set('testKey1', testValue1);
+      expect(listener1).not.toHaveBeenCalled();
+      expect(listener2).not.toHaveBeenCalled();
+      expect(listener3).not.toHaveBeenCalled();
+    });
+    test('should unregister a global callback', () => {
+      ns.unlisten(listener4);
+      ns.set('testKey1', testValue1);
+      expect(listener4).not.toHaveBeenCalled();
+      expect(listener5).toHaveBeenCalledWith(ns);
+    });
+  });
+  describe('destroy method', () => {
+    let child;
+    let listener1;
+    let testValue1;
+    beforeEach(() => {
+      listener1 = jest.fn();
+      testValue1 = { u: 21 };
+      ns = new Namespace();
+      child = ns.fork();
+      child.listen('testKey1', listener1);
+    });
+    test('should unlink listeners', () => {
+      child.destroy();
+      ns.set('testKey1', testValue1);
+      expect(listener1).not.toHaveBeenCalled();
+    });
+  });
+  describe('keys method', () => {
+    let parent;
+    let child;
+    beforeEach(() => {
+      parent = new Namespace();
+      ns = parent.fork();
+      child = ns.fork();
+    });
+    test('should return keys hosted by self and parent namespace', () => {
+      parent.set('testKey3', { x: 24 });
+      parent.set('testKey4', { y: 25 });
+      parent.set('testKey2', { z: 26 });
+      ns.set('testKey1', { v: 22 });
+      ns.set('testKey2', { w: 23 });
+      ns.set('testKey6', { ac: 29 });
+      child.set('testKey5', { aa: 27 });
+      child.set('testKey1', { ab: 28 });
+      expect(ns.keys().sort())
+        .toEqual(['testKey1', 'testKey2', 'testKey3', 'testKey4', 'testKey6']);
+    });
+  });
+});
diff --git a/test/core/tree.spec.js b/test/core/tree.spec.js
new file mode 100644
index 0000000..0589d6d
--- /dev/null
+++ b/test/core/tree.spec.js
@@ -0,0 +1,89 @@
+import { Object3D } from 'three';
+import Tree from '../../src/core/tree';
+
+describe('Tree class', () => {
+  let root1;
+  let root2;
+  let parent;
+  const parentInst = new Object3D();
+  let child1;
+  const child1Inst = new Object3D();
+  let child2;
+  const child2Inst = new Object3D();
+  let grandChild1;
+  const grandChild1Inst = new Object3D();
+  let grandChild2;
+  const grandChild2Inst = new Object3D();
+  let listener1;
+  let listener2;
+  let listener3;
+  let listener4;
+  let listener5;
+  let listener6;
+  let listener7;
+  let listener8;
+  let listener9;
+  beforeEach(() => {
+    listener1 = jest.fn();
+    listener2 = jest.fn();
+    listener3 = jest.fn();
+    listener4 = jest.fn();
+    listener5 = jest.fn();
+    listener6 = jest.fn();
+    listener7 = jest.fn();
+    listener8 = jest.fn();
+    listener9 = jest.fn();
+    root1 = new Tree();
+    root2 = new Tree();
+    parent = new Tree(root1, () => parentInst);
+    child1 = new Tree(parent, () => child1Inst);
+    child2 = new Tree(parent, () => child2Inst);
+    grandChild1 = new Tree(child1, () => grandChild1Inst);
+    grandChild2 = new Tree(child2, () => grandChild2Inst);
+    root1.listen(listener1);
+    root1.listen(listener2);
+    root2.listen(listener9);
+    child1.listen(listener3);
+    child1.listen(listener4);
+    child2.listen(listener5);
+    grandChild1.listen(listener6);
+    grandChild2.listen(listener7);
+    grandChild2.listen(listener8);
+  });
+
+  test('inst method should returns the result of getter function passed to constructor', () => {
+    expect(root1.inst()).toBeUndefined();
+    expect(root2.inst()).toBeUndefined();
+    expect(parent.inst()).toBe(parentInst);
+    expect(child1.inst()).toBe(child1Inst);
+    expect(child2.inst()).toBe(child2Inst);
+    expect(grandChild1.inst()).toBe(grandChild1Inst);
+    expect(grandChild2.inst()).toBe(grandChild2Inst);
+  });
+  test('emit method should trigger all listeners in tree', () => {
+    child1.emit();
+    expect(listener1).toHaveBeenCalledTimes(1);
+    expect(listener2).toHaveBeenCalledTimes(1);
+    expect(listener3).toHaveBeenCalledTimes(1);
+    expect(listener4).toHaveBeenCalledTimes(1);
+    expect(listener5).toHaveBeenCalledTimes(1);
+    expect(listener6).toHaveBeenCalledTimes(1);
+    expect(listener7).toHaveBeenCalledTimes(1);
+    expect(listener8).toHaveBeenCalledTimes(1);
+    expect(listener9).not.toHaveBeenCalled();
+  });
+  test('unlisten method should remove the listener', () => {
+    grandChild2.unlisten(listener7);
+    child2.unlisten(listener5);
+    child1.emit();
+    expect(listener1).toHaveBeenCalledTimes(1);
+    expect(listener2).toHaveBeenCalledTimes(1);
+    expect(listener3).toHaveBeenCalledTimes(1);
+    expect(listener4).toHaveBeenCalledTimes(1);
+    expect(listener6).toHaveBeenCalledTimes(1);
+    expect(listener8).toHaveBeenCalledTimes(1);
+    expect(listener7).not.toHaveBeenCalled();
+    expect(listener5).not.toHaveBeenCalled();
+    expect(listener9).not.toHaveBeenCalled();
+  });
+});
diff --git a/test/helpers/vgl-box-helper.spec.js b/test/helpers/vgl-box-helper.spec.js
index 2387a79..ebb89e5 100644
--- a/test/helpers/vgl-box-helper.spec.js
+++ b/test/helpers/vgl-box-helper.spec.js
@@ -6,7 +6,7 @@ describe('VglBoxHelper', () => {
   let inject;
   beforeEach(() => {
     const { vglNamespace } = new Vue({ parent: new Vue(VglNamespace), inject: ['vglNamespace'] });
-    vglNamespace.object3ds.testSphere = new Mesh(new SphereBufferGeometry(5.4, 31.3, 30.9));
+    vglNamespace.object3ds.set('testSphere', new Mesh(new SphereBufferGeometry(5.4, 31.3, 30.9)));
     inject = { vglNamespace: { default: vglNamespace } };
   });
   test('the inst property should be an instance of the BoxHelper', () => {
@@ -21,7 +21,6 @@ describe('VglBoxHelper', () => {
     test('in case with default values', () => {
       const vm = new (Vue.extend(VglBoxHelper))({ inject, propsData: { object: 'testSphere' } });
       const expected = new BoxHelper(new Mesh(new SphereBufferGeometry(5.4, 31.3, 30.9)));
-      vm.vglNamespace.beforeRender[0]();
       expect(vm.inst.geometry)
         .toHaveProperty('attributes', expected.geometry.attributes);
       expect(vm.inst.material).toHaveProperty('color', expected.material.color);
diff --git a/test/helpers/vgl-camera-helper.spec.js b/test/helpers/vgl-camera-helper.spec.js
index e16da03..1616ee5 100644
--- a/test/helpers/vgl-camera-helper.spec.js
+++ b/test/helpers/vgl-camera-helper.spec.js
@@ -6,13 +6,12 @@ describe('VglCameraHelper', () => {
   let inject;
   beforeEach(() => {
     const { vglNamespace } = new Vue({ parent: new Vue(VglNamespace), inject: ['vglNamespace'] });
-    vglNamespace.cameras.testCamera = new PerspectiveCamera(52, undefined, 0.11, 2100);
+    vglNamespace.cameras.set('testCamera', new PerspectiveCamera(52, undefined, 0.11, 2100));
     inject = { vglNamespace: { default: vglNamespace } };
   });
   test('the inst property should be an instance of the CameraHelper', () => {
     const vm = new (Vue.extend(VglCameraHelper))({ inject, propsData: { camera: 'testCamera' } });
-    vm.vglNamespace.beforeRender[0]();
-    expect(vm.inst.children[0]).toBeInstanceOf(CameraHelper);
+    expect(vm.inst).toBeInstanceOf(CameraHelper);
   });
   test('the component should have common props with VglObject3d', () => {
     const { $props } = new (Vue.extend(VglObject3d))({ inject });
@@ -22,8 +21,7 @@ describe('VglCameraHelper', () => {
   test('the state of the inst object should be specified by the camera', () => {
     const vm = new (Vue.extend(VglCameraHelper))({ inject, propsData: { camera: 'testCamera' } });
     const expected = new CameraHelper(new PerspectiveCamera(52, undefined, 0.11, 2100));
-    vm.vglNamespace.beforeRender[0]();
-    expect(vm.inst.children[0].geometry.getAttribute('position'))
+    expect(vm.inst.geometry.getAttribute('position'))
       .toEqual(expected.geometry.getAttribute('position'));
   });
 });
diff --git a/test/helpers/vgl-directional-light-helper.spec.js b/test/helpers/vgl-directional-light-helper.spec.js
index 9941223..fa5fc88 100644
--- a/test/helpers/vgl-directional-light-helper.spec.js
+++ b/test/helpers/vgl-directional-light-helper.spec.js
@@ -6,14 +6,13 @@ describe('VglDirectionalLightHelper', () => {
   let inject;
   beforeEach(() => {
     const { vglNamespace } = new Vue({ parent: new Vue(VglNamespace), inject: ['vglNamespace'] });
-    vglNamespace.object3ds.testLight = new DirectionalLight(0xe2f3b4);
-    vglNamespace.object3ds.testLight.position.set(3.8, 2.1, -0.5);
+    vglNamespace.object3ds.set('testLight', new DirectionalLight(0xe2f3b4));
+    vglNamespace.object3ds.get('testLight').position.set(3.8, 2.1, -0.5);
     inject = { vglNamespace: { default: vglNamespace } };
   });
   test('the inst property should be an instance of DirectionalLightelper', () => {
     const vm = new (Vue.extend(VglDirectionalLightHelper))({ inject, propsData: { light: 'testLight' } });
-    vm.vglNamespace.beforeRender[0]();
-    expect(vm.inst.children[0]).toBeInstanceOf(DirectionalLightHelper);
+    expect(vm.inst).toBeInstanceOf(DirectionalLightHelper);
   });
   test('the component should have common props with VglObject3d', () => {
     const { $props } = new (Vue.extend(VglObject3d))({ inject });
@@ -23,15 +22,14 @@ describe('VglDirectionalLightHelper', () => {
   describe('the state of the inst object should be specified by props', () => {
     test('in case with default values', () => {
       const vm = new (Vue.extend(VglDirectionalLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       const expectedLight = new DirectionalLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new DirectionalLightHelper(expectedLight);
-      expect(vm.inst.children[0].lightPlane.geometry.getAttribute('position'))
+      expect(vm.inst.lightPlane.geometry.getAttribute('position'))
         .toEqual(expected.lightPlane.geometry.getAttribute('position'));
-      expect(vm.inst.children[0].lightPlane.material)
+      expect(vm.inst.lightPlane.material)
         .toHaveProperty('color', expected.lightPlane.material.color);
-      expect(vm.inst.children[0].targetLine.geometry.getAttribute('position'))
+      expect(vm.inst.targetLine.geometry.getAttribute('position'))
         .toEqual(expected.targetLine.geometry.getAttribute('position'));
     });
     test('in case props given', () => {
@@ -43,35 +41,32 @@ describe('VglDirectionalLightHelper', () => {
           size: '18.7',
         },
       });
-      vm.vglNamespace.beforeRender[0]();
       const expectedLight = new DirectionalLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new DirectionalLightHelper(expectedLight, 18.7, 0xddf2ee);
-      expect(vm.inst.children[0].lightPlane.geometry.getAttribute('position'))
+      expect(vm.inst.lightPlane.geometry.getAttribute('position'))
         .toEqual(expected.lightPlane.geometry.getAttribute('position'));
-      expect(vm.inst.children[0].lightPlane.material)
+      expect(vm.inst.lightPlane.material)
         .toHaveProperty('color', expected.lightPlane.material.color);
     });
-    test('in case size changes', () => {
+    test('in case size changes', async () => {
       const vm = new (Vue.extend(VglDirectionalLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       vm.size = 18.7;
-      vm.vglNamespace.beforeRender[0]();
+      await Vue.nextTick();
       const expectedLight = new DirectionalLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new DirectionalLightHelper(expectedLight, 18.7);
-      expect(vm.inst.children[0].lightPlane.geometry.getAttribute('position'))
+      expect(vm.inst.lightPlane.geometry.getAttribute('position'))
         .toEqual(expected.lightPlane.geometry.getAttribute('position'));
     });
-    test('in case color changes', () => {
+    test('in case color changes', async () => {
       const vm = new (Vue.extend(VglDirectionalLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       vm.color = '#ddf2ee';
-      vm.vglNamespace.beforeRender[0]();
+      await Vue.nextTick();
       const expectedLight = new DirectionalLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new DirectionalLightHelper(expectedLight, undefined, 0xddf2ee);
-      expect(vm.inst.children[0].lightPlane.material)
+      expect(vm.inst.lightPlane.material)
         .toHaveProperty('color', expected.lightPlane.material.color);
     });
   });
diff --git a/test/helpers/vgl-rect-area-light-helper.spec.js b/test/helpers/vgl-rect-area-light-helper.spec.js
index fdbd973..5bbcbfc 100644
--- a/test/helpers/vgl-rect-area-light-helper.spec.js
+++ b/test/helpers/vgl-rect-area-light-helper.spec.js
@@ -6,14 +6,13 @@ describe('VglRectAreaLightHelper:', () => {
   let inject;
   beforeEach(() => {
     const { vglNamespace } = new Vue({ parent: new Vue(VglNamespace), inject: ['vglNamespace'] });
-    vglNamespace.object3ds.testLight = new RectAreaLight(0xe2f3b4);
-    vglNamespace.object3ds.testLight.position.set(3.8, 2, 0.5);
+    vglNamespace.object3ds.set('testLight', new RectAreaLight(0xe2f3b4));
+    vglNamespace.object3ds.get('testLight').position.set(3.8, 2, 0.5);
     inject = { vglNamespace: { default: vglNamespace } };
   });
   test('the inst property should be an instance of RectAreaLightelper', () => {
     const vm = new (Vue.extend(VglRectAreaLightHelper))({ inject, propsData: { light: 'testLight' } });
-    vm.vglNamespace.beforeRender[0]();
-    expect(vm.inst.children[0]).toBeInstanceOf(RectAreaLightHelper);
+    expect(vm.inst).toBeInstanceOf(RectAreaLightHelper);
   });
   test('the component should have common props with VglObject3d', () => {
     const { $props } = new (Vue.extend(VglObject3d))({ inject });
@@ -23,13 +22,12 @@ describe('VglRectAreaLightHelper:', () => {
   describe('the state of the inst object should be specified by props', () => {
     test('in case with default values', () => {
       const vm = new (Vue.extend(VglRectAreaLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       const expectedLight = new RectAreaLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, 0.5);
       const expected = new RectAreaLightHelper(expectedLight);
-      expect(vm.inst.children[0].geometry.getAttribute('position'))
+      expect(vm.inst.geometry.getAttribute('position'))
         .toEqual(expected.geometry.getAttribute('position'));
-      expect(vm.inst.children[0].material)
+      expect(vm.inst.material)
         .toHaveProperty('color', expected.material.color);
     });
     test('in case props given', () => {
@@ -40,24 +38,22 @@ describe('VglRectAreaLightHelper:', () => {
           color: '#ddf2ee',
         },
       });
-      vm.vglNamespace.beforeRender[0]();
       const expectedLight = new RectAreaLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, 0.5);
       const expected = new RectAreaLightHelper(expectedLight, 0xddf2ee);
-      expect(vm.inst.children[0].geometry.getAttribute('position'))
+      expect(vm.inst.geometry.getAttribute('position'))
         .toEqual(expected.geometry.getAttribute('position'));
-      expect(vm.inst.children[0].material)
+      expect(vm.inst.material)
         .toHaveProperty('color', expected.material.color);
     });
-    test('in case color changes', () => {
+    test('in case color changes', async () => {
       const vm = new (Vue.extend(VglRectAreaLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       vm.color = '#ddf2ee';
-      vm.vglNamespace.beforeRender[0]();
+      await Vue.nextTick();
       const expectedLight = new RectAreaLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, 0.5);
       const expected = new RectAreaLightHelper(expectedLight, 0xddf2ee);
-      expect(vm.inst.children[0].material)
+      expect(vm.inst.material)
         .toHaveProperty('color', expected.material.color);
     });
   });
diff --git a/test/helpers/vgl-spot-light-helper.spec.js b/test/helpers/vgl-spot-light-helper.spec.js
index 3fff9ee..6e71600 100644
--- a/test/helpers/vgl-spot-light-helper.spec.js
+++ b/test/helpers/vgl-spot-light-helper.spec.js
@@ -6,14 +6,13 @@ describe('VglSpotLightHelper:', () => {
   let inject;
   beforeEach(() => {
     const { vglNamespace } = new Vue({ parent: new Vue(VglNamespace), inject: ['vglNamespace'] });
-    vglNamespace.object3ds.testLight = new SpotLight(0xe2f3b4, undefined, 22.3, 1.1);
-    vglNamespace.object3ds.testLight.position.set(3.8, 2, -0.5);
+    vglNamespace.object3ds.set('testLight', new SpotLight(0xe2f3b4, undefined, 22.3, 1.1));
+    vglNamespace.object3ds.get('testLight').position.set(3.8, 2, -0.5);
     inject = { vglNamespace: { default: vglNamespace } };
   });
   test('the inst property should be an instance of SpotLightelper', () => {
     const vm = new (Vue.extend(VglSpotLightHelper))({ inject, propsData: { light: 'testLight' } });
-    vm.vglNamespace.beforeRender[0]();
-    expect(vm.inst.children[0]).toBeInstanceOf(SpotLightHelper);
+    expect(vm.inst).toBeInstanceOf(SpotLightHelper);
   });
   test('the component should have common props with VglObject3d', () => {
     const { $props } = new (Vue.extend(VglObject3d))({ inject });
@@ -23,13 +22,12 @@ describe('VglSpotLightHelper:', () => {
   describe('the state of the inst object should be specified by props', () => {
     test('in case with default values', () => {
       const vm = new (Vue.extend(VglSpotLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       const expectedLight = new SpotLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new SpotLightHelper(expectedLight);
-      expect(vm.inst.children[0].cone.geometry.getAttribute('position'))
+      expect(vm.inst.cone.geometry.getAttribute('position'))
         .toEqual(expected.cone.geometry.getAttribute('position'));
-      expect(vm.inst.children[0].cone.material)
+      expect(vm.inst.cone.material)
         .toHaveProperty('color', expected.cone.material.color);
     });
     test('in case props given', () => {
@@ -40,24 +38,22 @@ describe('VglSpotLightHelper:', () => {
           color: '#ddf2ee',
         },
       });
-      vm.vglNamespace.beforeRender[0]();
       const expectedLight = new SpotLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new SpotLightHelper(expectedLight, 0xddf2ee);
-      expect(vm.inst.children[0].cone.geometry.getAttribute('position'))
+      expect(vm.inst.cone.geometry.getAttribute('position'))
         .toEqual(expected.cone.geometry.getAttribute('position'));
-      expect(vm.inst.children[0].cone.material)
+      expect(vm.inst.cone.material)
         .toHaveProperty('color', expected.cone.material.color);
     });
-    test('in case color changes', () => {
+    test('in case color changes', async () => {
       const vm = new (Vue.extend(VglSpotLightHelper))({ inject, propsData: { light: 'testLight' } });
-      vm.vglNamespace.beforeRender[0]();
       vm.color = '#ddf2ee';
-      vm.vglNamespace.beforeRender[0]();
+      await Vue.nextTick();
       const expectedLight = new SpotLight(0xe2f3b4);
       expectedLight.position.set(3.8, 2.1, -0.5);
       const expected = new SpotLightHelper(expectedLight, 0xddf2ee);
-      expect(vm.inst.children[0].cone.material)
+      expect(vm.inst.cone.material)
         .toHaveProperty('color', expected.cone.material.color);
     });
   });
diff --git a/test/renderers/vgl-renderer.spec.js b/test/renderers/vgl-renderer.spec.js
index 0d73491..f42622d 100644
--- a/test/renderers/vgl-renderer.spec.js
+++ b/test/renderers/vgl-renderer.spec.js
@@ -1,7 +1,6 @@
 import Vue from 'vue/dist/vue';
 import { WebGLRenderer, Scene, PerspectiveCamera } from 'three';
 import { VglRenderer } from '../../src';
-import { scenePropRequiredMessage, cameraPropRequiredMessage } from '../../src/messages';
 
 jest.mock('three');
 WebGLRenderer.mockImplementation(function MockedWebGLRenderer() {
@@ -79,34 +78,67 @@ describe('VglRenderer', () => {
     await vm.$nextTick();
     expect(vm.$el.querySelector('canvas')).toBe(vm.inst.domElement);
   });
+  describe('render method', () => {
+    test('should be called with unnamed scene and camera', async () => {
+      const vm = new Vue(VglRenderer).$mount();
+      const scene = new Scene();
+      const camera = new PerspectiveCamera();
+      vm.vglNamespace.scenes.set(undefined, scene);
+      vm.vglNamespace.cameras.set(undefined, camera);
+      await vm.$nextTick();
+      expect(vm.inst.render).toHaveBeenLastCalledWith(scene, camera);
+    });
+    test('should render with named scene and camera', async () => {
+      const vm = new (Vue.extend(VglRenderer))({ propsData: { camera: 'c1', scene: 's1' } }).$mount();
+      const scene = new Scene();
+      const camera = new PerspectiveCamera();
+      vm.vglNamespace.scenes.set('s1', scene);
+      vm.vglNamespace.scenes.set('s2', new Scene());
+      vm.vglNamespace.cameras.set('c1', camera);
+      vm.vglNamespace.cameras.set('c2', new PerspectiveCamera());
+      await vm.$nextTick();
+      expect(vm.inst.render).toHaveBeenLastCalledWith(scene, camera);
+    });
+    test('should render with replaced scene and camera', async () => {
+      const vm = new (Vue.extend(VglRenderer))({ propsData: { camera: 'c1', scene: 's1' } }).$mount();
+      const scene = new Scene();
+      const camera = new PerspectiveCamera();
+      vm.vglNamespace.scenes.set('s1', new Scene());
+      vm.vglNamespace.cameras.set('c1', new PerspectiveCamera());
+      await vm.$nextTick();
+      vm.vglNamespace.scenes.set('s1', scene);
+      vm.vglNamespace.cameras.set('c1', camera);
+      await vm.$nextTick();
+      expect(vm.inst.render).toHaveBeenLastCalledWith(scene, camera);
+    });
+  });
   describe('specifying scene and camera to render', () => {
     beforeAll(() => { Vue.config.errorHandler = jest.fn(); });
     afterEach(() => Vue.config.errorHandler.mockClear());
     afterAll(() => { Vue.config.errorHandler = undefined; });
     test('an error shoud be thrown when multiple scenes are defined but scene prop is undefined', async () => {
       const vm = new Vue(VglRenderer);
-      Object.assign(vm.vglNamespace.scenes, { s1: new Scene(), s2: new Scene() });
+      vm.vglNamespace.scenes.set('s1', new Scene());
+      vm.vglNamespace.scenes.set('s2', new Scene());
       await vm.$mount().$nextTick();
-      expect(Vue.config.errorHandler).toHaveBeenCalledWith(new ReferenceError(scenePropRequiredMessage), vm, 'nextTick');
+      expect(Vue.config.errorHandler).toHaveBeenCalledWith(expect.any(ReferenceError), vm, 'nextTick');
     });
     test('any errors shoud not be thrown when scene prop is undefined but only one scene is defined', async () => {
       const vm = new Vue(VglRenderer);
-      Object.assign(vm.vglNamespace.scenes, { s1: new Scene() });
+      vm.vglNamespace.scenes.set('s1', new Scene());
       await vm.$mount().$nextTick();
       expect(Vue.config.errorHandler).not.toHaveBeenCalled();
     });
     test('an error shoud be thrown when multiple cameras are defined but camera prop is undefined', async () => {
       const vm = new Vue(VglRenderer);
-      Object.assign(vm.vglNamespace.cameras, {
-        c1: new PerspectiveCamera(),
-        c2: new PerspectiveCamera(),
-      });
+      vm.vglNamespace.cameras.set('c1', new PerspectiveCamera());
+      vm.vglNamespace.cameras.set('c2', new PerspectiveCamera());
       await vm.$mount().$nextTick();
-      expect(Vue.config.errorHandler).toHaveBeenCalledWith(new ReferenceError(cameraPropRequiredMessage), vm, 'nextTick');
+      expect(Vue.config.errorHandler).toHaveBeenCalledWith(expect.any(ReferenceError), vm, 'nextTick');
     });
     test('any errors shoud not be thrown when camera prop is undefined but only one camera is defined', async () => {
       const vm = new Vue(VglRenderer);
-      Object.assign(vm.vglNamespace.cameras, { c1: new PerspectiveCamera() });
+      vm.vglNamespace.cameras.set('c1', new PerspectiveCamera());
       await vm.$mount().$nextTick();
       expect(Vue.config.errorHandler).not.toHaveBeenCalled();
     });
diff --git a/test/scenes/vgl-scene.spec.js b/test/scenes/vgl-scene.spec.js
index 4f70af6..313dd86 100644
--- a/test/scenes/vgl-scene.spec.js
+++ b/test/scenes/vgl-scene.spec.js
@@ -1,5 +1,5 @@
 import Vue from 'vue/dist/vue';
-import { Scene } from 'three';
+import { Scene, TextureLoader } from 'three';
 import { VglScene, VglObject3d, VglNamespace } from '../../src';
 
 describe('VglScene', () => {
@@ -20,6 +20,12 @@ describe('VglScene', () => {
     const { inst } = new (Vue.extend(VglScene))({ inject, propsData: { backgroundColor: '#8aeda3' } });
     expect(inst.background.getHex()).toBe(0x8aeda3);
   });
+  test('the propertie "backgroundTexture" of the instance should be specified by props', () => {
+    const texture = new TextureLoader().load('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
+    inject.vglNamespace.default.textures.set('test tx', texture);
+    const { inst } = new (Vue.extend(VglScene))({ inject, propsData: { backgroundTexture: 'test tx' } });
+    expect(inst.background).toBe(texture);
+  });
   test('the propertie "fog" of the instance should be specified by props', () => {
     const { inst } = new (Vue.extend(VglScene))({ inject, propsData: { fog: '#8aeda3 10 100' } });
     expect(inst.fog.color.getHex()).toBe(0x8aeda3);
@@ -32,6 +38,24 @@ describe('VglScene', () => {
     await vm.$nextTick();
     expect(vm.inst.background.getHex()).toBe(0x6751f2);
   });
+  test('the properties "backgroundTexture" of the instance should change after props change', async () => {
+    const texture = new TextureLoader().load('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
+    inject.vglNamespace.default.textures.set('test tx', texture);
+    const vm = new (Vue.extend(VglScene))({ inject });
+    vm.backgroundTexture = 'test tx';
+    await vm.$nextTick();
+    expect(vm.inst.background).toBe(texture);
+  });
+  test('the properties "backgroundTexture" of the instance should change after texture change', async () => {
+    const texture1 = new TextureLoader().load('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
+    const texture2 = new TextureLoader().load('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
+    inject.vglNamespace.default.textures.set('test tx1', texture1);
+    inject.vglNamespace.default.textures.set('test tx2', texture2);
+    const vm = new (Vue.extend(VglScene))({ inject, propsData: { backgroundTexture: 'test tx1' } });
+    vm.backgroundTexture = 'test tx2';
+    await vm.$nextTick();
+    expect(vm.inst.background).toBe(texture2);
+  });
   test('the properties "fog" of the instance should change after props change', async () => {
     const vm = new (Vue.extend(VglScene))({ inject });
     vm.fog = '#6751f2 10 100';
@@ -40,4 +64,23 @@ describe('VglScene', () => {
     expect(vm.inst.fog.near).toBe(parseFloat(10));
     expect(vm.inst.fog.far).toBe(parseFloat(100));
   });
+  describe('the instance should be registered to scenes namespace', () => {
+    test('after created', () => {
+      const vm = new (Vue.extend(VglScene))({ inject, propsData: { name: 'test name 1' } });
+      expect(inject.vglNamespace.default.scenes.get('test name 1')).toBe(vm.inst);
+    });
+    test('after the name prop changes', async () => {
+      const vm = new (Vue.extend(VglScene))({ inject, propsData: { name: 'test scene 1' } });
+      vm.name = 'test scene 2';
+      await Vue.nextTick();
+      expect(inject.vglNamespace.default.scenes.get('test scene 1')).toBeUndefined();
+      expect(inject.vglNamespace.default.scenes.get('test scene 2')).toBe(vm.inst);
+    });
+    test('after destroyed', async () => {
+      const vm = new (Vue.extend(VglScene))({ inject, propsData: { name: 'test name 1' } });
+      vm.$destroy();
+      await Vue.nextTick();
+      expect(inject.vglNamespace.default.scenes.get('test name 1')).toBeUndefined();
+    });
+  });
 });