├── .gitignore
├── LICENSE
├── Readme.md
├── build
├── aframe-instancing.js
└── aframe-instancing.min.js
├── index.html
├── index.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Takahiro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # A-Frame instancing component
2 |
3 | This is an example component for A-Frame to take advantage of three.js instanced rendering support. This is useful if you have to render a large number of objects with the same geometry and material but with different world transformations. This will help you to reduce the number of draw calls and thus improve the overall rendering performance in your application.
4 |
5 | This is a proof of concept that replicates a large number of spheres with random colors and transformations in just one draw call.
6 |
7 | ### Component API
8 |
9 | | Property | Description | Default Value |
10 | | -------- | ----------- | ------------- |
11 | | `count` | The number of geometries to instance | `10000` |
12 | | `frustumCulled` | Attempt to hide the instances when out of view of the camera frustum to improve performance. Setting to `true` doesn't work well at the moment because the camera frustum doesn't "know" where the instanced geometries are located. | `false` |
13 |
14 | ### Installation and Usage
15 |
16 | #### Browser
17 |
18 | Install and use by directly including the [browser files](dist):
19 |
20 | ```html
21 |
22 | My A-Frame Scene
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ```
33 |
34 | ### Work in progress
35 | This is a work in progress and could be improved in the future, for example:
36 | - Allow other primitive geometries to be instanced in addition to the default sphere geometry
37 | - Provide support for more advanced materials
38 | - Provide support for imported glTF models to be instanced
39 | - Provide improved support for frustumCulling ([further research on frustum culling with instancing](https://stackoverflow.com/questions/45378920/three-js-with-instancing-cant-get-it-to-work-without-frustumculling-false))
40 |
--------------------------------------------------------------------------------
/build/aframe-instancing.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 |
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 |
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId])
10 | /******/ return installedModules[moduleId].exports;
11 |
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ exports: {},
15 | /******/ id: moduleId,
16 | /******/ loaded: false
17 | /******/ };
18 |
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 |
22 | /******/ // Flag the module as loaded
23 | /******/ module.loaded = true;
24 |
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 |
29 |
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 |
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 |
36 | /******/ // __webpack_public_path__
37 | /******/ __webpack_require__.p = "";
38 |
39 | /******/ // Load entry module and return exports
40 | /******/ return __webpack_require__(0);
41 | /******/ })
42 | /************************************************************************/
43 | /******/ ([
44 | /* 0 */
45 | /***/ (function(module, exports) {
46 |
47 | /**
48 | * @author Takahiro / https://github.com/takahirox
49 | */
50 |
51 | if (typeof AFRAME === 'undefined') {
52 | throw new Error('Component attempted to register before' +
53 | 'AFRAME was available.');
54 | }
55 |
56 | AFRAME.registerComponent('instancing', {
57 | schema: {
58 | count: {type: 'int', default: 10000},
59 | frustumCulled: {default: false}
60 | },
61 |
62 | init: function () {
63 | this.count = this.data.count;
64 | this.model = null;
65 | },
66 |
67 | update: function () {
68 | if (this.model !== null) { return; }
69 |
70 | var data = this.data;
71 | var el = this.el;
72 |
73 | var count = this.count;
74 |
75 | var geometry = new THREE.InstancedBufferGeometry();
76 | geometry.copy(new THREE.SphereBufferGeometry(5.0));
77 |
78 | var translateArray = new Float32Array(count*3);
79 | var vectorArray = new Float32Array(count*3);
80 | var colorArray = new Float32Array(count*3);
81 |
82 | for (var i = 0; i < count; i++) {
83 | translateArray[i*3+0] = (Math.random() - 0.5) * 100.0;
84 | translateArray[i*3+1] = (Math.random() - 0.5) * 100.0;
85 | translateArray[i*3+2] = (Math.random() - 0.5) * 100.0;
86 | }
87 |
88 | for (var i = 0; i < count; i++) {
89 | vectorArray[i*3+0] = (Math.random() - 0.5) * 100.0;
90 | vectorArray[i*3+1] = (Math.random() + 1.5) * 100.0;
91 | vectorArray[i*3+2] = (Math.random() - 0.5) * 100.0;
92 | }
93 |
94 | for (var i = 0; i < count; i++) {
95 | colorArray[i*3+0] = Math.random();
96 | colorArray[i*3+1] = Math.random();
97 | colorArray[i*3+2] = Math.random();
98 | }
99 |
100 | geometry.setAttribute('translate', new THREE.InstancedBufferAttribute(translateArray, 3, false));
101 | geometry.setAttribute('vector', new THREE.InstancedBufferAttribute(vectorArray, 3, false));
102 | geometry.setAttribute('color', new THREE.InstancedBufferAttribute(colorArray, 3, false));
103 |
104 | var material = new THREE.ShaderMaterial({
105 | uniforms: {
106 | time: {value: 0}
107 | },
108 | vertexShader: [
109 | 'attribute vec3 translate;',
110 | 'attribute vec3 vector;',
111 | 'attribute vec3 color;',
112 | 'uniform float time;',
113 | 'varying vec3 vColor;',
114 | 'const float g = 9.8 * 1.5;',
115 | 'void main() {',
116 | ' vec3 offset;',
117 | ' offset.xz = vector.xz * time;',
118 | ' offset.y = vector.y * time - 0.5 * g * time * time;',
119 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position + translate + offset, 1.0 );',
120 | ' vColor = color;',
121 | '}'
122 | ].join('\n'),
123 | fragmentShader: [
124 | 'varying vec3 vColor;',
125 | 'void main() {',
126 | ' gl_FragColor = vec4( vColor, 1.0 );',
127 | '}'
128 | ].join('\n')
129 | });
130 |
131 | var mesh = new THREE.Mesh(geometry, material);
132 | mesh.frustumCulled = this.data.frustumCulled;
133 |
134 | this.model = mesh;
135 | el.setObject3D('mesh', mesh);
136 | el.emit('model-loaded', {format:'mesh', model: mesh});
137 | },
138 |
139 | tick: function(time, delta) {
140 | if (this.model === null) { return; }
141 |
142 | var mesh = this.model;
143 | mesh.material.uniforms.time.value = (mesh.material.uniforms.time.value + delta / 1000) % 30.0;
144 | }
145 | });
146 |
147 |
148 | /***/ })
149 | /******/ ]);
--------------------------------------------------------------------------------
/build/aframe-instancing.min.js:
--------------------------------------------------------------------------------
1 | !function(t){function e(o){if(r[o])return r[o].exports;var a=r[o]={exports:{},id:o,loaded:!1};return t[o].call(a.exports,a,a.exports,e),a.loaded=!0,a.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register beforeAFRAME was available.");AFRAME.registerComponent("instancing",{schema:{count:{type:"int",default:1e4},frustumCulled:{default:!1}},init:function(){this.count=this.data.count,this.model=null},update:function(){if(null===this.model){var t=(this.data,this.el),e=this.count,r=new THREE.InstancedBufferGeometry;r.copy(new THREE.SphereBufferGeometry(5));for(var o=new Float32Array(3*e),a=new Float32Array(3*e),n=new Float32Array(3*e),i=0;i
2 |
3 |
4 |
5 | A-Frame Instancing component
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro / https://github.com/takahirox
3 | */
4 |
5 | if (typeof AFRAME === 'undefined') {
6 | throw new Error('Component attempted to register before' +
7 | 'AFRAME was available.');
8 | }
9 |
10 | AFRAME.registerComponent('instancing', {
11 | schema: {
12 | count: {type: 'int', default: 10000},
13 | frustumCulled: {default: false}
14 | },
15 |
16 | init: function () {
17 | this.count = this.data.count;
18 | this.model = null;
19 | },
20 |
21 | update: function () {
22 | if (this.model !== null) { return; }
23 |
24 | var data = this.data;
25 | var el = this.el;
26 |
27 | var count = this.count;
28 |
29 | var geometry = new THREE.InstancedBufferGeometry();
30 | geometry.copy(new THREE.SphereBufferGeometry(5.0));
31 |
32 | var translateArray = new Float32Array(count*3);
33 | var vectorArray = new Float32Array(count*3);
34 | var colorArray = new Float32Array(count*3);
35 |
36 | for (var i = 0; i < count; i++) {
37 | translateArray[i*3+0] = (Math.random() - 0.5) * 100.0;
38 | translateArray[i*3+1] = (Math.random() - 0.5) * 100.0;
39 | translateArray[i*3+2] = (Math.random() - 0.5) * 100.0;
40 | }
41 |
42 | for (var i = 0; i < count; i++) {
43 | vectorArray[i*3+0] = (Math.random() - 0.5) * 100.0;
44 | vectorArray[i*3+1] = (Math.random() + 1.5) * 100.0;
45 | vectorArray[i*3+2] = (Math.random() - 0.5) * 100.0;
46 | }
47 |
48 | for (var i = 0; i < count; i++) {
49 | colorArray[i*3+0] = Math.random();
50 | colorArray[i*3+1] = Math.random();
51 | colorArray[i*3+2] = Math.random();
52 | }
53 |
54 | geometry.setAttribute('translate', new THREE.InstancedBufferAttribute(translateArray, 3, false));
55 | geometry.setAttribute('vector', new THREE.InstancedBufferAttribute(vectorArray, 3, false));
56 | geometry.setAttribute('color', new THREE.InstancedBufferAttribute(colorArray, 3, false));
57 |
58 | var material = new THREE.ShaderMaterial({
59 | uniforms: {
60 | time: {value: 0}
61 | },
62 | vertexShader: [
63 | 'attribute vec3 translate;',
64 | 'attribute vec3 vector;',
65 | 'attribute vec3 color;',
66 | 'uniform float time;',
67 | 'varying vec3 vColor;',
68 | 'const float g = 9.8 * 1.5;',
69 | 'void main() {',
70 | ' vec3 offset;',
71 | ' offset.xz = vector.xz * time;',
72 | ' offset.y = vector.y * time - 0.5 * g * time * time;',
73 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position + translate + offset, 1.0 );',
74 | ' vColor = color;',
75 | '}'
76 | ].join('\n'),
77 | fragmentShader: [
78 | 'varying vec3 vColor;',
79 | 'void main() {',
80 | ' gl_FragColor = vec4( vColor, 1.0 );',
81 | '}'
82 | ].join('\n')
83 | });
84 |
85 | var mesh = new THREE.Mesh(geometry, material);
86 | mesh.frustumCulled = this.data.frustumCulled;
87 |
88 | this.model = mesh;
89 | el.setObject3D('mesh', mesh);
90 | el.emit('model-loaded', {format:'mesh', model: mesh});
91 | },
92 |
93 | tick: function(time, delta) {
94 | if (this.model === null) { return; }
95 |
96 | var mesh = this.model;
97 | mesh.material.uniforms.time.value = (mesh.material.uniforms.time.value + delta / 1000) % 30.0;
98 | }
99 | });
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aframe-instancing",
3 | "version": "0.0.2",
4 | "description": "A-Frame instancing component",
5 | "main": "index.js",
6 | "scripts": {
7 | "all": "npm run build && npm run build-uglify",
8 | "build": "webpack index.js build/aframe-instancing.js",
9 | "build-uglify": "webpack -p index.js build/aframe-instancing.min.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/takahirox/aframe-instancing.git"
14 | },
15 | "keywords": [
16 | "aframe",
17 | "aframe-vr",
18 | "vr",
19 | "three",
20 | "threejs",
21 | "mozvr",
22 | "webvr"
23 | ],
24 | "author": "Takahiro (https://github.com/takahirox)",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/takahirox/aframe-instancing/issues"
28 | },
29 | "homepage": "https://github.com/takahirox/aframe-instancing#readme",
30 | "devDependencies": {
31 | "webpack": "^1.13.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------