├── docs ├── _config.yml └── index.html ├── research └── 10.1.1.103.7187.pdf ├── Makefile ├── cachebust.sh ├── package.json ├── src ├── box.js ├── util.js ├── boidManager.js └── boid.js ├── lib ├── shaders │ ├── CopyShader.js │ ├── DigitalGlitch.js │ ├── GlitchPass.js │ └── DepthLimitedBlurShader.js ├── postprocessing │ ├── Pass.js │ ├── ShaderPass.js │ ├── RenderPass.js │ ├── MaskPass.js │ └── EffectComposer.js └── controls │ └── OrbitControls.js ├── .gitignore ├── index.html ├── index.js └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /research/10.1.1.103.7187.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/boids/master/research/10.1.1.103.7187.pdf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run dist 2 | 3 | dist: 4 | mkdir -p docs 5 | touch docs/_config.yml 6 | npm run dist 7 | ./cachebust.sh 8 | 9 | run: 10 | npm run start -------------------------------------------------------------------------------- /cachebust.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TS="$(date '+%s')" 4 | for asset in "js" "css"; do 5 | perl -pi -e 's/funwithcode.'$asset'.*?">/funwithcode.'$asset'?'$TS'">/g' docs/index.html 6 | done -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-doodle-2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "dist": "parcel build index.html -d docs --public-url /boids/", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "parcel-bundler": "^1.12.3" 15 | }, 16 | "dependencies": { 17 | "three": "^0.108.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/box.js: -------------------------------------------------------------------------------- 1 | var THREE = require('three') 2 | 3 | export default class Box { 4 | constructor(width = 100, height = 100, depth = 100, color = 0xffffff) { 5 | const geometry = new THREE.BoxGeometry(width, height, depth, 1, 1, 1); 6 | 7 | this.mesh = new THREE.Group(); 8 | var lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.05 }); 9 | const meshMaterial = new THREE.MeshLambertMaterial({ 10 | color, 11 | transparent: true, 12 | opacity: 0.9, 13 | wireframe: false, 14 | depthWrite: false, 15 | blending: THREE.NormalBlending 16 | }); 17 | this.mesh.add(new THREE.Mesh(geometry, meshMaterial)); 18 | this.mesh.add(new THREE.LineSegments(new THREE.WireframeGeometry(geometry), lineMaterial)); 19 | } 20 | } -------------------------------------------------------------------------------- /lib/shaders/CopyShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * 4 | * Full-screen textured quad shader 5 | */ 6 | 7 | 8 | 9 | var CopyShader = { 10 | 11 | uniforms: { 12 | 13 | "tDiffuse": { value: null }, 14 | "opacity": { value: 1.0 } 15 | 16 | }, 17 | 18 | vertexShader: [ 19 | 20 | "varying vec2 vUv;", 21 | 22 | "void main() {", 23 | 24 | " vUv = uv;", 25 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 26 | 27 | "}" 28 | 29 | ].join( "\n" ), 30 | 31 | fragmentShader: [ 32 | 33 | "uniform float opacity;", 34 | 35 | "uniform sampler2D tDiffuse;", 36 | 37 | "varying vec2 vUv;", 38 | 39 | "void main() {", 40 | 41 | " vec4 texel = texture2D( tDiffuse, vUv );", 42 | " gl_FragColor = opacity * texel;", 43 | 44 | "}" 45 | 46 | ].join( "\n" ) 47 | 48 | }; 49 | 50 | export { CopyShader }; 51 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Boids -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const THREE = require('three') 2 | import Box from './box' 3 | 4 | var sphereCastDirections = [] 5 | 6 | // from https://www.youtube.com/watch?v=bqtqltqcQhw 7 | function initSphereCast() { 8 | const numViewDirections = 300 9 | const goldenRatio = (1 + Math.sqrt(5)) / 2 10 | const angleIncrement = Math.PI * 2 * goldenRatio; 11 | 12 | for (var i = 0; i < numViewDirections; i++) { 13 | const t = i / numViewDirections 14 | const inclination = Math.acos(1 - 2 * t) 15 | const azimuth = angleIncrement * i 16 | 17 | const x = Math.sin (inclination) * Math.cos (azimuth) 18 | const y = Math.sin (inclination) * Math.sin (azimuth) 19 | const z = Math.cos (inclination) 20 | sphereCastDirections.push(new THREE.Vector3 (x, y, z)) 21 | } 22 | } 23 | 24 | initSphereCast() 25 | 26 | /** 27 | * Adds a simple box obstacle to the scene. 28 | * 29 | * @param {*} obstacles the array to push the obstacle to 30 | * @param {*} scene the scene to add the obstacle to 31 | * @param {*} w width of obstacle 32 | * @param {*} h height of obstacle 33 | * @param {*} d depth of obstacle 34 | * @param {*} c color of obstacle 35 | * @param {*} x coordinate of obstacle 36 | * @param {*} y coordinate of obstacle 37 | * @param {*} z coordinate of obstacle 38 | */ 39 | function addObstacle(obstacles, scene, w, h, d, c, x, y, z) { 40 | var obs1 = new Box(w, h, d, c); 41 | obs1.mesh.position.set(x, y, z) 42 | scene.add(obs1.mesh) 43 | obstacles.push(obs1) 44 | } 45 | 46 | export const utils = { 47 | sphereCastDirections, 48 | addObstacle 49 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | # custom 91 | dist 92 | reveal.min.css 93 | reveal.min.js 94 | -------------------------------------------------------------------------------- /src/boidManager.js: -------------------------------------------------------------------------------- 1 | var THREE = require('three') 2 | import Boid from './boid'; 3 | 4 | export default class BoidManager { 5 | /** 6 | * 7 | * @param {*} numberOfBoids 8 | * @param {*} obstacles other obstacles in the world to consider when avoiding collisions 9 | * @param {*} target a target for all boids to move towards 10 | */ 11 | constructor(scene, numberOfBoids = 20, obstacles = [], target = null) { 12 | 13 | // create the boids 14 | this.initBoids(scene, numberOfBoids, target) 15 | 16 | // for each boid, add the other boids to its collidableMeshList, and also add 17 | // the meshes from the common collidableMeshList 18 | 19 | this.obstacles = obstacles 20 | } 21 | 22 | initBoids(scene, numberOfBoids, target) { 23 | this.boids = this.boids || []; 24 | 25 | var randomX, randomY, randomZ, colour, followTarget, quaternion 26 | 27 | for (let i = 0; i < numberOfBoids; i++) { 28 | randomX = Math.random() * 250 - 125 29 | randomY = Math.random() * 250 - 125 30 | randomZ = Math.random() * 250 - 125 31 | colour = null // will use default color in getBoid 32 | followTarget = false 33 | quaternion = null 34 | 35 | // reference boid 36 | if (i === 0) { 37 | randomX = 0 38 | randomY = 0 39 | randomZ = 0 40 | colour = 0xe56289 41 | // followTarget = true 42 | quaternion = null 43 | } 44 | 45 | var position = new THREE.Vector3(randomX, randomY, randomZ) 46 | 47 | var boid = new Boid(scene, target, position, quaternion, colour, followTarget); 48 | this.boids.push(boid) 49 | } 50 | } 51 | 52 | update(delta) { 53 | this.boids.forEach(boid => { 54 | boid.update(delta, this.boids, this.obstacles) 55 | }) 56 | } 57 | } -------------------------------------------------------------------------------- /lib/postprocessing/Pass.js: -------------------------------------------------------------------------------- 1 | import { 2 | OrthographicCamera, 3 | PlaneBufferGeometry, 4 | Mesh 5 | } from "three"; 6 | 7 | function Pass() { 8 | 9 | // if set to true, the pass is processed by the composer 10 | this.enabled = true; 11 | 12 | // if set to true, the pass indicates to swap read and write buffer after rendering 13 | this.needsSwap = true; 14 | 15 | // if set to true, the pass clears its buffer before rendering 16 | this.clear = false; 17 | 18 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. 19 | this.renderToScreen = false; 20 | 21 | } 22 | 23 | Object.assign( Pass.prototype, { 24 | 25 | setSize: function ( /* width, height */ ) {}, 26 | 27 | render: function ( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 28 | 29 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); 30 | 31 | } 32 | 33 | } ); 34 | 35 | // Helper for passes that need to fill the viewport with a single quad. 36 | 37 | Pass.FullScreenQuad = ( function () { 38 | 39 | var camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 40 | var geometry = new PlaneBufferGeometry( 2, 2 ); 41 | 42 | var FullScreenQuad = function ( material ) { 43 | 44 | this._mesh = new Mesh( geometry, material ); 45 | 46 | }; 47 | 48 | Object.defineProperty( FullScreenQuad.prototype, 'material', { 49 | 50 | get: function () { 51 | 52 | return this._mesh.material; 53 | 54 | }, 55 | 56 | set: function ( value ) { 57 | 58 | this._mesh.material = value; 59 | 60 | } 61 | 62 | } ); 63 | 64 | Object.assign( FullScreenQuad.prototype, { 65 | 66 | render: function ( renderer ) { 67 | 68 | renderer.render( this._mesh, camera ); 69 | 70 | } 71 | 72 | } ); 73 | 74 | return FullScreenQuad; 75 | 76 | } )(); 77 | 78 | export { Pass }; 79 | -------------------------------------------------------------------------------- /lib/postprocessing/ShaderPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | import { 6 | ShaderMaterial, 7 | UniformsUtils 8 | } from "three"; 9 | import { Pass } from "../postprocessing/Pass.js"; 10 | 11 | var ShaderPass = function ( shader, textureID ) { 12 | 13 | Pass.call( this ); 14 | 15 | this.textureID = ( textureID !== undefined ) ? textureID : "tDiffuse"; 16 | 17 | if ( shader instanceof ShaderMaterial ) { 18 | 19 | this.uniforms = shader.uniforms; 20 | 21 | this.material = shader; 22 | 23 | } else if ( shader ) { 24 | 25 | this.uniforms = UniformsUtils.clone( shader.uniforms ); 26 | 27 | this.material = new ShaderMaterial( { 28 | 29 | defines: Object.assign( {}, shader.defines ), 30 | uniforms: this.uniforms, 31 | vertexShader: shader.vertexShader, 32 | fragmentShader: shader.fragmentShader 33 | 34 | } ); 35 | 36 | } 37 | 38 | this.fsQuad = new Pass.FullScreenQuad( this.material ); 39 | 40 | }; 41 | 42 | ShaderPass.prototype = Object.assign( Object.create( Pass.prototype ), { 43 | 44 | constructor: ShaderPass, 45 | 46 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 47 | 48 | if ( this.uniforms[ this.textureID ] ) { 49 | 50 | this.uniforms[ this.textureID ].value = readBuffer.texture; 51 | 52 | } 53 | 54 | this.fsQuad.material = this.material; 55 | 56 | if ( this.renderToScreen ) { 57 | 58 | renderer.setRenderTarget( null ); 59 | this.fsQuad.render( renderer ); 60 | 61 | } else { 62 | 63 | renderer.setRenderTarget( writeBuffer ); 64 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 65 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 66 | this.fsQuad.render( renderer ); 67 | 68 | } 69 | 70 | } 71 | 72 | } ); 73 | 74 | export { ShaderPass }; 75 | -------------------------------------------------------------------------------- /lib/postprocessing/RenderPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | 6 | import { Pass } from "../postprocessing/Pass.js"; 7 | 8 | var RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) { 9 | 10 | Pass.call( this ); 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.overrideMaterial = overrideMaterial; 16 | 17 | this.clearColor = clearColor; 18 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0; 19 | 20 | this.clear = true; 21 | this.clearDepth = false; 22 | this.needsSwap = false; 23 | 24 | }; 25 | 26 | RenderPass.prototype = Object.assign( Object.create( Pass.prototype ), { 27 | 28 | constructor: RenderPass, 29 | 30 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 31 | 32 | var oldAutoClear = renderer.autoClear; 33 | renderer.autoClear = false; 34 | 35 | this.scene.overrideMaterial = this.overrideMaterial; 36 | 37 | var oldClearColor, oldClearAlpha; 38 | 39 | if ( this.clearColor ) { 40 | 41 | oldClearColor = renderer.getClearColor().getHex(); 42 | oldClearAlpha = renderer.getClearAlpha(); 43 | 44 | renderer.setClearColor( this.clearColor, this.clearAlpha ); 45 | 46 | } 47 | 48 | if ( this.clearDepth ) { 49 | 50 | renderer.clearDepth(); 51 | 52 | } 53 | 54 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer ); 55 | 56 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 57 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 58 | renderer.render( this.scene, this.camera ); 59 | 60 | if ( this.clearColor ) { 61 | 62 | renderer.setClearColor( oldClearColor, oldClearAlpha ); 63 | 64 | } 65 | 66 | this.scene.overrideMaterial = null; 67 | renderer.autoClear = oldAutoClear; 68 | 69 | } 70 | 71 | } ); 72 | 73 | export { RenderPass }; 74 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Boids 8 | 9 | 10 | 11 | 12 | 13 | 14 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /lib/postprocessing/MaskPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | 6 | import { Pass } from "../postprocessing/Pass.js"; 7 | 8 | var MaskPass = function ( scene, camera ) { 9 | 10 | Pass.call( this ); 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.clear = true; 16 | this.needsSwap = false; 17 | 18 | this.inverse = false; 19 | 20 | }; 21 | 22 | MaskPass.prototype = Object.assign( Object.create( Pass.prototype ), { 23 | 24 | constructor: MaskPass, 25 | 26 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 27 | 28 | var context = renderer.getContext(); 29 | var state = renderer.state; 30 | 31 | // don't update color or depth 32 | 33 | state.buffers.color.setMask( false ); 34 | state.buffers.depth.setMask( false ); 35 | 36 | // lock buffers 37 | 38 | state.buffers.color.setLocked( true ); 39 | state.buffers.depth.setLocked( true ); 40 | 41 | // set up stencil 42 | 43 | var writeValue, clearValue; 44 | 45 | if ( this.inverse ) { 46 | 47 | writeValue = 0; 48 | clearValue = 1; 49 | 50 | } else { 51 | 52 | writeValue = 1; 53 | clearValue = 0; 54 | 55 | } 56 | 57 | state.buffers.stencil.setTest( true ); 58 | state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE ); 59 | state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff ); 60 | state.buffers.stencil.setClear( clearValue ); 61 | state.buffers.stencil.setLocked( true ); 62 | 63 | // draw into the stencil buffer 64 | 65 | renderer.setRenderTarget( readBuffer ); 66 | if ( this.clear ) renderer.clear(); 67 | renderer.render( this.scene, this.camera ); 68 | 69 | renderer.setRenderTarget( writeBuffer ); 70 | if ( this.clear ) renderer.clear(); 71 | renderer.render( this.scene, this.camera ); 72 | 73 | // unlock color and depth buffer for subsequent rendering 74 | 75 | state.buffers.color.setLocked( false ); 76 | state.buffers.depth.setLocked( false ); 77 | 78 | // only render where stencil is set to 1 79 | 80 | state.buffers.stencil.setLocked( false ); 81 | state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1 82 | state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP ); 83 | state.buffers.stencil.setLocked( true ); 84 | 85 | } 86 | 87 | } ); 88 | 89 | 90 | var ClearMaskPass = function () { 91 | 92 | Pass.call( this ); 93 | 94 | this.needsSwap = false; 95 | 96 | }; 97 | 98 | ClearMaskPass.prototype = Object.create( Pass.prototype ); 99 | 100 | Object.assign( ClearMaskPass.prototype, { 101 | 102 | render: function ( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 103 | 104 | renderer.state.buffers.stencil.setLocked( false ); 105 | renderer.state.buffers.stencil.setTest( false ); 106 | 107 | } 108 | 109 | } ); 110 | 111 | export { MaskPass, ClearMaskPass }; 112 | -------------------------------------------------------------------------------- /lib/shaders/DigitalGlitch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author felixturner / http://airtight.cc/ 3 | * 4 | * RGB Shift Shader 5 | * Shifts red and blue channels from center in opposite directions 6 | * Ported from http://kriss.cx/tom/2009/05/rgb-shift/ 7 | * by Tom Butterworth / http://kriss.cx/tom/ 8 | * 9 | * amount: shift distance (1 is width of input) 10 | * angle: shift angle in radians 11 | */ 12 | 13 | 14 | 15 | var DigitalGlitch = { 16 | 17 | uniforms: { 18 | 19 | "tDiffuse": { value: null }, //diffuse texture 20 | "tDisp": { value: null }, //displacement texture for digital glitch squares 21 | "byp": { value: 0 }, //apply the glitch ? 22 | "amount": { value: 0.08 }, 23 | "angle": { value: 0.02 }, 24 | "seed": { value: 0.02 }, 25 | "seed_x": { value: 0.02 }, //-1,1 26 | "seed_y": { value: 0.02 }, //-1,1 27 | "distortion_x": { value: 0.5 }, 28 | "distortion_y": { value: 0.6 }, 29 | "col_s": { value: 0.05 } 30 | }, 31 | 32 | vertexShader: [ 33 | 34 | "varying vec2 vUv;", 35 | "void main() {", 36 | " vUv = uv;", 37 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 38 | "}" 39 | ].join( "\n" ), 40 | 41 | fragmentShader: [ 42 | "uniform int byp;", //should we apply the glitch ? 43 | 44 | "uniform sampler2D tDiffuse;", 45 | "uniform sampler2D tDisp;", 46 | 47 | "uniform float amount;", 48 | "uniform float angle;", 49 | "uniform float seed;", 50 | "uniform float seed_x;", 51 | "uniform float seed_y;", 52 | "uniform float distortion_x;", 53 | "uniform float distortion_y;", 54 | "uniform float col_s;", 55 | 56 | "varying vec2 vUv;", 57 | 58 | 59 | "float rand(vec2 co){", 60 | " return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);", 61 | "}", 62 | 63 | "void main() {", 64 | " if(byp<1) {", 65 | " vec2 p = vUv;", 66 | " float xs = floor(gl_FragCoord.x / 0.5);", 67 | " float ys = floor(gl_FragCoord.y / 0.5);", 68 | //based on staffantans glitch shader for unity https://github.com/staffantan/unityglitch 69 | " vec4 normal = texture2D (tDisp, p*seed*seed);", 70 | " if(p.ydistortion_x-col_s*seed) {", 71 | " if(seed_x>0.){", 72 | " p.y = 1. - (p.y + distortion_y);", 73 | " }", 74 | " else {", 75 | " p.y = distortion_y;", 76 | " }", 77 | " }", 78 | " if(p.xdistortion_y-col_s*seed) {", 79 | " if(seed_y>0.){", 80 | " p.x=distortion_x;", 81 | " }", 82 | " else {", 83 | " p.x = 1. - (p.x + distortion_x);", 84 | " }", 85 | " }", 86 | " p.x+=normal.x*seed_x*(seed/5.);", 87 | " p.y+=normal.y*seed_y*(seed/5.);", 88 | //base from RGB shift shader 89 | " vec2 offset = amount * vec2( cos(angle), sin(angle));", 90 | " vec4 cr = texture2D(tDiffuse, p + offset);", 91 | " vec4 cga = texture2D(tDiffuse, p);", 92 | " vec4 cb = texture2D(tDiffuse, p - offset);", 93 | " gl_FragColor = vec4(cr.r, cga.g, cb.b, cga.a);", 94 | //add noise 95 | " vec4 snow = 200.*amount*vec4(rand(vec2(xs * seed,ys * seed*50.))*0.2);", 96 | " gl_FragColor = gl_FragColor+ snow;", 97 | " }", 98 | " else {", 99 | " gl_FragColor=texture2D (tDiffuse, vUv);", 100 | " }", 101 | "}" 102 | 103 | ].join( "\n" ) 104 | 105 | }; 106 | 107 | export { DigitalGlitch }; 108 | -------------------------------------------------------------------------------- /lib/shaders/GlitchPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | import { 6 | DataTexture, 7 | FloatType, 8 | Math as _Math, 9 | RGBFormat, 10 | ShaderMaterial, 11 | UniformsUtils 12 | } from "three"; 13 | import { Pass } from "../postprocessing/Pass.js"; 14 | import { DigitalGlitch } from "../shaders/DigitalGlitch.js"; 15 | 16 | var GlitchPass = function ( dt_size ) { 17 | 18 | Pass.call( this ); 19 | 20 | if ( DigitalGlitch === undefined ) console.error( "GlitchPass relies on DigitalGlitch" ); 21 | 22 | var shader = DigitalGlitch; 23 | this.uniforms = UniformsUtils.clone( shader.uniforms ); 24 | 25 | if ( dt_size == undefined ) dt_size = 64; 26 | 27 | 28 | this.uniforms[ "tDisp" ].value = this.generateHeightmap( dt_size ); 29 | 30 | 31 | this.material = new ShaderMaterial( { 32 | uniforms: this.uniforms, 33 | vertexShader: shader.vertexShader, 34 | fragmentShader: shader.fragmentShader 35 | } ); 36 | 37 | this.fsQuad = new Pass.FullScreenQuad( this.material ); 38 | 39 | this.goWild = false; 40 | this.curF = 0; 41 | this.generateTrigger(); 42 | 43 | }; 44 | 45 | GlitchPass.prototype = Object.assign( Object.create( Pass.prototype ), { 46 | 47 | constructor: GlitchPass, 48 | 49 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 50 | 51 | this.uniforms[ "tDiffuse" ].value = readBuffer.texture; 52 | this.uniforms[ 'seed' ].value = Math.random();//default seeding 53 | this.uniforms[ 'byp' ].value = 0; 54 | 55 | if ( this.curF % this.randX == 0 || this.goWild == true ) { 56 | 57 | this.uniforms[ 'amount' ].value = Math.random() / 30; 58 | this.uniforms[ 'angle' ].value = _Math.randFloat( - Math.PI, Math.PI ); 59 | this.uniforms[ 'seed_x' ].value = _Math.randFloat( - 1, 1 ); 60 | this.uniforms[ 'seed_y' ].value = _Math.randFloat( - 1, 1 ); 61 | this.uniforms[ 'distortion_x' ].value = _Math.randFloat( 0, 1 ); 62 | this.uniforms[ 'distortion_y' ].value = _Math.randFloat( 0, 1 ); 63 | this.curF = 0; 64 | this.generateTrigger(); 65 | 66 | } else if ( this.curF % this.randX < this.randX / 5 ) { 67 | 68 | this.uniforms[ 'amount' ].value = Math.random() / 90; 69 | this.uniforms[ 'angle' ].value = _Math.randFloat( - Math.PI, Math.PI ); 70 | this.uniforms[ 'distortion_x' ].value = _Math.randFloat( 0, 1 ); 71 | this.uniforms[ 'distortion_y' ].value = _Math.randFloat( 0, 1 ); 72 | this.uniforms[ 'seed_x' ].value = _Math.randFloat( - 0.3, 0.3 ); 73 | this.uniforms[ 'seed_y' ].value = _Math.randFloat( - 0.3, 0.3 ); 74 | 75 | } else if ( this.goWild == false ) { 76 | 77 | this.uniforms[ 'byp' ].value = 1; 78 | 79 | } 80 | 81 | this.curF ++; 82 | 83 | if ( this.renderToScreen ) { 84 | 85 | renderer.setRenderTarget( null ); 86 | this.fsQuad.render( renderer ); 87 | 88 | } else { 89 | 90 | renderer.setRenderTarget( writeBuffer ); 91 | if ( this.clear ) renderer.clear(); 92 | this.fsQuad.render( renderer ); 93 | 94 | } 95 | 96 | }, 97 | 98 | generateTrigger: function () { 99 | 100 | this.randX = _Math.randInt( 120, 240 ); 101 | 102 | }, 103 | 104 | generateHeightmap: function ( dt_size ) { 105 | 106 | var data_arr = new Float32Array( dt_size * dt_size * 3 ); 107 | var length = dt_size * dt_size; 108 | 109 | for ( var i = 0; i < length; i ++ ) { 110 | 111 | var val = _Math.randFloat( 0, 1 ); 112 | data_arr[ i * 3 + 0 ] = val; 113 | data_arr[ i * 3 + 1 ] = val; 114 | data_arr[ i * 3 + 2 ] = val; 115 | 116 | } 117 | 118 | return new DataTexture( data_arr, dt_size, dt_size, RGBFormat, FloatType ); 119 | 120 | } 121 | 122 | } ); 123 | 124 | export { GlitchPass }; 125 | -------------------------------------------------------------------------------- /lib/shaders/DepthLimitedBlurShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO 3 | */ 4 | 5 | import { 6 | Vector2 7 | } from "three"; 8 | 9 | var DepthLimitedBlurShader = { 10 | defines: { 11 | "KERNEL_RADIUS": 4, 12 | "DEPTH_PACKING": 1, 13 | "PERSPECTIVE_CAMERA": 1 14 | }, 15 | uniforms: { 16 | "tDiffuse": { value: null }, 17 | "size": { value: new Vector2( 512, 512 ) }, 18 | "sampleUvOffsets": { value: [ new Vector2( 0, 0 ) ] }, 19 | "sampleWeights": { value: [ 1.0 ] }, 20 | "tDepth": { value: null }, 21 | "cameraNear": { value: 10 }, 22 | "cameraFar": { value: 1000 }, 23 | "depthCutoff": { value: 10 }, 24 | }, 25 | vertexShader: [ 26 | "#include ", 27 | 28 | "uniform vec2 size;", 29 | 30 | "varying vec2 vUv;", 31 | "varying vec2 vInvSize;", 32 | 33 | "void main() {", 34 | " vUv = uv;", 35 | " vInvSize = 1.0 / size;", 36 | 37 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 38 | "}" 39 | 40 | ].join( "\n" ), 41 | fragmentShader: [ 42 | "#include ", 43 | "#include ", 44 | 45 | "uniform sampler2D tDiffuse;", 46 | "uniform sampler2D tDepth;", 47 | 48 | "uniform float cameraNear;", 49 | "uniform float cameraFar;", 50 | "uniform float depthCutoff;", 51 | 52 | "uniform vec2 sampleUvOffsets[ KERNEL_RADIUS + 1 ];", 53 | "uniform float sampleWeights[ KERNEL_RADIUS + 1 ];", 54 | 55 | "varying vec2 vUv;", 56 | "varying vec2 vInvSize;", 57 | 58 | "float getDepth( const in vec2 screenPosition ) {", 59 | " #if DEPTH_PACKING == 1", 60 | " return unpackRGBAToDepth( texture2D( tDepth, screenPosition ) );", 61 | " #else", 62 | " return texture2D( tDepth, screenPosition ).x;", 63 | " #endif", 64 | "}", 65 | 66 | "float getViewZ( const in float depth ) {", 67 | " #if PERSPECTIVE_CAMERA == 1", 68 | " return perspectiveDepthToViewZ( depth, cameraNear, cameraFar );", 69 | " #else", 70 | " return orthographicDepthToViewZ( depth, cameraNear, cameraFar );", 71 | " #endif", 72 | "}", 73 | 74 | "void main() {", 75 | " float depth = getDepth( vUv );", 76 | " if( depth >= ( 1.0 - EPSILON ) ) {", 77 | " discard;", 78 | " }", 79 | 80 | " float centerViewZ = -getViewZ( depth );", 81 | " bool rBreak = false, lBreak = false;", 82 | 83 | " float weightSum = sampleWeights[0];", 84 | " vec4 diffuseSum = texture2D( tDiffuse, vUv ) * weightSum;", 85 | 86 | " for( int i = 1; i <= KERNEL_RADIUS; i ++ ) {", 87 | 88 | " float sampleWeight = sampleWeights[i];", 89 | " vec2 sampleUvOffset = sampleUvOffsets[i] * vInvSize;", 90 | 91 | " vec2 sampleUv = vUv + sampleUvOffset;", 92 | " float viewZ = -getViewZ( getDepth( sampleUv ) );", 93 | 94 | " if( abs( viewZ - centerViewZ ) > depthCutoff ) rBreak = true;", 95 | 96 | " if( ! rBreak ) {", 97 | " diffuseSum += texture2D( tDiffuse, sampleUv ) * sampleWeight;", 98 | " weightSum += sampleWeight;", 99 | " }", 100 | 101 | " sampleUv = vUv - sampleUvOffset;", 102 | " viewZ = -getViewZ( getDepth( sampleUv ) );", 103 | 104 | " if( abs( viewZ - centerViewZ ) > depthCutoff ) lBreak = true;", 105 | 106 | " if( ! lBreak ) {", 107 | " diffuseSum += texture2D( tDiffuse, sampleUv ) * sampleWeight;", 108 | " weightSum += sampleWeight;", 109 | " }", 110 | 111 | " }", 112 | 113 | " gl_FragColor = diffuseSum / weightSum;", 114 | "}" 115 | ].join( "\n" ) 116 | }; 117 | 118 | var BlurShaderUtils = { 119 | 120 | createSampleWeights: function ( kernelRadius, stdDev ) { 121 | 122 | var gaussian = function ( x, stdDev ) { 123 | 124 | return Math.exp( - ( x * x ) / ( 2.0 * ( stdDev * stdDev ) ) ) / ( Math.sqrt( 2.0 * Math.PI ) * stdDev ); 125 | 126 | }; 127 | 128 | var weights = []; 129 | 130 | for ( var i = 0; i <= kernelRadius; i ++ ) { 131 | 132 | weights.push( gaussian( i, stdDev ) ); 133 | 134 | } 135 | 136 | return weights; 137 | 138 | }, 139 | 140 | createSampleOffsets: function ( kernelRadius, uvIncrement ) { 141 | 142 | var offsets = []; 143 | 144 | for ( var i = 0; i <= kernelRadius; i ++ ) { 145 | 146 | offsets.push( uvIncrement.clone().multiplyScalar( i ) ); 147 | 148 | } 149 | 150 | return offsets; 151 | 152 | }, 153 | 154 | configure: function ( material, kernelRadius, stdDev, uvIncrement ) { 155 | 156 | material.defines[ "KERNEL_RADIUS" ] = kernelRadius; 157 | material.uniforms[ "sampleUvOffsets" ].value = BlurShaderUtils.createSampleOffsets( kernelRadius, uvIncrement ); 158 | material.uniforms[ "sampleWeights" ].value = BlurShaderUtils.createSampleWeights( kernelRadius, stdDev ); 159 | material.needsUpdate = true; 160 | 161 | } 162 | 163 | }; 164 | 165 | export { DepthLimitedBlurShader, BlurShaderUtils }; 166 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 1 unit = 1 cm 3 | */ 4 | 5 | var THREE = require('three') 6 | import { OrbitControls } from './lib/controls/OrbitControls'; 7 | import { RenderPass } from './lib/postprocessing/RenderPass'; 8 | import { ShaderPass } from './lib/postprocessing/ShaderPass'; 9 | import { EffectComposer } from './lib/postprocessing/EffectComposer'; 10 | import { DepthLimitedBlurShader } from './lib/shaders/DepthLimitedBlurShader'; 11 | import { GlitchPass } from './lib/shaders/GlitchPass'; 12 | import BoidManager from './src/boidManager'; 13 | import { utils } from './src/util' 14 | 15 | var scene, camera, renderer, controls, light, lure, boidManager, clock; 16 | var composer 17 | var obstacles = [] 18 | 19 | function init() { 20 | scene = new THREE.Scene(); 21 | camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000); 22 | camera.position.z = 500; 23 | 24 | renderer = new THREE.WebGLRenderer({ 25 | antialias: true 26 | }); 27 | 28 | renderer.setSize(window.innerWidth, window.innerHeight); 29 | document.body.appendChild(renderer.domElement); 30 | 31 | // CONTROLS 32 | controls = new OrbitControls(camera, renderer.domElement); 33 | controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled 34 | controls.dampingFactor = 0.05; 35 | controls.screenSpacePanning = false; 36 | controls.minDistance = 100; 37 | controls.maxDistance = 1000; 38 | controls.maxPolarAngle = Math.PI / 2; 39 | 40 | // WORLD OBSTACLES 41 | var hasObstacles = true 42 | if (hasObstacles) { 43 | utils.addObstacle(obstacles, scene, 50, 50, 50, 0xFFFFFF, 200, 200, 200) 44 | utils.addObstacle(obstacles, scene, 50, 100, 50, 0xFFFFFF, 100, 100, -200) 45 | utils.addObstacle(obstacles, scene, 100, 50, 50, 0xFFFFFF, -200, 150, 200) 46 | utils.addObstacle(obstacles, scene, 50, 50, 50, 0xFFFFFF, -150, 150, -200) 47 | utils.addObstacle(obstacles, scene, 50, 100, 100, 0xA399EE, -20, 300, -20) 48 | utils.addObstacle(obstacles, scene, 50, 50, 50, 0x555555, 150, -200, 200) 49 | utils.addObstacle(obstacles, scene, 50, 100, 100, 0x555555, 80, -100, -180) 50 | utils.addObstacle(obstacles, scene, 100, 50, 100, 0x555555, -220, -150, 180) 51 | utils.addObstacle(obstacles, scene, 100, 50, 50, 0x555555, -150, -150, -150) 52 | utils.addObstacle(obstacles, scene, 100, 50, 100, 0x555555, 20, -300, -20) 53 | } 54 | 55 | // LIGHTS 56 | 57 | //ambient light 58 | scene.add(new THREE.AmbientLight(0xffffff, 0.5)); 59 | 60 | light = new THREE.PointLight(0xffffff, 0.5, 500); 61 | light.position.set(0, 100, 0); 62 | scene.add(light); 63 | 64 | // TARGET 65 | 66 | lure = null 67 | // lure = new THREE.PointLight(0xffffff, 3, 500); 68 | // lure.position.set(0, 50, 0); 69 | // scene.add(lure); 70 | // var lightHelper = new THREE.PointLightHelper(lure); 71 | // scene.add(lightHelper); 72 | 73 | // BOIDS 74 | const numberOfBoids = 300 75 | boidManager = new BoidManager(scene, numberOfBoids, obstacles, lure) 76 | boidManager.boids.forEach(boid => { 77 | scene.add(boid.mesh) 78 | }) 79 | 80 | // CLOCK 81 | clock = new THREE.Clock(); 82 | 83 | var axesHelper = new THREE.AxesHelper(50); 84 | scene.add(axesHelper); 85 | 86 | // COMPOSER + PASSES 87 | composer = new EffectComposer(renderer) 88 | 89 | var renderPass = new RenderPass(scene, camera) 90 | composer.addPass(renderPass) 91 | renderPass.renderToScreen = true; 92 | 93 | // var pass1 = new GlitchPass(64) 94 | // // pass1.goWild = true 95 | // composer.addPass(pass1) 96 | // pass1.renderToScreen = true 97 | } 98 | 99 | // loop vars 100 | var counter = 0; 101 | var paused = false 102 | var slowPanEnabled = false 103 | 104 | function update(delta) { 105 | counter += 0.001; 106 | 107 | boidManager.update(delta) 108 | 109 | if (slowPanEnabled) { 110 | camera.lookAt(light.position); 111 | camera.position.x = Math.sin(counter) * 500; 112 | camera.position.z = Math.cos(counter) * 500; 113 | } 114 | 115 | if (lure) { 116 | lure.position.x = Math.sin(counter * 5) * 400; 117 | lure.position.y = Math.cos(counter * 10) * 400; 118 | lure.position.z = Math.cos(counter * 15) * 400; 119 | } 120 | } 121 | 122 | function render() { 123 | var delta = clock.getDelta(); 124 | if (!paused) { 125 | update(delta) 126 | } 127 | 128 | // renderer.render(scene, camera); 129 | composer.render() 130 | } 131 | 132 | var animate = function () { 133 | requestAnimationFrame(animate); 134 | 135 | // only required if controls.enableDamping = true, or if controls.autoRotate = true 136 | controls.update(); 137 | 138 | render() 139 | }; 140 | 141 | window.addEventListener('resize', function () { 142 | const width = window.innerWidth; 143 | const height = window.innerHeight; 144 | renderer.setPixelRatio(window.devicePixelRatio); 145 | renderer.setSize(width, height); 146 | camera.aspect = width / height; 147 | camera.updateProjectionMatrix(); 148 | }); 149 | 150 | document.addEventListener("keydown", onDocumentKeyDown, false); 151 | function onDocumentKeyDown(event) { 152 | var keyCode = event.which; 153 | if (keyCode == 32) { 154 | paused = !paused; 155 | 156 | // disable slow-pan so when animation is resumed, the viewer has the controls. 157 | if (slowPanEnabled) { 158 | slowPanEnabled = false 159 | } 160 | } 161 | }; 162 | 163 | 164 | init() 165 | 166 | animate(); -------------------------------------------------------------------------------- /lib/postprocessing/EffectComposer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | import { 6 | Clock, 7 | LinearFilter, 8 | Mesh, 9 | OrthographicCamera, 10 | PlaneBufferGeometry, 11 | RGBAFormat, 12 | Vector2, 13 | WebGLRenderTarget 14 | } from "three"; 15 | import { CopyShader } from "../shaders/CopyShader.js"; 16 | import { ShaderPass } from "../postprocessing/ShaderPass.js"; 17 | import { MaskPass } from "../postprocessing/MaskPass.js"; 18 | import { ClearMaskPass } from "../postprocessing/MaskPass.js"; 19 | 20 | var EffectComposer = function ( renderer, renderTarget ) { 21 | 22 | this.renderer = renderer; 23 | 24 | if ( renderTarget === undefined ) { 25 | 26 | var parameters = { 27 | minFilter: LinearFilter, 28 | magFilter: LinearFilter, 29 | format: RGBAFormat, 30 | stencilBuffer: false 31 | }; 32 | 33 | var size = renderer.getSize( new Vector2() ); 34 | this._pixelRatio = renderer.getPixelRatio(); 35 | this._width = size.width; 36 | this._height = size.height; 37 | 38 | renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, parameters ); 39 | renderTarget.texture.name = 'EffectComposer.rt1'; 40 | 41 | } else { 42 | 43 | this._pixelRatio = 1; 44 | this._width = renderTarget.width; 45 | this._height = renderTarget.height; 46 | 47 | } 48 | 49 | this.renderTarget1 = renderTarget; 50 | this.renderTarget2 = renderTarget.clone(); 51 | this.renderTarget2.texture.name = 'EffectComposer.rt2'; 52 | 53 | this.writeBuffer = this.renderTarget1; 54 | this.readBuffer = this.renderTarget2; 55 | 56 | this.renderToScreen = true; 57 | 58 | this.passes = []; 59 | 60 | // dependencies 61 | 62 | if ( CopyShader === undefined ) { 63 | 64 | console.error( 'THREE.EffectComposer relies on CopyShader' ); 65 | 66 | } 67 | 68 | if ( ShaderPass === undefined ) { 69 | 70 | console.error( 'THREE.EffectComposer relies on ShaderPass' ); 71 | 72 | } 73 | 74 | this.copyPass = new ShaderPass( CopyShader ); 75 | 76 | this.clock = new Clock(); 77 | 78 | }; 79 | 80 | Object.assign( EffectComposer.prototype, { 81 | 82 | swapBuffers: function () { 83 | 84 | var tmp = this.readBuffer; 85 | this.readBuffer = this.writeBuffer; 86 | this.writeBuffer = tmp; 87 | 88 | }, 89 | 90 | addPass: function ( pass ) { 91 | 92 | this.passes.push( pass ); 93 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 94 | 95 | }, 96 | 97 | insertPass: function ( pass, index ) { 98 | 99 | this.passes.splice( index, 0, pass ); 100 | 101 | }, 102 | 103 | isLastEnabledPass: function ( passIndex ) { 104 | 105 | for ( var i = passIndex + 1; i < this.passes.length; i ++ ) { 106 | 107 | if ( this.passes[ i ].enabled ) { 108 | 109 | return false; 110 | 111 | } 112 | 113 | } 114 | 115 | return true; 116 | 117 | }, 118 | 119 | render: function ( deltaTime ) { 120 | 121 | // deltaTime value is in seconds 122 | 123 | if ( deltaTime === undefined ) { 124 | 125 | deltaTime = this.clock.getDelta(); 126 | 127 | } 128 | 129 | var currentRenderTarget = this.renderer.getRenderTarget(); 130 | 131 | var maskActive = false; 132 | 133 | var pass, i, il = this.passes.length; 134 | 135 | for ( i = 0; i < il; i ++ ) { 136 | 137 | pass = this.passes[ i ]; 138 | 139 | if ( pass.enabled === false ) continue; 140 | 141 | pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) ); 142 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive ); 143 | 144 | if ( pass.needsSwap ) { 145 | 146 | if ( maskActive ) { 147 | 148 | var context = this.renderer.getContext(); 149 | var stencil = this.renderer.state.buffers.stencil; 150 | 151 | //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff ); 152 | stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff ); 153 | 154 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime ); 155 | 156 | //context.stencilFunc( context.EQUAL, 1, 0xffffffff ); 157 | stencil.setFunc( context.EQUAL, 1, 0xffffffff ); 158 | 159 | } 160 | 161 | this.swapBuffers(); 162 | 163 | } 164 | 165 | if ( MaskPass !== undefined ) { 166 | 167 | if ( pass instanceof MaskPass ) { 168 | 169 | maskActive = true; 170 | 171 | } else if ( pass instanceof ClearMaskPass ) { 172 | 173 | maskActive = false; 174 | 175 | } 176 | 177 | } 178 | 179 | } 180 | 181 | this.renderer.setRenderTarget( currentRenderTarget ); 182 | 183 | }, 184 | 185 | reset: function ( renderTarget ) { 186 | 187 | if ( renderTarget === undefined ) { 188 | 189 | var size = this.renderer.getSize( new Vector2() ); 190 | this._pixelRatio = this.renderer.getPixelRatio(); 191 | this._width = size.width; 192 | this._height = size.height; 193 | 194 | renderTarget = this.renderTarget1.clone(); 195 | renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 196 | 197 | } 198 | 199 | this.renderTarget1.dispose(); 200 | this.renderTarget2.dispose(); 201 | this.renderTarget1 = renderTarget; 202 | this.renderTarget2 = renderTarget.clone(); 203 | 204 | this.writeBuffer = this.renderTarget1; 205 | this.readBuffer = this.renderTarget2; 206 | 207 | }, 208 | 209 | setSize: function ( width, height ) { 210 | 211 | this._width = width; 212 | this._height = height; 213 | 214 | var effectiveWidth = this._width * this._pixelRatio; 215 | var effectiveHeight = this._height * this._pixelRatio; 216 | 217 | this.renderTarget1.setSize( effectiveWidth, effectiveHeight ); 218 | this.renderTarget2.setSize( effectiveWidth, effectiveHeight ); 219 | 220 | for ( var i = 0; i < this.passes.length; i ++ ) { 221 | 222 | this.passes[ i ].setSize( effectiveWidth, effectiveHeight ); 223 | 224 | } 225 | 226 | }, 227 | 228 | setPixelRatio: function ( pixelRatio ) { 229 | 230 | this._pixelRatio = pixelRatio; 231 | 232 | this.setSize( this._width, this._height ); 233 | 234 | } 235 | 236 | } ); 237 | 238 | 239 | var Pass = function () { 240 | 241 | // if set to true, the pass is processed by the composer 242 | this.enabled = true; 243 | 244 | // if set to true, the pass indicates to swap read and write buffer after rendering 245 | this.needsSwap = true; 246 | 247 | // if set to true, the pass clears its buffer before rendering 248 | this.clear = false; 249 | 250 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. 251 | this.renderToScreen = false; 252 | 253 | }; 254 | 255 | Object.assign( Pass.prototype, { 256 | 257 | setSize: function ( /* width, height */ ) {}, 258 | 259 | render: function ( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 260 | 261 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); 262 | 263 | } 264 | 265 | } ); 266 | 267 | // Helper for passes that need to fill the viewport with a single quad. 268 | Pass.FullScreenQuad = ( function () { 269 | 270 | var camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 271 | var geometry = new PlaneBufferGeometry( 2, 2 ); 272 | 273 | var FullScreenQuad = function ( material ) { 274 | 275 | this._mesh = new Mesh( geometry, material ); 276 | 277 | }; 278 | 279 | Object.defineProperty( FullScreenQuad.prototype, 'material', { 280 | 281 | get: function () { 282 | 283 | return this._mesh.material; 284 | 285 | }, 286 | 287 | set: function ( value ) { 288 | 289 | this._mesh.material = value; 290 | 291 | } 292 | 293 | } ); 294 | 295 | Object.assign( FullScreenQuad.prototype, { 296 | 297 | render: function ( renderer ) { 298 | 299 | renderer.render( this._mesh, camera ); 300 | 301 | } 302 | 303 | } ); 304 | 305 | return FullScreenQuad; 306 | 307 | } )(); 308 | 309 | export { EffectComposer, Pass }; 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Show me the boids!](https://juanuys.com/boids/) 2 | 3 | (scroll to zoom; click-hold to rotate, double-click-hold to move) 4 | 5 | ![preview](https://repository-images.githubusercontent.com/210620137/86145c80-dee1-11e9-8f21-289b8f145371) 6 | 7 | 8 | # Resources 9 | 10 | - [Boids paper](./research/10.1.1.103.7187.pdf) 11 | - [pseudo-code](http://www.vergenet.net/~conrad/boids/pseudocode.html) 12 | 13 | # Notes 14 | 15 | ## Simulated flocks (p28) 16 | 17 | Boid model that supports geometric flight. 18 | 19 | The behaviors that lead to simulated flocking are: 20 | 21 | 1. Collision Avoidance: avoid collisions with nearby flockmates (and other obstacles) 22 | 2. Velocity Matching: attempt to match velocity with nearby flockmates 23 | 3. Flock Centering: attempt to stay close to nearby flockmates 24 | 25 | [Wikipedia](https://en.wikipedia.org/wiki/Boids) creates these definitions for the above: 26 | 27 | 1. separation: steer to avoid crowding local flockmates 28 | 2. alignment: steer towards the average heading of local flockmates 29 | 3. cohesion: steer to move towards the average position (center of mass) of local flockmates 30 | 31 | Extra: 32 | 4. obstacle avoidance 33 | 5. goal seeking (e.g. "fly North for the winter" or "fly towards a food source") 34 | 35 | ## Arbitrating Independent Behaviors (p29) 36 | 37 | The three behavioral urges associated with flocking (and others to be discussed later) each produce an isolated **acceleration request** - a unit 3D vector - about which way to steer the boid. 38 | 39 | Each behavior has several parameters that control its function; one is a "strength," a fractional value between zero and one that can further attenuate the **acceleration request**. 40 | 41 | It is up to the **navigation module** of the boid brain to collect all relevant **acceleration requests** and then determine a **single behaviorally desired acceleration** by combining, prioritizing, and arbitrating between potentially conflicting urges. 42 | 43 | The **pilot module** takes the acceleration desired by the **navigation module** and passes it to the **flight module**, which attempts to fly in that direction. 44 | 45 | ### How? 46 | 47 | The easiest way to do so is by a **weighted averaging the acceleration requests**, but this has problems on, say, a collision course where acceleration requests cancel each other out. 48 | 49 | Another way is **Prioritized acceleration allocation** which is based on a strict **priority ordering** of all component behaviors, hence of the consideration of their acceleration requests. 50 | 51 | (This ordering can change to suit dynamic conditions.) The acceleration requests are considered in priority order and added into an accumulator. 52 | 53 | The magnitude of each request is measured and added into another accumulator. 54 | This process continues until the sum of the accumulated magnitudes gets larger than the maximum acceleration value, which is a parameter of each boid. 55 | The last acceleration request is trimmed back to compensate for the excess of accumulated magnitude. 56 | The point is that a fixed amount of acceleration is under the control of the navigation module; this acceleration is parceled out to satisfy the acceleration request of the various behaviors in order of priority. 57 | In an emergency the acceleration would be allocated to satisfy the most pressing needs first; if all available acceleration is "'used up," the less pressing behaviors might be temporarily unsatisfied. 58 | 59 | For example, the flock centering urge could be correctly ignored temporarily in favor of a maneuver to avoid a static obstacle. 60 | 61 | ## Simulated Perception (p29) 62 | 63 | Real animals' field of vision is obscured by those animals around them (and by things like murky water, kicked-up dust, etc) so there are a *bunch of factors which combine to strongly localise the information available to each animal*. 64 | 65 | The FOV neighborhood is defined as a spherical zone of sensitivity centered at the boid's local origin. 66 | 67 | The magnitude of the sensitivity is defined as an inverse exponential of distance (using inverse square; gravity-like; less springy/bouncy flocking; more dampened). 68 | Hence the neighborhood is defined by two parameters: a radius and exponent. 69 | There is reason to believe that this field of sensitivity should realistically be exaggerated in the forward direction and probably by an amount proportional to the boid's speed. 70 | 71 | Being in motion requires an increased awareness of what lies ahead, and **this requirement increases with speed**. 72 | 73 | A forward-weighted sensitivity zone would probably also improve the behavior in the current implementation of boids at the leading edge of a flock, who tend to get distracted by the flock behind them. 74 | 75 | ## Scripted Flocking (p30) 76 | 77 | The primary tool for scripting the flock's path is the **migratory urge** built into the boid model. 78 | 79 | * "going Z for the winter" 80 | * a global position--a target point toward which all birds fly. 81 | 82 | The model computes a bounded acceleration that incrementally turns the boid toward its migratory target. 83 | 84 | With the scripting system, we can animate a **dynamic parameter** whose value is a **global position vector** or a **global direction vector**. 85 | This parameter can be passed to the flock, which can in turn pass it along to all boids, each of which sets its own "migratory goal register." 86 | Hence the global migratory behavior of all birds can be directly controlled from the script. 87 | 88 | Of course, it is not necessary to alter all boids at the same time, for example, the delay could be a function of their present position in space. 89 | Real flocks do not change direction simultaneously, but rather the turn starts with a single bird and spreads quickly across the flock like a shock wave. 90 | 91 | We can lead the flock around by animating the goal point along the desired path, somewhat ahead of the flock. 92 | Even if the migratory goal point is changed abruptly the path of each boid still is relatively smooth because of the flight model's simulated conservation of momentum. 93 | This means that the boid's own flight dynamics implement a form of smoothing interpolation between "control points." 94 | 95 | ## Avoiding Environmental Obstacles (p31) 96 | 97 | The paper discounts the "force field" concept, and discusses "steer-to-avoid" as a better simulation. 98 | 99 | The boid considers only obstacles directly in front of it: it finds the intersection, if any, of its local Z axis with the obstacle. 100 | Working in local perspective space, it finds the silhouette edge of the obstacle closest to the point of eventual impact. 101 | A radial vector is computed which will aim the boid at a point one body length beyond that silhouette edge. 102 | 103 | ## Future Work (p33) 104 | 105 | More interesting behavior models would take into account hunger, finding food, fear of predators, a periodic need to sleep, and so on. 106 | 107 | # Other ideas 108 | 109 | Make a boid highlightable, so we can track it easily when watching the simulation. 110 | Make a boid scriptable, so we can see flock behaviour with regards to it. 111 | Make the FOV dynamic, i.e. the faster the boid goes, the less it pays attention to what's next to it, and more to what's in front of it (and even futher ahead of it). 112 | What about a boid that flies into a corner? How does it get out? 113 | Can the navigation model be made less accurate under normal conditions, but then get more accurate in the presence of predators? What would it mean to be more accurate? (e.g. more precise computation or increase in speed with a cost of the boid getting "tired" quicker?) 114 | 115 | 116 | # Interesting 117 | 118 | ## p32 119 | 120 | The boids software was written in Symbolics Common Lisp. 121 | The code and animation were produced on a [Symbolics 3600 Lisp Machine](https://en.wikipedia.org/wiki/Symbolics#The_3600_series), a high-performance personal computer. (already 4 years old at the time of the paper, and as big as a fridge!) 122 | 123 | The boid software has not been optimized for speed. 124 | But this report would be incomplete without a rough estimate of the actual performance of the system. 125 | With a flock of 80 boids, using the naive O(N ~) algorithm (and so 6400 individual boidto-boid comparisons), on a single Lisp Machine without any special hardware accelerators, the simulation ran for about 95 seconds per frame. 126 | A ten-second (300 frame) motion test took about eight hours of real time to produce. 127 | 128 | ## Other sources 129 | 130 | [Steering Behaviors For Autonomous Characters](http://www.red3d.com/cwr/steer/) by the same author. 131 | 132 | # Funny 133 | 134 | ## Acknowledgements (p33) 135 | 136 | I would like to thank flocks, herds, and schools for existing; nature is the ultimate source of inspiration for computer graphics and animation. 137 | 138 | (Thanks) ...to the field of computer graphics, for giving professional 139 | respectability to advanced forms of play such as reported in 140 | this paper. 141 | 142 | # Three.js 143 | 144 | ``` 145 | Vector3 directionToSomething = something.position - my.position 146 | 147 | my.rotation = Quaternion.FromToRotation(Vector3.back, directionToSomething) 148 | 149 | // or 150 | my.rotation = Quaternion.LookRotation(-directionToSomething, VEctor3.up) 151 | ``` 152 | -------------------------------------------------------------------------------- /src/boid.js: -------------------------------------------------------------------------------- 1 | const THREE = require('three') 2 | import {utils} from './util' 3 | 4 | const minSpeed = 1 5 | const maxSpeed = 5 6 | 7 | const numSamplesForSmoothing = 20 8 | 9 | const wanderWeight = 0.2 10 | // Steer towards the average position of nearby boids 11 | const cohesionWeight = 1 12 | // Steers away from nearby boids 13 | const separationWeight = 1 14 | // Adopt the average velocity of bearby boids 15 | const alignmentWeight = 1 16 | 17 | const visionRange = 150 18 | 19 | const origin = new THREE.Vector3() 20 | const boundaryRadius = 370 21 | 22 | const clamp = function (it, min, max) { 23 | return Math.min(Math.max(it, min), max); 24 | }; 25 | 26 | export default class Boid { 27 | constructor(scene, target, position, quaternion, colour, followTarget = false) { 28 | this.scene = scene 29 | const { mesh, geometry } = this.getBoid(position, quaternion, colour) 30 | this.mesh = mesh 31 | this.geometry = geometry 32 | this.target = target 33 | 34 | // re-usable acceleration vector 35 | this.acceleration = new THREE.Vector3(); 36 | 37 | // velocity is speed in a given direction, and in the update method we'll 38 | // compute an acceleration that will change the velocity 39 | this.velocity = new THREE.Vector3((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2); 40 | 41 | // whether this boid will follow the target 42 | this.followTarget = followTarget; 43 | 44 | // remember the last however many velocities so we can smooth the heading of the boid 45 | this.velocitySamples = [] 46 | 47 | this.wanderTarget = new THREE.Vector3(mesh.position.x, mesh.position.y, 300) 48 | 49 | this.debug = false 50 | this.counter = 0 51 | this.wanderCounter = 0 52 | this.arrows = [] 53 | } 54 | 55 | getBoid(position = new THREE.Vector3(0, 0, 0), quaternion = null, color = 0x156289) { 56 | if (color === null) { 57 | color = 0x156289 58 | } 59 | 60 | var geometry = new THREE.ConeGeometry(5, 10, 8) 61 | // rotate the geometry, because the face used by lookAt is not the cone's "tip" 62 | geometry.rotateX(THREE.Math.degToRad(90)); 63 | 64 | var mesh = new THREE.Group(); 65 | var lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 }); 66 | var meshMaterial = new THREE.MeshPhongMaterial({ color, emissive: 0x072534, side: THREE.DoubleSide, flatShading: true }); 67 | mesh.add(new THREE.LineSegments(new THREE.WireframeGeometry(geometry), lineMaterial)); 68 | mesh.add(new THREE.Mesh(geometry, meshMaterial)); 69 | 70 | mesh.position.copy(position) 71 | if (quaternion) { 72 | mesh.quaternion.copy(quaternion) 73 | } 74 | 75 | return { mesh, geometry } 76 | } 77 | 78 | /** 79 | * The boid will update its "steer vector" based on: 80 | * - Collision Avoidance: avoid collisions with nearby flockmates (and other obstacles) 81 | * - Velocity Matching: attempt to match velocity with nearby flockmates 82 | * - Flock Centering: attempt to stay close to nearby flockmates 83 | * 84 | * Alternative definitions for the above terms are: 85 | * - separation: steer to avoid crowding local flockmates 86 | * - alignment: steer towards the average heading of local flockmates 87 | * - cohesion: steer to move towards the average position (center of mass) of local flockmates 88 | */ 89 | update(delta, neighbours, obstacles) { 90 | this.counter++ 91 | this.wanderCounter++ 92 | 93 | // fly towards the target 94 | if (this.target && this.followTarget) { 95 | 96 | // var pos = this.target.position.clone() 97 | // pos.sub(this.mesh.position); 98 | // var accelerationTowardsTarget = this.steerTowards(pos).multiplyScalar(maxForceSeek); 99 | 100 | var accelerationTowardsTarget = this.seek(delta, this.target.position) 101 | 102 | // "flee" would use sub 103 | this.acceleration.add(accelerationTowardsTarget) 104 | } else { 105 | if (this.mesh.position.distanceTo(origin) > boundaryRadius) { 106 | this.acceleration.add(this.wander(delta).multiplyScalar(20)) 107 | } else { 108 | this.acceleration.add(this.wander(delta).multiplyScalar(wanderWeight)) 109 | } 110 | } 111 | 112 | // steering behaviour: alignment 113 | this.acceleration.add(this.alignment(delta, neighbours).multiplyScalar(alignmentWeight)) 114 | 115 | // steering behaviour: cohesion 116 | this.acceleration.add(this.cohesion(delta, neighbours).multiplyScalar(cohesionWeight)) 117 | 118 | // steering behaviour: separation 119 | this.acceleration.add(this.separation(delta, neighbours).multiplyScalar(separationWeight)) 120 | 121 | // avoid collisions with world obstacles 122 | var originPoint = this.mesh.position.clone(); 123 | var localVertex = this.geometry.vertices[0].clone() 124 | var globalVertex = localVertex.applyMatrix4(this.mesh.matrix); 125 | var directionVector = globalVertex.sub(this.mesh.position); 126 | var raycaster = new THREE.Raycaster(originPoint, directionVector.clone().normalize(), 0, visionRange); 127 | 128 | if (this.debug) { 129 | const arrow = new THREE.ArrowHelper(raycaster.ray.direction, raycaster.ray.origin, 50, 0xff0000) 130 | if (this.counter % 50 === 0) { 131 | arrow.name = Math.random().toString(36).substring(2, 15) 132 | this.arrows.push(arrow) 133 | this.scene.add(arrow); 134 | if (this.arrows.length > 3) { 135 | var toBeRemoved = this.arrows.shift() 136 | this.scene.remove(this.scene.getObjectByName(toBeRemoved.name)) 137 | } 138 | } 139 | } 140 | 141 | // obstacle meshes are Group, and the first child is the mesh we want to ray-trace 142 | var collisionResults = raycaster.intersectObjects(obstacles.map(o => o.mesh.children[0])); 143 | if (collisionResults.length > 0) { 144 | // flee from the object 145 | // var seek = this.seek(delta, collisionResults[0].point) 146 | // this.acceleration.add(seek.negate().multiplyScalar(100)) 147 | 148 | // gently dodge object 149 | for (var i = 0; i < utils.sphereCastDirections.length; i++) { 150 | const direction = utils.sphereCastDirections[i] 151 | raycaster = new THREE.Raycaster(originPoint, direction, 0, visionRange); 152 | var spectrumCollision = raycaster.intersectObject(collisionResults[0].object) 153 | if (spectrumCollision.length === 0) { 154 | this.acceleration.add(direction.clone().multiplyScalar(100)) 155 | break 156 | } 157 | } 158 | } 159 | 160 | this.applyAcceleration(delta) 161 | 162 | this.lookWhereGoing() 163 | } 164 | 165 | applyAcceleration(delta) { 166 | this.velocity.add(this.acceleration); 167 | this.acceleration.set(0, 0, 0); // reset 168 | this.velocity.clampLength(minSpeed, maxSpeed) 169 | this.mesh.position.add(this.velocity) 170 | } 171 | 172 | /** 173 | * Once the boid reaches a stationary target, and the target doesn't change, it will flip/flop on the spot. 174 | * That's because the old velocity is retained. 175 | * @param {*} delta 176 | * @param {*} target 177 | */ 178 | seek(delta, target) { 179 | var steerVector = target.clone().sub(this.mesh.position); 180 | steerVector.normalize() 181 | steerVector.multiplyScalar(maxSpeed) 182 | steerVector.sub(this.velocity); 183 | 184 | var maxForce = delta * 5 185 | steerVector.clampLength(0, maxForce) 186 | return steerVector 187 | } 188 | 189 | /** 190 | * From the paper: 191 | * Collision Avoidance: avoid collisions with nearby flockmates (aka separation) 192 | * 193 | * Simply look at each neighbour, and if it's within a defined small distance (say 100 units), 194 | * then move it as far away again as it already is. This is done by subtracting from a vector 195 | * "steerVector" (initialised to zero) the displacement of each neighbour which is nearby. 196 | */ 197 | separation(delta, neighbours, range = 30) { 198 | 199 | const steerVector = new THREE.Vector3(); 200 | 201 | var neighbourInRangeCount = 0 202 | 203 | neighbours.forEach(neighbour => { 204 | 205 | // skip same object 206 | if (neighbour.mesh.id === this.mesh.id) return; 207 | 208 | const distance = neighbour.mesh.position.distanceTo(this.mesh.position) 209 | if (distance <= range) { 210 | var diff = this.mesh.position.clone().sub(neighbour.mesh.position) 211 | diff.divideScalar(distance) // weight by distance 212 | steerVector.add(diff); 213 | neighbourInRangeCount++; 214 | } 215 | }) 216 | 217 | if (neighbourInRangeCount !== 0) { 218 | steerVector.divideScalar(neighbourInRangeCount) 219 | steerVector.normalize() 220 | steerVector.multiplyScalar(maxSpeed) 221 | var maxForce = delta * 5 222 | steerVector.clampLength(0, maxForce); 223 | } 224 | 225 | return steerVector; 226 | } 227 | 228 | /** 229 | * Produces a steering force that keeps a boid's heading aligned with its neighbours. 230 | * (average velocity) 231 | * 232 | * @param {*} neighbours 233 | */ 234 | alignment(delta, neighbours, range = 50) { 235 | let steerVector = new THREE.Vector3(); 236 | const averageDirection = new THREE.Vector3(); 237 | 238 | var neighboursInRangeCount = 0; 239 | 240 | neighbours.forEach(neighbour => { 241 | 242 | // skip same object 243 | if (neighbour.mesh.id === this.mesh.id) return; 244 | 245 | const distance = neighbour.mesh.position.distanceTo(this.mesh.position) 246 | if (distance <= range) { 247 | neighboursInRangeCount++ 248 | averageDirection.add(neighbour.velocity.clone()); 249 | } 250 | }) 251 | 252 | if (neighboursInRangeCount > 0) { 253 | averageDirection.divideScalar(neighboursInRangeCount); 254 | averageDirection.normalize() 255 | averageDirection.multiplyScalar(maxSpeed) 256 | 257 | steerVector = averageDirection.sub(this.velocity) 258 | var maxForce = delta * 5 259 | steerVector.clampLength(0, maxForce) 260 | } 261 | 262 | return steerVector; 263 | } 264 | 265 | /** 266 | * Produces a steering force that moves a boid toward the average position of its neighbours. 267 | * 268 | * @param {*} neighbours 269 | */ 270 | cohesion(delta, neighbours, range = 50) { 271 | const centreOfMass = new THREE.Vector3(); 272 | 273 | var neighboursInRangeCount = 0; 274 | 275 | neighbours.forEach(neighbour => { 276 | 277 | // skip same object 278 | if (neighbour.mesh.id === this.mesh.id) return; 279 | 280 | const distance = neighbour.mesh.position.distanceTo(this.mesh.position) 281 | if (distance <= range) { 282 | neighboursInRangeCount++ 283 | centreOfMass.add(neighbour.mesh.position) 284 | } 285 | }) 286 | 287 | if (neighboursInRangeCount > 0) { 288 | centreOfMass.divideScalar(neighboursInRangeCount); 289 | 290 | // "seek" the centre of mass 291 | return this.seek(delta, centreOfMass) 292 | } else { 293 | return new THREE.Vector3() 294 | } 295 | } 296 | 297 | rndCoord(range = 295) { 298 | return (Math.random() - 0.5) * range * 2 299 | } 300 | wander(delta) { 301 | 302 | var distance = this.mesh.position.distanceTo(this.wanderTarget) 303 | if (distance < 5) { 304 | // when we reach the target, set a new random target 305 | this.wanderTarget = new THREE.Vector3(this.rndCoord(), this.rndCoord(), this.rndCoord()) 306 | this.wanderCounter = 0 307 | } else if (this.wanderCounter > 500) { 308 | this.wanderTarget = new THREE.Vector3(this.rndCoord(), this.rndCoord(), this.rndCoord()) 309 | this.wanderCounter = 0 310 | } 311 | 312 | return this.seek(delta, this.wanderTarget) 313 | } 314 | 315 | lookWhereGoing(smoothing = true) { 316 | var direction = this.velocity.clone() 317 | if (smoothing) { 318 | if (this.velocitySamples.length == numSamplesForSmoothing) { 319 | this.velocitySamples.shift(); 320 | } 321 | 322 | this.velocitySamples.push(this.velocity.clone()); 323 | direction.set(0, 0, 0); 324 | this.velocitySamples.forEach(sample => { 325 | direction.add(sample) 326 | }) 327 | direction.divideScalar(this.velocitySamples.length) 328 | } 329 | 330 | direction.add(this.mesh.position); 331 | this.mesh.lookAt(direction) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/controls/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 | * @author ScieCode / http://github.com/sciecode 8 | */ 9 | 10 | import { 11 | EventDispatcher, 12 | MOUSE, 13 | Quaternion, 14 | Spherical, 15 | TOUCH, 16 | Vector2, 17 | Vector3 18 | } from "three"; 19 | 20 | // This set of controls performs orbiting, dollying (zooming), and panning. 21 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 22 | // 23 | // Orbit - left mouse / touch: one-finger move 24 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 25 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 26 | 27 | var OrbitControls = function ( object, domElement ) { 28 | 29 | this.object = object; 30 | 31 | this.domElement = ( domElement !== undefined ) ? domElement : document; 32 | 33 | // Set to false to disable this control 34 | this.enabled = true; 35 | 36 | // "target" sets the location of focus, where the object orbits around 37 | this.target = new Vector3(); 38 | 39 | // How far you can dolly in and out ( PerspectiveCamera only ) 40 | this.minDistance = 0; 41 | this.maxDistance = Infinity; 42 | 43 | // How far you can zoom in and out ( OrthographicCamera only ) 44 | this.minZoom = 0; 45 | this.maxZoom = Infinity; 46 | 47 | // How far you can orbit vertically, upper and lower limits. 48 | // Range is 0 to Math.PI radians. 49 | this.minPolarAngle = 0; // radians 50 | this.maxPolarAngle = Math.PI; // radians 51 | 52 | // How far you can orbit horizontally, upper and lower limits. 53 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 54 | this.minAzimuthAngle = - Infinity; // radians 55 | this.maxAzimuthAngle = Infinity; // radians 56 | 57 | // Set to true to enable damping (inertia) 58 | // If damping is enabled, you must call controls.update() in your animation loop 59 | this.enableDamping = false; 60 | this.dampingFactor = 0.05; 61 | 62 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 63 | // Set to false to disable zooming 64 | this.enableZoom = true; 65 | this.zoomSpeed = 1.0; 66 | 67 | // Set to false to disable rotating 68 | this.enableRotate = true; 69 | this.rotateSpeed = 1.0; 70 | 71 | // Set to false to disable panning 72 | this.enablePan = true; 73 | this.panSpeed = 1.0; 74 | this.screenSpacePanning = false; // if true, pan in screen-space 75 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 76 | 77 | // Set to true to automatically rotate around the target 78 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 79 | this.autoRotate = false; 80 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 81 | 82 | // Set to false to disable use of the keys 83 | this.enableKeys = true; 84 | 85 | // The four arrow keys 86 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 87 | 88 | // Mouse buttons 89 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 90 | 91 | // Touch fingers 92 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 93 | 94 | // for reset 95 | this.target0 = this.target.clone(); 96 | this.position0 = this.object.position.clone(); 97 | this.zoom0 = this.object.zoom; 98 | 99 | // 100 | // public methods 101 | // 102 | 103 | this.getPolarAngle = function () { 104 | 105 | return spherical.phi; 106 | 107 | }; 108 | 109 | this.getAzimuthalAngle = function () { 110 | 111 | return spherical.theta; 112 | 113 | }; 114 | 115 | this.saveState = function () { 116 | 117 | scope.target0.copy( scope.target ); 118 | scope.position0.copy( scope.object.position ); 119 | scope.zoom0 = scope.object.zoom; 120 | 121 | }; 122 | 123 | this.reset = function () { 124 | 125 | scope.target.copy( scope.target0 ); 126 | scope.object.position.copy( scope.position0 ); 127 | scope.object.zoom = scope.zoom0; 128 | 129 | scope.object.updateProjectionMatrix(); 130 | scope.dispatchEvent( changeEvent ); 131 | 132 | scope.update(); 133 | 134 | state = STATE.NONE; 135 | 136 | }; 137 | 138 | // this method is exposed, but perhaps it would be better if we can make it private... 139 | this.update = function () { 140 | 141 | var offset = new Vector3(); 142 | 143 | // so camera.up is the orbit axis 144 | var quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 145 | var quatInverse = quat.clone().inverse(); 146 | 147 | var lastPosition = new Vector3(); 148 | var lastQuaternion = new Quaternion(); 149 | 150 | return function update() { 151 | 152 | var position = scope.object.position; 153 | 154 | offset.copy( position ).sub( scope.target ); 155 | 156 | // rotate offset to "y-axis-is-up" space 157 | offset.applyQuaternion( quat ); 158 | 159 | // angle from z-axis around y-axis 160 | spherical.setFromVector3( offset ); 161 | 162 | if ( scope.autoRotate && state === STATE.NONE ) { 163 | 164 | rotateLeft( getAutoRotationAngle() ); 165 | 166 | } 167 | 168 | if ( scope.enableDamping ) { 169 | 170 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 171 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 172 | 173 | } else { 174 | 175 | spherical.theta += sphericalDelta.theta; 176 | spherical.phi += sphericalDelta.phi; 177 | 178 | } 179 | 180 | // restrict theta to be between desired limits 181 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 182 | 183 | // restrict phi to be between desired limits 184 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 185 | 186 | spherical.makeSafe(); 187 | 188 | 189 | spherical.radius *= scale; 190 | 191 | // restrict radius to be between desired limits 192 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 193 | 194 | // move target to panned location 195 | 196 | if ( scope.enableDamping === true ) { 197 | 198 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 199 | 200 | } else { 201 | 202 | scope.target.add( panOffset ); 203 | 204 | } 205 | 206 | offset.setFromSpherical( spherical ); 207 | 208 | // rotate offset back to "camera-up-vector-is-up" space 209 | offset.applyQuaternion( quatInverse ); 210 | 211 | position.copy( scope.target ).add( offset ); 212 | 213 | scope.object.lookAt( scope.target ); 214 | 215 | if ( scope.enableDamping === true ) { 216 | 217 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 218 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 219 | 220 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 221 | 222 | } else { 223 | 224 | sphericalDelta.set( 0, 0, 0 ); 225 | 226 | panOffset.set( 0, 0, 0 ); 227 | 228 | } 229 | 230 | scale = 1; 231 | 232 | // update condition is: 233 | // min(camera displacement, camera rotation in radians)^2 > EPS 234 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 235 | 236 | if ( zoomChanged || 237 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 238 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 239 | 240 | scope.dispatchEvent( changeEvent ); 241 | 242 | lastPosition.copy( scope.object.position ); 243 | lastQuaternion.copy( scope.object.quaternion ); 244 | zoomChanged = false; 245 | 246 | return true; 247 | 248 | } 249 | 250 | return false; 251 | 252 | }; 253 | 254 | }(); 255 | 256 | this.dispose = function () { 257 | 258 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 259 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 260 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 261 | 262 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 263 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 264 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 265 | 266 | document.removeEventListener( 'mousemove', onMouseMove, false ); 267 | document.removeEventListener( 'mouseup', onMouseUp, false ); 268 | 269 | window.removeEventListener( 'keydown', onKeyDown, false ); 270 | 271 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 272 | 273 | }; 274 | 275 | // 276 | // internals 277 | // 278 | 279 | var scope = this; 280 | 281 | var changeEvent = { type: 'change' }; 282 | var startEvent = { type: 'start' }; 283 | var endEvent = { type: 'end' }; 284 | 285 | var STATE = { 286 | NONE: - 1, 287 | ROTATE: 0, 288 | DOLLY: 1, 289 | PAN: 2, 290 | TOUCH_ROTATE: 3, 291 | TOUCH_PAN: 4, 292 | TOUCH_DOLLY_PAN: 5, 293 | TOUCH_DOLLY_ROTATE: 6 294 | }; 295 | 296 | var state = STATE.NONE; 297 | 298 | var EPS = 0.000001; 299 | 300 | // current position in spherical coordinates 301 | var spherical = new Spherical(); 302 | var sphericalDelta = new Spherical(); 303 | 304 | var scale = 1; 305 | var panOffset = new Vector3(); 306 | var zoomChanged = false; 307 | 308 | var rotateStart = new Vector2(); 309 | var rotateEnd = new Vector2(); 310 | var rotateDelta = new Vector2(); 311 | 312 | var panStart = new Vector2(); 313 | var panEnd = new Vector2(); 314 | var panDelta = new Vector2(); 315 | 316 | var dollyStart = new Vector2(); 317 | var dollyEnd = new Vector2(); 318 | var dollyDelta = new Vector2(); 319 | 320 | function getAutoRotationAngle() { 321 | 322 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 323 | 324 | } 325 | 326 | function getZoomScale() { 327 | 328 | return Math.pow( 0.95, scope.zoomSpeed ); 329 | 330 | } 331 | 332 | function rotateLeft( angle ) { 333 | 334 | sphericalDelta.theta -= angle; 335 | 336 | } 337 | 338 | function rotateUp( angle ) { 339 | 340 | sphericalDelta.phi -= angle; 341 | 342 | } 343 | 344 | var panLeft = function () { 345 | 346 | var v = new Vector3(); 347 | 348 | return function panLeft( distance, objectMatrix ) { 349 | 350 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 351 | v.multiplyScalar( - distance ); 352 | 353 | panOffset.add( v ); 354 | 355 | }; 356 | 357 | }(); 358 | 359 | var panUp = function () { 360 | 361 | var v = new Vector3(); 362 | 363 | return function panUp( distance, objectMatrix ) { 364 | 365 | if ( scope.screenSpacePanning === true ) { 366 | 367 | v.setFromMatrixColumn( objectMatrix, 1 ); 368 | 369 | } else { 370 | 371 | v.setFromMatrixColumn( objectMatrix, 0 ); 372 | v.crossVectors( scope.object.up, v ); 373 | 374 | } 375 | 376 | v.multiplyScalar( distance ); 377 | 378 | panOffset.add( v ); 379 | 380 | }; 381 | 382 | }(); 383 | 384 | // deltaX and deltaY are in pixels; right and down are positive 385 | var pan = function () { 386 | 387 | var offset = new Vector3(); 388 | 389 | return function pan( deltaX, deltaY ) { 390 | 391 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 392 | 393 | if ( scope.object.isPerspectiveCamera ) { 394 | 395 | // perspective 396 | var position = scope.object.position; 397 | offset.copy( position ).sub( scope.target ); 398 | var targetDistance = offset.length(); 399 | 400 | // half of the fov is center to top of screen 401 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 402 | 403 | // we use only clientHeight here so aspect ratio does not distort speed 404 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 405 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 406 | 407 | } else if ( scope.object.isOrthographicCamera ) { 408 | 409 | // orthographic 410 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 411 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 412 | 413 | } else { 414 | 415 | // camera neither orthographic nor perspective 416 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 417 | scope.enablePan = false; 418 | 419 | } 420 | 421 | }; 422 | 423 | }(); 424 | 425 | function dollyIn( dollyScale ) { 426 | 427 | if ( scope.object.isPerspectiveCamera ) { 428 | 429 | scale /= dollyScale; 430 | 431 | } else if ( scope.object.isOrthographicCamera ) { 432 | 433 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 434 | scope.object.updateProjectionMatrix(); 435 | zoomChanged = true; 436 | 437 | } else { 438 | 439 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 440 | scope.enableZoom = false; 441 | 442 | } 443 | 444 | } 445 | 446 | function dollyOut( dollyScale ) { 447 | 448 | if ( scope.object.isPerspectiveCamera ) { 449 | 450 | scale *= dollyScale; 451 | 452 | } else if ( scope.object.isOrthographicCamera ) { 453 | 454 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 455 | scope.object.updateProjectionMatrix(); 456 | zoomChanged = true; 457 | 458 | } else { 459 | 460 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 461 | scope.enableZoom = false; 462 | 463 | } 464 | 465 | } 466 | 467 | // 468 | // event callbacks - update the object state 469 | // 470 | 471 | function handleMouseDownRotate( event ) { 472 | 473 | rotateStart.set( event.clientX, event.clientY ); 474 | 475 | } 476 | 477 | function handleMouseDownDolly( event ) { 478 | 479 | dollyStart.set( event.clientX, event.clientY ); 480 | 481 | } 482 | 483 | function handleMouseDownPan( event ) { 484 | 485 | panStart.set( event.clientX, event.clientY ); 486 | 487 | } 488 | 489 | function handleMouseMoveRotate( event ) { 490 | 491 | rotateEnd.set( event.clientX, event.clientY ); 492 | 493 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 494 | 495 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 496 | 497 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 498 | 499 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 500 | 501 | rotateStart.copy( rotateEnd ); 502 | 503 | scope.update(); 504 | 505 | } 506 | 507 | function handleMouseMoveDolly( event ) { 508 | 509 | dollyEnd.set( event.clientX, event.clientY ); 510 | 511 | dollyDelta.subVectors( dollyEnd, dollyStart ); 512 | 513 | if ( dollyDelta.y > 0 ) { 514 | 515 | dollyIn( getZoomScale() ); 516 | 517 | } else if ( dollyDelta.y < 0 ) { 518 | 519 | dollyOut( getZoomScale() ); 520 | 521 | } 522 | 523 | dollyStart.copy( dollyEnd ); 524 | 525 | scope.update(); 526 | 527 | } 528 | 529 | function handleMouseMovePan( event ) { 530 | 531 | panEnd.set( event.clientX, event.clientY ); 532 | 533 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 534 | 535 | pan( panDelta.x, panDelta.y ); 536 | 537 | panStart.copy( panEnd ); 538 | 539 | scope.update(); 540 | 541 | } 542 | 543 | function handleMouseUp( /*event*/ ) { 544 | 545 | // no-op 546 | 547 | } 548 | 549 | function handleMouseWheel( event ) { 550 | 551 | if ( event.deltaY < 0 ) { 552 | 553 | dollyOut( getZoomScale() ); 554 | 555 | } else if ( event.deltaY > 0 ) { 556 | 557 | dollyIn( getZoomScale() ); 558 | 559 | } 560 | 561 | scope.update(); 562 | 563 | } 564 | 565 | function handleKeyDown( event ) { 566 | 567 | var needsUpdate = false; 568 | 569 | switch ( event.keyCode ) { 570 | 571 | case scope.keys.UP: 572 | pan( 0, scope.keyPanSpeed ); 573 | needsUpdate = true; 574 | break; 575 | 576 | case scope.keys.BOTTOM: 577 | pan( 0, - scope.keyPanSpeed ); 578 | needsUpdate = true; 579 | break; 580 | 581 | case scope.keys.LEFT: 582 | pan( scope.keyPanSpeed, 0 ); 583 | needsUpdate = true; 584 | break; 585 | 586 | case scope.keys.RIGHT: 587 | pan( - scope.keyPanSpeed, 0 ); 588 | needsUpdate = true; 589 | break; 590 | 591 | } 592 | 593 | if ( needsUpdate ) { 594 | 595 | // prevent the browser from scrolling on cursor keys 596 | event.preventDefault(); 597 | 598 | scope.update(); 599 | 600 | } 601 | 602 | 603 | } 604 | 605 | function handleTouchStartRotate( event ) { 606 | 607 | if ( event.touches.length == 1 ) { 608 | 609 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 610 | 611 | } else { 612 | 613 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 614 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 615 | 616 | rotateStart.set( x, y ); 617 | 618 | } 619 | 620 | } 621 | 622 | function handleTouchStartPan( event ) { 623 | 624 | if ( event.touches.length == 1 ) { 625 | 626 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 627 | 628 | } else { 629 | 630 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 631 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 632 | 633 | panStart.set( x, y ); 634 | 635 | } 636 | 637 | } 638 | 639 | function handleTouchStartDolly( event ) { 640 | 641 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 642 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 643 | 644 | var distance = Math.sqrt( dx * dx + dy * dy ); 645 | 646 | dollyStart.set( 0, distance ); 647 | 648 | } 649 | 650 | function handleTouchStartDollyPan( event ) { 651 | 652 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 653 | 654 | if ( scope.enablePan ) handleTouchStartPan( event ); 655 | 656 | } 657 | 658 | function handleTouchStartDollyRotate( event ) { 659 | 660 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 661 | 662 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 663 | 664 | } 665 | 666 | function handleTouchMoveRotate( event ) { 667 | 668 | if ( event.touches.length == 1 ) { 669 | 670 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 671 | 672 | } else { 673 | 674 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 675 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 676 | 677 | rotateEnd.set( x, y ); 678 | 679 | } 680 | 681 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 682 | 683 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 684 | 685 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 686 | 687 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 688 | 689 | rotateStart.copy( rotateEnd ); 690 | 691 | } 692 | 693 | function handleTouchMovePan( event ) { 694 | 695 | if ( event.touches.length == 1 ) { 696 | 697 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 698 | 699 | } else { 700 | 701 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 702 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 703 | 704 | panEnd.set( x, y ); 705 | 706 | } 707 | 708 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 709 | 710 | pan( panDelta.x, panDelta.y ); 711 | 712 | panStart.copy( panEnd ); 713 | 714 | } 715 | 716 | function handleTouchMoveDolly( event ) { 717 | 718 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 719 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 720 | 721 | var distance = Math.sqrt( dx * dx + dy * dy ); 722 | 723 | dollyEnd.set( 0, distance ); 724 | 725 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 726 | 727 | dollyIn( dollyDelta.y ); 728 | 729 | dollyStart.copy( dollyEnd ); 730 | 731 | } 732 | 733 | function handleTouchMoveDollyPan( event ) { 734 | 735 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 736 | 737 | if ( scope.enablePan ) handleTouchMovePan( event ); 738 | 739 | } 740 | 741 | function handleTouchMoveDollyRotate( event ) { 742 | 743 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 744 | 745 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 746 | 747 | } 748 | 749 | function handleTouchEnd( /*event*/ ) { 750 | 751 | // no-op 752 | 753 | } 754 | 755 | // 756 | // event handlers - FSM: listen for events and reset state 757 | // 758 | 759 | function onMouseDown( event ) { 760 | 761 | if ( scope.enabled === false ) return; 762 | 763 | // Prevent the browser from scrolling. 764 | 765 | event.preventDefault(); 766 | 767 | // Manually set the focus since calling preventDefault above 768 | // prevents the browser from setting it automatically. 769 | 770 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 771 | 772 | switch ( event.button ) { 773 | 774 | case 0: 775 | 776 | switch ( scope.mouseButtons.LEFT ) { 777 | 778 | case MOUSE.ROTATE: 779 | 780 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 781 | 782 | if ( scope.enablePan === false ) return; 783 | 784 | handleMouseDownPan( event ); 785 | 786 | state = STATE.PAN; 787 | 788 | } else { 789 | 790 | if ( scope.enableRotate === false ) return; 791 | 792 | handleMouseDownRotate( event ); 793 | 794 | state = STATE.ROTATE; 795 | 796 | } 797 | 798 | break; 799 | 800 | case MOUSE.PAN: 801 | 802 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 803 | 804 | if ( scope.enableRotate === false ) return; 805 | 806 | handleMouseDownRotate( event ); 807 | 808 | state = STATE.ROTATE; 809 | 810 | } else { 811 | 812 | if ( scope.enablePan === false ) return; 813 | 814 | handleMouseDownPan( event ); 815 | 816 | state = STATE.PAN; 817 | 818 | } 819 | 820 | break; 821 | 822 | default: 823 | 824 | state = STATE.NONE; 825 | 826 | } 827 | 828 | break; 829 | 830 | 831 | case 1: 832 | 833 | switch ( scope.mouseButtons.MIDDLE ) { 834 | 835 | case MOUSE.DOLLY: 836 | 837 | if ( scope.enableZoom === false ) return; 838 | 839 | handleMouseDownDolly( event ); 840 | 841 | state = STATE.DOLLY; 842 | 843 | break; 844 | 845 | 846 | default: 847 | 848 | state = STATE.NONE; 849 | 850 | } 851 | 852 | break; 853 | 854 | case 2: 855 | 856 | switch ( scope.mouseButtons.RIGHT ) { 857 | 858 | case MOUSE.ROTATE: 859 | 860 | if ( scope.enableRotate === false ) return; 861 | 862 | handleMouseDownRotate( event ); 863 | 864 | state = STATE.ROTATE; 865 | 866 | break; 867 | 868 | case MOUSE.PAN: 869 | 870 | if ( scope.enablePan === false ) return; 871 | 872 | handleMouseDownPan( event ); 873 | 874 | state = STATE.PAN; 875 | 876 | break; 877 | 878 | default: 879 | 880 | state = STATE.NONE; 881 | 882 | } 883 | 884 | break; 885 | 886 | } 887 | 888 | if ( state !== STATE.NONE ) { 889 | 890 | document.addEventListener( 'mousemove', onMouseMove, false ); 891 | document.addEventListener( 'mouseup', onMouseUp, false ); 892 | 893 | scope.dispatchEvent( startEvent ); 894 | 895 | } 896 | 897 | } 898 | 899 | function onMouseMove( event ) { 900 | 901 | if ( scope.enabled === false ) return; 902 | 903 | event.preventDefault(); 904 | 905 | switch ( state ) { 906 | 907 | case STATE.ROTATE: 908 | 909 | if ( scope.enableRotate === false ) return; 910 | 911 | handleMouseMoveRotate( event ); 912 | 913 | break; 914 | 915 | case STATE.DOLLY: 916 | 917 | if ( scope.enableZoom === false ) return; 918 | 919 | handleMouseMoveDolly( event ); 920 | 921 | break; 922 | 923 | case STATE.PAN: 924 | 925 | if ( scope.enablePan === false ) return; 926 | 927 | handleMouseMovePan( event ); 928 | 929 | break; 930 | 931 | } 932 | 933 | } 934 | 935 | function onMouseUp( event ) { 936 | 937 | if ( scope.enabled === false ) return; 938 | 939 | handleMouseUp( event ); 940 | 941 | document.removeEventListener( 'mousemove', onMouseMove, false ); 942 | document.removeEventListener( 'mouseup', onMouseUp, false ); 943 | 944 | scope.dispatchEvent( endEvent ); 945 | 946 | state = STATE.NONE; 947 | 948 | } 949 | 950 | function onMouseWheel( event ) { 951 | 952 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 953 | 954 | event.preventDefault(); 955 | event.stopPropagation(); 956 | 957 | scope.dispatchEvent( startEvent ); 958 | 959 | handleMouseWheel( event ); 960 | 961 | scope.dispatchEvent( endEvent ); 962 | 963 | } 964 | 965 | function onKeyDown( event ) { 966 | 967 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 968 | 969 | handleKeyDown( event ); 970 | 971 | } 972 | 973 | function onTouchStart( event ) { 974 | 975 | if ( scope.enabled === false ) return; 976 | 977 | event.preventDefault(); 978 | 979 | switch ( event.touches.length ) { 980 | 981 | case 1: 982 | 983 | switch ( scope.touches.ONE ) { 984 | 985 | case TOUCH.ROTATE: 986 | 987 | if ( scope.enableRotate === false ) return; 988 | 989 | handleTouchStartRotate( event ); 990 | 991 | state = STATE.TOUCH_ROTATE; 992 | 993 | break; 994 | 995 | case TOUCH.PAN: 996 | 997 | if ( scope.enablePan === false ) return; 998 | 999 | handleTouchStartPan( event ); 1000 | 1001 | state = STATE.TOUCH_PAN; 1002 | 1003 | break; 1004 | 1005 | default: 1006 | 1007 | state = STATE.NONE; 1008 | 1009 | } 1010 | 1011 | break; 1012 | 1013 | case 2: 1014 | 1015 | switch ( scope.touches.TWO ) { 1016 | 1017 | case TOUCH.DOLLY_PAN: 1018 | 1019 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1020 | 1021 | handleTouchStartDollyPan( event ); 1022 | 1023 | state = STATE.TOUCH_DOLLY_PAN; 1024 | 1025 | break; 1026 | 1027 | case TOUCH.DOLLY_ROTATE: 1028 | 1029 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1030 | 1031 | handleTouchStartDollyRotate( event ); 1032 | 1033 | state = STATE.TOUCH_DOLLY_ROTATE; 1034 | 1035 | break; 1036 | 1037 | default: 1038 | 1039 | state = STATE.NONE; 1040 | 1041 | } 1042 | 1043 | break; 1044 | 1045 | default: 1046 | 1047 | state = STATE.NONE; 1048 | 1049 | } 1050 | 1051 | if ( state !== STATE.NONE ) { 1052 | 1053 | scope.dispatchEvent( startEvent ); 1054 | 1055 | } 1056 | 1057 | } 1058 | 1059 | function onTouchMove( event ) { 1060 | 1061 | if ( scope.enabled === false ) return; 1062 | 1063 | event.preventDefault(); 1064 | event.stopPropagation(); 1065 | 1066 | switch ( state ) { 1067 | 1068 | case STATE.TOUCH_ROTATE: 1069 | 1070 | if ( scope.enableRotate === false ) return; 1071 | 1072 | handleTouchMoveRotate( event ); 1073 | 1074 | scope.update(); 1075 | 1076 | break; 1077 | 1078 | case STATE.TOUCH_PAN: 1079 | 1080 | if ( scope.enablePan === false ) return; 1081 | 1082 | handleTouchMovePan( event ); 1083 | 1084 | scope.update(); 1085 | 1086 | break; 1087 | 1088 | case STATE.TOUCH_DOLLY_PAN: 1089 | 1090 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1091 | 1092 | handleTouchMoveDollyPan( event ); 1093 | 1094 | scope.update(); 1095 | 1096 | break; 1097 | 1098 | case STATE.TOUCH_DOLLY_ROTATE: 1099 | 1100 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1101 | 1102 | handleTouchMoveDollyRotate( event ); 1103 | 1104 | scope.update(); 1105 | 1106 | break; 1107 | 1108 | default: 1109 | 1110 | state = STATE.NONE; 1111 | 1112 | } 1113 | 1114 | } 1115 | 1116 | function onTouchEnd( event ) { 1117 | 1118 | if ( scope.enabled === false ) return; 1119 | 1120 | handleTouchEnd( event ); 1121 | 1122 | scope.dispatchEvent( endEvent ); 1123 | 1124 | state = STATE.NONE; 1125 | 1126 | } 1127 | 1128 | function onContextMenu( event ) { 1129 | 1130 | if ( scope.enabled === false ) return; 1131 | 1132 | event.preventDefault(); 1133 | 1134 | } 1135 | 1136 | // 1137 | 1138 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 1139 | 1140 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 1141 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 1142 | 1143 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 1144 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 1145 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 1146 | 1147 | window.addEventListener( 'keydown', onKeyDown, false ); 1148 | 1149 | // force an update at start 1150 | 1151 | this.update(); 1152 | 1153 | }; 1154 | 1155 | OrbitControls.prototype = Object.create( EventDispatcher.prototype ); 1156 | OrbitControls.prototype.constructor = OrbitControls; 1157 | 1158 | 1159 | // This set of controls performs orbiting, dollying (zooming), and panning. 1160 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1161 | // This is very similar to OrbitControls, another set of touch behavior 1162 | // 1163 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1164 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1165 | // Pan - left mouse, or arrow keys / touch: one-finger move 1166 | 1167 | var MapControls = function ( object, domElement ) { 1168 | 1169 | OrbitControls.call( this, object, domElement ); 1170 | 1171 | this.mouseButtons.LEFT = MOUSE.PAN; 1172 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1173 | 1174 | this.touches.ONE = TOUCH.PAN; 1175 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1176 | 1177 | }; 1178 | 1179 | MapControls.prototype = Object.create( EventDispatcher.prototype ); 1180 | MapControls.prototype.constructor = MapControls; 1181 | 1182 | export { OrbitControls, MapControls }; 1183 | --------------------------------------------------------------------------------