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