├── about ├── pano-cru·ci·form-1471040116139.jpg └── pano-The Polygon Shredder-1471041904038.jpg ├── examples ├── package.json ├── js │ ├── ImageData-polyfill.js │ ├── canvas.toBlob-polyfill.js │ ├── CubemapToEquirectangular.js │ └── OrbitControls.js ├── index-unmanaged-require.html ├── index.js ├── index-managed.html └── index-unmanaged.html ├── package.json ├── LICENSE ├── README.md └── src └── CubemapToEquirectangular.js /about/pano-cru·ci·form-1471040116139.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/THREE.CubemapToEquirectangular/HEAD/about/pano-cru·ci·form-1471040116139.jpg -------------------------------------------------------------------------------- /about/pano-The Polygon Shredder-1471041904038.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/THREE.CubemapToEquirectangular/HEAD/about/pano-The Polygon Shredder-1471041904038.jpg -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "three": "^0.81.2", 4 | "three-orbit-controls": "^72.0.0", 5 | "three.cubemap-to-equirectangular": "^1.0.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/js/ImageData-polyfill.js: -------------------------------------------------------------------------------- 1 | ( function() { 2 | 3 | try { 4 | var i = new ImageData( new Uint8ClampedArray( [ 1, 1, 1, 1 ] ), 1, 1 ); 5 | } catch( e ) { 6 | window.ImageData = function( data, width, height ) { 7 | var canvas = document.createElement( 'canvas' ); 8 | var ctx = canvas.getContext( '2d' ); 9 | var imageData = ctx.createImageData( width, height ); 10 | imageData.data.set( data ); 11 | return imageData 12 | } 13 | } 14 | 15 | })(); 16 | -------------------------------------------------------------------------------- /examples/js/canvas.toBlob-polyfill.js: -------------------------------------------------------------------------------- 1 | if (!HTMLCanvasElement.prototype.toBlob) { 2 | Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { 3 | value: function (callback, type, quality) { 4 | 5 | var binStr = atob( this.toDataURL(type, quality).split(',')[1] ), 6 | len = binStr.length, 7 | arr = new Uint8Array(len); 8 | 9 | for (var i=0; i (https://www.clicktorelease.com)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/spite/THREE.CubemapToEquirectangular/issues" 27 | }, 28 | "homepage": "https://github.com/spite/THREE.CubemapToEquirectangular#readme" 29 | } 30 | -------------------------------------------------------------------------------- /examples/index-unmanaged-require.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CubemapToEquirectangular - Unmanaged 6 | 7 | 8 | 27 | 28 | 29 | 30 |
Capture
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jaume Sanchez Elias, http://www.clicktorelease.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 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | var THREE = require('three'); 2 | var OrbitControls = require('three-orbit-controls')(THREE) 3 | var CubemapToEquirectangular = require('three.cubemap-to-equirectangular'); 4 | 5 | var equi; 6 | 7 | var container, stats; 8 | var camera, scene, renderer; 9 | var controls; 10 | 11 | var radius = 100, theta = 0; 12 | 13 | window.addEventListener( 'load', function() { 14 | init(); 15 | animate(); 16 | }); 17 | 18 | var cubeCamera; 19 | var sphere; 20 | 21 | function init() { 22 | 23 | container = document.createElement( 'div' ); 24 | document.body.appendChild( container ); 25 | 26 | camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 10000 ); 27 | camera.position.set( 1,1,1 ); 28 | 29 | scene = new THREE.Scene(); 30 | 31 | var light = new THREE.DirectionalLight( 0xffffff, 1 ); 32 | light.position.set( 1, 1, 1 ).normalize(); 33 | scene.add( light ); 34 | 35 | var geometry = new THREE.BoxBufferGeometry( 20, 20, 20 ); 36 | 37 | for ( var i = 0; i < 2000; i ++ ) { 38 | 39 | var object = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) ); 40 | 41 | object.position.x = Math.random() * 800 - 400; 42 | object.position.y = Math.random() * 800 - 400; 43 | object.position.z = Math.random() * 800 - 400; 44 | 45 | object.rotation.x = Math.random() * 2 * Math.PI; 46 | object.rotation.y = Math.random() * 2 * Math.PI; 47 | object.rotation.z = Math.random() * 2 * Math.PI; 48 | 49 | object.scale.x = Math.random() + 0.5; 50 | object.scale.y = Math.random() + 0.5; 51 | object.scale.z = Math.random() + 0.5; 52 | 53 | object.material.color.setRGB( object.position.x / 800 + .5, object.position.y / 800 + .5, object.position.z / 800 + .5 ); 54 | 55 | scene.add( object ); 56 | 57 | } 58 | 59 | renderer = new THREE.WebGLRenderer(); 60 | renderer.setClearColor( 0xf0f0f0 ); 61 | renderer.setPixelRatio( window.devicePixelRatio ); 62 | renderer.setSize( window.innerWidth, window.innerHeight ); 63 | renderer.sortObjects = false; 64 | container.appendChild(renderer.domElement); 65 | 66 | equi = new CubemapToEquirectangular( renderer, false ); 67 | cubeCamera = new THREE.CubeCamera( .1, 1000, 2048 ); 68 | 69 | controls = new OrbitControls( camera, renderer.domElement ); 70 | 71 | window.addEventListener( 'resize', onWindowResize, false ); 72 | onWindowResize(); 73 | 74 | document.getElementById( 'capture' ).addEventListener( 'click', function( e ) { 75 | 76 | cubeCamera.position.copy( camera.position ); 77 | cubeCamera.updateCubeMap( renderer, scene ); 78 | equi.convert( cubeCamera ); 79 | 80 | } ); 81 | 82 | } 83 | 84 | function onWindowResize() { 85 | 86 | camera.aspect = window.innerWidth / window.innerHeight; 87 | camera.updateProjectionMatrix(); 88 | 89 | renderer.setSize( window.innerWidth, window.innerHeight ); 90 | 91 | } 92 | 93 | 94 | function animate() { 95 | 96 | requestAnimationFrame( animate ); 97 | 98 | controls.update(); 99 | render(); 100 | 101 | } 102 | 103 | function render() { 104 | 105 | renderer.render( scene, camera ); 106 | 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THREE.CubemapToEquirectangular 2 | 3 | Helper to extract an equirectangular panorama PNG from any three.js scene. 4 | 5 | Here's a demo with some cubes: [Demo](http://clicktorelease.com/tools/CubemapToEquirectangular/index-managed.html) 6 | 7 | 8 | ![](https://raw.githubusercontent.com/spite/THREE.CubemapToEquirectangular/master/about/pano-cru%C2%B7ci%C2%B7form-1471040116139.jpg) 9 | ![](https://raw.githubusercontent.com/spite/THREE.CubemapToEquirectangular/master/about/pano-The%20Polygon%20Shredder-1471041904038.jpg) 10 | 11 | #### How to use #### 12 | Include script after THREE is included 13 | ```js 14 | 15 | ``` 16 | or use npm to install it 17 | ``` 18 | npm i three.cubemap-to-equirectangular 19 | ``` 20 | and include it in your code with (remember to require three.js) 21 | ```js 22 | var THREE = require('three'); 23 | var CubemapToEquirectangular = require('three.cubemap-to-equirectangular'); 24 | ``` 25 | 26 | Define a new instance of THREE.CubemapToEquirectangular. 27 | ```js 28 | // create renderer, scene, camera, etc. 29 | var renderer = new THREE.WebGLRenderer(); 30 | var scene = new THREE.Scene(); 31 | var camera = new THREE.Camera( /* ... */ ); 32 | 33 | // Create a managed CubemapToEquirectangular 34 | var equiManaged = new CubemapToEquirectangular( renderer, true ); 35 | 36 | // or create an unmanaged CubemapToEquirectangular 37 | var equiUnmanaged = new CubemapToEquirectangular( renderer, false ); 38 | ``` 39 | 40 | #### Managed CubemapToEquirectangular #### 41 | With Managed mode, the THREE.CubeCamera creation, update, render, etc. is all taken care of. You only have to call: 42 | ```js 43 | equiManaged.update( camera, scene ); 44 | ``` 45 | at any point in your code that you want to extract a panorama. 46 | The cube map created will be 2048x2048 and the exported panorama will be 4096x2048. 47 | *Note: The cubemap can easily be 4096x4096, but that seems to work on most mobiles, too* 48 | 49 | Demo of Managed mode: [Demo](http://clicktorelease.com/tools/CubemapToEquirectangular/index-managed.html) 50 | 51 | 52 | #### Unmanaged CubemapToEquirectangular #### 53 | If you want to use a different CubeMap camera, or do something custom with the render, you will have to set the Unmanaged mode. 54 | 55 | You will have to create and manage your THREE.CubeCamera: 56 | ```js 57 | var cubeCamera = new THREE.CubeCamera( .1, 1000, 4096 ); 58 | ``` 59 | and manage all your scene update and rendering. When you want to export a panorama, call: 60 | ```js 61 | // this is where the developer updates the scene and creates a cubemap of the scene 62 | cubeCamera.position.copy( camera.position ); 63 | cubeCamera.updateCubeMap( renderer, scene ); 64 | 65 | // call this to convert the cubemap rendertarget to a panorama 66 | equiUnmanaged.convert( cubeCamera ); 67 | ``` 68 | 69 | Demo of Unmanaged mode: [Demo](http://clicktorelease.com/tools/CubemapToEquirectangular/index-unmanaged.html) 70 | 71 | #### Changing output size #### 72 | To export a different size, call ```setSize( width, height )```: 73 | ```js 74 | equi.setSize( 2048, 1024 ); 75 | ``` 76 | 77 | #### Notes ### 78 | 79 | Built using ES6 template strings. 80 | Needs canvas.toBlob, polyfill in the examples folder 81 | 82 | #### TODO #### 83 | 84 | - ~~Fix for Android (if it's a relevant use case)~~ (seems to work with 2048x2048) 85 | - Check for mobile (if it's a relevant use case) 86 | - Add importance sampling (improves quality of output) 87 | - Handle postprocessing 88 | - Handle Safari not supporting download attribute 89 | - Let users have more control over file name, callbacks, progress 90 | 91 | #### License #### 92 | 93 | MIT licensed 94 | 95 | Copyright (C) 2016 Jaume Sanchez Elias, http://www.clicktorelease.com 96 | -------------------------------------------------------------------------------- /examples/index-managed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CubemapToEquirectangular - Managed 6 | 7 | 8 | 27 | 28 | 29 | 30 |
Capture
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /examples/index-unmanaged.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CubemapToEquirectangular - Unmanaged 6 | 7 | 8 | 27 | 28 | 29 | 30 |
Capture
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/CubemapToEquirectangular.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | 3 | "use strict"; 4 | 5 | var root = this 6 | 7 | var has_require = typeof require !== 'undefined' 8 | 9 | var THREE = root.THREE || has_require && require('three') 10 | if( !THREE ) 11 | throw new Error( 'CubemapToEquirectangular requires three.js' ) 12 | 13 | var vertexShader = ` 14 | attribute vec3 position; 15 | attribute vec2 uv; 16 | 17 | uniform mat4 projectionMatrix; 18 | uniform mat4 modelViewMatrix; 19 | 20 | varying vec2 vUv; 21 | 22 | void main() { 23 | 24 | vUv = vec2( 1.- uv.x, uv.y ); 25 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 26 | 27 | } 28 | `; 29 | 30 | var fragmentShader = ` 31 | precision mediump float; 32 | 33 | uniform samplerCube map; 34 | 35 | varying vec2 vUv; 36 | 37 | #define M_PI 3.1415926535897932384626433832795 38 | 39 | void main() { 40 | 41 | vec2 uv = vUv; 42 | 43 | float longitude = uv.x * 2. * M_PI - M_PI + M_PI / 2.; 44 | float latitude = uv.y * M_PI; 45 | 46 | vec3 dir = vec3( 47 | - sin( longitude ) * sin( latitude ), 48 | cos( latitude ), 49 | - cos( longitude ) * sin( latitude ) 50 | ); 51 | normalize( dir ); 52 | 53 | gl_FragColor = textureCube( map, dir ); 54 | 55 | } 56 | `; 57 | 58 | function CubemapToEquirectangular( renderer, provideCubeCamera ) { 59 | 60 | this.width = 1; 61 | this.height = 1; 62 | 63 | this.renderer = renderer; 64 | 65 | this.material = new THREE.RawShaderMaterial( { 66 | uniforms: { 67 | map: { type: 't', value: null } 68 | }, 69 | vertexShader: vertexShader, 70 | fragmentShader: fragmentShader, 71 | side: THREE.DoubleSide, 72 | transparent: true 73 | } ); 74 | 75 | this.scene = new THREE.Scene(); 76 | this.quad = new THREE.Mesh( 77 | new THREE.PlaneBufferGeometry( 1, 1 ), 78 | this.material 79 | ); 80 | this.scene.add( this.quad ); 81 | this.camera = new THREE.OrthographicCamera( 1 / - 2, 1 / 2, 1 / 2, 1 / - 2, -10000, 10000 ); 82 | 83 | this.canvas = document.createElement( 'canvas' ); 84 | this.ctx = this.canvas.getContext( '2d' ); 85 | 86 | this.cubeCamera = null; 87 | this.attachedCamera = null; 88 | 89 | this.setSize( 4096, 2048 ); 90 | 91 | var gl = this.renderer.getContext(); 92 | this.cubeMapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE ) 93 | 94 | if( provideCubeCamera ) { 95 | this.getCubeCamera( 2048 ) 96 | } 97 | 98 | } 99 | 100 | CubemapToEquirectangular.prototype.setSize = function( width, height ) { 101 | 102 | this.width = width; 103 | this.height = height; 104 | 105 | this.quad.scale.set( this.width, this.height, 1 ); 106 | 107 | this.camera.left = this.width / - 2; 108 | this.camera.right = this.width / 2; 109 | this.camera.top = this.height / 2; 110 | this.camera.bottom = this.height / - 2; 111 | 112 | this.camera.updateProjectionMatrix(); 113 | 114 | this.output = new THREE.WebGLRenderTarget( this.width, this.height, { 115 | minFilter: THREE.LinearFilter, 116 | magFilter: THREE.LinearFilter, 117 | wrapS: THREE.ClampToEdgeWrapping, 118 | wrapT: THREE.ClampToEdgeWrapping, 119 | format: THREE.RGBAFormat, 120 | type: THREE.UnsignedByteType 121 | }); 122 | 123 | this.canvas.width = this.width; 124 | this.canvas.height = this.height; 125 | 126 | } 127 | 128 | CubemapToEquirectangular.prototype.getCubeCamera = function( size ) { 129 | 130 | var cubeMapSize = Math.min( this.cubeMapSize, size ); 131 | this.cubeCamera = new THREE.CubeCamera( .1, 1000, cubeMapSize ); 132 | 133 | var options = { format: THREE.RGBAFormat, magFilter: THREE.LinearFilter, minFilter: THREE.LinearFilter }; 134 | this.cubeCamera.renderTarget = new THREE.WebGLRenderTargetCube( cubeMapSize, cubeMapSize, options ); 135 | 136 | return this.cubeCamera; 137 | 138 | } 139 | 140 | CubemapToEquirectangular.prototype.attachCubeCamera = function( camera ) { 141 | 142 | this.getCubeCamera(); 143 | this.attachedCamera = camera; 144 | 145 | } 146 | 147 | CubemapToEquirectangular.prototype.convert = function( cubeCamera, download ) { 148 | 149 | this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture; 150 | this.renderer.render( this.scene, this.camera, this.output, true ); 151 | 152 | var pixels = new Uint8Array( 4 * this.width * this.height ); 153 | this.renderer.readRenderTargetPixels( this.output, 0, 0, this.width, this.height, pixels ); 154 | 155 | var imageData = new ImageData( new Uint8ClampedArray( pixels ), this.width, this.height ); 156 | 157 | if( download !== false ) { 158 | this.download( imageData ); 159 | } 160 | 161 | return imageData 162 | 163 | }; 164 | 165 | CubemapToEquirectangular.prototype.download = function( imageData ) { 166 | 167 | this.ctx.putImageData( imageData, 0, 0 ); 168 | 169 | this.canvas.toBlob( function( blob ) { 170 | 171 | var url = URL.createObjectURL(blob); 172 | var fileName = 'pano-' + document.title + '-' + Date.now() + '.png'; 173 | var anchor = document.createElement( 'a' ); 174 | anchor.href = url; 175 | anchor.setAttribute("download", fileName); 176 | anchor.className = "download-js-link"; 177 | anchor.innerHTML = "downloading..."; 178 | anchor.style.display = "none"; 179 | document.body.appendChild(anchor); 180 | setTimeout(function() { 181 | anchor.click(); 182 | document.body.removeChild(anchor); 183 | }, 1 ); 184 | 185 | }, 'image/png' ); 186 | 187 | }; 188 | 189 | CubemapToEquirectangular.prototype.update = function( camera, scene ) { 190 | 191 | var autoClear = this.renderer.autoClear; 192 | this.renderer.autoClear = true; 193 | this.cubeCamera.position.copy( camera.position ); 194 | this.cubeCamera.updateCubeMap( this.renderer, scene ); 195 | this.renderer.autoClear = autoClear; 196 | 197 | this.convert( this.cubeCamera ); 198 | 199 | } 200 | 201 | if( typeof exports !== 'undefined' ) { 202 | if( typeof module !== 'undefined' && module.exports ) { 203 | exports = module.exports = CubemapToEquirectangular 204 | } 205 | exports.CubemapToEquirectangular = CubemapToEquirectangular 206 | } 207 | else { 208 | root.CubemapToEquirectangular = CubemapToEquirectangular 209 | } 210 | 211 | }).call(this); 212 | -------------------------------------------------------------------------------- /examples/js/CubemapToEquirectangular.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | 3 | "use strict"; 4 | 5 | var root = this 6 | 7 | var has_require = typeof require !== 'undefined' 8 | 9 | var THREE = root.THREE || has_require && require('three') 10 | if( !THREE ) 11 | throw new Error( 'CubemapToEquirectangular requires three.js' ) 12 | 13 | var vertexShader = ` 14 | attribute vec3 position; 15 | attribute vec2 uv; 16 | 17 | uniform mat4 projectionMatrix; 18 | uniform mat4 modelViewMatrix; 19 | 20 | varying vec2 vUv; 21 | 22 | void main() { 23 | 24 | vUv = vec2( 1.- uv.x, uv.y ); 25 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 26 | 27 | } 28 | `; 29 | 30 | var fragmentShader = ` 31 | precision mediump float; 32 | 33 | uniform samplerCube map; 34 | 35 | varying vec2 vUv; 36 | 37 | #define M_PI 3.1415926535897932384626433832795 38 | 39 | void main() { 40 | 41 | vec2 uv = vUv; 42 | 43 | float longitude = uv.x * 2. * M_PI - M_PI + M_PI / 2.; 44 | float latitude = uv.y * M_PI; 45 | 46 | vec3 dir = vec3( 47 | - sin( longitude ) * sin( latitude ), 48 | cos( latitude ), 49 | - cos( longitude ) * sin( latitude ) 50 | ); 51 | normalize( dir ); 52 | 53 | gl_FragColor = textureCube( map, dir ); 54 | 55 | } 56 | `; 57 | 58 | function CubemapToEquirectangular( renderer, provideCubeCamera ) { 59 | 60 | this.width = 1; 61 | this.height = 1; 62 | 63 | this.renderer = renderer; 64 | 65 | this.material = new THREE.RawShaderMaterial( { 66 | uniforms: { 67 | map: { type: 't', value: null } 68 | }, 69 | vertexShader: vertexShader, 70 | fragmentShader: fragmentShader, 71 | side: THREE.DoubleSide, 72 | transparent: true 73 | } ); 74 | 75 | this.scene = new THREE.Scene(); 76 | this.quad = new THREE.Mesh( 77 | new THREE.PlaneBufferGeometry( 1, 1 ), 78 | this.material 79 | ); 80 | this.scene.add( this.quad ); 81 | this.camera = new THREE.OrthographicCamera( 1 / - 2, 1 / 2, 1 / 2, 1 / - 2, -10000, 10000 ); 82 | 83 | this.canvas = document.createElement( 'canvas' ); 84 | this.ctx = this.canvas.getContext( '2d' ); 85 | 86 | this.cubeCamera = null; 87 | this.attachedCamera = null; 88 | 89 | this.setSize( 4096, 2048 ); 90 | 91 | var gl = this.renderer.getContext(); 92 | this.cubeMapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE ) 93 | 94 | if( provideCubeCamera ) { 95 | this.getCubeCamera( 2048 ) 96 | } 97 | 98 | } 99 | 100 | CubemapToEquirectangular.prototype.setSize = function( width, height ) { 101 | 102 | this.width = width; 103 | this.height = height; 104 | 105 | this.quad.scale.set( this.width, this.height, 1 ); 106 | 107 | this.camera.left = this.width / - 2; 108 | this.camera.right = this.width / 2; 109 | this.camera.top = this.height / 2; 110 | this.camera.bottom = this.height / - 2; 111 | 112 | this.camera.updateProjectionMatrix(); 113 | 114 | this.output = new THREE.WebGLRenderTarget( this.width, this.height, { 115 | minFilter: THREE.LinearFilter, 116 | magFilter: THREE.LinearFilter, 117 | wrapS: THREE.ClampToEdgeWrapping, 118 | wrapT: THREE.ClampToEdgeWrapping, 119 | format: THREE.RGBAFormat, 120 | type: THREE.UnsignedByteType 121 | }); 122 | 123 | this.canvas.width = this.width; 124 | this.canvas.height = this.height; 125 | 126 | } 127 | 128 | CubemapToEquirectangular.prototype.getCubeCamera = function( size ) { 129 | 130 | var cubeMapSize = Math.min( this.cubeMapSize, size ); 131 | this.cubeCamera = new THREE.CubeCamera( .1, 1000, cubeMapSize ); 132 | 133 | var options = { format: THREE.RGBAFormat, magFilter: THREE.LinearFilter, minFilter: THREE.LinearFilter }; 134 | this.cubeCamera.renderTarget = new THREE.WebGLRenderTargetCube( cubeMapSize, cubeMapSize, options ); 135 | 136 | return this.cubeCamera; 137 | 138 | } 139 | 140 | CubemapToEquirectangular.prototype.attachCubeCamera = function( camera ) { 141 | 142 | this.getCubeCamera(); 143 | this.attachedCamera = camera; 144 | 145 | } 146 | 147 | CubemapToEquirectangular.prototype.convert = function( cubeCamera, download ) { 148 | 149 | this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture; 150 | this.renderer.render( this.scene, this.camera, this.output, true ); 151 | 152 | var pixels = new Uint8Array( 4 * this.width * this.height ); 153 | this.renderer.readRenderTargetPixels( this.output, 0, 0, this.width, this.height, pixels ); 154 | 155 | var imageData = new ImageData( new Uint8ClampedArray( pixels ), this.width, this.height ); 156 | 157 | if( download !== false ) { 158 | this.download( imageData ); 159 | } 160 | 161 | return imageData 162 | 163 | }; 164 | 165 | CubemapToEquirectangular.prototype.download = function( imageData ) { 166 | 167 | this.ctx.putImageData( imageData, 0, 0 ); 168 | 169 | this.canvas.toBlob( function( blob ) { 170 | 171 | var url = URL.createObjectURL(blob); 172 | var fileName = 'pano-' + document.title + '-' + Date.now() + '.png'; 173 | var anchor = document.createElement( 'a' ); 174 | anchor.href = url; 175 | anchor.setAttribute("download", fileName); 176 | anchor.className = "download-js-link"; 177 | anchor.innerHTML = "downloading..."; 178 | anchor.style.display = "none"; 179 | document.body.appendChild(anchor); 180 | setTimeout(function() { 181 | anchor.click(); 182 | document.body.removeChild(anchor); 183 | }, 1 ); 184 | 185 | }, 'image/png' ); 186 | 187 | }; 188 | 189 | CubemapToEquirectangular.prototype.update = function( camera, scene ) { 190 | 191 | var autoClear = this.renderer.autoClear; 192 | this.renderer.autoClear = true; 193 | this.cubeCamera.position.copy( camera.position ); 194 | this.cubeCamera.updateCubeMap( this.renderer, scene ); 195 | this.renderer.autoClear = autoClear; 196 | 197 | this.convert( this.cubeCamera ); 198 | 199 | } 200 | 201 | if( typeof exports !== 'undefined' ) { 202 | if( typeof module !== 'undefined' && module.exports ) { 203 | exports = module.exports = CubemapToEquirectangular 204 | } 205 | exports.CubemapToEquirectangular = CubemapToEquirectangular 206 | } 207 | else { 208 | root.CubemapToEquirectangular = CubemapToEquirectangular 209 | } 210 | 211 | }).call(this); 212 | -------------------------------------------------------------------------------- /examples/js/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | 9 | // This set of controls performs orbiting, dollying (zooming), and panning. 10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 11 | // 12 | // Orbit - left mouse / touch: one finger move 13 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 14 | // Pan - right mouse, or arrow keys / touch: three finter swipe 15 | 16 | THREE.OrbitControls = function ( object, domElement ) { 17 | 18 | this.object = object; 19 | 20 | this.domElement = ( domElement !== undefined ) ? domElement : document; 21 | 22 | // Set to false to disable this control 23 | this.enabled = true; 24 | 25 | // "target" sets the location of focus, where the object orbits around 26 | this.target = new THREE.Vector3(); 27 | 28 | // How far you can dolly in and out ( PerspectiveCamera only ) 29 | this.minDistance = 0; 30 | this.maxDistance = Infinity; 31 | 32 | // How far you can zoom in and out ( OrthographicCamera only ) 33 | this.minZoom = 0; 34 | this.maxZoom = Infinity; 35 | 36 | // How far you can orbit vertically, upper and lower limits. 37 | // Range is 0 to Math.PI radians. 38 | this.minPolarAngle = 0; // radians 39 | this.maxPolarAngle = Math.PI; // radians 40 | 41 | // How far you can orbit horizontally, upper and lower limits. 42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 43 | this.minAzimuthAngle = - Infinity; // radians 44 | this.maxAzimuthAngle = Infinity; // radians 45 | 46 | // Set to true to enable damping (inertia) 47 | // If damping is enabled, you must call controls.update() in your animation loop 48 | this.enableDamping = false; 49 | this.dampingFactor = 0.25; 50 | 51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 52 | // Set to false to disable zooming 53 | this.enableZoom = true; 54 | this.zoomSpeed = 1.0; 55 | 56 | // Set to false to disable rotating 57 | this.enableRotate = true; 58 | this.rotateSpeed = 1.0; 59 | 60 | // Set to false to disable panning 61 | this.enablePan = true; 62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 63 | 64 | // Set to true to automatically rotate around the target 65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 66 | this.autoRotate = false; 67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 68 | 69 | // Set to false to disable use of the keys 70 | this.enableKeys = true; 71 | 72 | // The four arrow keys 73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 74 | 75 | // Mouse buttons 76 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 77 | 78 | // for reset 79 | this.target0 = this.target.clone(); 80 | this.position0 = this.object.position.clone(); 81 | this.zoom0 = this.object.zoom; 82 | 83 | // 84 | // public methods 85 | // 86 | 87 | this.getPolarAngle = function () { 88 | 89 | return spherical.phi; 90 | 91 | }; 92 | 93 | this.getAzimuthalAngle = function () { 94 | 95 | return spherical.theta; 96 | 97 | }; 98 | 99 | this.reset = function () { 100 | 101 | scope.target.copy( scope.target0 ); 102 | scope.object.position.copy( scope.position0 ); 103 | scope.object.zoom = scope.zoom0; 104 | 105 | scope.object.updateProjectionMatrix(); 106 | scope.dispatchEvent( changeEvent ); 107 | 108 | scope.update(); 109 | 110 | state = STATE.NONE; 111 | 112 | }; 113 | 114 | // this method is exposed, but perhaps it would be better if we can make it private... 115 | this.update = function() { 116 | 117 | var offset = new THREE.Vector3(); 118 | 119 | // so camera.up is the orbit axis 120 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 121 | var quatInverse = quat.clone().inverse(); 122 | 123 | var lastPosition = new THREE.Vector3(); 124 | var lastQuaternion = new THREE.Quaternion(); 125 | 126 | return function update () { 127 | 128 | var position = scope.object.position; 129 | 130 | offset.copy( position ).sub( scope.target ); 131 | 132 | // rotate offset to "y-axis-is-up" space 133 | offset.applyQuaternion( quat ); 134 | 135 | // angle from z-axis around y-axis 136 | spherical.setFromVector3( offset ); 137 | 138 | if ( scope.autoRotate && state === STATE.NONE ) { 139 | 140 | rotateLeft( getAutoRotationAngle() ); 141 | 142 | } 143 | 144 | spherical.theta += sphericalDelta.theta; 145 | spherical.phi += sphericalDelta.phi; 146 | 147 | // restrict theta to be between desired limits 148 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 149 | 150 | // restrict phi to be between desired limits 151 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 152 | 153 | spherical.makeSafe(); 154 | 155 | 156 | spherical.radius *= scale; 157 | 158 | // restrict radius to be between desired limits 159 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 160 | 161 | // move target to panned location 162 | scope.target.add( panOffset ); 163 | 164 | offset.setFromSpherical( spherical ); 165 | 166 | // rotate offset back to "camera-up-vector-is-up" space 167 | offset.applyQuaternion( quatInverse ); 168 | 169 | position.copy( scope.target ).add( offset ); 170 | 171 | scope.object.lookAt( scope.target ); 172 | 173 | if ( scope.enableDamping === true ) { 174 | 175 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 176 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 177 | 178 | } else { 179 | 180 | sphericalDelta.set( 0, 0, 0 ); 181 | 182 | } 183 | 184 | scale = 1; 185 | panOffset.set( 0, 0, 0 ); 186 | 187 | // update condition is: 188 | // min(camera displacement, camera rotation in radians)^2 > EPS 189 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 190 | 191 | if ( zoomChanged || 192 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 193 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 194 | 195 | scope.dispatchEvent( changeEvent ); 196 | 197 | lastPosition.copy( scope.object.position ); 198 | lastQuaternion.copy( scope.object.quaternion ); 199 | zoomChanged = false; 200 | 201 | return true; 202 | 203 | } 204 | 205 | return false; 206 | 207 | }; 208 | 209 | }(); 210 | 211 | this.dispose = function() { 212 | 213 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 214 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 215 | scope.domElement.removeEventListener( 'mousewheel', onMouseWheel, false ); 216 | scope.domElement.removeEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox 217 | 218 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 219 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 220 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 221 | 222 | document.removeEventListener( 'mousemove', onMouseMove, false ); 223 | document.removeEventListener( 'mouseup', onMouseUp, false ); 224 | 225 | window.removeEventListener( 'keydown', onKeyDown, false ); 226 | 227 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 228 | 229 | }; 230 | 231 | // 232 | // internals 233 | // 234 | 235 | var scope = this; 236 | 237 | var changeEvent = { type: 'change' }; 238 | var startEvent = { type: 'start' }; 239 | var endEvent = { type: 'end' }; 240 | 241 | var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 242 | 243 | var state = STATE.NONE; 244 | 245 | var EPS = 0.000001; 246 | 247 | // current position in spherical coordinates 248 | var spherical = new THREE.Spherical(); 249 | var sphericalDelta = new THREE.Spherical(); 250 | 251 | var scale = 1; 252 | var panOffset = new THREE.Vector3(); 253 | var zoomChanged = false; 254 | 255 | var rotateStart = new THREE.Vector2(); 256 | var rotateEnd = new THREE.Vector2(); 257 | var rotateDelta = new THREE.Vector2(); 258 | 259 | var panStart = new THREE.Vector2(); 260 | var panEnd = new THREE.Vector2(); 261 | var panDelta = new THREE.Vector2(); 262 | 263 | var dollyStart = new THREE.Vector2(); 264 | var dollyEnd = new THREE.Vector2(); 265 | var dollyDelta = new THREE.Vector2(); 266 | 267 | function getAutoRotationAngle() { 268 | 269 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 270 | 271 | } 272 | 273 | function getZoomScale() { 274 | 275 | return Math.pow( 0.95, scope.zoomSpeed ); 276 | 277 | } 278 | 279 | function rotateLeft( angle ) { 280 | 281 | sphericalDelta.theta -= angle; 282 | 283 | } 284 | 285 | function rotateUp( angle ) { 286 | 287 | sphericalDelta.phi -= angle; 288 | 289 | } 290 | 291 | var panLeft = function() { 292 | 293 | var v = new THREE.Vector3(); 294 | 295 | return function panLeft( distance, objectMatrix ) { 296 | 297 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 298 | v.multiplyScalar( - distance ); 299 | 300 | panOffset.add( v ); 301 | 302 | }; 303 | 304 | }(); 305 | 306 | var panUp = function() { 307 | 308 | var v = new THREE.Vector3(); 309 | 310 | return function panUp( distance, objectMatrix ) { 311 | 312 | v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix 313 | v.multiplyScalar( distance ); 314 | 315 | panOffset.add( v ); 316 | 317 | }; 318 | 319 | }(); 320 | 321 | // deltaX and deltaY are in pixels; right and down are positive 322 | var pan = function() { 323 | 324 | var offset = new THREE.Vector3(); 325 | 326 | return function pan ( deltaX, deltaY ) { 327 | 328 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 329 | 330 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 331 | 332 | // perspective 333 | var position = scope.object.position; 334 | offset.copy( position ).sub( scope.target ); 335 | var targetDistance = offset.length(); 336 | 337 | // half of the fov is center to top of screen 338 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 339 | 340 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 341 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 342 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 343 | 344 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 345 | 346 | // orthographic 347 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 348 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 349 | 350 | } else { 351 | 352 | // camera neither orthographic nor perspective 353 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 354 | scope.enablePan = false; 355 | 356 | } 357 | 358 | }; 359 | 360 | }(); 361 | 362 | function dollyIn( dollyScale ) { 363 | 364 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 365 | 366 | scale /= dollyScale; 367 | 368 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 369 | 370 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 371 | scope.object.updateProjectionMatrix(); 372 | zoomChanged = true; 373 | 374 | } else { 375 | 376 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 377 | scope.enableZoom = false; 378 | 379 | } 380 | 381 | } 382 | 383 | function dollyOut( dollyScale ) { 384 | 385 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 386 | 387 | scale *= dollyScale; 388 | 389 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 390 | 391 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 392 | scope.object.updateProjectionMatrix(); 393 | zoomChanged = true; 394 | 395 | } else { 396 | 397 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 398 | scope.enableZoom = false; 399 | 400 | } 401 | 402 | } 403 | 404 | // 405 | // event callbacks - update the object state 406 | // 407 | 408 | function handleMouseDownRotate( event ) { 409 | 410 | //console.log( 'handleMouseDownRotate' ); 411 | 412 | rotateStart.set( event.clientX, event.clientY ); 413 | 414 | } 415 | 416 | function handleMouseDownDolly( event ) { 417 | 418 | //console.log( 'handleMouseDownDolly' ); 419 | 420 | dollyStart.set( event.clientX, event.clientY ); 421 | 422 | } 423 | 424 | function handleMouseDownPan( event ) { 425 | 426 | //console.log( 'handleMouseDownPan' ); 427 | 428 | panStart.set( event.clientX, event.clientY ); 429 | 430 | } 431 | 432 | function handleMouseMoveRotate( event ) { 433 | 434 | //console.log( 'handleMouseMoveRotate' ); 435 | 436 | rotateEnd.set( event.clientX, event.clientY ); 437 | rotateDelta.subVectors( rotateEnd, rotateStart ); 438 | 439 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 440 | 441 | // rotating across whole screen goes 360 degrees around 442 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 443 | 444 | // rotating up and down along whole screen attempts to go 360, but limited to 180 445 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 446 | 447 | rotateStart.copy( rotateEnd ); 448 | 449 | scope.update(); 450 | 451 | } 452 | 453 | function handleMouseMoveDolly( event ) { 454 | 455 | //console.log( 'handleMouseMoveDolly' ); 456 | 457 | dollyEnd.set( event.clientX, event.clientY ); 458 | 459 | dollyDelta.subVectors( dollyEnd, dollyStart ); 460 | 461 | if ( dollyDelta.y > 0 ) { 462 | 463 | dollyIn( getZoomScale() ); 464 | 465 | } else if ( dollyDelta.y < 0 ) { 466 | 467 | dollyOut( getZoomScale() ); 468 | 469 | } 470 | 471 | dollyStart.copy( dollyEnd ); 472 | 473 | scope.update(); 474 | 475 | } 476 | 477 | function handleMouseMovePan( event ) { 478 | 479 | //console.log( 'handleMouseMovePan' ); 480 | 481 | panEnd.set( event.clientX, event.clientY ); 482 | 483 | panDelta.subVectors( panEnd, panStart ); 484 | 485 | pan( panDelta.x, panDelta.y ); 486 | 487 | panStart.copy( panEnd ); 488 | 489 | scope.update(); 490 | 491 | } 492 | 493 | function handleMouseUp( event ) { 494 | 495 | //console.log( 'handleMouseUp' ); 496 | 497 | } 498 | 499 | function handleMouseWheel( event ) { 500 | 501 | //console.log( 'handleMouseWheel' ); 502 | 503 | var delta = 0; 504 | 505 | if ( event.wheelDelta !== undefined ) { 506 | 507 | // WebKit / Opera / Explorer 9 508 | 509 | delta = event.wheelDelta; 510 | 511 | } else if ( event.detail !== undefined ) { 512 | 513 | // Firefox 514 | 515 | delta = - event.detail; 516 | 517 | } 518 | 519 | if ( delta > 0 ) { 520 | 521 | dollyOut( getZoomScale() ); 522 | 523 | } else if ( delta < 0 ) { 524 | 525 | dollyIn( getZoomScale() ); 526 | 527 | } 528 | 529 | scope.update(); 530 | 531 | } 532 | 533 | function handleKeyDown( event ) { 534 | 535 | //console.log( 'handleKeyDown' ); 536 | 537 | switch ( event.keyCode ) { 538 | 539 | case scope.keys.UP: 540 | pan( 0, scope.keyPanSpeed ); 541 | scope.update(); 542 | break; 543 | 544 | case scope.keys.BOTTOM: 545 | pan( 0, - scope.keyPanSpeed ); 546 | scope.update(); 547 | break; 548 | 549 | case scope.keys.LEFT: 550 | pan( scope.keyPanSpeed, 0 ); 551 | scope.update(); 552 | break; 553 | 554 | case scope.keys.RIGHT: 555 | pan( - scope.keyPanSpeed, 0 ); 556 | scope.update(); 557 | break; 558 | 559 | } 560 | 561 | } 562 | 563 | function handleTouchStartRotate( event ) { 564 | 565 | //console.log( 'handleTouchStartRotate' ); 566 | 567 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 568 | 569 | } 570 | 571 | function handleTouchStartDolly( event ) { 572 | 573 | //console.log( 'handleTouchStartDolly' ); 574 | 575 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 576 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 577 | 578 | var distance = Math.sqrt( dx * dx + dy * dy ); 579 | 580 | dollyStart.set( 0, distance ); 581 | 582 | } 583 | 584 | function handleTouchStartPan( event ) { 585 | 586 | //console.log( 'handleTouchStartPan' ); 587 | 588 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 589 | 590 | } 591 | 592 | function handleTouchMoveRotate( event ) { 593 | 594 | //console.log( 'handleTouchMoveRotate' ); 595 | 596 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 597 | rotateDelta.subVectors( rotateEnd, rotateStart ); 598 | 599 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 600 | 601 | // rotating across whole screen goes 360 degrees around 602 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 603 | 604 | // rotating up and down along whole screen attempts to go 360, but limited to 180 605 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 606 | 607 | rotateStart.copy( rotateEnd ); 608 | 609 | scope.update(); 610 | 611 | } 612 | 613 | function handleTouchMoveDolly( event ) { 614 | 615 | //console.log( 'handleTouchMoveDolly' ); 616 | 617 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 618 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 619 | 620 | var distance = Math.sqrt( dx * dx + dy * dy ); 621 | 622 | dollyEnd.set( 0, distance ); 623 | 624 | dollyDelta.subVectors( dollyEnd, dollyStart ); 625 | 626 | if ( dollyDelta.y > 0 ) { 627 | 628 | dollyOut( getZoomScale() ); 629 | 630 | } else if ( dollyDelta.y < 0 ) { 631 | 632 | dollyIn( getZoomScale() ); 633 | 634 | } 635 | 636 | dollyStart.copy( dollyEnd ); 637 | 638 | scope.update(); 639 | 640 | } 641 | 642 | function handleTouchMovePan( event ) { 643 | 644 | //console.log( 'handleTouchMovePan' ); 645 | 646 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 647 | 648 | panDelta.subVectors( panEnd, panStart ); 649 | 650 | pan( panDelta.x, panDelta.y ); 651 | 652 | panStart.copy( panEnd ); 653 | 654 | scope.update(); 655 | 656 | } 657 | 658 | function handleTouchEnd( event ) { 659 | 660 | //console.log( 'handleTouchEnd' ); 661 | 662 | } 663 | 664 | // 665 | // event handlers - FSM: listen for events and reset state 666 | // 667 | 668 | function onMouseDown( event ) { 669 | 670 | if ( scope.enabled === false ) return; 671 | 672 | event.preventDefault(); 673 | 674 | if ( event.button === scope.mouseButtons.ORBIT ) { 675 | 676 | if ( scope.enableRotate === false ) return; 677 | 678 | handleMouseDownRotate( event ); 679 | 680 | state = STATE.ROTATE; 681 | 682 | } else if ( event.button === scope.mouseButtons.ZOOM ) { 683 | 684 | if ( scope.enableZoom === false ) return; 685 | 686 | handleMouseDownDolly( event ); 687 | 688 | state = STATE.DOLLY; 689 | 690 | } else if ( event.button === scope.mouseButtons.PAN ) { 691 | 692 | if ( scope.enablePan === false ) return; 693 | 694 | handleMouseDownPan( event ); 695 | 696 | state = STATE.PAN; 697 | 698 | } 699 | 700 | if ( state !== STATE.NONE ) { 701 | 702 | document.addEventListener( 'mousemove', onMouseMove, false ); 703 | document.addEventListener( 'mouseup', onMouseUp, false ); 704 | 705 | scope.dispatchEvent( startEvent ); 706 | 707 | } 708 | 709 | } 710 | 711 | function onMouseMove( event ) { 712 | 713 | if ( scope.enabled === false ) return; 714 | 715 | event.preventDefault(); 716 | 717 | if ( state === STATE.ROTATE ) { 718 | 719 | if ( scope.enableRotate === false ) return; 720 | 721 | handleMouseMoveRotate( event ); 722 | 723 | } else if ( state === STATE.DOLLY ) { 724 | 725 | if ( scope.enableZoom === false ) return; 726 | 727 | handleMouseMoveDolly( event ); 728 | 729 | } else if ( state === STATE.PAN ) { 730 | 731 | if ( scope.enablePan === false ) return; 732 | 733 | handleMouseMovePan( event ); 734 | 735 | } 736 | 737 | } 738 | 739 | function onMouseUp( event ) { 740 | 741 | if ( scope.enabled === false ) return; 742 | 743 | handleMouseUp( event ); 744 | 745 | document.removeEventListener( 'mousemove', onMouseMove, false ); 746 | document.removeEventListener( 'mouseup', onMouseUp, false ); 747 | 748 | scope.dispatchEvent( endEvent ); 749 | 750 | state = STATE.NONE; 751 | 752 | } 753 | 754 | function onMouseWheel( event ) { 755 | 756 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 757 | 758 | event.preventDefault(); 759 | event.stopPropagation(); 760 | 761 | handleMouseWheel( event ); 762 | 763 | scope.dispatchEvent( startEvent ); // not sure why these are here... 764 | scope.dispatchEvent( endEvent ); 765 | 766 | } 767 | 768 | function onKeyDown( event ) { 769 | 770 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 771 | 772 | handleKeyDown( event ); 773 | 774 | } 775 | 776 | function onTouchStart( event ) { 777 | 778 | if ( scope.enabled === false ) return; 779 | 780 | switch ( event.touches.length ) { 781 | 782 | case 1: // one-fingered touch: rotate 783 | 784 | if ( scope.enableRotate === false ) return; 785 | 786 | handleTouchStartRotate( event ); 787 | 788 | state = STATE.TOUCH_ROTATE; 789 | 790 | break; 791 | 792 | case 2: // two-fingered touch: dolly 793 | 794 | if ( scope.enableZoom === false ) return; 795 | 796 | handleTouchStartDolly( event ); 797 | 798 | state = STATE.TOUCH_DOLLY; 799 | 800 | break; 801 | 802 | case 3: // three-fingered touch: pan 803 | 804 | if ( scope.enablePan === false ) return; 805 | 806 | handleTouchStartPan( event ); 807 | 808 | state = STATE.TOUCH_PAN; 809 | 810 | break; 811 | 812 | default: 813 | 814 | state = STATE.NONE; 815 | 816 | } 817 | 818 | if ( state !== STATE.NONE ) { 819 | 820 | scope.dispatchEvent( startEvent ); 821 | 822 | } 823 | 824 | } 825 | 826 | function onTouchMove( event ) { 827 | 828 | if ( scope.enabled === false ) return; 829 | 830 | event.preventDefault(); 831 | event.stopPropagation(); 832 | 833 | switch ( event.touches.length ) { 834 | 835 | case 1: // one-fingered touch: rotate 836 | 837 | if ( scope.enableRotate === false ) return; 838 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?... 839 | 840 | handleTouchMoveRotate( event ); 841 | 842 | break; 843 | 844 | case 2: // two-fingered touch: dolly 845 | 846 | if ( scope.enableZoom === false ) return; 847 | if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?... 848 | 849 | handleTouchMoveDolly( event ); 850 | 851 | break; 852 | 853 | case 3: // three-fingered touch: pan 854 | 855 | if ( scope.enablePan === false ) return; 856 | if ( state !== STATE.TOUCH_PAN ) return; // is this needed?... 857 | 858 | handleTouchMovePan( event ); 859 | 860 | break; 861 | 862 | default: 863 | 864 | state = STATE.NONE; 865 | 866 | } 867 | 868 | } 869 | 870 | function onTouchEnd( event ) { 871 | 872 | if ( scope.enabled === false ) return; 873 | 874 | handleTouchEnd( event ); 875 | 876 | scope.dispatchEvent( endEvent ); 877 | 878 | state = STATE.NONE; 879 | 880 | } 881 | 882 | function onContextMenu( event ) { 883 | 884 | event.preventDefault(); 885 | 886 | } 887 | 888 | // 889 | 890 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 891 | 892 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 893 | scope.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 894 | scope.domElement.addEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox 895 | 896 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 897 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 898 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 899 | 900 | window.addEventListener( 'keydown', onKeyDown, false ); 901 | 902 | // force an update at start 903 | 904 | this.update(); 905 | 906 | }; 907 | 908 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 909 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 910 | 911 | Object.defineProperties( THREE.OrbitControls.prototype, { 912 | 913 | center: { 914 | 915 | get: function () { 916 | 917 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); 918 | return this.target; 919 | 920 | } 921 | 922 | }, 923 | 924 | // backward compatibility 925 | 926 | noZoom: { 927 | 928 | get: function () { 929 | 930 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 931 | return ! this.enableZoom; 932 | 933 | }, 934 | 935 | set: function ( value ) { 936 | 937 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 938 | this.enableZoom = ! value; 939 | 940 | } 941 | 942 | }, 943 | 944 | noRotate: { 945 | 946 | get: function () { 947 | 948 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 949 | return ! this.enableRotate; 950 | 951 | }, 952 | 953 | set: function ( value ) { 954 | 955 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 956 | this.enableRotate = ! value; 957 | 958 | } 959 | 960 | }, 961 | 962 | noPan: { 963 | 964 | get: function () { 965 | 966 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 967 | return ! this.enablePan; 968 | 969 | }, 970 | 971 | set: function ( value ) { 972 | 973 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 974 | this.enablePan = ! value; 975 | 976 | } 977 | 978 | }, 979 | 980 | noKeys: { 981 | 982 | get: function () { 983 | 984 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 985 | return ! this.enableKeys; 986 | 987 | }, 988 | 989 | set: function ( value ) { 990 | 991 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 992 | this.enableKeys = ! value; 993 | 994 | } 995 | 996 | }, 997 | 998 | staticMoving : { 999 | 1000 | get: function () { 1001 | 1002 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1003 | return ! this.enableDamping; 1004 | 1005 | }, 1006 | 1007 | set: function ( value ) { 1008 | 1009 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1010 | this.enableDamping = ! value; 1011 | 1012 | } 1013 | 1014 | }, 1015 | 1016 | dynamicDampingFactor : { 1017 | 1018 | get: function () { 1019 | 1020 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1021 | return this.dampingFactor; 1022 | 1023 | }, 1024 | 1025 | set: function ( value ) { 1026 | 1027 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1028 | this.dampingFactor = value; 1029 | 1030 | } 1031 | 1032 | } 1033 | 1034 | } ); 1035 | --------------------------------------------------------------------------------