├── .travis.yml ├── tests ├── index.test.js ├── karma.conf.js ├── __init.test.js └── helpers.js ├── .gitignore ├── LICENSE ├── package.json ├── index.html ├── examples └── basic │ └── index.html ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | firefox: 'latest' 4 | node_js: 5 | - '6.9' 6 | 7 | install: 8 | - npm install 9 | - ./node_modules/.bin/mozilla-download ./firefox/ --product firefox --branch mozilla-aurora 10 | - export FIREFOX_NIGHTLY_BIN="./firefox/firefox/firefox-bin" 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | 16 | script: 17 | - $CI_ACTION 18 | 19 | env: 20 | global: 21 | - TEST_SUITE=unit 22 | matrix: 23 | - CI_ACTION="npm run test" 24 | - CI_ACTION="npm run dist" 25 | # - CI_ACTION="npm run lint" 26 | 27 | branches: 28 | only: 29 | - master 30 | 31 | cache: 32 | directories: 33 | - node_modules 34 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | /* global assert, setup, suite, test */ 2 | require('aframe'); 3 | require('../index.js'); 4 | var entityFactory = require('./helpers').entityFactory; 5 | 6 | suite('potree-loader component', function () { 7 | var component; 8 | var el; 9 | 10 | setup(function (done) { 11 | el = entityFactory(); 12 | el.addEventListener('componentinitialized', function (evt) { 13 | if (evt.detail.name !== 'potree-loader') { return; } 14 | component = el.components['potree-loader']; 15 | done(); 16 | }); 17 | el.setAttribute('potree-loader', {}); 18 | }); 19 | 20 | suite('foo property', function () { 21 | test('is good', function () { 22 | assert.equal(1, 1); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration. 2 | module.exports = function (config) { 3 | config.set({ 4 | basePath: '../', 5 | browserify: { 6 | debug: true, 7 | paths: ['./'] 8 | }, 9 | browsers: ['Firefox', 'Chrome'], 10 | client: { 11 | captureConsole: true, 12 | mocha: {ui: 'tdd'} 13 | }, 14 | envPreprocessor: ['TEST_ENV'], 15 | files: [ 16 | // Define test files. 17 | {pattern: 'tests/**/*.test.js'}, 18 | // Serve test assets. 19 | {pattern: 'tests/assets/**/*', included: false, served: true} 20 | ], 21 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'], 22 | preprocessors: {'tests/**/*.js': ['browserify', 'env']}, 23 | reporters: ['mocha'] 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cert.pem 2 | key.pem 3 | 4 | .sw[ponm] 5 | #examples/build.js 6 | examples/node_modules/ 7 | gh-pages 8 | node_modules/ 9 | npm-debug.log 10 | 11 | # Created by https://www.gitignore.io/api/node 12 | 13 | ### Node ### 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (http://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules 45 | jspm_packages 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | Session.vim 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Matthias Treitler <matthias.treitler@gmail.com> 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 | 23 | -------------------------------------------------------------------------------- /tests/__init.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon, setup, teardown */ 2 | 3 | /** 4 | * __init.test.js is run before every test case. 5 | */ 6 | window.debug = true; 7 | var AScene = require('aframe').AScene; 8 | 9 | navigator.getVRDisplays = function () { 10 | var resolvePromise = Promise.resolve(); 11 | var mockVRDisplay = { 12 | requestPresent: resolvePromise, 13 | exitPresent: resolvePromise, 14 | getPose: function () { return {orientation: null, position: null}; }, 15 | requestAnimationFrame: function () { return 1; } 16 | }; 17 | return Promise.resolve([mockVRDisplay]); 18 | }; 19 | 20 | setup(function () { 21 | this.sinon = sinon.sandbox.create(); 22 | // Stubs to not create a WebGL context since Travis CI runs headless. 23 | this.sinon.stub(AScene.prototype, 'render'); 24 | this.sinon.stub(AScene.prototype, 'resize'); 25 | this.sinon.stub(AScene.prototype, 'setupRenderer'); 26 | }); 27 | 28 | teardown(function () { 29 | // Clean up any attached elements. 30 | var attachedEls = ['canvas', 'a-assets', 'a-scene']; 31 | var els = document.querySelectorAll(attachedEls.join(',')); 32 | for (var i = 0; i < els.length; i++) { 33 | els[i].parentNode.removeChild(els[i]); 34 | } 35 | this.sinon.restore(); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /* global suite */ 2 | 3 | /** 4 | * Helper method to create a scene, create an entity, add entity to scene, 5 | * add scene to document. 6 | * 7 | * @returns {object} An `` element. 8 | */ 9 | module.exports.entityFactory = function (opts) { 10 | var scene = document.createElement('a-scene'); 11 | var assets = document.createElement('a-assets'); 12 | var entity = document.createElement('a-entity'); 13 | scene.appendChild(assets); 14 | scene.appendChild(entity); 15 | 16 | opts = opts || {}; 17 | 18 | if (opts.assets) { 19 | opts.assets.forEach(function (asset) { 20 | assets.appendChild(asset); 21 | }); 22 | } 23 | 24 | document.body.appendChild(scene); 25 | return entity; 26 | }; 27 | 28 | /** 29 | * Creates and attaches a mixin element (and an `` element if necessary). 30 | * 31 | * @param {string} id - ID of mixin. 32 | * @param {object} obj - Map of component names to attribute values. 33 | * @param {Element} scene - Indicate which scene to apply mixin to if necessary. 34 | * @returns {object} An attached `` element. 35 | */ 36 | module.exports.mixinFactory = function (id, obj, scene) { 37 | var mixinEl = document.createElement('a-mixin'); 38 | mixinEl.setAttribute('id', id); 39 | Object.keys(obj).forEach(function (componentName) { 40 | mixinEl.setAttribute(componentName, obj[componentName]); 41 | }); 42 | 43 | var assetsEl = scene ? scene.querySelector('a-assets') : document.querySelector('a-assets'); 44 | assetsEl.appendChild(mixinEl); 45 | 46 | return mixinEl; 47 | }; 48 | 49 | /** 50 | * Test that is only run locally and is skipped on CI. 51 | */ 52 | module.exports.getSkipCISuite = function () { 53 | if (window.__env__.TEST_ENV === 'ci') { 54 | return suite.skip; 55 | } else { 56 | return suite; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-potree-loader-component", 3 | "version": "0.12.0", 4 | "description": "Loads PointClouds using Potree", 5 | "main": "index.js", 6 | "unpkg": "dist/aframe-potree-loader-component.min.js", 7 | "scripts": { 8 | "build": "webpack index.js dist/aframe-potree-loader-component.js", 9 | "dev": "budo index.js:dist/aframe-potree-loader-component.min.js --port 7000 --live", 10 | "dist": "npm run build && uglifyjs dist/aframe-potree-loader-component.js > dist/aframe-potree-loader-component.min.js", 11 | "lint": "semistandard -v --fix | snazzy", 12 | "prepublish": "npm run dist", 13 | "ghpages": "ghpages", 14 | "start": "npm run dev", 15 | "test": "karma start ./tests/karma.conf.js", 16 | "test:firefox": "karma start ./tests/karma.conf.js --browsers Firefox", 17 | "test:chrome": "karma start ./tests/karma.conf.js --browsers Chrome" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/mattrei/aframe-potree-loader-component.git" 22 | }, 23 | "keywords": [ 24 | "aframe", 25 | "aframe-component", 26 | "aframe-vr", 27 | "vr", 28 | "mozvr", 29 | "webvr", 30 | "potree" 31 | ], 32 | "author": "Matthias Treitler ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/mattrei/aframe-potree-loader-component/issues" 36 | }, 37 | "homepage": "https://github.com/mattrei/aframe-potree-loader-component#readme", 38 | "devDependencies": { 39 | "aframe": "*", 40 | "browserify": "^13.0.0", 41 | "budo": "^8.2.2", 42 | "chai": "^3.4.1", 43 | "chai-shallow-deep-equal": "^1.3.0", 44 | "ghpages": "^0.0.8", 45 | "karma": "^0.13.15", 46 | "karma-browserify": "^4.4.2", 47 | "karma-chai-shallow-deep-equal": "0.0.4", 48 | "karma-chrome-launcher": "2.0.0", 49 | "karma-env-preprocessor": "^0.1.1", 50 | "karma-firefox-launcher": "^0.1.7", 51 | "karma-mocha": "^0.2.1", 52 | "karma-mocha-reporter": "^1.1.3", 53 | "karma-sinon-chai": "^1.1.0", 54 | "mocha": "^2.3.4", 55 | "semistandard": "^8.0.0", 56 | "shelljs": "^0.7.0", 57 | "shx": "^0.1.1", 58 | "sinon": "^1.17.5", 59 | "sinon-chai": "^2.8.0", 60 | "snazzy": "^4.0.0", 61 | "uglify-es": "github:mishoo/UglifyJS2#harmony", 62 | "webpack": "^2.7.0" 63 | }, 64 | "semistandard": { 65 | "globals": [ 66 | "AFRAME", 67 | "THREE" 68 | ], 69 | "ignore": [ 70 | "examples/build.js", 71 | "dist/**" 72 | ] 73 | }, 74 | "dependencies": { 75 | "@pnext/three-loader": "^0.1.5", 76 | "three": "^0.109.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Potree Loader Component 6 | 7 | 48 | 49 | 50 |

A-Frame Potree Loader Component

51 | 52 |
    53 |
  • 54 | 55 |

    Basic

    56 |

    This is a basic example.

    57 |
  • 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Potree Loader Component - Basic 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## aframe-potree-loader-component 2 | 3 | [![Version](http://img.shields.io/npm/v/aframe-potree-loader-component.svg?style=flat-square)](https://npmjs.org/package/aframe-potree-loader-component) 4 | [![License](http://img.shields.io/npm/l/aframe-potree-loader-component.svg?style=flat-square)](https://npmjs.org/package/aframe-potree-loader-component) 5 | 6 | Loads point clouds using [Potree](http://potree.org/) 7 | 8 | For [A-Frame](https://aframe.io). 9 | 10 | ### API 11 | 12 | #### Properties 13 | 14 | | Property | Description | Default Value | 15 | | -------- | ----------- | ------------- | 16 | | src | Url to the PointCloud files. Expects a _cloud.json_ | '' | 17 | | pointSize | Semantic size of a single point. The lower the more space is between the points, higher values result in low resolution objects. Has no performance impact. | 1 | 18 | | minimumNodePixelSize | Pixel size of a point within a node. The lower the more points will be shown per octree node. Has performance impact. | 150 | 19 | | pointSizeType | How to point adapts to the camera frustum. Either _fixed_, _adaptive_ or | _adaptive_ | 20 | | pointShape | The shape of a single point. Either _square_, _circle_ or | _sqaure_ | 21 | | pointColorType | Type of color of a single point in respect of the pointcloud. See for all possible values. | 'rgb' | 22 | 23 | The initial position and rotation is specific for each point cloud and has to be set accordingly. 24 | 25 | #### Scene Properties 26 | 27 | | Property | Description | Default Value | 28 | | -------- | ----------- | ------------- | 29 | | pointBudget | Point Budget in millions of all pointclouds in your scene | 1 | 30 | 31 | #### Events 32 | 33 | | Property | Description | 34 | | -------- | ----------- | 35 | | model-loaded | The point cloud had been loaded into the scene | 36 | | model-error | The point cloud could not be loaded loading | 37 | 38 | ## Notes on fidelity 39 | _Potree_ automatically adapts the point budget according to the underlying hardware, that said mobile devices are not so well suited for detailed point cloud representations. 40 | 41 | ### Installation 42 | 43 | #### Browser 44 | 45 | Install and use by directly including the [browser files](dist): 46 | 47 | ```html 48 | 49 | My A-Frame Scene 50 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 68 | ``` 69 | 70 | #### npm 71 | 72 | Install via npm: 73 | 74 | ```bash 75 | npm install aframe-potree-loader-component 76 | ``` 77 | 78 | Then require and use. 79 | 80 | ```js 81 | require('aframe'); 82 | require('aframe-potree-loader-component'); 83 | ``` 84 | 85 | ### Implementation notes 86 | This is basically just a wrapper around PIX4D's [three-potree-loader package](https://github.com/Pix4D/three-potree-loader). They did all the hard work (and saved myself lots of time) of extracting the core loading and rendering functionality of the [Potree point cloud viewer](https://github.com/potree/potree). That's the spirit of open-source. We all build up on each other and adapt to certain needs, in that case point clouds within _WebVR_. 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME THREE */ 2 | 3 | const potreeLoader = require('@pnext/three-loader') 4 | 5 | const PointCloudOctree = potreeLoader.PointCloudOctree; 6 | const Potree = potreeLoader.Potree; 7 | const PointColorType = potreeLoader.PointColorType; 8 | const PointShape = potreeLoader.PointShape; 9 | const PointSizeType = potreeLoader.PointSizeType; 10 | const TreeType = potreeLoader.TreeType; 11 | 12 | if (typeof AFRAME === 'undefined') { 13 | throw new Error('Component attempted to register before AFRAME was available.'); 14 | } 15 | 16 | AFRAME.registerSystem('potree-loader', { 17 | schema: { 18 | pointBudget: {default: 1} 19 | }, 20 | init: function() { 21 | const el = this.el; 22 | const data = this.data; 23 | this.potree = new Potree(); 24 | this.potree.pointBudget = data.pointBudget * 1000000; 25 | this.pointClouds = []; 26 | 27 | // we need a sepearte renderer otherwise we get strange artifacts 28 | this.renderer = new THREE.WebGLRenderer({canvas: el.canvas}); 29 | const size = new THREE.Vector2(); 30 | el.renderer.getSize(size); 31 | this.renderer.setSize(size.x, size.y) 32 | }, 33 | getPotree: function() { 34 | return this.potree; 35 | }, 36 | addPointCloud: function(pco) { 37 | this.pointClouds.push(pco); 38 | }, 39 | removePointCloud: function(pco) { 40 | this.pointClouds.forEach(pco => { 41 | pco.dispose(); 42 | }); 43 | }, 44 | tick: function(time, delta) { 45 | this._render() 46 | }, 47 | _render: function() { 48 | const camera = this.el.camera; 49 | const result = this.potree.updatePointClouds(this.pointClouds, camera, this.renderer); 50 | 51 | } 52 | }) 53 | 54 | AFRAME.registerComponent('potree-loader', { 55 | schema: { 56 | src: { 57 | 58 | }, 59 | pointSize: { 60 | type: 'number', 61 | default: 1 62 | }, 63 | minimumNodePixelSize: { 64 | default: 150, 65 | min: 0, 66 | max: 1000, 67 | type: 'number' 68 | }, 69 | pointSizeType: { 70 | default: 'adaptive', 71 | oneOf: Object.values(PointSizeType).filter(v => !Number.isInteger(v)).map(v => v.toLowerCase()) 72 | }, 73 | pointShape: { 74 | default: 'square', 75 | oneOf: Object.values(PointShape).filter(v => !Number.isInteger(v)).map(v => v.toLowerCase()) 76 | }, 77 | pointColorType: { 78 | default: 'rgb', 79 | oneOf: Object.values(PointColorType).filter(v => !Number.isInteger(v)).map(v => v.toLowerCase()) 80 | }, 81 | weighted: { 82 | default: false, 83 | type: 'boolean' 84 | }, 85 | }, 86 | multiple: false, 87 | init: function () { 88 | 89 | const data = this.data; 90 | const el = this.el; 91 | 92 | const potree = this.system.getPotree(); 93 | potree 94 | .loadPointCloud( 95 | 'cloud.js', 96 | url => `${data.src}/${url}`, 97 | ) 98 | .then(pco => { 99 | this.pco = pco; 100 | this.system.addPointCloud(pco); 101 | this._updatePointCloud(pco); 102 | this.system._render() 103 | 104 | 105 | const box = pco.boundingBox; 106 | const boundingSphere = new THREE.Sphere(); 107 | box.getBoundingSphere(boundingSphere); 108 | const center = boundingSphere.center; 109 | const radius = boundingSphere.radius; 110 | 111 | const span = 1; 112 | const s = (radius === 0 ? 1 : 1.0 / radius) * span; 113 | //pco.scale.set(s, s, s); 114 | 115 | const obj = new THREE.Object3D(); 116 | obj.add(pco) 117 | el.setObject3D('mesh', obj) 118 | 119 | el.emit('model-loaded', pco); 120 | }) 121 | .catch(err => { 122 | console.warn(err) 123 | el.emit('model-error', {src: `${data.src}`}); 124 | } ); 125 | 126 | }, 127 | 128 | 129 | update: function (oldData) { 130 | const data = this.data; 131 | if (AFRAME.utils.deepEqual(data, oldData)) return; 132 | 133 | if (this.pco) { 134 | this._updatePointCloud(this.pco); 135 | } 136 | }, 137 | 138 | _updatePointCloud: function (pco) { 139 | const data = this.data; 140 | pco.material.size = data.pointSize; 141 | 142 | pco.material.pointColorType = PointColorType[data.pointColorType.toUpperCase()]; 143 | pco.material.pointShapeType = PointShape[data.pointShape.toUpperCase()]; 144 | pco.material.pointSizeType = PointSizeType[data.pointSizeType.toUpperCase()]; 145 | 146 | pco.minimumNodePixelSize = data.minimumNodePixelSize; 147 | }, 148 | 149 | remove: function () { 150 | this.system.removePointCloud(this.pco); 151 | //this.el.object3D.remove(pco); 152 | }, 153 | 154 | tick: function (time, delta) { 155 | }, 156 | }); 157 | --------------------------------------------------------------------------------