├── .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 | --------------------------------------------------------------------------------