├── .gitignore ├── .gitattributes ├── tsconfig.json ├── rollup.config.js ├── .eslintrc.js ├── LICENSE ├── package.json ├── src ├── CSMFrustum.ts ├── CSMHelper.ts ├── CSMShader.ts └── CSM.ts ├── README.md └── examples └── basic ├── index.html ├── OrbitControls.js └── dat.gui.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .idea 4 | .DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "moduleResolution": "node", 5 | "target": "es6", 6 | "sourceMap": false, 7 | "outDir": "build", 8 | "declaration": true, 9 | "declarationDir": ".", 10 | "rootDir": "src/" 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | let pkg = require('./package.json'); 2 | import typescript from '@rollup/plugin-typescript'; 3 | import eslint from '@rollup/plugin-eslint'; 4 | 5 | export default { 6 | input: 'src/CSM.ts', 7 | output: [{ 8 | file: pkg.main, 9 | format: 'umd', 10 | name: 'THREE.CSM', 11 | globals: { 12 | 'three': 'THREE' 13 | }, 14 | indent: '\t' 15 | }, { 16 | file: pkg.module, 17 | format: 'esm', 18 | globals: { 19 | 'three': 'THREE' 20 | }, 21 | indent: '\t' 22 | }], 23 | external: [ 24 | 'three' 25 | ], 26 | plugins: [eslint({fix: true}), typescript()] 27 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: [ 7 | 'mdcs', // should i use it?.. it's ugly af :c 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended' 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['@typescript-eslint'], 13 | root: true, 14 | overrides: [], 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module' 18 | }, 19 | rules: { 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "@typescript-eslint/explicit-member-accessibility": "error" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 vtHawk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-csm", 3 | "version": "4.2.1", 4 | "description": "Cascaded shadow mapping (CSM) implementation for three.js", 5 | "main": "build/three-csm.js", 6 | "module": "build/three-csm.module.js", 7 | "types": "build/CSM.d.ts", 8 | "scripts": { 9 | "build": "rollup -c --bundleConfigAsCjs", 10 | "dev": "concurrently \"ws\" \"rollup -c -w --bundleConfigAsCjs\"", 11 | "lint": "eslint src --fix", 12 | "server": "ws" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/vtHawk/three-csm.git" 17 | }, 18 | "keywords": [ 19 | "three", 20 | "three.js", 21 | "3d", 22 | "shadows" 23 | ], 24 | "author": "vtHawk", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@rollup/plugin-eslint": "^9.0.1", 28 | "@rollup/plugin-typescript": "^9.0.2", 29 | "@types/three": "^0.155.0", 30 | "@typescript-eslint/eslint-plugin": "^5.43.0", 31 | "@typescript-eslint/parser": "^5.43.0", 32 | "concurrently": "^4.1.2", 33 | "eslint": "^8.27.0", 34 | "eslint-config-mdcs": "^5.0.0", 35 | "local-web-server": "^3.0.7", 36 | "rollup": "^3.3.0", 37 | "typescript": "^4.8.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CSMFrustum.ts: -------------------------------------------------------------------------------- 1 | import { Vector3, Matrix4 } from 'three'; 2 | 3 | const inverseProjectionMatrix = new Matrix4(); 4 | 5 | interface Params { 6 | projectionMatrix?: Matrix4; 7 | maxFar?: number; 8 | } 9 | 10 | interface FrustumVertices { 11 | far: Vector3[]; 12 | near: Vector3[] 13 | } 14 | 15 | export default class CSMFrustum { 16 | 17 | public vertices: FrustumVertices = { 18 | near: [ 19 | new Vector3(), 20 | new Vector3(), 21 | new Vector3(), 22 | new Vector3() 23 | ], 24 | far: [ 25 | new Vector3(), 26 | new Vector3(), 27 | new Vector3(), 28 | new Vector3() 29 | ] 30 | }; 31 | 32 | public constructor( data: Params = {} ) { 33 | 34 | if ( data.projectionMatrix !== undefined ) { 35 | 36 | this.setFromProjectionMatrix( data.projectionMatrix, data.maxFar || 10000 ); 37 | 38 | } 39 | 40 | } 41 | 42 | public setFromProjectionMatrix( projectionMatrix: Matrix4, maxFar: number ): FrustumVertices { 43 | 44 | const isOrthographic = projectionMatrix.elements[ 2 * 4 + 3 ] === 0; 45 | 46 | inverseProjectionMatrix.copy( projectionMatrix ).invert(); 47 | 48 | // 3 --- 0 vertices.near/far order 49 | // | | 50 | // 2 --- 1 51 | // clip space spans from [-1, 1] 52 | 53 | this.vertices.near[ 0 ].set( 1, 1, - 1 ); 54 | this.vertices.near[ 1 ].set( 1, - 1, - 1 ); 55 | this.vertices.near[ 2 ].set( - 1, - 1, - 1 ); 56 | this.vertices.near[ 3 ].set( - 1, 1, - 1 ); 57 | this.vertices.near.forEach( function ( v ) { 58 | 59 | v.applyMatrix4( inverseProjectionMatrix ); 60 | 61 | } ); 62 | 63 | this.vertices.far[ 0 ].set( 1, 1, 1 ); 64 | this.vertices.far[ 1 ].set( 1, - 1, 1 ); 65 | this.vertices.far[ 2 ].set( - 1, - 1, 1 ); 66 | this.vertices.far[ 3 ].set( - 1, 1, 1 ); 67 | this.vertices.far.forEach( function ( v ) { 68 | 69 | v.applyMatrix4( inverseProjectionMatrix ); 70 | 71 | const absZ = Math.abs( v.z ); 72 | if ( isOrthographic ) { 73 | 74 | v.z *= Math.min( maxFar / absZ, 1.0 ); 75 | 76 | } else { 77 | 78 | v.multiplyScalar( Math.min( maxFar / absZ, 1.0 ) ); 79 | 80 | } 81 | 82 | } ); 83 | 84 | return this.vertices; 85 | 86 | } 87 | 88 | public split( breaks: number[], target: CSMFrustum[] ) { 89 | 90 | while ( breaks.length > target.length ) { 91 | 92 | target.push( new CSMFrustum() ); 93 | 94 | } 95 | 96 | target.length = breaks.length; 97 | 98 | for ( let i = 0; i < breaks.length; i ++ ) { 99 | 100 | const cascade = target[ i ]; 101 | 102 | if ( i === 0 ) { 103 | 104 | for ( let j = 0; j < 4; j ++ ) { 105 | 106 | cascade.vertices.near[ j ].copy( this.vertices.near[ j ] ); 107 | 108 | } 109 | 110 | } else { 111 | 112 | for ( let j = 0; j < 4; j ++ ) { 113 | 114 | cascade.vertices.near[ j ].lerpVectors( this.vertices.near[ j ], this.vertices.far[ j ], breaks[ i - 1 ] ); 115 | 116 | } 117 | 118 | } 119 | 120 | if ( i === breaks.length - 1 ) { 121 | 122 | for ( let j = 0; j < 4; j ++ ) { 123 | 124 | cascade.vertices.far[ j ].copy( this.vertices.far[ j ] ); 125 | 126 | } 127 | 128 | } else { 129 | 130 | for ( let j = 0; j < 4; j ++ ) { 131 | 132 | cascade.vertices.far[ j ].lerpVectors( this.vertices.near[ j ], this.vertices.far[ j ], breaks[ i ] ); 133 | 134 | } 135 | 136 | } 137 | 138 | } 139 | 140 | } 141 | 142 | public toSpace( cameraMatrix: Matrix4, target: CSMFrustum ) { 143 | 144 | for ( let i = 0; i < 4; i ++ ) { 145 | 146 | target.vertices.near[ i ] 147 | .copy( this.vertices.near[ i ] ) 148 | .applyMatrix4( cameraMatrix ); 149 | 150 | target.vertices.far[ i ] 151 | .copy( this.vertices.far[ i ] ) 152 | .applyMatrix4( cameraMatrix ); 153 | 154 | } 155 | 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/CSMHelper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Group, 3 | Mesh, 4 | LineSegments, 5 | BufferGeometry, 6 | LineBasicMaterial, 7 | Box3Helper, 8 | Box3, 9 | PlaneGeometry, 10 | MeshBasicMaterial, 11 | BufferAttribute, 12 | DoubleSide, 13 | Color 14 | } from 'three'; 15 | import CSM from './CSM'; 16 | 17 | class CSMHelper extends Group { 18 | 19 | private readonly csm: CSM; 20 | public displayFrustum = true; 21 | public displayPlanes = true; 22 | public displayShadowBounds = true; 23 | private frustumLines: LineSegments; 24 | private cascadeLines: Box3Helper[] = []; 25 | private cascadePlanes: Mesh[] = []; 26 | private shadowLines: Group[] = []; 27 | 28 | public constructor( csm: CSM ) { 29 | 30 | super(); 31 | this.csm = csm; 32 | 33 | const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] ); 34 | const positions = new Float32Array( 24 ); 35 | const frustumGeometry = new BufferGeometry(); 36 | frustumGeometry.setIndex( new BufferAttribute( indices, 1 ) ); 37 | frustumGeometry.setAttribute( 'position', new BufferAttribute( positions, 3, false ) ); 38 | const frustumLines = new LineSegments( frustumGeometry, new LineBasicMaterial() ); 39 | this.add( frustumLines ); 40 | 41 | this.frustumLines = frustumLines; 42 | 43 | } 44 | 45 | public updateVisibility() { 46 | 47 | const displayFrustum = this.displayFrustum; 48 | const displayPlanes = this.displayPlanes; 49 | const displayShadowBounds = this.displayShadowBounds; 50 | 51 | const frustumLines = this.frustumLines; 52 | const cascadeLines = this.cascadeLines; 53 | const cascadePlanes = this.cascadePlanes; 54 | const shadowLines = this.shadowLines; 55 | for ( let i = 0, l = cascadeLines.length; i < l; i ++ ) { 56 | 57 | const cascadeLine = cascadeLines[ i ]; 58 | const cascadePlane = cascadePlanes[ i ]; 59 | const shadowLineGroup = shadowLines[ i ]; 60 | 61 | cascadeLine.visible = displayFrustum; 62 | cascadePlane.visible = displayFrustum && displayPlanes; 63 | shadowLineGroup.visible = displayShadowBounds; 64 | 65 | } 66 | 67 | frustumLines.visible = displayFrustum; 68 | 69 | } 70 | 71 | public update() { 72 | 73 | const csm = this.csm; 74 | const camera = csm.camera; 75 | const cascades = csm.cascades; 76 | const mainFrustum = csm.mainFrustum; 77 | const frustums = csm.frustums; 78 | const lights = csm.lights; 79 | 80 | const frustumLines = this.frustumLines; 81 | const frustumLinePositions = frustumLines.geometry.getAttribute( 'position' ); 82 | const cascadeLines = this.cascadeLines; 83 | const cascadePlanes = this.cascadePlanes; 84 | const shadowLines = this.shadowLines; 85 | 86 | this.position.copy( camera.position ); 87 | this.quaternion.copy( camera.quaternion ); 88 | this.scale.copy( camera.scale ); 89 | this.updateMatrixWorld( true ); 90 | 91 | while ( cascadeLines.length > cascades ) { 92 | 93 | this.remove( cascadeLines.pop() ); 94 | this.remove( cascadePlanes.pop() ); 95 | this.remove( shadowLines.pop() ); 96 | 97 | } 98 | 99 | while ( cascadeLines.length < cascades ) { 100 | 101 | const cascadeLine = new Box3Helper( new Box3(), new Color( 0xffffff ) ); 102 | const planeMat = new MeshBasicMaterial( { transparent: true, opacity: 0.1, depthWrite: false, side: DoubleSide } ); 103 | const cascadePlane = new Mesh( new PlaneGeometry(), planeMat ); 104 | const shadowLineGroup = new Group(); 105 | const shadowLine = new Box3Helper( new Box3(), new Color( 0xffff00 ) ); 106 | shadowLineGroup.add( shadowLine ); 107 | 108 | this.add( cascadeLine ); 109 | this.add( cascadePlane ); 110 | this.add( shadowLineGroup ); 111 | 112 | cascadeLines.push( cascadeLine ); 113 | cascadePlanes.push( cascadePlane ); 114 | shadowLines.push( shadowLineGroup ); 115 | 116 | } 117 | 118 | for ( let i = 0; i < cascades; i ++ ) { 119 | 120 | const frustum = frustums[ i ]; 121 | const light = lights[ i ]; 122 | const shadowCam = light.shadow.camera; 123 | const farVerts = frustum.vertices.far; 124 | 125 | const cascadeLine = cascadeLines[ i ]; 126 | const cascadePlane = cascadePlanes[ i ]; 127 | const shadowLineGroup = shadowLines[ i ]; 128 | const shadowLine = shadowLineGroup.children[ 0 ] as Box3Helper; 129 | 130 | cascadeLine.box.min.copy( farVerts[ 2 ] ); 131 | cascadeLine.box.max.copy( farVerts[ 0 ] ); 132 | cascadeLine.box.max.z += 1e-4; 133 | 134 | cascadePlane.position.addVectors( farVerts[ 0 ], farVerts[ 2 ] ); 135 | cascadePlane.position.multiplyScalar( 0.5 ); 136 | cascadePlane.scale.subVectors( farVerts[ 0 ], farVerts[ 2 ] ); 137 | cascadePlane.scale.z = 1e-4; 138 | 139 | this.remove( shadowLineGroup ); 140 | shadowLineGroup.position.copy( shadowCam.position ); 141 | shadowLineGroup.quaternion.copy( shadowCam.quaternion ); 142 | shadowLineGroup.scale.copy( shadowCam.scale ); 143 | shadowLineGroup.updateMatrixWorld( true ); 144 | this.attach( shadowLineGroup ); 145 | 146 | shadowLine.box.min.set( shadowCam.bottom, shadowCam.left, - shadowCam.far ); 147 | shadowLine.box.max.set( shadowCam.top, shadowCam.right, - shadowCam.near ); 148 | 149 | } 150 | 151 | const nearVerts = mainFrustum.vertices.near; 152 | const farVerts = mainFrustum.vertices.far; 153 | frustumLinePositions.setXYZ( 0, farVerts[ 0 ].x, farVerts[ 0 ].y, farVerts[ 0 ].z ); 154 | frustumLinePositions.setXYZ( 1, farVerts[ 3 ].x, farVerts[ 3 ].y, farVerts[ 3 ].z ); 155 | frustumLinePositions.setXYZ( 2, farVerts[ 2 ].x, farVerts[ 2 ].y, farVerts[ 2 ].z ); 156 | frustumLinePositions.setXYZ( 3, farVerts[ 1 ].x, farVerts[ 1 ].y, farVerts[ 1 ].z ); 157 | 158 | frustumLinePositions.setXYZ( 4, nearVerts[ 0 ].x, nearVerts[ 0 ].y, nearVerts[ 0 ].z ); 159 | frustumLinePositions.setXYZ( 5, nearVerts[ 3 ].x, nearVerts[ 3 ].y, nearVerts[ 3 ].z ); 160 | frustumLinePositions.setXYZ( 6, nearVerts[ 2 ].x, nearVerts[ 2 ].y, nearVerts[ 2 ].z ); 161 | frustumLinePositions.setXYZ( 7, nearVerts[ 1 ].x, nearVerts[ 1 ].y, nearVerts[ 1 ].z ); 162 | frustumLinePositions.needsUpdate = true; 163 | 164 | } 165 | 166 | } 167 | 168 | export default CSMHelper; 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-csm 2 | 3 | Cascaded shadow maps (CSMs) implementation for [Three.js](https://threejs.org/). This approach provides higher resolution of shadows near the camera and lower resolution far away by using several shadow maps. CSMs are usually used for shadows cast by the sun over a large terrain. 4 | 5 | ## Examples 6 | 7 | - [Basic](http://strandedkitty.github.io/three-csm/examples/basic/) 8 | 9 | ![Cascaded Shadow Maps](https://i.imgur.com/YSvYi2g.png) 10 | 11 | ## Installation 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | Using CommonJS: 18 | 19 | ``` 20 | npm i three-csm 21 | ``` 22 | 23 | ```javascript 24 | const THREE = require('three'); 25 | THREE.CSM = require('three-csm'); 26 | ``` 27 | 28 | Using ES6 modules: 29 | 30 | ```javascript 31 | import * as THREE from 'three'; 32 | import CSM from 'three-csm'; 33 | THREE.CSM = CSM; 34 | ``` 35 | 36 | ## Basic usage 37 | 38 | ```javascript 39 | let camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000); 40 | let renderer = new THREE.WebGLRenderer(); 41 | 42 | renderer.shadowMap.enabled = true; 43 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; // or any other type of shadowmap 44 | 45 | let csm = new THREE.CSM({ 46 | maxFar: camera.far, 47 | cascades: 4, 48 | shadowMapSize: 1024, 49 | lightDirection: new THREE.Vector3(1, -1, 1).normalize(), 50 | camera: camera, 51 | parent: scene 52 | }); 53 | 54 | let material = new THREE.MeshPhongMaterial(); // works with Phong and Standard materials 55 | csm.setupMaterial(material); // must be called to pass all CSM-related uniforms to the shader 56 | 57 | let mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(), material); 58 | mesh.castShadow = true; 59 | mesh.receiveShadow = true; 60 | 61 | scene.add(mesh); 62 | ``` 63 | 64 | Finally, in your update loop, call the update function before rendering: 65 | 66 | ```javascript 67 | csm.update(camera.matrix); 68 | ``` 69 | 70 | ## API 71 | 72 | ### `CSM` 73 | 74 | ### `constructor(settings: CSMParams)` 75 | 76 | **Parameters** 77 | 78 | - `settings` — `Object` which contains all setting for CSMs. 79 | 80 | - `settings.camera` — Instance of `THREE.PerspectiveCamera` which is currently used for rendering. 81 | 82 | - `settings.parent` — Instance of `THREE.Object3D` that will contain all directional lights. 83 | 84 | - `settings.cascades` — Number of shadow cascades. Optional. 85 | 86 | - `settings.maxCascades` — Maximum number of shadow cascades. Should be greater or equal to `cascades`. Important if you want to change the number of shadow cascades at runtime using `CSM.updateCascades()` method. Optional. 87 | 88 | - `settings.maxFar` — Frustum far plane distance (i.e. shadows are not visible farther this distance from camera). May be smaller than `camera.far` value. Optional. 89 | 90 | - `settings.mode` — Defines a split scheme (how large frustum is splitted into smaller ones). Can be `uniform` (linear), `logarithmic`, `practical` or `custom`. For most cases `practical` may be the best choice. Equations used for each scheme can be found in [*GPU Gems 3. Chapter 10*](https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html). If mode is set to `custom`, you'll need to define your own `customSplitsCallback`. Optional. 91 | 92 | - `settings.practicalModeLambda` — Lambda parameter for `practical` mode. Optional.` 93 | 94 | - `settings.customSplitsCallback` — A callback to compute custom cascade splits when mode is set to `custom`. Callback should accept three number parameters: `cascadeCount`, `nearDistance`, `farDistance` and return an array of split distances ranging from 0 to 1, where 0 is equal to `nearDistance` and 1 is equal to `farDistance`. Check out the official modes in CSM.js to learn how they work. 95 | 96 | - `settings.shadowMapSize` — Resolution of shadow maps (one per cascade). Optional. 97 | 98 | - `settings.shadowBias` — Serves the same purpose as [THREE.LightShadow.bias](https://threejs.org/docs/#api/en/lights/shadows/LightShadow.bias). Gets multiplied by the size of a shadow frustum. Optional. 99 | 100 | - `settings.shadowNormalBias` — Serves the same purpose as [THREE.LightShadow.normalBias](https://threejs.org/docs/#api/en/lights/shadows/LightShadow.normalBias). Gets multiplied by the size of a shadow frustum. Optional. 101 | 102 | - `settings.lightIntensity` — Same as [THREE.DirectionalLight.intensity](https://threejs.org/docs/#api/en/lights/DirectionalLight). Optional. 103 | 104 | - `settings.lightColor` — Same as [THREE.DirectionalLight.color](https://threejs.org/docs/#api/en/lights/DirectionalLight). Optional. 105 | 106 | - `settings.lightDirection` — Normalized `THREE.Vector3()`. Optional. 107 | 108 | - `settings.lightDirectionUp` — Up vector used for `settings.lightDirection`. Optional, defaults to [THREE.Object3D.DEFAULT_UP](https://threejs.org/docs/?q=object#api/en/core/Object3D.DEFAULT_UP). 109 | 110 | - `settings.lightMargin` — Defines how far shadow camera is moved along z axis in cascade frustum space. The larger is the value the more space `LightShadow` will be able to cover. Should be set to high values for scenes with large or tall shadow casters. Optional. 111 | 112 | - `settings.fade` — If `true`, enables smooth transition between cascades. Optional. 113 | 114 | - `settings.noLastCascadeCutOff` — If `true`, disables cutting off the last cascade for better shadow coverage. Optional. 115 | 116 | ### `setupMaterial(material: THREE.Material)` 117 | 118 | Updates defines and uniforms of passed material. Should be called for every material which must use CSMs. 119 | 120 | **Parameters** 121 | 122 | - `material` — Material to add uniforms and defines to. 123 | 124 | ### `update()` 125 | 126 | Updates positions of frustum splits in world space. Should be called before every frame before rendering. 127 | 128 | ### `updateFrustums()` 129 | 130 | Recalculates frustums for shadow cascades. Must be called after changing the camera projection matrix, split mode, `maxFar` or `shadowBias` settings. 131 | 132 | ### `updateCascades(cascades: number)` 133 | 134 | Updates number of shadow cascades, automatically recompiles all materials previously passed to `setupMaterial()`. 135 | 136 | **Parameters** 137 | 138 | - `cascades` — New number of shadow cascades. 139 | 140 | ### `updateShadowMapSize(size: number)` 141 | 142 | Updates shadow map size for all directional lights used by `CSM` instance. 143 | 144 | **Parameters** 145 | 146 | - `size` — New shadow map size. 147 | 148 | ### `dispose()` 149 | 150 | Removes and disposes all directional lights used by `CSM` instance. 151 | 152 | 153 | ## Contributing 154 | 155 | Feel free to contribute. Use `npm run dev` to run a dev server. 156 | 157 | ## References 158 | 159 | 1. [Rouslan Dimitrov. *Cascaded ShadowMaps*](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf) 160 | 2. [*Cascaded Shadow Maps* on Windows Dev Center](https://docs.microsoft.com/en-us/windows/win32/dxtecharts/cascaded-shadow-maps) 161 | 3. [*3D Game Development with LWJGL 3. Cascaded Shadow Maps*](https://ahbejarano.gitbook.io/lwjglgamedev/chapter26) 162 | 4. [*GPU Gems 3. Chapter 10*](https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html) 163 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | THREE.CSM Example 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /src/CSMShader.ts: -------------------------------------------------------------------------------- 1 | import { ShaderChunk } from 'three'; 2 | 3 | const lightParsBeginInitial = ShaderChunk.lights_pars_begin; 4 | 5 | const CSMShader = { 6 | lights_fragment_begin: ( cascades: number ) => /* glsl */` 7 | GeometricContext geometry; 8 | 9 | geometry.position = - vViewPosition; 10 | geometry.normal = normal; 11 | geometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); 12 | 13 | #ifdef CLEARCOAT 14 | 15 | geometry.clearcoatNormal = clearcoatNormal; 16 | 17 | #endif 18 | 19 | IncidentLight directLight; 20 | 21 | #if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) 22 | 23 | PointLight pointLight; 24 | #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 25 | PointLightShadow pointLightShadow; 26 | #endif 27 | 28 | #pragma unroll_loop_start 29 | for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { 30 | 31 | pointLight = pointLights[ i ]; 32 | 33 | getPointLightInfo( pointLight, geometry, directLight ); 34 | 35 | #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) 36 | pointLightShadow = pointLightShadows[ i ]; 37 | directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; 38 | #endif 39 | 40 | RE_Direct( directLight, geometry, material, reflectedLight ); 41 | 42 | } 43 | #pragma unroll_loop_end 44 | 45 | #endif 46 | 47 | #if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) 48 | 49 | SpotLight spotLight; 50 | #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 51 | SpotLightShadow spotLightShadow; 52 | #endif 53 | 54 | #pragma unroll_loop_start 55 | for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { 56 | 57 | spotLight = spotLights[ i ]; 58 | 59 | getSpotLightInfo( spotLight, geometry, directLight ); 60 | 61 | #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) 62 | spotLightShadow = spotLightShadows[ i ]; 63 | directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0; 64 | #endif 65 | 66 | RE_Direct( directLight, geometry, material, reflectedLight ); 67 | 68 | } 69 | #pragma unroll_loop_end 70 | 71 | #endif 72 | 73 | #if ( NUM_DIR_LIGHTS > 0) && defined( RE_Direct ) && defined( USE_CSM ) && defined( CSM_CASCADES ) 74 | 75 | DirectionalLight directionalLight; 76 | float linearDepth = (vViewPosition.z) / (shadowFar - cameraNear); 77 | #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 78 | DirectionalLightShadow directionalLightShadow; 79 | #endif 80 | 81 | #if defined( USE_SHADOWMAP ) && defined( CSM_FADE ) && CSM_FADE == 1 82 | vec2 cascade; 83 | float cascadeCenter; 84 | float closestEdge; 85 | float margin; 86 | float csmx; 87 | float csmy; 88 | 89 | #pragma unroll_loop_start 90 | for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { 91 | 92 | directionalLight = directionalLights[ i ]; 93 | getDirectionalLightInfo( directionalLight, geometry, directLight ); 94 | 95 | #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) 96 | // NOTE: Depth gets larger away from the camera. 97 | // cascade.x is closer, cascade.y is further 98 | 99 | #if ( UNROLLED_LOOP_INDEX < ${cascades} ) 100 | 101 | // NOTE: Apply CSM shadows 102 | 103 | cascade = CSM_cascades[ i ]; 104 | cascadeCenter = ( cascade.x + cascade.y ) / 2.0; 105 | closestEdge = linearDepth < cascadeCenter ? cascade.x : cascade.y; 106 | margin = 0.25 * pow( closestEdge, 2.0 ); 107 | csmx = cascade.x - margin / 2.0; 108 | csmy = cascade.y + margin / 2.0; 109 | if( linearDepth >= csmx && ( linearDepth < csmy || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 ) ) { 110 | 111 | float dist = min( linearDepth - csmx, csmy - linearDepth ); 112 | float ratio = clamp( dist / margin, 0.0, 1.0 ); 113 | 114 | vec3 prevColor = directLight.color; 115 | directionalLightShadow = directionalLightShadows[ i ]; 116 | directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; 117 | 118 | bool shouldFadeLastCascade = UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 && linearDepth > cascadeCenter; 119 | directLight.color = mix( prevColor, directLight.color, shouldFadeLastCascade ? ratio : 1.0 ); 120 | 121 | ReflectedLight prevLight = reflectedLight; 122 | RE_Direct( directLight, geometry, material, reflectedLight ); 123 | 124 | bool shouldBlend = UNROLLED_LOOP_INDEX != CSM_CASCADES - 1 || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 && linearDepth < cascadeCenter; 125 | float blendRatio = shouldBlend ? ratio : 1.0; 126 | 127 | reflectedLight.directDiffuse = mix( prevLight.directDiffuse, reflectedLight.directDiffuse, blendRatio ); 128 | reflectedLight.directSpecular = mix( prevLight.directSpecular, reflectedLight.directSpecular, blendRatio ); 129 | reflectedLight.indirectDiffuse = mix( prevLight.indirectDiffuse, reflectedLight.indirectDiffuse, blendRatio ); 130 | reflectedLight.indirectSpecular = mix( prevLight.indirectSpecular, reflectedLight.indirectSpecular, blendRatio ); 131 | 132 | } 133 | 134 | #else 135 | 136 | // NOTE: Apply the reminder of directional lights 137 | 138 | directionalLightShadow = directionalLightShadows[ i ]; 139 | directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; 140 | 141 | RE_Direct( directLight, geometry, material, reflectedLight ); 142 | 143 | #endif 144 | 145 | #endif 146 | 147 | } 148 | #pragma unroll_loop_end 149 | #else 150 | 151 | #pragma unroll_loop_start 152 | for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { 153 | 154 | directionalLight = directionalLights[ i ]; 155 | getDirectionalLightInfo( directionalLight, geometry, directLight ); 156 | 157 | #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) 158 | 159 | #if ( UNROLLED_LOOP_INDEX < ${cascades} ) 160 | 161 | // NOTE: Apply CSM shadows 162 | 163 | directionalLightShadow = directionalLightShadows[ i ]; 164 | if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y) directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; 165 | 166 | if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && (linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1)) RE_Direct( directLight, geometry, material, reflectedLight ); 167 | 168 | #else 169 | 170 | // NOTE: Apply the reminder of directional lights 171 | 172 | directionalLightShadow = directionalLightShadows[ i ]; 173 | directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; 174 | 175 | RE_Direct( directLight, geometry, material, reflectedLight ); 176 | 177 | #endif 178 | 179 | #endif 180 | 181 | } 182 | #pragma unroll_loop_end 183 | 184 | #endif 185 | 186 | #if ( NUM_DIR_LIGHTS > NUM_DIR_LIGHT_SHADOWS) 187 | // compute the lights not casting shadows (if any) 188 | 189 | #pragma unroll_loop_start 190 | for ( int i = NUM_DIR_LIGHT_SHADOWS; i < NUM_DIR_LIGHTS; i ++ ) { 191 | 192 | directionalLight = directionalLights[ i ]; 193 | 194 | getDirectionalLightInfo( directionalLight, geometry, directLight ); 195 | 196 | RE_Direct( directLight, geometry, material, reflectedLight ); 197 | 198 | } 199 | #pragma unroll_loop_end 200 | 201 | #endif 202 | 203 | #endif 204 | 205 | 206 | #if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) && !defined( USE_CSM ) && !defined( CSM_CASCADES ) 207 | 208 | DirectionalLight directionalLight; 209 | #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 210 | DirectionalLightShadow directionalLightShadow; 211 | #endif 212 | 213 | #pragma unroll_loop_start 214 | for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { 215 | 216 | directionalLight = directionalLights[ i ]; 217 | 218 | getDirectionalLightInfo( directionalLight, geometry, directLight ); 219 | 220 | #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) 221 | directionalLightShadow = directionalLightShadows[ i ]; 222 | directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; 223 | #endif 224 | 225 | RE_Direct( directLight, geometry, material, reflectedLight ); 226 | 227 | } 228 | #pragma unroll_loop_end 229 | 230 | #endif 231 | 232 | #if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) 233 | 234 | RectAreaLight rectAreaLight; 235 | 236 | #pragma unroll_loop_start 237 | for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { 238 | 239 | rectAreaLight = rectAreaLights[ i ]; 240 | RE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight ); 241 | 242 | } 243 | #pragma unroll_loop_end 244 | 245 | #endif 246 | 247 | #if defined( RE_IndirectDiffuse ) 248 | 249 | vec3 iblIrradiance = vec3( 0.0 ); 250 | 251 | vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); 252 | 253 | irradiance += getLightProbeIrradiance( lightProbe, geometry.normal ); 254 | 255 | #if ( NUM_HEMI_LIGHTS > 0 ) 256 | 257 | #pragma unroll_loop_start 258 | for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { 259 | 260 | irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry.normal ); 261 | 262 | } 263 | #pragma unroll_loop_end 264 | 265 | #endif 266 | 267 | #endif 268 | 269 | #if defined( RE_IndirectSpecular ) 270 | 271 | vec3 radiance = vec3( 0.0 ); 272 | vec3 clearcoatRadiance = vec3( 0.0 ); 273 | 274 | #endif 275 | `, 276 | lights_pars_begin: ( maxCascades: number ) => /* glsl */` 277 | #if defined( USE_CSM ) && defined( CSM_CASCADES ) 278 | uniform vec2 CSM_cascades[${maxCascades}]; // This value is the max. number supported of cascades 279 | uniform float cameraNear; 280 | uniform float shadowFar; 281 | #endif 282 | ` + lightParsBeginInitial 283 | }; 284 | 285 | export default CSMShader; 286 | -------------------------------------------------------------------------------- /src/CSM.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Vector2, 3 | Vector3, 4 | DirectionalLight, 5 | MathUtils, 6 | ShaderChunk, 7 | Matrix4, 8 | Box3, 9 | Object3D, 10 | Material, 11 | Shader, 12 | PerspectiveCamera, 13 | OrthographicCamera, 14 | Color 15 | } from 'three'; 16 | import CSMShader from './CSMShader'; 17 | import CSMHelper from './CSMHelper'; 18 | import CSMFrustum from './CSMFrustum'; 19 | 20 | function uniformSplit( amount: number, near: number, far: number, target: number[] ) { 21 | 22 | for ( let i = 1; i < amount; i ++ ) { 23 | 24 | target.push( ( near + ( far - near ) * i / amount ) / far ); 25 | 26 | } 27 | 28 | target.push( 1 ); 29 | 30 | } 31 | 32 | function logarithmicSplit( amount: number, near: number, far: number, target: number[] ) { 33 | 34 | for ( let i = 1; i < amount; i ++ ) { 35 | 36 | target.push( ( near * ( far / near ) ** ( i / amount ) ) / far ); 37 | 38 | } 39 | 40 | target.push( 1 ); 41 | 42 | } 43 | 44 | function practicalSplit( amount: number, near: number, far: number, lambda: number, target: number[] ) { 45 | 46 | _uniformArray.length = 0; 47 | _logArray.length = 0; 48 | logarithmicSplit( amount, near, far, _logArray ); 49 | uniformSplit( amount, near, far, _uniformArray ); 50 | 51 | for ( let i = 1; i < amount; i ++ ) { 52 | 53 | target.push( MathUtils.lerp( _uniformArray[ i - 1 ], _logArray[ i - 1 ], lambda ) ); 54 | 55 | } 56 | 57 | target.push( 1 ); 58 | 59 | } 60 | 61 | const _origin = new Vector3( 0, 0, 0 ); 62 | const _lightOrientationMatrix = new Matrix4(); 63 | const _lightOrientationMatrixInverse = new Matrix4(); 64 | const _cameraToLightParentMatrix = new Matrix4(); 65 | const _cameraToLightMatrix = new Matrix4(); 66 | const _lightSpaceFrustum = new CSMFrustum(); 67 | const _center = new Vector3(); 68 | const _bbox = new Box3(); 69 | const _uniformArray = []; 70 | const _logArray = []; 71 | 72 | export type CustomSplitsCallbackType = ( cascadeCount: number, nearDistance: number, farDistance: number ) => number[]; 73 | 74 | export interface CSMParams { 75 | camera: PerspectiveCamera | OrthographicCamera; 76 | parent: Object3D; 77 | cascades?: number; 78 | maxCascades?: number; 79 | maxFar?: number; 80 | mode?: 'uniform' | 'logarithmic' | 'practical' | 'custom'; 81 | practicalModeLambda?: number; 82 | customSplitsCallback?: CustomSplitsCallbackType; 83 | shadowMapSize?: number; 84 | shadowBias?: number; 85 | shadowNormalBias?: number; 86 | lightIntensity?: number; 87 | lightColor?: Color; 88 | lightDirection?: Vector3; 89 | lightDirectionUp?: Vector3; 90 | lightMargin?: number; 91 | fade?: boolean; 92 | noLastCascadeCutOff?: boolean; 93 | } 94 | 95 | class CSM { 96 | 97 | public camera: PerspectiveCamera | OrthographicCamera; 98 | public parent: Object3D; 99 | public cascades: number; 100 | public maxCascades: number; 101 | public maxFar: number; 102 | public mode: string; 103 | public practicalModeLambda: number; 104 | public shadowMapSize: number; 105 | public shadowBias: number; 106 | public shadowNormalBias: number; 107 | public lightDirection: Vector3; 108 | public lightDirectionUp: Vector3; 109 | public lightIntensity: number; 110 | public lightColor: Color; 111 | public lightMargin: number; 112 | public customSplitsCallback: CustomSplitsCallbackType; 113 | public fade: boolean; 114 | public noLastCascadeCutOff: boolean; 115 | public mainFrustum: CSMFrustum = new CSMFrustum(); 116 | public frustums: CSMFrustum[] = []; 117 | public breaks: number[] = []; 118 | public lights: DirectionalLight[] = []; 119 | private readonly shaders: Map = new Map(); 120 | 121 | public constructor( data: CSMParams ) { 122 | 123 | this.camera = data.camera; 124 | this.parent = data.parent; 125 | this.cascades = data.cascades ?? 3; 126 | this.maxCascades = data.maxCascades ?? data.cascades; 127 | this.maxFar = data.maxFar ?? 100000; 128 | this.mode = data.mode ?? 'practical'; 129 | this.practicalModeLambda = data.practicalModeLambda ?? 0.5; 130 | this.shadowMapSize = data.shadowMapSize ?? 2048; 131 | this.shadowBias = data.shadowBias ?? 0; 132 | this.shadowNormalBias = data.shadowNormalBias ?? 0; 133 | this.lightDirection = data.lightDirection ?? new Vector3( 1, - 1, 1 ).normalize(); 134 | this.lightDirectionUp = data.lightDirectionUp ?? Object3D.DEFAULT_UP; 135 | this.lightIntensity = data.lightIntensity ?? 1; 136 | this.lightColor = data.lightColor ?? new Color( 0xffffff ); 137 | this.lightMargin = data.lightMargin ?? 200; 138 | this.fade = data.fade ?? false; 139 | this.noLastCascadeCutOff = data.noLastCascadeCutOff ?? false; 140 | this.customSplitsCallback = data.customSplitsCallback; 141 | 142 | this.createLights(); 143 | this.updateFrustums(); 144 | this.injectInclude(); 145 | 146 | } 147 | 148 | private createLights() { 149 | 150 | for ( let i = 0; i < this.cascades; i ++ ) { 151 | 152 | const light = new DirectionalLight( this.lightColor, this.lightIntensity ); 153 | light.castShadow = true; 154 | light.shadow.mapSize.width = this.shadowMapSize; 155 | light.shadow.mapSize.height = this.shadowMapSize; 156 | 157 | light.shadow.camera.near = 0; 158 | light.shadow.camera.far = 1; 159 | 160 | this.parent.add( light.target ); 161 | this.lights.push( light ); 162 | 163 | } 164 | 165 | // NOTE: Prepend lights to the parent as we assume CSM shadows come from first light sources in the world 166 | 167 | for ( let i = this.lights.length - 1; i >= 0; i -- ) { 168 | 169 | const light = this.lights[ i ]; 170 | 171 | light.parent = this.parent; 172 | this.parent.children.unshift( light ); 173 | 174 | } 175 | 176 | } 177 | 178 | private initCascades() { 179 | 180 | this.mainFrustum.setFromProjectionMatrix( this.camera.projectionMatrix, this.maxFar ); 181 | this.mainFrustum.split( this.breaks, this.frustums ); 182 | 183 | } 184 | 185 | private updateShadowBounds() { 186 | 187 | const frustums = this.frustums; 188 | for ( let i = 0; i < frustums.length; i ++ ) { 189 | 190 | const light = this.lights[ i ]; 191 | const shadowCam = light.shadow.camera; 192 | const frustum = this.frustums[ i ]; 193 | 194 | // Get the two points that represent that furthest points on the frustum assuming 195 | // that's either the diagonal across the far plane or the diagonal across the whole 196 | // frustum itself. 197 | const nearVerts = frustum.vertices.near; 198 | const farVerts = frustum.vertices.far; 199 | const point1 = farVerts[ 0 ]; 200 | let point2; 201 | if ( point1.distanceTo( farVerts[ 2 ] ) > point1.distanceTo( nearVerts[ 2 ] ) ) { 202 | 203 | point2 = farVerts[ 2 ]; 204 | 205 | } else { 206 | 207 | point2 = nearVerts[ 2 ]; 208 | 209 | } 210 | 211 | let squaredBBWidth = point1.distanceTo( point2 ); 212 | if ( this.fade ) { 213 | 214 | // expand the shadow extents by the fade margin if fade is enabled. 215 | const camera = this.camera; 216 | const far = Math.max( camera.far, this.maxFar ); 217 | const linearDepth = frustum.vertices.far[ 0 ].z / ( far - camera.near ); 218 | const margin = 0.25 * Math.pow( linearDepth, 2.0 ) * ( far - camera.near ); 219 | 220 | squaredBBWidth += margin; 221 | 222 | } 223 | 224 | shadowCam.left = - squaredBBWidth / 2; 225 | shadowCam.right = squaredBBWidth / 2; 226 | shadowCam.top = squaredBBWidth / 2; 227 | shadowCam.bottom = - squaredBBWidth / 2; 228 | shadowCam.near = 0; 229 | shadowCam.far = squaredBBWidth + this.lightMargin; 230 | shadowCam.updateProjectionMatrix(); 231 | 232 | light.shadow.bias = this.shadowBias * squaredBBWidth; 233 | light.shadow.normalBias = this.shadowNormalBias * squaredBBWidth; 234 | 235 | } 236 | 237 | } 238 | 239 | private updateBreaks() { 240 | 241 | const camera = this.camera; 242 | const far = Math.min( camera.far, this.maxFar ); 243 | this.breaks.length = 0; 244 | 245 | switch ( this.mode ) { 246 | 247 | case 'uniform': 248 | uniformSplit( this.cascades, camera.near, far, this.breaks ); 249 | break; 250 | case 'logarithmic': 251 | logarithmicSplit( this.cascades, camera.near, far, this.breaks ); 252 | break; 253 | case 'practical': 254 | practicalSplit( this.cascades, camera.near, far, this.practicalModeLambda, this.breaks ); 255 | break; 256 | case 'custom': 257 | if ( this.customSplitsCallback === undefined ) { 258 | 259 | throw new Error( 'CSM: Custom split scheme callback not defined.' ); 260 | 261 | } 262 | 263 | this.breaks.push( ...this.customSplitsCallback( this.cascades, camera.near, far ) ); 264 | break; 265 | 266 | } 267 | 268 | } 269 | 270 | public update() { 271 | 272 | for ( let i = 0; i < this.frustums.length; i ++ ) { 273 | 274 | const light = this.lights[ i ]; 275 | const shadowCam = light.shadow.camera; 276 | const texelWidth = ( shadowCam.right - shadowCam.left ) / this.shadowMapSize; 277 | const texelHeight = ( shadowCam.top - shadowCam.bottom ) / this.shadowMapSize; 278 | 279 | // This matrix only represents sun orientation, origin is zero 280 | _lightOrientationMatrix.lookAt( _origin, this.lightDirection, this.lightDirectionUp ); 281 | _lightOrientationMatrixInverse.copy( _lightOrientationMatrix ).invert(); 282 | 283 | // Go from camera space to world space using camera.matrixWorld, then go to parent space using inverse of parent.matrixWorld 284 | _cameraToLightParentMatrix.copy( this.parent.matrixWorld ).invert().multiply( this.camera.matrixWorld ); 285 | // Go from camera space to light parent space, then apply light orientation 286 | _cameraToLightMatrix.multiplyMatrices( _lightOrientationMatrixInverse, _cameraToLightParentMatrix ); 287 | this.frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum ); 288 | 289 | const nearVerts = _lightSpaceFrustum.vertices.near; 290 | const farVerts = _lightSpaceFrustum.vertices.far; 291 | _bbox.makeEmpty(); 292 | for ( let j = 0; j < 4; j ++ ) { 293 | 294 | _bbox.expandByPoint( nearVerts[ j ] ); 295 | _bbox.expandByPoint( farVerts[ j ] ); 296 | 297 | } 298 | 299 | _bbox.getCenter( _center ); 300 | _center.z = _bbox.max.z + this.lightMargin; 301 | // Round X and Y to avoid shadow shimmering when moving or rotating the camera 302 | _center.x = Math.floor( _center.x / texelWidth ) * texelWidth; 303 | _center.y = Math.floor( _center.y / texelHeight ) * texelHeight; 304 | // Center is currently in light space, so we need to go back to light parent space 305 | _center.applyMatrix4( _lightOrientationMatrix ); 306 | 307 | // New positions are relative to this.parent 308 | light.position.copy( _center ); 309 | light.target.position.copy( _center ); 310 | 311 | light.target.position.x += this.lightDirection.x; 312 | light.target.position.y += this.lightDirection.y; 313 | light.target.position.z += this.lightDirection.z; 314 | 315 | } 316 | 317 | } 318 | 319 | private injectInclude() { 320 | 321 | ShaderChunk.lights_fragment_begin = CSMShader.lights_fragment_begin( this.cascades ); 322 | ShaderChunk.lights_pars_begin = CSMShader.lights_pars_begin( this.maxCascades ); 323 | 324 | } 325 | 326 | public setupMaterial( material: Material ) { 327 | 328 | const fn = ( shader ) => { 329 | 330 | const breaksVec2 = this.getExtendedBreaks(); 331 | 332 | const far = Math.min( this.camera.far, this.maxFar ); 333 | 334 | shader.uniforms.CSM_cascades = { value: breaksVec2 }; 335 | shader.uniforms.cameraNear = { value: Math.min( this.maxFar, this.camera.near ) }; 336 | shader.uniforms.shadowFar = { value: far }; 337 | 338 | material.defines = material.defines || {}; 339 | material.defines.USE_CSM = 1; 340 | material.defines.CSM_CASCADES = this.cascades; 341 | material.defines.CSM_FADE = this.fade ? '1' : '0'; 342 | 343 | material.needsUpdate = true; 344 | 345 | this.shaders.set( material, shader ); 346 | 347 | material.addEventListener( 'dispose', () => { 348 | 349 | this.shaders.delete( material ); 350 | 351 | } ); 352 | 353 | }; 354 | 355 | if ( ! material.onBeforeCompile ) { 356 | 357 | material.onBeforeCompile = fn; 358 | 359 | } else { 360 | 361 | const previousFn = material.onBeforeCompile; 362 | 363 | material.onBeforeCompile = ( ...args ) => { 364 | 365 | previousFn( ...args ); 366 | fn( args[ 0 ] ); 367 | 368 | }; 369 | 370 | } 371 | 372 | } 373 | 374 | private updateUniforms() { 375 | 376 | const far = Math.min( this.camera.far, this.maxFar ); 377 | 378 | const breaks = this.getExtendedBreaks(); 379 | 380 | this.shaders.forEach( ( shader, material ) => { 381 | 382 | if ( shader !== null ) { 383 | 384 | const uniforms = shader.uniforms; 385 | uniforms.CSM_cascades.value = breaks; 386 | uniforms.cameraNear.value = Math.min( this.maxFar, this.camera.near ); 387 | uniforms.shadowFar.value = far; 388 | 389 | } 390 | 391 | let definesChanged = false; 392 | 393 | const fadeValue = this.fade ? '0' : '1'; 394 | if ( material.defines.CSM_FADE !== fadeValue ) { 395 | 396 | material.defines.CSM_FADE = fadeValue; 397 | definesChanged = true; 398 | 399 | } 400 | 401 | if ( material.defines.CSM_CASCADES !== this.cascades ) { 402 | 403 | material.defines.CSM_CASCADES = this.cascades; 404 | definesChanged = true; 405 | 406 | } 407 | 408 | if ( definesChanged ) { 409 | 410 | material.needsUpdate = true; 411 | 412 | } 413 | 414 | } ); 415 | 416 | } 417 | 418 | private getExtendedBreaks(): Vector2[] { 419 | 420 | const target: Vector2[] = []; 421 | 422 | for ( let i = 0; i < this.maxCascades; i ++ ) { 423 | 424 | const amount = this.breaks[ i ] || 0; 425 | const prev = this.breaks[ i - 1 ] || 0; 426 | target.push( new Vector2( prev, amount ) ); 427 | 428 | } 429 | 430 | if ( this.noLastCascadeCutOff ) { 431 | 432 | target[ this.breaks.length - 1 ].y = Infinity; 433 | 434 | } 435 | 436 | return target; 437 | 438 | } 439 | 440 | public updateFrustums() { 441 | 442 | this.updateBreaks(); 443 | this.initCascades(); 444 | this.updateShadowBounds(); 445 | this.updateUniforms(); 446 | 447 | } 448 | 449 | public updateCascades( cascades: number ) { 450 | 451 | this.cascades = cascades; 452 | 453 | for ( const light of this.lights ) { 454 | 455 | this.parent.remove( light ); 456 | light.dispose(); 457 | 458 | } 459 | 460 | this.lights.length = 0; 461 | 462 | this.createLights(); 463 | 464 | this.injectInclude(); 465 | 466 | this.updateFrustums(); 467 | 468 | } 469 | 470 | public updateShadowMapSize( size: number ) { 471 | 472 | this.shadowMapSize = size; 473 | 474 | for ( let i = 0; i < this.lights.length; i ++ ) { 475 | 476 | const light = this.lights[ i ]; 477 | light.shadow.mapSize.width = size; 478 | light.shadow.mapSize.height = size; 479 | 480 | if ( light.shadow.map ) { 481 | 482 | // Dispose old shadow map so that three.js automatically creates a new one using the updated 483 | // mapSize dimensions. See https://stackoverflow.com/a/31858963/8886455 484 | light.shadow.map.dispose(); 485 | light.shadow.map = null; 486 | 487 | } 488 | 489 | } 490 | 491 | } 492 | 493 | public dispose() { 494 | 495 | this.shaders.forEach( function ( shader, material ) { 496 | 497 | delete material.onBeforeCompile; 498 | delete material.defines.USE_CSM; 499 | delete material.defines.CSM_CASCADES; 500 | delete material.defines.CSM_FADE; 501 | 502 | if ( shader !== null ) { 503 | 504 | delete shader.uniforms.CSM_cascades; 505 | delete shader.uniforms.cameraNear; 506 | delete shader.uniforms.shadowFar; 507 | 508 | } 509 | 510 | material.needsUpdate = true; 511 | 512 | } ); 513 | this.shaders.clear(); 514 | 515 | for ( let i = 0; i < this.lights.length; i ++ ) { 516 | 517 | this.lights[ i ].dispose(); 518 | this.parent.remove( this.lights[ i ] ); 519 | 520 | } 521 | 522 | } 523 | 524 | public static Helper = CSMHelper; 525 | 526 | } 527 | 528 | export default CSM; 529 | -------------------------------------------------------------------------------- /examples/basic/OrbitControls.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 4 | // 5 | // Orbit - left mouse / touch: one-finger move 6 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 7 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 8 | 9 | const _changeEvent = { 10 | type: 'change' 11 | }; 12 | const _startEvent = { 13 | type: 'start' 14 | }; 15 | const _endEvent = { 16 | type: 'end' 17 | }; 18 | 19 | class OrbitControls extends THREE.EventDispatcher { 20 | 21 | constructor( object, domElement ) { 22 | 23 | super(); 24 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 25 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 26 | this.object = object; 27 | this.domElement = domElement; // Set to false to disable this control 28 | 29 | this.enabled = true; // "target" sets the location of focus, where the object orbits around 30 | 31 | this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) 32 | 33 | this.minDistance = 0; 34 | this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) 35 | 36 | this.minZoom = 0; 37 | this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. 38 | // Range is 0 to Math.PI radians. 39 | 40 | this.minPolarAngle = 0; // radians 41 | 42 | this.maxPolarAngle = Math.PI; // radians 43 | // How far you can orbit horizontally, upper and lower limits. 44 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 45 | 46 | this.minAzimuthAngle = - Infinity; // radians 47 | 48 | this.maxAzimuthAngle = Infinity; // radians 49 | // Set to true to enable damping (inertia) 50 | // If damping is enabled, you must call controls.update() in your animation loop 51 | 52 | this.enableDamping = false; 53 | this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 54 | // Set to false to disable zooming 55 | 56 | this.enableZoom = true; 57 | this.zoomSpeed = 1.0; // Set to false to disable rotating 58 | 59 | this.enableRotate = true; 60 | this.rotateSpeed = 1.0; // Set to false to disable panning 61 | 62 | this.enablePan = true; 63 | this.panSpeed = 1.0; 64 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 65 | 66 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 67 | // Set to true to automatically rotate around the target 68 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 69 | 70 | this.autoRotate = false; 71 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 72 | // The four arrow keys 73 | 74 | this.keys = { 75 | LEFT: 'ArrowLeft', 76 | UP: 'ArrowUp', 77 | RIGHT: 'ArrowRight', 78 | BOTTOM: 'ArrowDown' 79 | }; // Mouse buttons 80 | 81 | this.mouseButtons = { 82 | LEFT: THREE.MOUSE.ROTATE, 83 | MIDDLE: THREE.MOUSE.DOLLY, 84 | RIGHT: THREE.MOUSE.PAN 85 | }; // Touch fingers 86 | 87 | this.touches = { 88 | ONE: THREE.TOUCH.ROTATE, 89 | TWO: THREE.TOUCH.DOLLY_PAN 90 | }; // for reset 91 | 92 | this.target0 = this.target.clone(); 93 | this.position0 = this.object.position.clone(); 94 | this.zoom0 = this.object.zoom; // the target DOM element for key events 95 | 96 | this._domElementKeyEvents = null; // 97 | // public methods 98 | // 99 | 100 | this.getPolarAngle = function () { 101 | 102 | return spherical.phi; 103 | 104 | }; 105 | 106 | this.getAzimuthalAngle = function () { 107 | 108 | return spherical.theta; 109 | 110 | }; 111 | 112 | this.listenToKeyEvents = function ( domElement ) { 113 | 114 | domElement.addEventListener( 'keydown', onKeyDown ); 115 | this._domElementKeyEvents = domElement; 116 | 117 | }; 118 | 119 | this.saveState = function () { 120 | 121 | scope.target0.copy( scope.target ); 122 | scope.position0.copy( scope.object.position ); 123 | scope.zoom0 = scope.object.zoom; 124 | 125 | }; 126 | 127 | this.reset = function () { 128 | 129 | scope.target.copy( scope.target0 ); 130 | scope.object.position.copy( scope.position0 ); 131 | scope.object.zoom = scope.zoom0; 132 | scope.object.updateProjectionMatrix(); 133 | scope.dispatchEvent( _changeEvent ); 134 | scope.update(); 135 | state = STATE.NONE; 136 | 137 | }; // this method is exposed, but perhaps it would be better if we can make it private... 138 | 139 | 140 | this.update = function () { 141 | 142 | const offset = new THREE.Vector3(); // so camera.up is the orbit axis 143 | 144 | const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 145 | const quatInverse = quat.clone().invert(); 146 | const lastPosition = new THREE.Vector3(); 147 | const lastQuaternion = new THREE.Quaternion(); 148 | const twoPI = 2 * Math.PI; 149 | return function update() { 150 | 151 | const position = scope.object.position; 152 | offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space 153 | 154 | offset.applyQuaternion( quat ); // angle from z-axis around y-axis 155 | 156 | spherical.setFromVector3( offset ); 157 | 158 | if ( scope.autoRotate && state === STATE.NONE ) { 159 | 160 | rotateLeft( getAutoRotationAngle() ); 161 | 162 | } 163 | 164 | if ( scope.enableDamping ) { 165 | 166 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 167 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 168 | 169 | } else { 170 | 171 | spherical.theta += sphericalDelta.theta; 172 | spherical.phi += sphericalDelta.phi; 173 | 174 | } // restrict theta to be between desired limits 175 | 176 | 177 | let min = scope.minAzimuthAngle; 178 | let max = scope.maxAzimuthAngle; 179 | 180 | if ( isFinite( min ) && isFinite( max ) ) { 181 | 182 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 183 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 184 | 185 | if ( min <= max ) { 186 | 187 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 188 | 189 | } else { 190 | 191 | spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta ); 192 | 193 | } 194 | 195 | } // restrict phi to be between desired limits 196 | 197 | 198 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 199 | spherical.makeSafe(); 200 | spherical.radius *= scale; // restrict radius to be between desired limits 201 | 202 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location 203 | 204 | if ( scope.enableDamping === true ) { 205 | 206 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 207 | 208 | } else { 209 | 210 | scope.target.add( panOffset ); 211 | 212 | } 213 | 214 | offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space 215 | 216 | offset.applyQuaternion( quatInverse ); 217 | position.copy( scope.target ).add( offset ); 218 | scope.object.lookAt( scope.target ); 219 | 220 | if ( scope.enableDamping === true ) { 221 | 222 | sphericalDelta.theta *= 1 - scope.dampingFactor; 223 | sphericalDelta.phi *= 1 - scope.dampingFactor; 224 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 225 | 226 | } else { 227 | 228 | sphericalDelta.set( 0, 0, 0 ); 229 | panOffset.set( 0, 0, 0 ); 230 | 231 | } 232 | 233 | scale = 1; // update condition is: 234 | // min(camera displacement, camera rotation in radians)^2 > EPS 235 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 236 | 237 | if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 238 | 239 | scope.dispatchEvent( _changeEvent ); 240 | lastPosition.copy( scope.object.position ); 241 | lastQuaternion.copy( scope.object.quaternion ); 242 | zoomChanged = false; 243 | return true; 244 | 245 | } 246 | 247 | return false; 248 | 249 | }; 250 | 251 | }(); 252 | 253 | this.dispose = function () { 254 | 255 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 256 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 257 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 258 | scope.domElement.removeEventListener( 'touchstart', onTouchStart ); 259 | scope.domElement.removeEventListener( 'touchend', onTouchEnd ); 260 | scope.domElement.removeEventListener( 'touchmove', onTouchMove ); 261 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 262 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 263 | 264 | if ( scope._domElementKeyEvents !== null ) { 265 | 266 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 267 | 268 | } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 269 | 270 | }; // 271 | // internals 272 | // 273 | 274 | 275 | const scope = this; 276 | const STATE = { 277 | NONE: - 1, 278 | ROTATE: 0, 279 | DOLLY: 1, 280 | PAN: 2, 281 | TOUCH_ROTATE: 3, 282 | TOUCH_PAN: 4, 283 | TOUCH_DOLLY_PAN: 5, 284 | TOUCH_DOLLY_ROTATE: 6 285 | }; 286 | let state = STATE.NONE; 287 | const EPS = 0.000001; // current position in spherical coordinates 288 | 289 | const spherical = new THREE.Spherical(); 290 | const sphericalDelta = new THREE.Spherical(); 291 | let scale = 1; 292 | const panOffset = new THREE.Vector3(); 293 | let zoomChanged = false; 294 | const rotateStart = new THREE.Vector2(); 295 | const rotateEnd = new THREE.Vector2(); 296 | const rotateDelta = new THREE.Vector2(); 297 | const panStart = new THREE.Vector2(); 298 | const panEnd = new THREE.Vector2(); 299 | const panDelta = new THREE.Vector2(); 300 | const dollyStart = new THREE.Vector2(); 301 | const dollyEnd = new THREE.Vector2(); 302 | const dollyDelta = new THREE.Vector2(); 303 | 304 | function getAutoRotationAngle() { 305 | 306 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 307 | 308 | } 309 | 310 | function getZoomScale() { 311 | 312 | return Math.pow( 0.95, scope.zoomSpeed ); 313 | 314 | } 315 | 316 | function rotateLeft( angle ) { 317 | 318 | sphericalDelta.theta -= angle; 319 | 320 | } 321 | 322 | function rotateUp( angle ) { 323 | 324 | sphericalDelta.phi -= angle; 325 | 326 | } 327 | 328 | const panLeft = function () { 329 | 330 | const v = new THREE.Vector3(); 331 | return function panLeft( distance, objectMatrix ) { 332 | 333 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 334 | 335 | v.multiplyScalar( - distance ); 336 | panOffset.add( v ); 337 | 338 | }; 339 | 340 | }(); 341 | 342 | const panUp = function () { 343 | 344 | const v = new THREE.Vector3(); 345 | return function panUp( distance, objectMatrix ) { 346 | 347 | if ( scope.screenSpacePanning === true ) { 348 | 349 | v.setFromMatrixColumn( objectMatrix, 1 ); 350 | 351 | } else { 352 | 353 | v.setFromMatrixColumn( objectMatrix, 0 ); 354 | v.crossVectors( scope.object.up, v ); 355 | 356 | } 357 | 358 | v.multiplyScalar( distance ); 359 | panOffset.add( v ); 360 | 361 | }; 362 | 363 | }(); // deltaX and deltaY are in pixels; right and down are positive 364 | 365 | 366 | const pan = function () { 367 | 368 | const offset = new THREE.Vector3(); 369 | return function pan( deltaX, deltaY ) { 370 | 371 | const element = scope.domElement; 372 | 373 | if ( scope.object.isPerspectiveCamera ) { 374 | 375 | // perspective 376 | const position = scope.object.position; 377 | offset.copy( position ).sub( scope.target ); 378 | let targetDistance = offset.length(); // half of the fov is center to top of screen 379 | 380 | targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed 381 | 382 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 383 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 384 | 385 | } else if ( scope.object.isOrthographicCamera ) { 386 | 387 | // orthographic 388 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 389 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 390 | 391 | } else { 392 | 393 | // camera neither orthographic nor perspective 394 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 395 | scope.enablePan = false; 396 | 397 | } 398 | 399 | }; 400 | 401 | }(); 402 | 403 | function dollyOut( dollyScale ) { 404 | 405 | if ( scope.object.isPerspectiveCamera ) { 406 | 407 | scale /= dollyScale; 408 | 409 | } else if ( scope.object.isOrthographicCamera ) { 410 | 411 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 412 | scope.object.updateProjectionMatrix(); 413 | zoomChanged = true; 414 | 415 | } else { 416 | 417 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 418 | scope.enableZoom = false; 419 | 420 | } 421 | 422 | } 423 | 424 | function dollyIn( dollyScale ) { 425 | 426 | if ( scope.object.isPerspectiveCamera ) { 427 | 428 | scale *= dollyScale; 429 | 430 | } else if ( scope.object.isOrthographicCamera ) { 431 | 432 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 433 | scope.object.updateProjectionMatrix(); 434 | zoomChanged = true; 435 | 436 | } else { 437 | 438 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 439 | scope.enableZoom = false; 440 | 441 | } 442 | 443 | } // 444 | // event callbacks - update the object state 445 | // 446 | 447 | 448 | function handleMouseDownRotate( event ) { 449 | 450 | rotateStart.set( event.clientX, event.clientY ); 451 | 452 | } 453 | 454 | function handleMouseDownDolly( event ) { 455 | 456 | dollyStart.set( event.clientX, event.clientY ); 457 | 458 | } 459 | 460 | function handleMouseDownPan( event ) { 461 | 462 | panStart.set( event.clientX, event.clientY ); 463 | 464 | } 465 | 466 | function handleMouseMoveRotate( event ) { 467 | 468 | rotateEnd.set( event.clientX, event.clientY ); 469 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 470 | const element = scope.domElement; 471 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 472 | 473 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 474 | rotateStart.copy( rotateEnd ); 475 | scope.update(); 476 | 477 | } 478 | 479 | function handleMouseMoveDolly( event ) { 480 | 481 | dollyEnd.set( event.clientX, event.clientY ); 482 | dollyDelta.subVectors( dollyEnd, dollyStart ); 483 | 484 | if ( dollyDelta.y > 0 ) { 485 | 486 | dollyOut( getZoomScale() ); 487 | 488 | } else if ( dollyDelta.y < 0 ) { 489 | 490 | dollyIn( getZoomScale() ); 491 | 492 | } 493 | 494 | dollyStart.copy( dollyEnd ); 495 | scope.update(); 496 | 497 | } 498 | 499 | function handleMouseMovePan( event ) { 500 | 501 | panEnd.set( event.clientX, event.clientY ); 502 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 503 | pan( panDelta.x, panDelta.y ); 504 | panStart.copy( panEnd ); 505 | scope.update(); 506 | 507 | } 508 | 509 | function handleMouseUp( ) { // no-op 510 | } 511 | 512 | function handleMouseWheel( event ) { 513 | 514 | if ( event.deltaY < 0 ) { 515 | 516 | dollyIn( getZoomScale() ); 517 | 518 | } else if ( event.deltaY > 0 ) { 519 | 520 | dollyOut( getZoomScale() ); 521 | 522 | } 523 | 524 | scope.update(); 525 | 526 | } 527 | 528 | function handleKeyDown( event ) { 529 | 530 | let needsUpdate = false; 531 | 532 | switch ( event.code ) { 533 | 534 | case scope.keys.UP: 535 | pan( 0, scope.keyPanSpeed ); 536 | needsUpdate = true; 537 | break; 538 | 539 | case scope.keys.BOTTOM: 540 | pan( 0, - scope.keyPanSpeed ); 541 | needsUpdate = true; 542 | break; 543 | 544 | case scope.keys.LEFT: 545 | pan( scope.keyPanSpeed, 0 ); 546 | needsUpdate = true; 547 | break; 548 | 549 | case scope.keys.RIGHT: 550 | pan( - scope.keyPanSpeed, 0 ); 551 | needsUpdate = true; 552 | break; 553 | 554 | } 555 | 556 | if ( needsUpdate ) { 557 | 558 | // prevent the browser from scrolling on cursor keys 559 | event.preventDefault(); 560 | scope.update(); 561 | 562 | } 563 | 564 | } 565 | 566 | function handleTouchStartRotate( event ) { 567 | 568 | if ( event.touches.length == 1 ) { 569 | 570 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 571 | 572 | } else { 573 | 574 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 575 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 576 | rotateStart.set( x, y ); 577 | 578 | } 579 | 580 | } 581 | 582 | function handleTouchStartPan( event ) { 583 | 584 | if ( event.touches.length == 1 ) { 585 | 586 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 587 | 588 | } else { 589 | 590 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 591 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 592 | panStart.set( x, y ); 593 | 594 | } 595 | 596 | } 597 | 598 | function handleTouchStartDolly( event ) { 599 | 600 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 601 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 602 | const distance = Math.sqrt( dx * dx + dy * dy ); 603 | dollyStart.set( 0, distance ); 604 | 605 | } 606 | 607 | function handleTouchStartDollyPan( event ) { 608 | 609 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 610 | if ( scope.enablePan ) handleTouchStartPan( event ); 611 | 612 | } 613 | 614 | function handleTouchStartDollyRotate( event ) { 615 | 616 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 617 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 618 | 619 | } 620 | 621 | function handleTouchMoveRotate( event ) { 622 | 623 | if ( event.touches.length == 1 ) { 624 | 625 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 626 | 627 | } else { 628 | 629 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 630 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 631 | rotateEnd.set( x, y ); 632 | 633 | } 634 | 635 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 636 | const element = scope.domElement; 637 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 638 | 639 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 640 | rotateStart.copy( rotateEnd ); 641 | 642 | } 643 | 644 | function handleTouchMovePan( event ) { 645 | 646 | if ( event.touches.length == 1 ) { 647 | 648 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 649 | 650 | } else { 651 | 652 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 653 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 654 | panEnd.set( x, y ); 655 | 656 | } 657 | 658 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 659 | pan( panDelta.x, panDelta.y ); 660 | panStart.copy( panEnd ); 661 | 662 | } 663 | 664 | function handleTouchMoveDolly( event ) { 665 | 666 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 667 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 668 | const distance = Math.sqrt( dx * dx + dy * dy ); 669 | dollyEnd.set( 0, distance ); 670 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 671 | dollyOut( dollyDelta.y ); 672 | dollyStart.copy( dollyEnd ); 673 | 674 | } 675 | 676 | function handleTouchMoveDollyPan( event ) { 677 | 678 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 679 | if ( scope.enablePan ) handleTouchMovePan( event ); 680 | 681 | } 682 | 683 | function handleTouchMoveDollyRotate( event ) { 684 | 685 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 686 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 687 | 688 | } 689 | 690 | function handleTouchEnd( ) { // no-op 691 | } // 692 | // event handlers - FSM: listen for events and reset state 693 | // 694 | 695 | 696 | function onPointerDown( event ) { 697 | 698 | if ( scope.enabled === false ) return; 699 | 700 | switch ( event.pointerType ) { 701 | 702 | case 'mouse': 703 | case 'pen': 704 | onMouseDown( event ); 705 | break; 706 | // TODO touch 707 | 708 | } 709 | 710 | } 711 | 712 | function onPointerMove( event ) { 713 | 714 | if ( scope.enabled === false ) return; 715 | 716 | switch ( event.pointerType ) { 717 | 718 | case 'mouse': 719 | case 'pen': 720 | onMouseMove( event ); 721 | break; 722 | // TODO touch 723 | 724 | } 725 | 726 | } 727 | 728 | function onPointerUp( event ) { 729 | 730 | switch ( event.pointerType ) { 731 | 732 | case 'mouse': 733 | case 'pen': 734 | onMouseUp( event ); 735 | break; 736 | // TODO touch 737 | 738 | } 739 | 740 | } 741 | 742 | function onMouseDown( event ) { 743 | 744 | // Prevent the browser from scrolling. 745 | event.preventDefault(); // Manually set the focus since calling preventDefault above 746 | // prevents the browser from setting it automatically. 747 | 748 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 749 | let mouseAction; 750 | 751 | switch ( event.button ) { 752 | 753 | case 0: 754 | mouseAction = scope.mouseButtons.LEFT; 755 | break; 756 | 757 | case 1: 758 | mouseAction = scope.mouseButtons.MIDDLE; 759 | break; 760 | 761 | case 2: 762 | mouseAction = scope.mouseButtons.RIGHT; 763 | break; 764 | 765 | default: 766 | mouseAction = - 1; 767 | 768 | } 769 | 770 | switch ( mouseAction ) { 771 | 772 | case THREE.MOUSE.DOLLY: 773 | if ( scope.enableZoom === false ) return; 774 | handleMouseDownDolly( event ); 775 | state = STATE.DOLLY; 776 | break; 777 | 778 | case THREE.MOUSE.ROTATE: 779 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 780 | 781 | if ( scope.enablePan === false ) return; 782 | handleMouseDownPan( event ); 783 | state = STATE.PAN; 784 | 785 | } else { 786 | 787 | if ( scope.enableRotate === false ) return; 788 | handleMouseDownRotate( event ); 789 | state = STATE.ROTATE; 790 | 791 | } 792 | 793 | break; 794 | 795 | case THREE.MOUSE.PAN: 796 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 797 | 798 | if ( scope.enableRotate === false ) return; 799 | handleMouseDownRotate( event ); 800 | state = STATE.ROTATE; 801 | 802 | } else { 803 | 804 | if ( scope.enablePan === false ) return; 805 | handleMouseDownPan( event ); 806 | state = STATE.PAN; 807 | 808 | } 809 | 810 | break; 811 | 812 | default: 813 | state = STATE.NONE; 814 | 815 | } 816 | 817 | if ( state !== STATE.NONE ) { 818 | 819 | scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); 820 | scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); 821 | scope.dispatchEvent( _startEvent ); 822 | 823 | } 824 | 825 | } 826 | 827 | function onMouseMove( event ) { 828 | 829 | if ( scope.enabled === false ) return; 830 | event.preventDefault(); 831 | 832 | switch ( state ) { 833 | 834 | case STATE.ROTATE: 835 | if ( scope.enableRotate === false ) return; 836 | handleMouseMoveRotate( event ); 837 | break; 838 | 839 | case STATE.DOLLY: 840 | if ( scope.enableZoom === false ) return; 841 | handleMouseMoveDolly( event ); 842 | break; 843 | 844 | case STATE.PAN: 845 | if ( scope.enablePan === false ) return; 846 | handleMouseMovePan( event ); 847 | break; 848 | 849 | } 850 | 851 | } 852 | 853 | function onMouseUp( event ) { 854 | 855 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 856 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 857 | if ( scope.enabled === false ) return; 858 | handleMouseUp( event ); 859 | scope.dispatchEvent( _endEvent ); 860 | state = STATE.NONE; 861 | 862 | } 863 | 864 | function onMouseWheel( event ) { 865 | 866 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return; 867 | event.preventDefault(); 868 | scope.dispatchEvent( _startEvent ); 869 | handleMouseWheel( event ); 870 | scope.dispatchEvent( _endEvent ); 871 | 872 | } 873 | 874 | function onKeyDown( event ) { 875 | 876 | if ( scope.enabled === false || scope.enablePan === false ) return; 877 | handleKeyDown( event ); 878 | 879 | } 880 | 881 | function onTouchStart( event ) { 882 | 883 | if ( scope.enabled === false ) return; 884 | event.preventDefault(); // prevent scrolling 885 | 886 | switch ( event.touches.length ) { 887 | 888 | case 1: 889 | switch ( scope.touches.ONE ) { 890 | 891 | case THREE.TOUCH.ROTATE: 892 | if ( scope.enableRotate === false ) return; 893 | handleTouchStartRotate( event ); 894 | state = STATE.TOUCH_ROTATE; 895 | break; 896 | 897 | case THREE.TOUCH.PAN: 898 | if ( scope.enablePan === false ) return; 899 | handleTouchStartPan( event ); 900 | state = STATE.TOUCH_PAN; 901 | break; 902 | 903 | default: 904 | state = STATE.NONE; 905 | 906 | } 907 | 908 | break; 909 | 910 | case 2: 911 | switch ( scope.touches.TWO ) { 912 | 913 | case THREE.TOUCH.DOLLY_PAN: 914 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 915 | handleTouchStartDollyPan( event ); 916 | state = STATE.TOUCH_DOLLY_PAN; 917 | break; 918 | 919 | case THREE.TOUCH.DOLLY_ROTATE: 920 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 921 | handleTouchStartDollyRotate( event ); 922 | state = STATE.TOUCH_DOLLY_ROTATE; 923 | break; 924 | 925 | default: 926 | state = STATE.NONE; 927 | 928 | } 929 | 930 | break; 931 | 932 | default: 933 | state = STATE.NONE; 934 | 935 | } 936 | 937 | if ( state !== STATE.NONE ) { 938 | 939 | scope.dispatchEvent( _startEvent ); 940 | 941 | } 942 | 943 | } 944 | 945 | function onTouchMove( event ) { 946 | 947 | if ( scope.enabled === false ) return; 948 | event.preventDefault(); // prevent scrolling 949 | 950 | switch ( state ) { 951 | 952 | case STATE.TOUCH_ROTATE: 953 | if ( scope.enableRotate === false ) return; 954 | handleTouchMoveRotate( event ); 955 | scope.update(); 956 | break; 957 | 958 | case STATE.TOUCH_PAN: 959 | if ( scope.enablePan === false ) return; 960 | handleTouchMovePan( event ); 961 | scope.update(); 962 | break; 963 | 964 | case STATE.TOUCH_DOLLY_PAN: 965 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 966 | handleTouchMoveDollyPan( event ); 967 | scope.update(); 968 | break; 969 | 970 | case STATE.TOUCH_DOLLY_ROTATE: 971 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 972 | handleTouchMoveDollyRotate( event ); 973 | scope.update(); 974 | break; 975 | 976 | default: 977 | state = STATE.NONE; 978 | 979 | } 980 | 981 | } 982 | 983 | function onTouchEnd( event ) { 984 | 985 | if ( scope.enabled === false ) return; 986 | handleTouchEnd( event ); 987 | scope.dispatchEvent( _endEvent ); 988 | state = STATE.NONE; 989 | 990 | } 991 | 992 | function onContextMenu( event ) { 993 | 994 | if ( scope.enabled === false ) return; 995 | event.preventDefault(); 996 | 997 | } // 998 | 999 | 1000 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1001 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1002 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { 1003 | passive: false 1004 | } ); 1005 | scope.domElement.addEventListener( 'touchstart', onTouchStart, { 1006 | passive: false 1007 | } ); 1008 | scope.domElement.addEventListener( 'touchend', onTouchEnd ); 1009 | scope.domElement.addEventListener( 'touchmove', onTouchMove, { 1010 | passive: false 1011 | } ); // force an update at start 1012 | 1013 | this.update(); 1014 | 1015 | } 1016 | 1017 | } // This set of controls performs orbiting, dollying (zooming), and panning. 1018 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1019 | // This is very similar to OrbitControls, another set of touch behavior 1020 | // 1021 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1022 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1023 | // Pan - left mouse, or arrow keys / touch: one-finger move 1024 | 1025 | 1026 | class MapControls extends OrbitControls { 1027 | 1028 | constructor( object, domElement ) { 1029 | 1030 | super( object, domElement ); 1031 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1032 | 1033 | this.mouseButtons.LEFT = THREE.MOUSE.PAN; 1034 | this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; 1035 | this.touches.ONE = THREE.TOUCH.PAN; 1036 | this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; 1037 | 1038 | } 1039 | 1040 | } 1041 | 1042 | THREE.MapControls = MapControls; 1043 | THREE.OrbitControls = OrbitControls; 1044 | 1045 | } )(); 1046 | -------------------------------------------------------------------------------- /examples/basic/dat.gui.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dat-gui JavaScript Controller Library 3 | * http://code.google.com/p/dat-gui 4 | * 5 | * Copyright 2011 Data Arts Team, Google Creative Lab 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | */ 13 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.dat={})}(this,function(e){"use strict";function t(e,t){var n=e.__state.conversionName.toString(),o=Math.round(e.r),i=Math.round(e.g),r=Math.round(e.b),s=e.a,a=Math.round(e.h),l=e.s.toFixed(1),d=e.v.toFixed(1);if(t||"THREE_CHAR_HEX"===n||"SIX_CHAR_HEX"===n){for(var c=e.hex.toString(16);c.length<6;)c="0"+c;return"#"+c}return"CSS_RGB"===n?"rgb("+o+","+i+","+r+")":"CSS_RGBA"===n?"rgba("+o+","+i+","+r+","+s+")":"HEX"===n?"0x"+e.hex.toString(16):"RGB_ARRAY"===n?"["+o+","+i+","+r+"]":"RGBA_ARRAY"===n?"["+o+","+i+","+r+","+s+"]":"RGB_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+"}":"RGBA_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+",a:"+s+"}":"HSV_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+"}":"HSVA_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+",a:"+s+"}":"unknown format"}function n(e,t,n){Object.defineProperty(e,t,{get:function(){return"RGB"===this.__state.space?this.__state[t]:(I.recalculateRGB(this,t,n),this.__state[t])},set:function(e){"RGB"!==this.__state.space&&(I.recalculateRGB(this,t,n),this.__state.space="RGB"),this.__state[t]=e}})}function o(e,t){Object.defineProperty(e,t,{get:function(){return"HSV"===this.__state.space?this.__state[t]:(I.recalculateHSV(this),this.__state[t])},set:function(e){"HSV"!==this.__state.space&&(I.recalculateHSV(this),this.__state.space="HSV"),this.__state[t]=e}})}function i(e){if("0"===e||S.isUndefined(e))return 0;var t=e.match(U);return S.isNull(t)?0:parseFloat(t[1])}function r(e){var t=e.toString();return t.indexOf(".")>-1?t.length-t.indexOf(".")-1:0}function s(e,t){var n=Math.pow(10,t);return Math.round(e*n)/n}function a(e,t,n,o,i){return o+(e-t)/(n-t)*(i-o)}function l(e,t,n,o){e.style.background="",S.each(ee,function(i){e.style.cssText+="background: "+i+"linear-gradient("+t+", "+n+" 0%, "+o+" 100%); "})}function d(e){e.style.background="",e.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);",e.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}function c(e,t,n){var o=document.createElement("li");return t&&o.appendChild(t),n?e.__ul.insertBefore(o,n):e.__ul.appendChild(o),e.onResize(),o}function u(e){X.unbind(window,"resize",e.__resizeHandler),e.saveToLocalStorageIfPossible&&X.unbind(window,"unload",e.saveToLocalStorageIfPossible)}function _(e,t){var n=e.__preset_select[e.__preset_select.selectedIndex];n.innerHTML=t?n.value+"*":n.value}function h(e,t,n){if(n.__li=t,n.__gui=e,S.extend(n,{options:function(t){if(arguments.length>1){var o=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:o,factoryArgs:[S.toArray(arguments)]})}if(S.isArray(t)||S.isObject(t)){var i=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:i,factoryArgs:[t]})}},name:function(e){return n.__li.firstElementChild.firstElementChild.innerHTML=e,n},listen:function(){return n.__gui.listen(n),n},remove:function(){return n.__gui.remove(n),n}}),n instanceof q){var o=new Q(n.object,n.property,{min:n.__min,max:n.__max,step:n.__step});S.each(["updateDisplay","onChange","onFinishChange","step","min","max"],function(e){var t=n[e],i=o[e];n[e]=o[e]=function(){var e=Array.prototype.slice.call(arguments);return i.apply(o,e),t.apply(n,e)}}),X.addClass(t,"has-slider"),n.domElement.insertBefore(o.domElement,n.domElement.firstElementChild)}else if(n instanceof Q){var i=function(t){if(S.isNumber(n.__min)&&S.isNumber(n.__max)){var o=n.__li.firstElementChild.firstElementChild.innerHTML,i=n.__gui.__listening.indexOf(n)>-1;n.remove();var r=f(e,n.object,n.property,{before:n.__li.nextElementSibling,factoryArgs:[n.__min,n.__max,n.__step]});return r.name(o),i&&r.listen(),r}return t};n.min=S.compose(i,n.min),n.max=S.compose(i,n.max)}else n instanceof K?(X.bind(t,"click",function(){X.fakeEvent(n.__checkbox,"click")}),X.bind(n.__checkbox,"click",function(e){e.stopPropagation()})):n instanceof Z?(X.bind(t,"click",function(){X.fakeEvent(n.__button,"click")}),X.bind(t,"mouseover",function(){X.addClass(n.__button,"hover")}),X.bind(t,"mouseout",function(){X.removeClass(n.__button,"hover")})):n instanceof $&&(X.addClass(t,"color"),n.updateDisplay=S.compose(function(e){return t.style.borderLeftColor=n.__color.toString(),e},n.updateDisplay),n.updateDisplay());n.setValue=S.compose(function(t){return e.getRoot().__preset_select&&n.isModified()&&_(e.getRoot(),!0),t},n.setValue)}function p(e,t){var n=e.getRoot(),o=n.__rememberedObjects.indexOf(t.object);if(-1!==o){var i=n.__rememberedObjectIndecesToControllers[o];if(void 0===i&&(i={},n.__rememberedObjectIndecesToControllers[o]=i),i[t.property]=t,n.load&&n.load.remembered){var r=n.load.remembered,s=void 0;if(r[e.preset])s=r[e.preset];else{if(!r[se])return;s=r[se]}if(s[o]&&void 0!==s[o][t.property]){var a=s[o][t.property];t.initialValue=a,t.setValue(a)}}}}function f(e,t,n,o){if(void 0===t[n])throw new Error('Object "'+t+'" has no property "'+n+'"');var i=void 0;if(o.color)i=new $(t,n);else{var r=[t,n].concat(o.factoryArgs);i=ne.apply(e,r)}o.before instanceof z&&(o.before=o.before.__li),p(e,i),X.addClass(i.domElement,"c");var s=document.createElement("span");X.addClass(s,"property-name"),s.innerHTML=i.property;var a=document.createElement("div");a.appendChild(s),a.appendChild(i.domElement);var l=c(e,a,o.before);return X.addClass(l,he.CLASS_CONTROLLER_ROW),i instanceof $?X.addClass(l,"color"):X.addClass(l,H(i.getValue())),h(e,l,i),e.__controllers.push(i),i}function m(e,t){return document.location.href+"."+t}function g(e,t,n){var o=document.createElement("option");o.innerHTML=t,o.value=t,e.__preset_select.appendChild(o),n&&(e.__preset_select.selectedIndex=e.__preset_select.length-1)}function b(e,t){t.style.display=e.useLocalStorage?"block":"none"}function v(e){var t=e.__save_row=document.createElement("li");X.addClass(e.domElement,"has-save"),e.__ul.insertBefore(t,e.__ul.firstChild),X.addClass(t,"save-row");var n=document.createElement("span");n.innerHTML=" ",X.addClass(n,"button gears");var o=document.createElement("span");o.innerHTML="Save",X.addClass(o,"button"),X.addClass(o,"save");var i=document.createElement("span");i.innerHTML="New",X.addClass(i,"button"),X.addClass(i,"save-as");var r=document.createElement("span");r.innerHTML="Revert",X.addClass(r,"button"),X.addClass(r,"revert");var s=e.__preset_select=document.createElement("select");if(e.load&&e.load.remembered?S.each(e.load.remembered,function(t,n){g(e,n,n===e.preset)}):g(e,se,!1),X.bind(s,"change",function(){for(var t=0;t=0;n--)t=[e[n].apply(this,t)];return t[0]}},each:function(e,t,n){if(e)if(A&&e.forEach&&e.forEach===A)e.forEach(t,n);else if(e.length===e.length+0){var o=void 0,i=void 0;for(o=0,i=e.length;o1?S.toArray(arguments):arguments[0];return S.each(O,function(t){if(t.litmus(e))return S.each(t.conversions,function(t,n){if(T=t.read(e),!1===L&&!1!==T)return L=T,T.conversionName=n,T.conversion=t,S.BREAK}),S.BREAK}),L},B=void 0,N={hsv_to_rgb:function(e,t,n){var o=Math.floor(e/60)%6,i=e/60-Math.floor(e/60),r=n*(1-t),s=n*(1-i*t),a=n*(1-(1-i)*t),l=[[n,a,r],[s,n,r],[r,n,a],[r,s,n],[a,r,n],[n,r,s]][o];return{r:255*l[0],g:255*l[1],b:255*l[2]}},rgb_to_hsv:function(e,t,n){var o=Math.min(e,t,n),i=Math.max(e,t,n),r=i-o,s=void 0,a=void 0;return 0===i?{h:NaN,s:0,v:0}:(a=r/i,s=e===i?(t-n)/r:t===i?2+(n-e)/r:4+(e-t)/r,(s/=6)<0&&(s+=1),{h:360*s,s:a,v:i/255})},rgb_to_hex:function(e,t,n){var o=this.hex_with_component(0,2,e);return o=this.hex_with_component(o,1,t),o=this.hex_with_component(o,0,n)},component_from_hex:function(e,t){return e>>8*t&255},hex_with_component:function(e,t,n){return n<<(B=8*t)|e&~(255<this.__max&&(n=this.__max),void 0!==this.__step&&n%this.__step!=0&&(n=Math.round(n/this.__step)*this.__step),j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"setValue",this).call(this,n)}},{key:"min",value:function(e){return this.__min=e,this}},{key:"max",value:function(e){return this.__max=e,this}},{key:"step",value:function(e){return this.__step=e,this.__impliedStep=e,this.__precision=r(e),this}}]),t}(),Q=function(e){function t(e,n,o){function i(){l.__onFinishChange&&l.__onFinishChange.call(l,l.getValue())}function r(e){var t=d-e.clientY;l.setValue(l.getValue()+t*l.__impliedStep),d=e.clientY}function s(){X.unbind(window,"mousemove",r),X.unbind(window,"mouseup",s),i()}F(this,t);var a=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,o));a.__truncationSuspended=!1;var l=a,d=void 0;return a.__input=document.createElement("input"),a.__input.setAttribute("type","text"),X.bind(a.__input,"change",function(){var e=parseFloat(l.__input.value);S.isNaN(e)||l.setValue(e)}),X.bind(a.__input,"blur",function(){i()}),X.bind(a.__input,"mousedown",function(e){X.bind(window,"mousemove",r),X.bind(window,"mouseup",s),d=e.clientY}),X.bind(a.__input,"keydown",function(e){13===e.keyCode&&(l.__truncationSuspended=!0,this.blur(),l.__truncationSuspended=!1,i())}),a.updateDisplay(),a.domElement.appendChild(a.__input),a}return D(t,W),P(t,[{key:"updateDisplay",value:function(){return this.__input.value=this.__truncationSuspended?this.getValue():s(this.getValue(),this.__precision),j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),q=function(e){function t(e,n,o,i,r){function s(e){e.preventDefault();var t=_.__background.getBoundingClientRect();return _.setValue(a(e.clientX,t.left,t.right,_.__min,_.__max)),!1}function l(){X.unbind(window,"mousemove",s),X.unbind(window,"mouseup",l),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}function d(e){var t=e.touches[0].clientX,n=_.__background.getBoundingClientRect();_.setValue(a(t,n.left,n.right,_.__min,_.__max))}function c(){X.unbind(window,"touchmove",d),X.unbind(window,"touchend",c),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}F(this,t);var u=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,{min:o,max:i,step:r})),_=u;return u.__background=document.createElement("div"),u.__foreground=document.createElement("div"),X.bind(u.__background,"mousedown",function(e){document.activeElement.blur(),X.bind(window,"mousemove",s),X.bind(window,"mouseup",l),s(e)}),X.bind(u.__background,"touchstart",function(e){1===e.touches.length&&(X.bind(window,"touchmove",d),X.bind(window,"touchend",c),d(e))}),X.addClass(u.__background,"slider"),X.addClass(u.__foreground,"slider-fg"),u.updateDisplay(),u.__background.appendChild(u.__foreground),u.domElement.appendChild(u.__background),u}return D(t,W),P(t,[{key:"updateDisplay",value:function(){var e=(this.getValue()-this.__min)/(this.__max-this.__min);return this.__foreground.style.width=100*e+"%",j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),Z=function(e){function t(e,n,o){F(this,t);var i=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n)),r=i;return i.__button=document.createElement("div"),i.__button.innerHTML=void 0===o?"Fire":o,X.bind(i.__button,"click",function(e){return e.preventDefault(),r.fire(),!1}),X.addClass(i.__button,"button"),i.domElement.appendChild(i.__button),i}return D(t,z),P(t,[{key:"fire",value:function(){this.__onChange&&this.__onChange.call(this),this.getValue().call(this.object),this.__onFinishChange&&this.__onFinishChange.call(this,this.getValue())}}]),t}(),$=function(e){function t(e,n){function o(e){u(e),X.bind(window,"mousemove",u),X.bind(window,"touchmove",u),X.bind(window,"mouseup",r),X.bind(window,"touchend",r)}function i(e){_(e),X.bind(window,"mousemove",_),X.bind(window,"touchmove",_),X.bind(window,"mouseup",s),X.bind(window,"touchend",s)}function r(){X.unbind(window,"mousemove",u),X.unbind(window,"touchmove",u),X.unbind(window,"mouseup",r),X.unbind(window,"touchend",r),c()}function s(){X.unbind(window,"mousemove",_),X.unbind(window,"touchmove",_),X.unbind(window,"mouseup",s),X.unbind(window,"touchend",s),c()}function a(){var e=R(this.value);!1!==e?(p.__color.__state=e,p.setValue(p.__color.toOriginal())):this.value=p.__color.toString()}function c(){p.__onFinishChange&&p.__onFinishChange.call(p,p.__color.toOriginal())}function u(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__saturation_field.getBoundingClientRect(),n=e.touches&&e.touches[0]||e,o=n.clientX,i=n.clientY,r=(o-t.left)/(t.right-t.left),s=1-(i-t.top)/(t.bottom-t.top);return s>1?s=1:s<0&&(s=0),r>1?r=1:r<0&&(r=0),p.__color.v=s,p.__color.s=r,p.setValue(p.__color.toOriginal()),!1}function _(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__hue_field.getBoundingClientRect(),n=1-((e.touches&&e.touches[0]||e).clientY-t.top)/(t.bottom-t.top);return n>1?n=1:n<0&&(n=0),p.__color.h=360*n,p.setValue(p.__color.toOriginal()),!1}F(this,t);var h=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n));h.__color=new I(h.getValue()),h.__temp=new I(0);var p=h;h.domElement=document.createElement("div"),X.makeSelectable(h.domElement,!1),h.__selector=document.createElement("div"),h.__selector.className="selector",h.__saturation_field=document.createElement("div"),h.__saturation_field.className="saturation-field",h.__field_knob=document.createElement("div"),h.__field_knob.className="field-knob",h.__field_knob_border="2px solid ",h.__hue_knob=document.createElement("div"),h.__hue_knob.className="hue-knob",h.__hue_field=document.createElement("div"),h.__hue_field.className="hue-field",h.__input=document.createElement("input"),h.__input.type="text",h.__input_textShadow="0 1px 1px ",X.bind(h.__input,"keydown",function(e){13===e.keyCode&&a.call(this)}),X.bind(h.__input,"blur",a),X.bind(h.__selector,"mousedown",function(){X.addClass(this,"drag").bind(window,"mouseup",function(){X.removeClass(p.__selector,"drag")})}),X.bind(h.__selector,"touchstart",function(){X.addClass(this,"drag").bind(window,"touchend",function(){X.removeClass(p.__selector,"drag")})});var f=document.createElement("div");return S.extend(h.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"}),S.extend(h.__field_knob.style,{position:"absolute",width:"12px",height:"12px",border:h.__field_knob_border+(h.__color.v<.5?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1}),S.extend(h.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1}),S.extend(h.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"}),S.extend(f.style,{width:"100%",height:"100%",background:"none"}),l(f,"top","rgba(0,0,0,0)","#000"),S.extend(h.__hue_field.style,{width:"15px",height:"100px",border:"1px solid #555",cursor:"ns-resize",position:"absolute",top:"3px",right:"3px"}),d(h.__hue_field),S.extend(h.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:h.__input_textShadow+"rgba(0,0,0,0.7)"}),X.bind(h.__saturation_field,"mousedown",o),X.bind(h.__saturation_field,"touchstart",o),X.bind(h.__field_knob,"mousedown",o),X.bind(h.__field_knob,"touchstart",o),X.bind(h.__hue_field,"mousedown",i),X.bind(h.__hue_field,"touchstart",i),h.__saturation_field.appendChild(f),h.__selector.appendChild(h.__field_knob),h.__selector.appendChild(h.__saturation_field),h.__selector.appendChild(h.__hue_field),h.__hue_field.appendChild(h.__hue_knob),h.domElement.appendChild(h.__input),h.domElement.appendChild(h.__selector),h.updateDisplay(),h}return D(t,z),P(t,[{key:"updateDisplay",value:function(){var e=R(this.getValue());if(!1!==e){var t=!1;S.each(I.COMPONENTS,function(n){if(!S.isUndefined(e[n])&&!S.isUndefined(this.__color.__state[n])&&e[n]!==this.__color.__state[n])return t=!0,{}},this),t&&S.extend(this.__color.__state,e)}S.extend(this.__temp.__state,this.__color.__state),this.__temp.a=1;var n=this.__color.v<.5||this.__color.s>.5?255:0,o=255-n;S.extend(this.__field_knob.style,{marginLeft:100*this.__color.s-7+"px",marginTop:100*(1-this.__color.v)-7+"px",backgroundColor:this.__temp.toHexString(),border:this.__field_knob_border+"rgb("+n+","+n+","+n+")"}),this.__hue_knob.style.marginTop=100*(1-this.__color.h/360)+"px",this.__temp.s=1,this.__temp.v=1,l(this.__saturation_field,"left","#fff",this.__temp.toHexString()),this.__input.value=this.__color.toString(),S.extend(this.__input.style,{backgroundColor:this.__color.toHexString(),color:"rgb("+n+","+n+","+n+")",textShadow:this.__input_textShadow+"rgba("+o+","+o+","+o+",.7)"})}}]),t}(),ee=["-moz-","-o-","-webkit-","-ms-",""],te={load:function(e,t){var n=t||document,o=n.createElement("link");o.type="text/css",o.rel="stylesheet",o.href=e,n.getElementsByTagName("head")[0].appendChild(o)},inject:function(e,t){var n=t||document,o=document.createElement("style");o.type="text/css",o.innerHTML=e;var i=n.getElementsByTagName("head")[0];try{i.appendChild(o)}catch(e){}}},ne=function(e,t){var n=e[t];return S.isArray(arguments[2])||S.isObject(arguments[2])?new Y(e,t,arguments[2]):S.isNumber(n)?S.isNumber(arguments[2])&&S.isNumber(arguments[3])?S.isNumber(arguments[4])?new q(e,t,arguments[2],arguments[3],arguments[4]):new q(e,t,arguments[2],arguments[3]):S.isNumber(arguments[4])?new Q(e,t,{min:arguments[2],max:arguments[3],step:arguments[4]}):new Q(e,t,{min:arguments[2],max:arguments[3]}):S.isString(n)?new J(e,t):S.isFunction(n)?new Z(e,t,""):S.isBoolean(n)?new K(e,t):null},oe=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){setTimeout(e,1e3/60)},ie=function(){function e(){F(this,e),this.backgroundElement=document.createElement("div"),S.extend(this.backgroundElement.style,{backgroundColor:"rgba(0,0,0,0.8)",top:0,left:0,display:"none",zIndex:"1000",opacity:0,WebkitTransition:"opacity 0.2s linear",transition:"opacity 0.2s linear"}),X.makeFullscreen(this.backgroundElement),this.backgroundElement.style.position="fixed",this.domElement=document.createElement("div"),S.extend(this.domElement.style,{position:"fixed",display:"none",zIndex:"1001",opacity:0,WebkitTransition:"-webkit-transform 0.2s ease-out, opacity 0.2s linear",transition:"transform 0.2s ease-out, opacity 0.2s linear"}),document.body.appendChild(this.backgroundElement),document.body.appendChild(this.domElement);var t=this;X.bind(this.backgroundElement,"click",function(){t.hide()})}return P(e,[{key:"show",value:function(){var e=this;this.backgroundElement.style.display="block",this.domElement.style.display="block",this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)",this.layout(),S.defer(function(){e.backgroundElement.style.opacity=1,e.domElement.style.opacity=1,e.domElement.style.webkitTransform="scale(1)"})}},{key:"hide",value:function(){var e=this,t=function t(){e.domElement.style.display="none",e.backgroundElement.style.display="none",X.unbind(e.domElement,"webkitTransitionEnd",t),X.unbind(e.domElement,"transitionend",t),X.unbind(e.domElement,"oTransitionEnd",t)};X.bind(this.domElement,"webkitTransitionEnd",t),X.bind(this.domElement,"transitionend",t),X.bind(this.domElement,"oTransitionEnd",t),this.backgroundElement.style.opacity=0,this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)"}},{key:"layout",value:function(){this.domElement.style.left=window.innerWidth/2-X.getWidth(this.domElement)/2+"px",this.domElement.style.top=window.innerHeight/2-X.getHeight(this.domElement)/2+"px"}}]),e}(),re=function(e){if(e&&"undefined"!=typeof window){var t=document.createElement("style");return t.setAttribute("type","text/css"),t.innerHTML=e,document.head.appendChild(t),e}}(".dg ul{list-style:none;margin:0;padding:0;width:100%;clear:both}.dg.ac{position:fixed;top:0;left:0;right:0;height:0;z-index:0}.dg:not(.ac) .main{overflow:hidden}.dg.main{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear}.dg.main.taller-than-window{overflow-y:auto}.dg.main.taller-than-window .close-button{opacity:1;margin-top:-1px;border-top:1px solid #2c2c2c}.dg.main ul.closed .close-button{opacity:1 !important}.dg.main:hover .close-button,.dg.main .close-button.drag{opacity:1}.dg.main .close-button{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear;border:0;line-height:19px;height:20px;cursor:pointer;text-align:center;background-color:#000}.dg.main .close-button.close-top{position:relative}.dg.main .close-button.close-bottom{position:absolute}.dg.main .close-button:hover{background-color:#111}.dg.a{float:right;margin-right:15px;overflow-y:visible}.dg.a.has-save>ul.close-top{margin-top:0}.dg.a.has-save>ul.close-bottom{margin-top:27px}.dg.a.has-save>ul.closed{margin-top:0}.dg.a .save-row{top:0;z-index:1002}.dg.a .save-row.close-top{position:relative}.dg.a .save-row.close-bottom{position:fixed}.dg li{-webkit-transition:height .1s ease-out;-o-transition:height .1s ease-out;-moz-transition:height .1s ease-out;transition:height .1s ease-out;-webkit-transition:overflow .1s linear;-o-transition:overflow .1s linear;-moz-transition:overflow .1s linear;transition:overflow .1s linear}.dg li:not(.folder){cursor:auto;height:27px;line-height:27px;padding:0 4px 0 5px}.dg li.folder{padding:0;border-left:4px solid rgba(0,0,0,0)}.dg li.title{cursor:pointer;margin-left:-4px}.dg .closed li:not(.title),.dg .closed ul li,.dg .closed ul li>*{height:0;overflow:hidden;border:0}.dg .cr{clear:both;padding-left:3px;height:27px;overflow:hidden}.dg .property-name{cursor:default;float:left;clear:left;width:40%;overflow:hidden;text-overflow:ellipsis}.dg .c{float:left;width:60%;position:relative}.dg .c input[type=text]{border:0;margin-top:4px;padding:3px;width:100%;float:right}.dg .has-slider input[type=text]{width:30%;margin-left:0}.dg .slider{float:left;width:66%;margin-left:-5px;margin-right:0;height:19px;margin-top:4px}.dg .slider-fg{height:100%}.dg .c input[type=checkbox]{margin-top:7px}.dg .c select{margin-top:5px}.dg .cr.function,.dg .cr.function .property-name,.dg .cr.function *,.dg .cr.boolean,.dg .cr.boolean *{cursor:pointer}.dg .cr.color{overflow:visible}.dg .selector{display:none;position:absolute;margin-left:-9px;margin-top:23px;z-index:10}.dg .c:hover .selector,.dg .selector.drag{display:block}.dg li.save-row{padding:0}.dg li.save-row .button{display:inline-block;padding:0px 6px}.dg.dialogue{background-color:#222;width:460px;padding:15px;font-size:13px;line-height:15px}#dg-new-constructor{padding:10px;color:#222;font-family:Monaco, monospace;font-size:10px;border:0;resize:none;box-shadow:inset 1px 1px 1px #888;word-wrap:break-word;margin:12px 0;display:block;width:440px;overflow-y:scroll;height:100px;position:relative}#dg-local-explain{display:none;font-size:11px;line-height:17px;border-radius:3px;background-color:#333;padding:8px;margin-top:10px}#dg-local-explain code{font-size:10px}#dat-gui-save-locally{display:none}.dg{color:#eee;font:11px 'Lucida Grande', sans-serif;text-shadow:0 -1px 0 #111}.dg.main::-webkit-scrollbar{width:5px;background:#1a1a1a}.dg.main::-webkit-scrollbar-corner{height:0;display:none}.dg.main::-webkit-scrollbar-thumb{border-radius:5px;background:#676767}.dg li:not(.folder){background:#1a1a1a;border-bottom:1px solid #2c2c2c}.dg li.save-row{line-height:25px;background:#dad5cb;border:0}.dg li.save-row select{margin-left:5px;width:108px}.dg li.save-row .button{margin-left:5px;margin-top:1px;border-radius:2px;font-size:9px;line-height:7px;padding:4px 4px 5px 4px;background:#c5bdad;color:#fff;text-shadow:0 1px 0 #b0a58f;box-shadow:0 -1px 0 #b0a58f;cursor:pointer}.dg li.save-row .button.gears{background:#c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;height:7px;width:8px}.dg li.save-row .button:hover{background-color:#bab19e;box-shadow:0 -1px 0 #b0a58f}.dg li.folder{border-bottom:0}.dg li.title{padding-left:16px;background:#000 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.2)}.dg .closed li.title{background-image:url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==)}.dg .cr.boolean{border-left:3px solid #806787}.dg .cr.color{border-left:3px solid}.dg .cr.function{border-left:3px solid #e61d5f}.dg .cr.number{border-left:3px solid #2FA1D6}.dg .cr.number input[type=text]{color:#2FA1D6}.dg .cr.string{border-left:3px solid #1ed36f}.dg .cr.string input[type=text]{color:#1ed36f}.dg .cr.function:hover,.dg .cr.boolean:hover{background:#111}.dg .c input[type=text]{background:#303030;outline:none}.dg .c input[type=text]:hover{background:#3c3c3c}.dg .c input[type=text]:focus{background:#494949;color:#fff}.dg .c .slider{background:#303030;cursor:ew-resize}.dg .c .slider-fg{background:#2FA1D6;max-width:100%}.dg .c .slider:hover{background:#3c3c3c}.dg .c .slider:hover .slider-fg{background:#44abda}\n");te.inject(re);var se="Default",ae=function(){try{return!!window.localStorage}catch(e){return!1}}(),le=void 0,de=!0,ce=void 0,ue=!1,_e=[],he=function e(t){var n=this,o=t||{};this.domElement=document.createElement("div"),this.__ul=document.createElement("ul"),this.domElement.appendChild(this.__ul),X.addClass(this.domElement,"dg"),this.__folders={},this.__controllers=[],this.__rememberedObjects=[],this.__rememberedObjectIndecesToControllers=[],this.__listening=[],o=S.defaults(o,{closeOnTop:!1,autoPlace:!0,width:e.DEFAULT_WIDTH}),o=S.defaults(o,{resizable:o.autoPlace,hideable:o.autoPlace}),S.isUndefined(o.load)?o.load={preset:se}:o.preset&&(o.load.preset=o.preset),S.isUndefined(o.parent)&&o.hideable&&_e.push(this),o.resizable=S.isUndefined(o.parent)&&o.resizable,o.autoPlace&&S.isUndefined(o.scrollable)&&(o.scrollable=!0);var i=ae&&"true"===localStorage.getItem(m(this,"isLocal")),r=void 0,s=void 0;if(Object.defineProperties(this,{parent:{get:function(){return o.parent}},scrollable:{get:function(){return o.scrollable}},autoPlace:{get:function(){return o.autoPlace}},closeOnTop:{get:function(){return o.closeOnTop}},preset:{get:function(){return n.parent?n.getRoot().preset:o.load.preset},set:function(e){n.parent?n.getRoot().preset=e:o.load.preset=e,E(this),n.revert()}},width:{get:function(){return o.width},set:function(e){o.width=e,w(n,e)}},name:{get:function(){return o.name},set:function(e){o.name=e,s&&(s.innerHTML=o.name)}},closed:{get:function(){return o.closed},set:function(t){o.closed=t,o.closed?X.addClass(n.__ul,e.CLASS_CLOSED):X.removeClass(n.__ul,e.CLASS_CLOSED),this.onResize(),n.__closeButton&&(n.__closeButton.innerHTML=t?e.TEXT_OPEN:e.TEXT_CLOSED)}},load:{get:function(){return o.load}},useLocalStorage:{get:function(){return i},set:function(e){ae&&(i=e,e?X.bind(window,"unload",r):X.unbind(window,"unload",r),localStorage.setItem(m(n,"isLocal"),e))}}}),S.isUndefined(o.parent)){if(this.closed=o.closed||!1,X.addClass(this.domElement,e.CLASS_MAIN),X.makeSelectable(this.domElement,!1),ae&&i){n.useLocalStorage=!0;var a=localStorage.getItem(m(this,"gui"));a&&(o.load=JSON.parse(a))}this.__closeButton=document.createElement("div"),this.__closeButton.innerHTML=e.TEXT_CLOSED,X.addClass(this.__closeButton,e.CLASS_CLOSE_BUTTON),o.closeOnTop?(X.addClass(this.__closeButton,e.CLASS_CLOSE_TOP),this.domElement.insertBefore(this.__closeButton,this.domElement.childNodes[0])):(X.addClass(this.__closeButton,e.CLASS_CLOSE_BOTTOM),this.domElement.appendChild(this.__closeButton)),X.bind(this.__closeButton,"click",function(){n.closed=!n.closed})}else{void 0===o.closed&&(o.closed=!0);var l=document.createTextNode(o.name);X.addClass(l,"controller-name"),s=c(n,l);X.addClass(this.__ul,e.CLASS_CLOSED),X.addClass(s,"title"),X.bind(s,"click",function(e){return e.preventDefault(),n.closed=!n.closed,!1}),o.closed||(this.closed=!1)}o.autoPlace&&(S.isUndefined(o.parent)&&(de&&(ce=document.createElement("div"),X.addClass(ce,"dg"),X.addClass(ce,e.CLASS_AUTO_PLACE_CONTAINER),document.body.appendChild(ce),de=!1),ce.appendChild(this.domElement),X.addClass(this.domElement,e.CLASS_AUTO_PLACE)),this.parent||w(n,o.width)),this.__resizeHandler=function(){n.onResizeDebounced()},X.bind(window,"resize",this.__resizeHandler),X.bind(this.__ul,"webkitTransitionEnd",this.__resizeHandler),X.bind(this.__ul,"transitionend",this.__resizeHandler),X.bind(this.__ul,"oTransitionEnd",this.__resizeHandler),this.onResize(),o.resizable&&y(this),r=function(){ae&&"true"===localStorage.getItem(m(n,"isLocal"))&&localStorage.setItem(m(n,"gui"),JSON.stringify(n.getSaveObject()))},this.saveToLocalStorageIfPossible=r,o.parent||function(){var e=n.getRoot();e.width+=1,S.defer(function(){e.width-=1})}()};he.toggleHide=function(){ue=!ue,S.each(_e,function(e){e.domElement.style.display=ue?"none":""})},he.CLASS_AUTO_PLACE="a",he.CLASS_AUTO_PLACE_CONTAINER="ac",he.CLASS_MAIN="main",he.CLASS_CONTROLLER_ROW="cr",he.CLASS_TOO_TALL="taller-than-window",he.CLASS_CLOSED="closed",he.CLASS_CLOSE_BUTTON="close-button",he.CLASS_CLOSE_TOP="close-top",he.CLASS_CLOSE_BOTTOM="close-bottom",he.CLASS_DRAG="drag",he.DEFAULT_WIDTH=245,he.TEXT_CLOSED="Close Controls",he.TEXT_OPEN="Open Controls",he._keydownHandler=function(e){"text"===document.activeElement.type||72!==e.which&&72!==e.keyCode||he.toggleHide()},X.bind(window,"keydown",he._keydownHandler,!1),S.extend(he.prototype,{add:function(e,t){return f(this,e,t,{factoryArgs:Array.prototype.slice.call(arguments,2)})},addColor:function(e,t){return f(this,e,t,{color:!0})},remove:function(e){this.__ul.removeChild(e.__li),this.__controllers.splice(this.__controllers.indexOf(e),1);var t=this;S.defer(function(){t.onResize()})},destroy:function(){if(this.parent)throw new Error("Only the root GUI should be removed with .destroy(). For subfolders, use gui.removeFolder(folder) instead.");this.autoPlace&&ce.removeChild(this.domElement);var e=this;S.each(this.__folders,function(t){e.removeFolder(t)}),X.unbind(window,"keydown",he._keydownHandler,!1),u(this)},addFolder:function(e){if(void 0!==this.__folders[e])throw new Error('You already have a folder in this GUI by the name "'+e+'"');var t={name:e,parent:this};t.autoPlace=this.autoPlace,this.load&&this.load.folders&&this.load.folders[e]&&(t.closed=this.load.folders[e].closed,t.load=this.load.folders[e]);var n=new he(t);this.__folders[e]=n;var o=c(this,n.domElement);return X.addClass(o,"folder"),n},removeFolder:function(e){this.__ul.removeChild(e.domElement.parentElement),delete this.__folders[e.name],this.load&&this.load.folders&&this.load.folders[e.name]&&delete this.load.folders[e.name],u(e);var t=this;S.each(e.__folders,function(t){e.removeFolder(t)}),S.defer(function(){t.onResize()})},open:function(){this.closed=!1},close:function(){this.closed=!0},hide:function(){this.domElement.style.display="none"},show:function(){this.domElement.style.display=""},onResize:function(){var e=this.getRoot();if(e.scrollable){var t=X.getOffset(e.__ul).top,n=0;S.each(e.__ul.childNodes,function(t){e.autoPlace&&t===e.__save_row||(n+=X.getHeight(t))}),window.innerHeight-t-20GUI\'s constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n\n
\n\n
\n\n'),this.parent)throw new Error("You can only call remember on a top level GUI.");var e=this;S.each(Array.prototype.slice.call(arguments),function(t){0===e.__rememberedObjects.length&&v(e),-1===e.__rememberedObjects.indexOf(t)&&e.__rememberedObjects.push(t)}),this.autoPlace&&w(this,this.width)},getRoot:function(){for(var e=this;e.parent;)e=e.parent;return e},getSaveObject:function(){var e=this.load;return e.closed=this.closed,this.__rememberedObjects.length>0&&(e.preset=this.preset,e.remembered||(e.remembered={}),e.remembered[this.preset]=x(this)),e.folders={},S.each(this.__folders,function(t,n){e.folders[n]=t.getSaveObject()}),e},save:function(){this.load.remembered||(this.load.remembered={}),this.load.remembered[this.preset]=x(this),_(this,!1),this.saveToLocalStorageIfPossible()},saveAs:function(e){this.load.remembered||(this.load.remembered={},this.load.remembered[se]=x(this,!0)),this.load.remembered[e]=x(this),this.preset=e,g(this,e,!0),this.saveToLocalStorageIfPossible()},revert:function(e){S.each(this.__controllers,function(t){this.getRoot().load.remembered?p(e||this.getRoot(),t):t.setValue(t.initialValue),t.__onFinishChange&&t.__onFinishChange.call(t,t.getValue())},this),S.each(this.__folders,function(e){e.revert(e)}),e||_(this.getRoot(),!1)},listen:function(e){var t=0===this.__listening.length;this.__listening.push(e),t&&C(this.__listening)},updateDisplay:function(){S.each(this.__controllers,function(e){e.updateDisplay()}),S.each(this.__folders,function(e){e.updateDisplay()})}});var pe={Color:I,math:N,interpret:R},fe={Controller:z,BooleanController:K,OptionController:Y,StringController:J,NumberController:W,NumberControllerBox:Q,NumberControllerSlider:q,FunctionController:Z,ColorController:$},me={dom:X},ge={GUI:he},be=he,ve={color:pe,controllers:fe,dom:me,gui:ge,GUI:be};e.color=pe,e.controllers=fe,e.dom=me,e.gui=ge,e.GUI=be,e.default=ve,Object.defineProperty(e,"__esModule",{value:!0})}); 14 | --------------------------------------------------------------------------------