├── AntiGravityPool.html ├── LICENSE ├── README.md ├── css └── default.css ├── js ├── AntiGravityPool.js ├── InitCommon.js ├── MobileJoystickControls.js ├── PathTracingCommon.js ├── lil-gui.module.min.js ├── oimo.min.js ├── stats.module.js ├── three.core.min.js └── three.module.min.js ├── shaders ├── AntiGravityPool_Fragment.glsl ├── AntiGravityPool_Fragment_Mobile.glsl ├── ScreenCopy_Fragment.glsl ├── ScreenOutput_Fragment.glsl └── common_PathTracing_Vertex.glsl ├── sounds ├── chalk.mp3 ├── click.mp3 ├── click2.mp3 ├── cuestick.mp3 ├── ping_pong.mp3 ├── pocket.mp3 ├── rack.mp3 └── rail.mp3 └── textures └── BlueNoise_R_128.png /AntiGravityPool.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Anti-Gravity Pool (a fully path traced game) 5 | 6 | 7 | 8 | 9 | 16 | 17 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 |
35 |
Anti-Gravity Pool (a fully path traced game)
36 | 37 |
38 |
39 | Desktop: press SPACEBAR to start shot 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AntiGravity-Pool 2 | The first real-time pathtraced game for desktop and mobile using WebGL.
3 | Click to Play --> https://erichlof.github.io/AntiGravity-Pool/AntiGravityPool.html 4 |

5 | 6 |

Desktop Controls

7 | 8 | * Click anywhere to capture mouse 9 | * move Mouse to aim cueball 10 | * Mousewheel to dolly camera in or out 11 | * SPACEBAR to enter shot mode. Power will oscillate up and down 12 | * SPACEBAR again to shoot! 13 | * when shot has been made and balls are moving, WASD to fly around the scene 14 |

15 | 16 |

Mobile Controls

17 | 18 | * Swipe to aim cueball 19 | * Pinch to dolly camera in or out 20 | * small up button above directional controls to enter shot mode. Power will oscillate 21 | * small up button again to shoot! 22 | * when shot has been made and balls are moving, directional arrows to fly around the scene 23 | 24 |

TODO

25 | 26 | * Squash sound fx bug due to the physics engine continually reporting collisions between balls and between balls and rails/walls. This results in sound fx playing repeatedly until the offending ball is pocketed. This bug is highly annoying so it gets the highest priority! 27 | * Create simple banners to display game state (for example, "Player 1 Wins!") 28 | * Create widgets to display current target-ball color (red, yellow, or black), as well as shot power meter
29 | 30 |

ABOUT

31 | 32 | * To my knowledge in 2019 this is the first real-time fully path traced game for all devices with a browser, including mobile. The technology behind this simple game is a combination of my three.js path tracing [project](https://github.com/erichlof/THREE.js-PathTracing-Renderer), physics simulation through [Oimo.js](https://github.com/lo-th/Oimo.js), and the WebAudio API for sound effects. The goal of this project is enabling path traced real-time games for all players, regardless of their system specs and GPU power.
33 | -------------------------------------------------------------------------------- /css/default.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | font-family: Monospace; 5 | background-color: #000; 6 | color: #000; 7 | margin: 0px; 8 | overflow: hidden; 9 | touch-action: none; 10 | cursor: default; 11 | user-select: none; 12 | -webkit-user-select: none; 13 | } 14 | 15 | #info { 16 | position: fixed; 17 | top: 5px; 18 | width: 100%; 19 | text-align: center; 20 | color: #ffffff; 21 | cursor: default; 22 | user-select: none; 23 | } 24 | 25 | #cameraInfo { 26 | position: fixed; 27 | left: 3%; 28 | bottom: 2%; 29 | font-family: Arial; 30 | color: #ffffff; 31 | cursor: default; 32 | user-select: none; 33 | } 34 | 35 | #instructions { 36 | position: fixed; 37 | left: 3%; 38 | bottom: 1%; 39 | font-family: Arial; 40 | color: #ffffff; 41 | cursor: default; 42 | user-select: none; 43 | } 44 | 45 | .toggleButton { 46 | position:fixed; 47 | background-color: gray; 48 | border: none; 49 | color: white; 50 | top: 5px; 51 | right: 5px; 52 | padding: 10px 20px; 53 | text-align: center; 54 | text-decoration: none; 55 | font-size: 14px; 56 | margin: 4px 2px; 57 | cursor: pointer; 58 | user-select: none; 59 | z-index: 11; 60 | } 61 | #timePauseButton { 62 | top: 50px; 63 | } 64 | 65 | /* begin Web Audio user-interaction requirement for MacOS & iOS */ 66 | #overlay { 67 | position: absolute; 68 | font-size: 16px; 69 | z-index: 2; 70 | top: 0; 71 | left: 0; 72 | width: 100%; 73 | height: 100%; 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | flex-direction: column; 78 | background: rgba(0,0,0,0.7); 79 | } 80 | 81 | #overlay button { 82 | background: transparent; 83 | border: 0; 84 | border: 1px solid rgb(255, 255, 255); 85 | border-radius: 4px; 86 | color: #ffffff; 87 | padding: 12px 18px; 88 | text-transform: uppercase; 89 | cursor: pointer; 90 | } 91 | /* end Web Audio user-interaction requirement for MacOS & iOS */ 92 | -------------------------------------------------------------------------------- /js/AntiGravityPool.js: -------------------------------------------------------------------------------- 1 | // game-specific variables go here 2 | let initialCameraZ; 3 | let cameraZOffset = 0; 4 | let poolTableWalls = []; 5 | let ballObjects = []; 6 | let ballPositions = []; 7 | let pocketSounds = []; 8 | let railWallSounds = []; 9 | let ballClickSounds = []; 10 | let cueStickSound = null; 11 | let chalkSound = null; 12 | let rackSound = null; 13 | let sphereSize = 2; 14 | let pocketSize = 10; 15 | let pocketPosX = 52; 16 | let pocketPosY = 52; 17 | let pocketPosZ = 52; 18 | let sphereDensity = 1.0; 19 | let light0, light1; 20 | let aimOrigin = new THREE.Vector3(); 21 | let aimVector = new THREE.Vector3(); 22 | let frictionVector = new THREE.Vector3(); 23 | let sml = 2.2; 24 | let lrg = sml * 2; 25 | let rnd0, rnd1, rnd2; 26 | let range = 0.5; 27 | let x, y, z; 28 | let shotIsInProgress = false; 29 | let allBallsHaveStopped = true; 30 | let playerIsAiming = true; 31 | let launchGhostAimingBall = false; 32 | let canLaunchGhostAimingBall = false; 33 | let playerOneTurn = true; 34 | let playerTwoTurn = false; 35 | let willBePlayerOneTurn = false; 36 | let willBePlayerTwoTurn = false; 37 | let playerOneColor = 'undecided'; 38 | let playerTwoColor = 'undecided'; 39 | let redBallsRemaining = 7; 40 | let yellowBallsRemaining = 7; 41 | let playerOneCanShootBlackBall = false; 42 | let playerTwoCanShootBlackBall = false; 43 | let spotCueBall = false; 44 | let spotBlackBall = false; 45 | let playerOneWins = false; 46 | let playerTwoWins = false; 47 | let shouldStartNewGame = false; 48 | let isBreakShot = true; 49 | let canPlayBallSounds = false; 50 | let isShooting = false; 51 | let canPressSpacebar = false; 52 | let minShotPower = 0.2; 53 | let shotPower = minShotPower; 54 | let shotFlip = 1; 55 | 56 | // oimo physics variables 57 | let world = null; 58 | let rigidBodies = []; 59 | 60 | // WebAudio variables 61 | let audioLoader; 62 | let listener; 63 | 64 | 65 | // called automatically from within initTHREEjs() function (located in InitCommon.js file) 66 | function initSceneData() 67 | { 68 | if (!mouseControl) 69 | demoFragmentShaderFileName = 'AntiGravityPool_Fragment_Mobile.glsl'; 70 | else demoFragmentShaderFileName = 'AntiGravityPool_Fragment.glsl'; 71 | 72 | // game-specific settings and three.js variables / Oimo.js physics setup goes here 73 | sceneIsDynamic = true; 74 | 75 | allowOrthographicCamera = false; 76 | 77 | cameraFlightSpeed = 30; 78 | 79 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution) 80 | pixelRatio = mouseControl ? 1.0 : 0.75; 81 | 82 | EPS_intersect = 0.001; 83 | 84 | // we will use our own custom input handling for this game 85 | useGenericInput = false; 86 | 87 | // set camera's field of view 88 | worldCamera.fov = mouseControl ? 40 : 30; 89 | 90 | initialCameraZ = 0; // close to cueball is better 91 | 92 | for (let i = 0; i < 24; i++) 93 | { 94 | ballObjects[i] = new THREE.Object3D(); 95 | ballPositions[i] = new THREE.Vector3(); 96 | } 97 | 98 | world = new OIMO.World({timestep: 1/60, worldscale: 1} ); 99 | world.gravity = new OIMO.Vec3(0, 0, 0); 100 | 101 | audioLoader = new THREE.AudioLoader(); 102 | listener = new THREE.AudioListener(); 103 | //cameraControlsObject.add( listener ); 104 | worldCamera.add( listener ); 105 | 106 | 107 | audioLoader.load( 'sounds/pocket.mp3', function ( buffer ) 108 | { 109 | for ( let i = 16; i < 24; i ++ ) 110 | { 111 | pocketSounds[i] = new THREE.PositionalAudio( listener ); 112 | pocketSounds[i].setBuffer( buffer ); 113 | pocketSounds[i].setVolume(2); 114 | ballObjects[i].add( pocketSounds[i] ); 115 | } 116 | } ); 117 | 118 | audioLoader.load( 'sounds/rail.mp3', function ( buffer ) 119 | { 120 | for ( let i = 0; i < 16; i ++ ) 121 | { 122 | railWallSounds[i] = new THREE.PositionalAudio( listener ); 123 | railWallSounds[i].setBuffer( buffer ); 124 | railWallSounds[i].setVolume(0.3); 125 | ballObjects[i].add( railWallSounds[i] ); 126 | } 127 | } ); 128 | 129 | audioLoader.load( 'sounds/click2.mp3', function ( buffer ) 130 | { 131 | for ( let i = 0; i < 16; i ++ ) 132 | { 133 | ballClickSounds[i] = new THREE.PositionalAudio( listener ); 134 | ballClickSounds[i].setBuffer( buffer ); 135 | ballObjects[i].add( ballClickSounds[i] ); 136 | } 137 | } ); 138 | 139 | audioLoader.load( 'sounds/cuestick.mp3', function ( buffer ) 140 | { 141 | cueStickSound = new THREE.PositionalAudio( listener ); 142 | cueStickSound.setBuffer( buffer ); 143 | cueStickSound.setVolume(0.2); 144 | worldCamera.add(cueStickSound); 145 | } ); 146 | 147 | audioLoader.load( 'sounds/chalk.mp3', function ( buffer ) 148 | { 149 | chalkSound = new THREE.PositionalAudio( listener ); 150 | chalkSound.setBuffer( buffer ); 151 | chalkSound.setVolume(0.05); 152 | worldCamera.add(chalkSound); 153 | } ); 154 | 155 | audioLoader.load( 'sounds/rack.mp3', function ( buffer ) 156 | { 157 | rackSound = new THREE.PositionalAudio( listener ); 158 | rackSound.setBuffer( buffer ); 159 | rackSound.setVolume(0.2); 160 | worldCamera.add(rackSound); 161 | } ); 162 | 163 | // app/game-specific uniforms go here 164 | pathTracingUniforms.uShotIsInProgress = { value: false }; 165 | pathTracingUniforms.uBallPositions = { value: ballPositions }; 166 | 167 | 168 | startNewGame(); 169 | 170 | } // end function initSceneData() 171 | 172 | 173 | 174 | 175 | function startNewGame() 176 | { 177 | 178 | // reset all flags and variables 179 | isBreakShot = true; 180 | canPlayBallSounds = false; 181 | isShooting = false; 182 | shotPower = minShotPower; 183 | shotFlip = 1; 184 | shotIsInProgress = false; 185 | playerIsAiming = true; 186 | cameraRotationSpeed = mouseControl ? 0.5 : 0.35; 187 | launchGhostAimingBall = true; 188 | playerOneTurn = true; 189 | playerTwoTurn = false; 190 | willBePlayerOneTurn = false; 191 | willBePlayerTwoTurn = false; 192 | playerOneColor = 'undecided'; 193 | playerTwoColor = 'undecided'; 194 | redBallsRemaining = 7; 195 | yellowBallsRemaining = 7; 196 | spotCueBall = false; 197 | spotBlackBall = false; 198 | playerOneCanShootBlackBall = false; 199 | playerTwoCanShootBlackBall = false; 200 | playerOneWins = false; 201 | playerTwoWins = false; 202 | shouldStartNewGame = false; 203 | rigidBodies = []; 204 | 205 | world.clear(); 206 | 207 | poolTableWalls[0] = world.add({size:[100, 10, 100], pos:[0,-55,0], world:world, density: 1.0, friction: 0.0, restitution: 0.1}); 208 | poolTableWalls[1] = world.add({size:[100, 10, 100], pos:[0, 55,0], world:world, density: 1.0, friction: 0.0, restitution: 0.1}); 209 | poolTableWalls[2] = world.add({size:[10, 100, 100], pos:[-55,0,0], world:world, density: 1.0, friction: 0.0, restitution: 0.1}); 210 | poolTableWalls[3] = world.add({size:[10, 100, 100], pos:[ 55,0,0], world:world, density: 1.0, friction: 0.0, restitution: 0.1}); 211 | poolTableWalls[4] = world.add({size:[100, 100, 10], pos:[0,0,-55], world:world, density: 1.0, friction: 0.0, restitution: 0.1}); 212 | poolTableWalls[5] = world.add({size:[100, 100, 10], pos:[0,0, 55], world:world, density: 1.0, friction: 0.0, restitution: 0.1}); 213 | 214 | // add static balls for aiming purposes 215 | 216 | // cueball 217 | x = 0; y = 0; z = 40; 218 | aimOrigin.set(x, y, z); 219 | rigidBodies[0] = world.add({type:'sphere', name:'cueball', size:[sphereSize], pos:[x, y, z], move:true, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 220 | 221 | // camera 222 | cameraControlsObject.position.copy(rigidBodies[0].position); 223 | worldCamera.position.set(0, 0, initialCameraZ); 224 | 225 | // blackball 226 | x = 0; y = 0; z = 0; 227 | rigidBodies[1] = world.add({type:'sphere', name:'blackball', size:[sphereSize], pos:[x, y, z], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 228 | 229 | // red balls 230 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 231 | rigidBodies[2] = world.add({type:'sphere', name:'redball2', size:[sphereSize], pos:[-sml + rnd0,sml + rnd1,sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 232 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 233 | rigidBodies[3] = world.add({type:'sphere', name:'redball3', size:[sphereSize], pos:[sml + rnd0,sml + rnd1,-sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 234 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 235 | rigidBodies[4] = world.add({type:'sphere', name:'redball4', size:[sphereSize], pos:[-sml + rnd0,-sml + rnd1,-sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 236 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 237 | rigidBodies[5] = world.add({type:'sphere', name:'redball5', size:[sphereSize], pos:[sml + rnd0,-sml + rnd1,sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 238 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 239 | rigidBodies[6] = world.add({type:'sphere', name:'redball6', size:[sphereSize], pos:[0 + rnd0,lrg + rnd1,0 + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 240 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 241 | rigidBodies[7] = world.add({type:'sphere', name:'redball7', size:[sphereSize], pos:[lrg + rnd0,0 + rnd1,0 + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 242 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 243 | rigidBodies[8] = world.add({type:'sphere', name:'redball8', size:[sphereSize], pos:[0 + rnd0,0 + rnd1,-lrg + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 244 | 245 | 246 | // yellow balls 247 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 248 | rigidBodies[9] = world.add({type:'sphere', name:'yellowball9', size:[sphereSize], pos:[sml + rnd0,sml + rnd1,sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 249 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 250 | rigidBodies[10] = world.add({type:'sphere', name:'yellowball10', size:[sphereSize], pos:[-sml + rnd0,sml + rnd1,-sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 251 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 252 | rigidBodies[11] = world.add({type:'sphere', name:'yellowball11', size:[sphereSize], pos:[sml + rnd0,-sml + rnd1,-sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 253 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 254 | rigidBodies[12] = world.add({type:'sphere', name:'yellowball12', size:[sphereSize], pos:[-sml + rnd0,-sml + rnd1,sml + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 255 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 256 | rigidBodies[13] = world.add({type:'sphere', name:'yellowball13', size:[sphereSize], pos:[0 + rnd0,-lrg + rnd1,0 + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 257 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 258 | rigidBodies[14] = world.add({type:'sphere', name:'yellowball14', size:[sphereSize], pos:[-lrg + rnd0,0 + rnd1,0 + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 259 | rnd0 = THREE.MathUtils.randFloat(-range, range); rnd1 = THREE.MathUtils.randFloat(-range, range); rnd2 = THREE.MathUtils.randFloat(-range, range); 260 | rigidBodies[15] = world.add({type:'sphere', name:'yellowball15', size:[sphereSize], pos:[0 + rnd0,0 + rnd1,lrg + rnd2], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 261 | 262 | 263 | // pockets 264 | rigidBodies[16] = world.add({type:'sphere', name:'pocket0', size:[pocketSize], pos:[-pocketPosX, -pocketPosY, pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 265 | rigidBodies[17] = world.add({type:'sphere', name:'pocket1', size:[pocketSize], pos:[pocketPosX, -pocketPosY, pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 266 | rigidBodies[18] = world.add({type:'sphere', name:'pocket2', size:[pocketSize], pos:[-pocketPosX, pocketPosY, pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 267 | rigidBodies[19] = world.add({type:'sphere', name:'pocket3', size:[pocketSize], pos:[pocketPosX, pocketPosY, pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 268 | rigidBodies[20] = world.add({type:'sphere', name:'pocket4', size:[pocketSize], pos:[-pocketPosX, -pocketPosY, -pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 269 | rigidBodies[21] = world.add({type:'sphere', name:'pocket5', size:[pocketSize], pos:[pocketPosX, -pocketPosY, -pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 270 | rigidBodies[22] = world.add({type:'sphere', name:'pocket6', size:[pocketSize], pos:[-pocketPosX, pocketPosY, -pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 271 | rigidBodies[23] = world.add({type:'sphere', name:'pocket7', size:[pocketSize], pos:[pocketPosX, pocketPosY, -pocketPosZ], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 272 | 273 | if (rackSound) 274 | { 275 | if (!rackSound.isPlaying) 276 | rackSound.play(); 277 | } 278 | } // end function startNewGame() 279 | 280 | 281 | 282 | function updateOimoPhysics() 283 | { 284 | 285 | // step physics simulation forward 286 | world.step(); 287 | 288 | if (playerIsAiming) 289 | { 290 | // if player has moved the line of aim and comes to rest, send out another 291 | // ghost aiming cueball to aid in lining up the shot 292 | if (launchGhostAimingBall) 293 | { 294 | launchGhostAimingBall = false; 295 | rigidBodies[0].position.copy(aimOrigin); 296 | rigidBodies[0].linearVelocity.set(0, 0, 0); 297 | rigidBodies[0].angularVelocity.set(0, 0, 0); 298 | aimVector.copy(cameraDirectionVector).multiplyScalar(1000); 299 | rigidBodies[0].applyImpulse(rigidBodies[0].position, aimVector); 300 | } 301 | } 302 | 303 | // check for balls being pocketed 304 | if (!playerIsAiming && shotIsInProgress) 305 | { 306 | // pocket sound 307 | for (let i = 0; i < 16; i++) 308 | { 309 | for (let j = 16; j < 24; j++) 310 | { 311 | if (rigidBodies[i] != null && world.getContact(rigidBodies[i], rigidBodies[j])) 312 | { 313 | if (!pocketSounds[j].isPlaying) 314 | pocketSounds[j].play(); 315 | 316 | doGameStateLogic(i); 317 | //console.log("ball " + i + " was pocketed"); 318 | } 319 | } 320 | } 321 | 322 | // rail/wall impact sound 323 | for (let i = 0; i < 16; i++) 324 | { 325 | for (let j = 0; j < 6; j++) 326 | { 327 | if (rigidBodies[i] != null && world.getContact(rigidBodies[i], poolTableWalls[j])) 328 | { 329 | if (!railWallSounds[i].isPlaying) 330 | railWallSounds[i].play(); 331 | } 332 | } 333 | } 334 | 335 | if (!canPlayBallSounds) 336 | { 337 | for (let i = 1; i < 24; i++) 338 | { 339 | if (rigidBodies[0] != null && rigidBodies[i] != null && 340 | world.getContact(rigidBodies[0], rigidBodies[i])) 341 | { 342 | canPlayBallSounds = true; 343 | } 344 | } 345 | // for (let j = 0; j < 6; j++) 346 | // { 347 | // if (rigidBodies[0] != null && world.getContact(rigidBodies[0], poolTableWalls[j])) 348 | // { 349 | // canPlayBallSounds = true; 350 | // } 351 | // } 352 | } 353 | 354 | 355 | if (canPlayBallSounds) 356 | { 357 | // balls impact clacking sound 358 | for (let i = 0; i < 16; i++) 359 | { 360 | for (let j = 0; j < 16; j++) 361 | { 362 | if (i==j) continue; 363 | if (rigidBodies[i] != null && rigidBodies[j] != null && 364 | world.getContact(rigidBodies[i], rigidBodies[j])) 365 | { 366 | if (!ballClickSounds[i].isPlaying) 367 | ballClickSounds[i].play(); 368 | //if (!ballClickSounds[j].isPlaying) 369 | // ballClickSounds[j].play(); 370 | } 371 | } 372 | } 373 | } 374 | 375 | } 376 | 377 | // if shot has been taken and balls are moving, keep checking for all balls to come to rest 378 | if (!playerIsAiming && shotIsInProgress) 379 | { 380 | allBallsHaveStopped = true; // try to set allBallsHaveStopped flag to true 381 | 382 | for (let i = 0; i < 16; i++) 383 | { 384 | if (rigidBodies[i] == null) 385 | continue; 386 | 387 | if ( rigidBodies[i].sleeping == false ) 388 | { 389 | allBallsHaveStopped = false; // balls are still moving 390 | frictionVector.copy(rigidBodies[i].linearVelocity).negate().normalize().multiplyScalar(1.25); 391 | rigidBodies[i].applyImpulse(rigidBodies[i].position, frictionVector); 392 | } 393 | } 394 | } 395 | 396 | // if all balls have just come to rest (allBallsHaveStopped is true), switch to aiming mode 397 | if (!playerIsAiming && shotIsInProgress && allBallsHaveStopped) 398 | { 399 | if (shouldStartNewGame) 400 | { 401 | startNewGame(); 402 | return; 403 | } 404 | 405 | shotPower = minShotPower; 406 | shotFlip = 1; 407 | shotIsInProgress = false; 408 | isBreakShot = false; 409 | playerIsAiming = true; 410 | cameraRotationSpeed = mouseControl ? 0.5 : 0.35; 411 | launchGhostAimingBall = true; 412 | 413 | // no balls were pocketed, switch turns 414 | if (!willBePlayerOneTurn && !willBePlayerTwoTurn) 415 | { 416 | if (!chalkSound.isPlaying) 417 | chalkSound.play(); 418 | 419 | 420 | if (playerOneTurn) 421 | { 422 | playerOneTurn = false; 423 | playerTwoTurn = true; 424 | } 425 | else if (playerTwoTurn) 426 | { 427 | playerTwoTurn = false; 428 | playerOneTurn = true; 429 | } 430 | } 431 | else // ball or balls were pocketed 432 | { 433 | if (willBePlayerOneTurn) 434 | { 435 | if (!playerOneTurn) 436 | { 437 | if (!chalkSound.isPlaying) 438 | chalkSound.play(); 439 | } 440 | 441 | playerOneTurn = true; 442 | playerTwoTurn = false; 443 | willBePlayerOneTurn = false; // reset 444 | } 445 | else if (willBePlayerTwoTurn) 446 | { 447 | if (!playerTwoTurn) 448 | { 449 | if (!chalkSound.isPlaying) 450 | chalkSound.play(); 451 | } 452 | 453 | playerTwoTurn = true; 454 | playerOneTurn = false; 455 | willBePlayerTwoTurn = false; // reset 456 | } 457 | } 458 | 459 | // remove dynamic balls that were used for shot making and 460 | // add static balls for aiming purposes 461 | 462 | // cueball 463 | if (spotCueBall) 464 | { 465 | if (!chalkSound.isPlaying) 466 | chalkSound.play(); 467 | 468 | x = 0; y = 0; z = 40; 469 | aimOrigin.set(x, y, z); 470 | spotCueBall = false; 471 | } 472 | else 473 | { 474 | // record current position before deleting 475 | aimOrigin.copy(rigidBodies[0].position); 476 | x = rigidBodies[0].position.x; 477 | y = rigidBodies[0].position.y; 478 | z = rigidBodies[0].position.z; 479 | 480 | rigidBodies[0].remove(); 481 | rigidBodies[0] = null; 482 | } 483 | 484 | rigidBodies[0] = world.add({type:'sphere', size:[sphereSize], pos:[x,y,z], move:true, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 485 | 486 | cameraControlsObject.position.copy(rigidBodies[0].position); 487 | worldCamera.position.set(0, 0, initialCameraZ); 488 | 489 | // blackball 490 | if (spotBlackBall) 491 | { 492 | x = 0; y = 0; z = 0; 493 | spotBlackBall = false; 494 | } 495 | else 496 | { 497 | // record current position before deleting 498 | x = rigidBodies[1].position.x; 499 | y = rigidBodies[1].position.y; 500 | z = rigidBodies[1].position.z; 501 | rigidBodies[1].remove(); 502 | rigidBodies[1] = null; 503 | } 504 | rigidBodies[1] = world.add({type:'sphere', size:[sphereSize], pos:[x,y,z], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 505 | 506 | 507 | // red and yellow object balls 508 | for (let i = 2; i < 16; i++) 509 | { 510 | if (rigidBodies[i] == null) 511 | continue; 512 | // record current position before deleting 513 | x = rigidBodies[i].position.x; 514 | y = rigidBodies[i].position.y; 515 | z = rigidBodies[i].position.z; 516 | 517 | rigidBodies[i].remove(); 518 | rigidBodies[i] = null; 519 | rigidBodies[i] = world.add({type:'sphere', size:[sphereSize], pos:[x,y,z], move:false, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 520 | } 521 | 522 | } // end if (shotIsInProgress && allBallsHaveStopped) 523 | 524 | } // end function updateOimoPhysics() 525 | 526 | 527 | 528 | function doGameStateLogic(ballPocketed) 529 | { 530 | if (ballPocketed == 0) // cueball was pocketed 531 | { 532 | if (playerOneTurn) 533 | { 534 | willBePlayerOneTurn = false; 535 | willBePlayerTwoTurn = true; 536 | } 537 | else 538 | { 539 | willBePlayerTwoTurn = false; 540 | willBePlayerOneTurn = true; 541 | } 542 | 543 | spotCueBall = true; 544 | //console.log("spotCueBall = true"); 545 | } 546 | else if (ballPocketed == 1) // blackball was pocketed 547 | { 548 | if (playerOneTurn) 549 | { 550 | if (playerOneCanShootBlackBall) 551 | { 552 | playerOneWins = true; 553 | shouldStartNewGame = true; 554 | //console.log("player One wins!"); 555 | } 556 | else 557 | { 558 | spotBlackBall = true; 559 | //console.log("spotBlackBall = true"); 560 | willBePlayerOneTurn = false; 561 | willBePlayerTwoTurn = true; 562 | } 563 | } 564 | else if (playerTwoTurn) 565 | { 566 | if (playerTwoCanShootBlackBall) 567 | { 568 | playerTwoWins = true; 569 | shouldStartNewGame = true; 570 | //console.log("player Two wins!"); 571 | } 572 | else 573 | { 574 | spotBlackBall = true; 575 | //console.log("spotBlackBall = true"); 576 | willBePlayerTwoTurn = false; 577 | willBePlayerOneTurn = true; 578 | } 579 | } 580 | 581 | } 582 | else if (ballPocketed > 1) // yellow or red object ball was pocketed 583 | { 584 | if (rigidBodies[ballPocketed].name == ('redball' + ballPocketed)) 585 | { 586 | redBallsRemaining -= 1; 587 | if (redBallsRemaining == 0) 588 | { 589 | if (playerOneColor == 'red') 590 | playerOneCanShootBlackBall = true; 591 | 592 | if (playerTwoColor == 'red') 593 | playerTwoCanShootBlackBall = true; 594 | } 595 | if (playerOneTurn) 596 | { 597 | if (playerOneColor == 'red') 598 | { 599 | willBePlayerOneTurn = true; 600 | willBePlayerTwoTurn = false; 601 | } 602 | if (playerOneColor == 'yellow' && !isBreakShot) 603 | { 604 | willBePlayerOneTurn = false; 605 | willBePlayerTwoTurn = true; 606 | } 607 | if (playerOneColor == 'undecided') 608 | { 609 | playerOneColor = 'red'; 610 | playerTwoColor = 'yellow'; 611 | willBePlayerOneTurn = true; 612 | willBePlayerTwoTurn = false; 613 | } 614 | } 615 | else if (playerTwoTurn) 616 | { 617 | if (playerTwoColor == 'red') 618 | { 619 | willBePlayerTwoTurn = true; 620 | willBePlayerOneTurn = false; 621 | } 622 | if (playerTwoColor == 'yellow' && !isBreakShot) 623 | { 624 | willBePlayerTwoTurn = false; 625 | willBePlayerOneTurn = true; 626 | } 627 | if (playerTwoColor == 'undecided') 628 | { 629 | playerTwoColor = 'red'; 630 | playerOneColor = 'yellow'; 631 | willBePlayerTwoTurn = true; 632 | willBePlayerOneTurn = false; 633 | } 634 | } 635 | } 636 | else if (rigidBodies[ballPocketed].name == ('yellowball' + ballPocketed)) 637 | { 638 | yellowBallsRemaining -= 1; 639 | if (yellowBallsRemaining == 0) 640 | { 641 | if (playerOneColor == 'yellow') 642 | playerOneCanShootBlackBall = true; 643 | 644 | if (playerTwoColor == 'yellow') 645 | playerTwoCanShootBlackBall = true; 646 | } 647 | if (playerOneTurn) 648 | { 649 | if (playerOneColor == 'yellow') 650 | { 651 | willBePlayerOneTurn = true; 652 | willBePlayerTwoTurn = false; 653 | } 654 | if (playerOneColor == 'red' && !isBreakShot) 655 | { 656 | willBePlayerOneTurn = false; 657 | willBePlayerTwoTurn = true; 658 | } 659 | if (playerOneColor == 'undecided') 660 | { 661 | playerOneColor = 'yellow'; 662 | playerTwoColor = 'red'; 663 | willBePlayerOneTurn = true; 664 | willBePlayerTwoTurn = false; 665 | } 666 | } 667 | else if (playerTwoTurn) 668 | { 669 | if (playerTwoColor == 'yellow') 670 | { 671 | willBePlayerTwoTurn = true; 672 | willBePlayerOneTurn = false; 673 | } 674 | if (playerTwoColor == 'red' && !isBreakShot) 675 | { 676 | willBePlayerTwoTurn = false; 677 | willBePlayerOneTurn = true; 678 | } 679 | if (playerTwoColor == 'undecided') 680 | { 681 | playerTwoColor = 'yellow'; 682 | playerOneColor = 'red'; 683 | willBePlayerTwoTurn = true; 684 | willBePlayerOneTurn = false; 685 | } 686 | } 687 | } 688 | } 689 | 690 | // remove pocketed ball from physics bodies list and turn off rendering 691 | rigidBodies[ballPocketed].remove(); 692 | rigidBodies[ballPocketed] = null; 693 | 694 | } // end function doGameStateLogic(ballPocketed) 695 | 696 | 697 | 698 | 699 | // called automatically from within the animate() function (located in InitCommon.js file) 700 | function updateVariablesAndUniforms() 701 | { 702 | 703 | // disable horizontal pinch FOV changing on mobile 704 | increaseFOV = decreaseFOV = false; 705 | // disable vertical pinch aperture size changing on mobile 706 | increaseAperture = decreaseAperture = false; 707 | 708 | if (playerIsAiming) 709 | { 710 | if ( dollyCameraIn ) 711 | { 712 | cameraZOffset -= 1; 713 | if (cameraZOffset < -initialCameraZ) 714 | cameraZOffset = -initialCameraZ; 715 | worldCamera.position.set(0, 0, initialCameraZ + cameraZOffset); 716 | cameraIsMoving = true; 717 | dollyCameraIn = false; 718 | } 719 | if ( dollyCameraOut ) 720 | { 721 | cameraZOffset += 1; 722 | if (cameraZOffset > 200) 723 | cameraZOffset = 200; 724 | worldCamera.position.set(0, 0, initialCameraZ + cameraZOffset); 725 | cameraIsMoving = true; 726 | dollyCameraOut = false; 727 | } 728 | } 729 | 730 | if (!keyPressed('Space') && !button5Pressed && !shotIsInProgress) 731 | { 732 | canPressSpacebar = true; 733 | } 734 | if ((keyPressed('Space') || button5Pressed) && canPressSpacebar) 735 | { 736 | canPressSpacebar = false; 737 | 738 | if (!isShooting) 739 | { 740 | isShooting = true; 741 | } 742 | else if (isShooting) 743 | { 744 | if (!cueStickSound.isPlaying) 745 | cueStickSound.play(); 746 | 747 | 748 | isShooting = false; 749 | playerIsAiming = false; 750 | cameraRotationSpeed = 1; 751 | shotIsInProgress = true; 752 | cameraZOffset = 0; 753 | 754 | // remove static balls that were used for aiming and 755 | // add dynamic balls for shot making physics simulation 756 | 757 | // white cueball 758 | // record current position before deleting 759 | x = aimOrigin.x; 760 | y = aimOrigin.y; 761 | z = aimOrigin.z; 762 | rigidBodies[0].remove(); 763 | rigidBodies[0] = null; 764 | rigidBodies[0] = world.add({type:'sphere', name:'cueball', size:[sphereSize], pos:[x,y,z], move:true, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 765 | aimVector.copy(cameraDirectionVector).multiplyScalar(shotPower * 5000); 766 | rigidBodies[0].applyImpulse(rigidBodies[0].position, aimVector); 767 | 768 | cameraControlsObject.position.set(worldCamera.matrixWorld.elements[12], 769 | worldCamera.matrixWorld.elements[13], 770 | worldCamera.matrixWorld.elements[14]); 771 | worldCamera.position.set(0, 0, 0); 772 | 773 | 774 | // black ball 775 | // record current position before deleting 776 | x = rigidBodies[1].position.x; 777 | y = rigidBodies[1].position.y; 778 | z = rigidBodies[1].position.z; 779 | rigidBodies[1].remove(); 780 | rigidBodies[1] = null; 781 | rigidBodies[1] = world.add({type:'sphere', name:'blackball', size:[sphereSize], pos:[x,y,z], move:true, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 782 | 783 | // red balls 784 | for (let i = 2; i < 9; i++) 785 | { 786 | if (rigidBodies[i] == null) 787 | continue; 788 | // record current position before deleting 789 | x = rigidBodies[i].position.x; 790 | y = rigidBodies[i].position.y; 791 | z = rigidBodies[i].position.z; 792 | 793 | rigidBodies[i].remove(); 794 | rigidBodies[i] = null; 795 | rigidBodies[i] = world.add({type:'sphere', name:'redball' + i, size:[sphereSize], pos:[x,y,z], move:true, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 796 | } 797 | 798 | // yellow balls 799 | for (let i = 9; i < 16; i++) 800 | { 801 | if (rigidBodies[i] == null) 802 | continue; 803 | // record current position before deleting 804 | x = rigidBodies[i].position.x; 805 | y = rigidBodies[i].position.y; 806 | z = rigidBodies[i].position.z; 807 | 808 | rigidBodies[i].remove(); 809 | rigidBodies[i] = null; 810 | rigidBodies[i] = world.add({type:'sphere', name:'yellowball' + i, size:[sphereSize], pos:[x,y,z], move:true, world:world, density: sphereDensity, friction: 0.0, restitution: 0.9}); 811 | } 812 | 813 | } // end else if (isShooting) 814 | 815 | } // end if ((keyPressed('Space') || button5Pressed) && canPressSpacebar) 816 | 817 | 818 | if (shotIsInProgress) 819 | { 820 | // allow flying camera 821 | if ((keyPressed('KeyW') || button3Pressed) && !(keyPressed('KeyS') || button4Pressed)) 822 | { 823 | cameraControlsObject.position.add(cameraDirectionVector.multiplyScalar(cameraFlightSpeed * frameTime)); 824 | cameraIsMoving = true; 825 | } 826 | if ((keyPressed('KeyS') || button4Pressed) && !(keyPressed('KeyW') || button3Pressed)) 827 | { 828 | cameraControlsObject.position.sub(cameraDirectionVector.multiplyScalar(cameraFlightSpeed * frameTime)); 829 | cameraIsMoving = true; 830 | } 831 | if ((keyPressed('KeyA') || button1Pressed) && !(keyPressed('KeyD') || button2Pressed)) 832 | { 833 | cameraControlsObject.position.sub(cameraRightVector.multiplyScalar(cameraFlightSpeed * frameTime)); 834 | cameraIsMoving = true; 835 | } 836 | if ((keyPressed('KeyD') || button2Pressed) && !(keyPressed('KeyA') || button1Pressed)) 837 | { 838 | cameraControlsObject.position.add(cameraRightVector.multiplyScalar(cameraFlightSpeed * frameTime)); 839 | cameraIsMoving = true; 840 | } 841 | if (keyPressed('KeyQ') && !keyPressed('KeyZ')) 842 | { 843 | cameraControlsObject.position.add(cameraUpVector.multiplyScalar(cameraFlightSpeed * frameTime)); 844 | cameraIsMoving = true; 845 | } 846 | if (keyPressed('KeyZ') && !keyPressed('KeyQ')) 847 | { 848 | cameraControlsObject.position.sub(cameraUpVector.multiplyScalar(cameraFlightSpeed * frameTime)); 849 | cameraIsMoving = true; 850 | } 851 | } // end if (shotIsInProgress) 852 | 853 | 854 | if (isShooting) 855 | { 856 | shotPower += shotFlip * 0.5 * frameTime; 857 | if (shotPower > 1.0) 858 | { 859 | shotPower = 1.0; 860 | shotFlip = -1; 861 | } 862 | if (shotPower < minShotPower) 863 | { 864 | shotPower = minShotPower; 865 | shotFlip = 1; 866 | } 867 | } 868 | 869 | world.timeStep = Math.min(frameTime, 0.03333); // if frameTime takes too long, default to 1/30 sec update 870 | updateOimoPhysics(); 871 | 872 | // update pathtraced sphere ballObjects to match their physics proxy bodies 873 | for (let i = 0; i < 24; i++) 874 | { 875 | if (rigidBodies[i] == null) 876 | { 877 | ballObjects[i].position.set(10000,10000,10000); 878 | ballObjects[i].updateMatrixWorld(true); 879 | ballPositions[i].copy(ballObjects[i].position); 880 | continue; 881 | } 882 | 883 | ballObjects[i].position.copy(rigidBodies[i].getPosition()); 884 | ballObjects[i].updateMatrixWorld(true); 885 | //ballRotations[i].copy(rigidBodies[i].getQuaternion()); 886 | 887 | ballPositions[i].copy(ballObjects[i].position); 888 | } 889 | 890 | 891 | if ( !cameraIsMoving ) { 892 | if (playerIsAiming && canLaunchGhostAimingBall) 893 | { 894 | launchGhostAimingBall = true; 895 | canLaunchGhostAimingBall = false; 896 | } 897 | } 898 | 899 | if (cameraIsMoving) { 900 | canLaunchGhostAimingBall = true; 901 | launchGhostAimingBall = false; 902 | } 903 | 904 | 905 | pathTracingUniforms.uShotIsInProgress.value = shotIsInProgress; 906 | 907 | 908 | // INFO 909 | if (playerOneTurn) 910 | { 911 | if (playerOneWins) 912 | cameraInfoElement.innerHTML = "player 1 WINS!"; 913 | else if (playerOneCanShootBlackBall) 914 | cameraInfoElement.innerHTML = "player 1's turn | color: BLACK!"; 915 | else 916 | cameraInfoElement.innerHTML = "player 1's turn | color: " + playerOneColor; 917 | } 918 | 919 | if (playerTwoTurn) 920 | { 921 | if (playerTwoWins) 922 | cameraInfoElement.innerHTML = "player 2 WINS!"; 923 | else if (playerTwoCanShootBlackBall) 924 | cameraInfoElement.innerHTML = "player 2's turn | color: BLACK!"; 925 | else 926 | cameraInfoElement.innerHTML = "player 2's turn | color: " + playerTwoColor; 927 | } 928 | 929 | if (isShooting) 930 | { 931 | cameraInfoElement.innerHTML = "shotPower: " + shotPower.toFixed(1); 932 | } 933 | 934 | } // end function updateVariablesAndUniforms() 935 | 936 | 937 | // begin Web Audio user-interaction requirement for MacOS & iOS 938 | const startButton = document.getElementById('startButton'); 939 | startButton.addEventListener('click', beginInit); 940 | 941 | function beginInit() 942 | { 943 | const overlay = document.getElementById('overlay'); 944 | overlay.remove(); 945 | init(); // init app and start animating 946 | } 947 | // end Web Audio user-interaction requirement for MacOS & iOS 948 | -------------------------------------------------------------------------------- /js/InitCommon.js: -------------------------------------------------------------------------------- 1 | let SCREEN_WIDTH; 2 | let SCREEN_HEIGHT; 3 | let canvas, context; 4 | let container, stats; 5 | let controls; 6 | let pathTracingScene, screenCopyScene, screenOutputScene; 7 | let pathTracingUniforms = {}; 8 | let pathTracingUniformsGroups = []; 9 | let screenCopyUniforms, screenOutputUniforms; 10 | let pathTracingDefines; 11 | let pathTracingVertexShader, pathTracingFragmentShader; 12 | let demoFragmentShaderFileName; 13 | let screenCopyVertexShader, screenCopyFragmentShader; 14 | let screenOutputVertexShader, screenOutputFragmentShader; 15 | let triangleGeometry = new THREE.BufferGeometry(); 16 | let trianglePositions = []; 17 | let pathTracingMaterial, pathTracingMesh; 18 | let screenCopyMaterial, screenCopyMesh; 19 | let screenOutputMaterial, screenOutputMesh; 20 | let pathTracingRenderTarget, screenCopyRenderTarget; 21 | let orthoCamera, worldCamera; 22 | let renderer, clock; 23 | let frameTime, elapsedTime; 24 | let sceneIsDynamic = false; 25 | let cameraFlightSpeed = 60; 26 | let cameraRotationSpeed = 1; 27 | let fovScale; 28 | let storedFOV = 0; 29 | let increaseFOV = false; 30 | let decreaseFOV = false; 31 | let dollyCameraIn = false; 32 | let dollyCameraOut = false; 33 | let apertureSize = 0.0; 34 | let increaseAperture = false; 35 | let decreaseAperture = false; 36 | let apertureChangeSpeed = 1; 37 | let focusDistance = 132.0; 38 | let increaseFocusDist = false; 39 | let decreaseFocusDist = false; 40 | let pixelRatio = 1.0; 41 | let windowIsBeingResized = false; 42 | let TWO_PI = Math.PI * 2; 43 | let sampleCounter = 0.0; // will get increased by 1 in animation loop before rendering 44 | let frameCounter = 1.0; // 1 instead of 0 because it is used as a rng() seed in pathtracing shader 45 | let cameraIsMoving = false; 46 | let cameraRecentlyMoving = false; 47 | let isPaused = true; 48 | let inputMovementHorizontal = 0; 49 | let inputMovementVertical = 0; 50 | let oldYawRotation, oldPitchRotation; 51 | let mobileJoystickControls = null; 52 | let mobileShowButtons = true; 53 | let mobileUseDarkButtons = false; 54 | let oldDeltaX = 0; 55 | let oldDeltaY = 0; 56 | let newDeltaX = 0; 57 | let newDeltaY = 0; 58 | let mobileControlsMoveX = 0; 59 | let mobileControlsMoveY = 0; 60 | let oldPinchWidthX = 0; 61 | let oldPinchWidthY = 0; 62 | let pinchDeltaX = 0; 63 | let pinchDeltaY = 0; 64 | let useGenericInput = true; 65 | let EPS_intersect = 0.01; // default precision 66 | let textureLoader = new THREE.TextureLoader(); 67 | let blueNoiseTexture; 68 | let useToneMapping = true; 69 | let canPress_O = true; 70 | let canPress_P = true; 71 | let allowOrthographicCamera = true; 72 | let changeToOrthographicCamera = false; 73 | let changeToPerspectiveCamera = false; 74 | let pixelEdgeSharpness = 0.75; 75 | let edgeSharpenSpeed = 0.05; 76 | //let filterDecaySpeed = 0.0001; 77 | 78 | let gui; 79 | let ableToEngagePointerLock = true; 80 | let pixel_ResolutionController, pixel_ResolutionObject; 81 | let needChangePixelResolution = false; 82 | let orthographicCamera_ToggleController, orthographicCamera_ToggleObject; 83 | let currentlyUsingOrthographicCamera = false; 84 | 85 | // the following variables will be used to calculate rotations and directions from the camera 86 | let cameraDirectionVector = new THREE.Vector3(); //for moving where the camera is looking 87 | let cameraRightVector = new THREE.Vector3(); //for strafing the camera right and left 88 | let cameraUpVector = new THREE.Vector3(); //for moving camera up and down 89 | let cameraControlsObject; //for positioning and moving the camera itself 90 | let cameraControlsYawObject; //allows access to control camera's left/right movements through mobile input 91 | let cameraControlsPitchObject; //allows access to control camera's up/down movements through mobile input 92 | let PI_2 = Math.PI / 2; //used by controls below 93 | let inputRotationHorizontal = 0; 94 | let inputRotationVertical = 0; 95 | 96 | let infoElement = document.getElementById('info'); 97 | infoElement.style.cursor = "default"; 98 | infoElement.style.userSelect = "none"; 99 | infoElement.style.MozUserSelect = "none"; 100 | 101 | let cameraInfoElement = document.getElementById('cameraInfo'); 102 | cameraInfoElement.style.cursor = "default"; 103 | cameraInfoElement.style.userSelect = "none"; 104 | cameraInfoElement.style.MozUserSelect = "none"; 105 | 106 | let mouseControl = true; 107 | let pointerlockChange; 108 | let fileLoader = new THREE.FileLoader(); 109 | 110 | // The following list of keys is not exhaustive, but it should be more than enough to build interactive demos and games 111 | let KeyboardState = { 112 | KeyA: false, KeyB: false, KeyC: false, KeyD: false, KeyE: false, KeyF: false, KeyG: false, KeyH: false, KeyI: false, KeyJ: false, KeyK: false, KeyL: false, KeyM: false, 113 | KeyN: false, KeyO: false, KeyP: false, KeyQ: false, KeyR: false, KeyS: false, KeyT: false, KeyU: false, KeyV: false, KeyW: false, KeyX: false, KeyY: false, KeyZ: false, 114 | ArrowLeft: false, ArrowUp: false, ArrowRight: false, ArrowDown: false, Space: false, Enter: false, PageUp: false, PageDown: false, Tab: false, 115 | Minus: false, Equal: false, BracketLeft: false, BracketRight: false, Semicolon: false, Quote: false, Backquote: false, 116 | Comma: false, Period: false, ShiftLeft: false, ShiftRight: false, Slash: false, Backslash: false, Backspace: false, 117 | Digit1: false, Digit2: false, Digit3: false, Digit4: false, Digit5: false, Digit6: false, Digit7: false, Digit8: false, Digit9: false, Digit0: false 118 | } 119 | 120 | function onKeyDown(event) 121 | { 122 | event.preventDefault(); 123 | 124 | KeyboardState[event.code] = true; 125 | } 126 | 127 | function onKeyUp(event) 128 | { 129 | event.preventDefault(); 130 | 131 | KeyboardState[event.code] = false; 132 | } 133 | 134 | function keyPressed(keyName) 135 | { 136 | if (!mouseControl) 137 | return; 138 | 139 | return KeyboardState[keyName]; 140 | } 141 | 142 | 143 | function onMouseWheel(event) 144 | { 145 | if (isPaused) 146 | return; 147 | 148 | // use the following instead, because event.preventDefault() gives errors in console 149 | event.stopPropagation(); 150 | 151 | if (event.deltaY > 0) 152 | { 153 | increaseFOV = true; 154 | dollyCameraOut = true; 155 | } 156 | else if (event.deltaY < 0) 157 | { 158 | decreaseFOV = true; 159 | dollyCameraIn = true; 160 | } 161 | } 162 | 163 | /** 164 | * originally from https://github.com/mrdoob/three.js/blob/dev/examples/js/controls/PointerLockControls.js 165 | * @author mrdoob / http://mrdoob.com/ 166 | * 167 | * edited by Erich Loftis (erichlof on GitHub) 168 | * https://github.com/erichlof 169 | * Btw, this is the most concise and elegant way to implement first person camera rotation/movement that I've ever seen - 170 | * look at how short it is, without spaces/braces it would be around 30 lines! Way to go, mrdoob! 171 | */ 172 | 173 | function FirstPersonCameraControls(camera) 174 | { 175 | camera.rotation.set(0, 0, 0); 176 | 177 | let pitchObject = new THREE.Object3D(); 178 | pitchObject.add(camera); 179 | 180 | let yawObject = new THREE.Object3D(); 181 | yawObject.add(pitchObject); 182 | 183 | function onMouseMove(event) 184 | { 185 | if (isPaused) 186 | return; 187 | inputMovementHorizontal = event.movementX || event.mozMovementX || 0; 188 | inputMovementVertical = event.movementY || event.mozMovementY || 0; 189 | 190 | inputMovementHorizontal = -inputMovementHorizontal * 0.0012 * cameraRotationSpeed; 191 | inputMovementVertical = -inputMovementVertical * 0.001 * cameraRotationSpeed; 192 | 193 | if (inputMovementHorizontal) // prevent NaNs due to invalid mousemove data from browser 194 | inputRotationHorizontal += inputMovementHorizontal; 195 | if (inputMovementVertical) // prevent NaNs due to invalid mousemove data from browser 196 | inputRotationVertical += inputMovementVertical; 197 | // clamp the camera's vertical movement (around the x-axis) to the scene's 'ceiling' and 'floor' 198 | inputRotationVertical = Math.max(- PI_2, Math.min(PI_2, inputRotationVertical)); 199 | } 200 | 201 | document.addEventListener('mousemove', onMouseMove, false); 202 | 203 | 204 | this.getObject = function() 205 | { 206 | return yawObject; 207 | }; 208 | 209 | this.getYawObject = function() 210 | { 211 | return yawObject; 212 | }; 213 | 214 | this.getPitchObject = function() 215 | { 216 | return pitchObject; 217 | }; 218 | 219 | this.getDirection = function() 220 | { 221 | const te = pitchObject.matrixWorld.elements; 222 | 223 | return function(v) 224 | { 225 | v.set(te[8], te[9], te[10]).negate(); 226 | return v; 227 | }; 228 | }(); 229 | 230 | this.getUpVector = function() 231 | { 232 | const te = pitchObject.matrixWorld.elements; 233 | 234 | return function(v) 235 | { 236 | v.set(te[4], te[5], te[6]); 237 | return v; 238 | }; 239 | }(); 240 | 241 | this.getRightVector = function() 242 | { 243 | const te = pitchObject.matrixWorld.elements; 244 | 245 | return function(v) 246 | { 247 | v.set(te[0], te[1], te[2]); 248 | return v; 249 | }; 250 | }(); 251 | 252 | } // end function FirstPersonCameraControls(camera) 253 | 254 | 255 | function onWindowResize(event) 256 | { 257 | 258 | windowIsBeingResized = true; 259 | 260 | // the following change to document.body.clientWidth and Height works better for mobile, especially iOS 261 | // suggestion from Github user q750831855 - Thank you! 262 | SCREEN_WIDTH = document.body.clientWidth; //window.innerWidth; 263 | SCREEN_HEIGHT = document.body.clientHeight; //window.innerHeight; 264 | 265 | renderer.setPixelRatio(pixelRatio); 266 | renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); 267 | 268 | pathTracingUniforms.uResolution.value.x = context.drawingBufferWidth; 269 | pathTracingUniforms.uResolution.value.y = context.drawingBufferHeight; 270 | 271 | pathTracingRenderTarget.setSize(context.drawingBufferWidth, context.drawingBufferHeight); 272 | screenCopyRenderTarget.setSize(context.drawingBufferWidth, context.drawingBufferHeight); 273 | 274 | worldCamera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; 275 | // the following is normally used with traditional rasterized rendering, but it is not needed for our fragment shader raytraced rendering 276 | ///worldCamera.updateProjectionMatrix(); 277 | 278 | // the following scales all scene objects by the worldCamera's field of view, 279 | // taking into account the screen aspect ratio and multiplying the uniform uULen, 280 | // the x-coordinate, by this ratio 281 | fovScale = worldCamera.fov * 0.5 * (Math.PI / 180.0); 282 | pathTracingUniforms.uVLen.value = Math.tan(fovScale); 283 | pathTracingUniforms.uULen.value = pathTracingUniforms.uVLen.value * worldCamera.aspect; 284 | 285 | if (!mouseControl && mobileShowButtons) 286 | { 287 | button1Element.style.display = ""; 288 | button2Element.style.display = ""; 289 | button3Element.style.display = ""; 290 | button4Element.style.display = ""; 291 | button5Element.style.display = ""; 292 | button6Element.style.display = ""; 293 | // check if mobile device is in portrait or landscape mode and position buttons accordingly 294 | if (SCREEN_WIDTH < SCREEN_HEIGHT) 295 | { 296 | button1Element.style.right = 36 + "%"; 297 | button2Element.style.right = 2 + "%"; 298 | button3Element.style.right = 16 + "%"; 299 | button4Element.style.right = 16 + "%"; 300 | button5Element.style.right = 3 + "%"; 301 | button6Element.style.right = 3 + "%"; 302 | 303 | button1Element.style.bottom = 5 + "%"; 304 | button2Element.style.bottom = 5 + "%"; 305 | button3Element.style.bottom = 13 + "%"; 306 | button4Element.style.bottom = 2 + "%"; 307 | button5Element.style.bottom = 25 + "%"; 308 | button6Element.style.bottom = 18 + "%"; 309 | } 310 | else 311 | { 312 | button1Element.style.right = 22 + "%"; 313 | button2Element.style.right = 3 + "%"; 314 | button3Element.style.right = 11 + "%"; 315 | button4Element.style.right = 11 + "%"; 316 | button5Element.style.right = 3 + "%"; 317 | button6Element.style.right = 3 + "%"; 318 | 319 | button1Element.style.bottom = 10 + "%"; 320 | button2Element.style.bottom = 10 + "%"; 321 | button3Element.style.bottom = 26 + "%"; 322 | button4Element.style.bottom = 4 + "%"; 323 | button5Element.style.bottom = 48 + "%"; 324 | button6Element.style.bottom = 34 + "%"; 325 | } 326 | } // end if ( !mouseControl ) { 327 | 328 | } // end function onWindowResize( event ) 329 | 330 | 331 | 332 | function init() 333 | { 334 | 335 | window.addEventListener('resize', onWindowResize, false); 336 | window.addEventListener('orientationchange', onWindowResize, false); 337 | 338 | if ('ontouchstart' in window) 339 | { 340 | mouseControl = false; 341 | // if on mobile device, unpause the app because there is no ESC key and no mouse capture to do 342 | isPaused = false; 343 | 344 | ableToEngagePointerLock = true; 345 | } 346 | 347 | // default GUI elements for all demos 348 | 349 | pixel_ResolutionObject = { 350 | pixel_Resolution: 0.5 // will be set by each demo's js file 351 | } 352 | orthographicCamera_ToggleObject = { 353 | Orthographic_Camera: false 354 | } 355 | 356 | function handlePixelResolutionChange() 357 | { 358 | needChangePixelResolution = true; 359 | } 360 | function handleCameraProjectionChange() 361 | { 362 | if (!currentlyUsingOrthographicCamera) 363 | changeToOrthographicCamera = true; 364 | else if (currentlyUsingOrthographicCamera) 365 | changeToPerspectiveCamera = true; 366 | // toggle boolean flag 367 | currentlyUsingOrthographicCamera = !currentlyUsingOrthographicCamera; 368 | } 369 | 370 | 371 | gui = new GUI(); 372 | 373 | gui.domElement.style.userSelect = "none"; 374 | gui.domElement.style.MozUserSelect = "none"; 375 | 376 | 377 | if (mouseControl) // on desktop 378 | { 379 | pixel_ResolutionController = gui.add(pixel_ResolutionObject, 'pixel_Resolution', 0.5, 2.0, 0.1).onChange(handlePixelResolutionChange); 380 | 381 | gui.domElement.addEventListener("mouseenter", function (event) 382 | { 383 | ableToEngagePointerLock = false; 384 | }, false); 385 | gui.domElement.addEventListener("mouseleave", function (event) 386 | { 387 | ableToEngagePointerLock = true; 388 | }, false); 389 | 390 | window.addEventListener('wheel', onMouseWheel, false); 391 | 392 | // window.addEventListener("click", function(event) 393 | // { 394 | // event.preventDefault(); 395 | // }, false); 396 | window.addEventListener("dblclick", function (event) 397 | { 398 | event.preventDefault(); 399 | }, false); 400 | 401 | document.body.addEventListener("click", function (event) 402 | { 403 | if (!ableToEngagePointerLock) 404 | return; 405 | this.requestPointerLock = this.requestPointerLock || this.mozRequestPointerLock; 406 | this.requestPointerLock(); 407 | }, false); 408 | 409 | 410 | pointerlockChange = function (event) 411 | { 412 | if (document.pointerLockElement === document.body || 413 | document.mozPointerLockElement === document.body || document.webkitPointerLockElement === document.body) 414 | { 415 | document.addEventListener('keydown', onKeyDown, false); 416 | document.addEventListener('keyup', onKeyUp, false); 417 | isPaused = false; 418 | } 419 | else 420 | { 421 | document.removeEventListener('keydown', onKeyDown, false); 422 | document.removeEventListener('keyup', onKeyUp, false); 423 | isPaused = true; 424 | } 425 | }; 426 | 427 | // Hook pointer lock state change events 428 | document.addEventListener('pointerlockchange', pointerlockChange, false); 429 | document.addEventListener('mozpointerlockchange', pointerlockChange, false); 430 | document.addEventListener('webkitpointerlockchange', pointerlockChange, false); 431 | 432 | } // end if (mouseControl) 433 | 434 | if (!mouseControl) // on mobile 435 | { 436 | pixel_ResolutionController = gui.add(pixel_ResolutionObject, 'pixel_Resolution', 0.5, 1.0, 0.05).onChange(handlePixelResolutionChange); 437 | orthographicCamera_ToggleController = gui.add(orthographicCamera_ToggleObject, 'Orthographic_Camera', false).onChange(handleCameraProjectionChange); 438 | } 439 | 440 | 441 | /* // Fullscreen API (optional) 442 | document.addEventListener("click", function() 443 | { 444 | if ( !document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement ) 445 | { 446 | if (document.documentElement.requestFullscreen) 447 | document.documentElement.requestFullscreen(); 448 | else if (document.documentElement.mozRequestFullScreen) 449 | document.documentElement.mozRequestFullScreen(); 450 | else if (document.documentElement.webkitRequestFullscreen) 451 | document.documentElement.webkitRequestFullscreen(); 452 | } 453 | }); */ 454 | 455 | // load a resource 456 | blueNoiseTexture = textureLoader.load( 457 | // resource URL 458 | 'textures/BlueNoise_R_128.png', 459 | 460 | // onLoad callback 461 | function (texture) 462 | { 463 | texture.wrapS = THREE.RepeatWrapping; 464 | texture.wrapT = THREE.RepeatWrapping; 465 | texture.flipY = false; 466 | texture.minFilter = THREE.NearestFilter; 467 | texture.magFilter = THREE.NearestFilter; 468 | texture.generateMipmaps = false; 469 | //console.log("blue noise texture loaded"); 470 | 471 | initTHREEjs(); // boilerplate: init necessary three.js items and scene/demo-specific objects 472 | } 473 | ); 474 | 475 | 476 | } // end function init() 477 | 478 | 479 | 480 | function initTHREEjs() 481 | { 482 | 483 | canvas = document.createElement('canvas'); 484 | 485 | renderer = new THREE.WebGLRenderer({ canvas: canvas, context: canvas.getContext('webgl2') }); 486 | //suggestion: set to false for production 487 | renderer.debug.checkShaderErrors = true; 488 | 489 | renderer.autoClear = false; 490 | 491 | renderer.toneMapping = THREE.ReinhardToneMapping; 492 | 493 | //required by WebGL 2.0 for rendering to FLOAT textures 494 | context = renderer.getContext(); 495 | context.getExtension('EXT_color_buffer_float'); 496 | 497 | container = document.getElementById('container'); 498 | container.appendChild(renderer.domElement); 499 | 500 | stats = new Stats(); 501 | stats.domElement.style.position = 'absolute'; 502 | stats.domElement.style.top = '0px'; 503 | stats.domElement.style.cursor = "default"; 504 | stats.domElement.style.userSelect = "none"; 505 | stats.domElement.style.MozUserSelect = "none"; 506 | container.appendChild(stats.domElement); 507 | 508 | 509 | clock = new THREE.Clock(); 510 | 511 | pathTracingScene = new THREE.Scene(); 512 | screenCopyScene = new THREE.Scene(); 513 | screenOutputScene = new THREE.Scene(); 514 | 515 | // orthoCamera is the camera to help render the oversized full-screen triangle, which is stretched across the 516 | // screen (and a little outside the viewport). orthoCamera is an orthographic camera that sits facing the view plane, 517 | // which serves as the window into our 3d world. This camera will not move or rotate for the duration of the app. 518 | orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); 519 | screenCopyScene.add(orthoCamera); 520 | screenOutputScene.add(orthoCamera); 521 | 522 | // worldCamera is the dynamic camera 3d object that will be positioned, oriented and constantly updated inside 523 | // the 3d scene. Its view will ultimately get passed back to the stationary orthoCamera that renders 524 | // the scene to a full-screen triangle, which is stretched across the viewport. 525 | worldCamera = new THREE.PerspectiveCamera(60, document.body.clientWidth / document.body.clientHeight, 1, 1000); 526 | storedFOV = worldCamera.fov; 527 | pathTracingScene.add(worldCamera); 528 | 529 | controls = new FirstPersonCameraControls(worldCamera); 530 | 531 | cameraControlsObject = controls.getObject(); 532 | cameraControlsYawObject = controls.getYawObject(); 533 | cameraControlsPitchObject = controls.getPitchObject(); 534 | 535 | pathTracingScene.add(cameraControlsObject); 536 | 537 | 538 | // setup render targets... 539 | pathTracingRenderTarget = new THREE.WebGLRenderTarget(context.drawingBufferWidth, context.drawingBufferHeight, { 540 | minFilter: THREE.NearestFilter, 541 | magFilter: THREE.NearestFilter, 542 | format: THREE.RGBAFormat, 543 | type: THREE.FloatType, 544 | depthBuffer: false, 545 | stencilBuffer: false 546 | }); 547 | pathTracingRenderTarget.texture.generateMipmaps = false; 548 | 549 | screenCopyRenderTarget = new THREE.WebGLRenderTarget(context.drawingBufferWidth, context.drawingBufferHeight, { 550 | minFilter: THREE.NearestFilter, 551 | magFilter: THREE.NearestFilter, 552 | format: THREE.RGBAFormat, 553 | type: THREE.FloatType, 554 | depthBuffer: false, 555 | stencilBuffer: false 556 | }); 557 | screenCopyRenderTarget.texture.generateMipmaps = false; 558 | 559 | 560 | 561 | // setup scene/demo-specific objects, variables, GUI elements, and data 562 | initSceneData(); 563 | 564 | 565 | if ( !mouseControl ) 566 | { 567 | mobileJoystickControls = new MobileJoystickControls({ 568 | //showJoystick: true, 569 | showButtons: mobileShowButtons, 570 | useDarkButtons: mobileUseDarkButtons 571 | }); 572 | } 573 | 574 | pixel_ResolutionController.setValue(pixelRatio); 575 | if (!allowOrthographicCamera && !mouseControl) 576 | { 577 | orthographicCamera_ToggleController.domElement.hidden = true; 578 | orthographicCamera_ToggleController.domElement.remove(); 579 | } 580 | 581 | 582 | // setup oversized full-screen triangle geometry and shaders.... 583 | 584 | // this full-screen single triangle mesh will perform the path tracing operations, producing a screen-sized image 585 | 586 | trianglePositions.push(-1,-1, 0 ); // start in lower left corner of viewport 587 | trianglePositions.push( 3,-1, 0 ); // go beyond right side of viewport, in order to have full-screen coverage 588 | trianglePositions.push(-1, 3, 0 ); // go beyond top of viewport, in order to have full-screen coverage 589 | triangleGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( trianglePositions, 3 )); 590 | 591 | 592 | pathTracingUniforms.tPreviousTexture = { type: "t", value: screenCopyRenderTarget.texture }; 593 | pathTracingUniforms.tBlueNoiseTexture = { type: "t", value: blueNoiseTexture }; 594 | 595 | pathTracingUniforms.uCameraMatrix = { type: "m4", value: new THREE.Matrix4() }; 596 | 597 | pathTracingUniforms.uResolution = { type: "v2", value: new THREE.Vector2() }; 598 | pathTracingUniforms.uRandomVec2 = { type: "v2", value: new THREE.Vector2() }; 599 | 600 | pathTracingUniforms.uEPS_intersect = { type: "f", value: EPS_intersect }; 601 | pathTracingUniforms.uTime = { type: "f", value: 0.0 }; 602 | pathTracingUniforms.uSampleCounter = { type: "f", value: 0.0 }; //0.0 603 | pathTracingUniforms.uPreviousSampleCount = { type: "f", value: 1.0 }; 604 | pathTracingUniforms.uFrameCounter = { type: "f", value: 1.0 }; //1.0 605 | pathTracingUniforms.uULen = { type: "f", value: 1.0 }; 606 | pathTracingUniforms.uVLen = { type: "f", value: 1.0 }; 607 | pathTracingUniforms.uApertureSize = { type: "f", value: apertureSize }; 608 | pathTracingUniforms.uFocusDistance = { type: "f", value: focusDistance }; 609 | 610 | pathTracingUniforms.uCameraIsMoving = { type: "b1", value: false }; 611 | pathTracingUniforms.uUseOrthographicCamera = { type: "b1", value: false }; 612 | 613 | pathTracingDefines = { 614 | //NUMBER_OF_TRIANGLES: total_number_of_triangles 615 | }; 616 | 617 | // load vertex and fragment shader files that are used in the pathTracing material, mesh and scene 618 | fileLoader.load('shaders/common_PathTracing_Vertex.glsl', function (vertexShaderText) 619 | { 620 | pathTracingVertexShader = vertexShaderText; 621 | 622 | fileLoader.load('shaders/' + demoFragmentShaderFileName, function (fragmentShaderText) 623 | { 624 | 625 | pathTracingFragmentShader = fragmentShaderText; 626 | 627 | pathTracingMaterial = new THREE.ShaderMaterial({ 628 | uniforms: pathTracingUniforms, 629 | uniformsGroups: pathTracingUniformsGroups, 630 | defines: pathTracingDefines, 631 | vertexShader: pathTracingVertexShader, 632 | fragmentShader: pathTracingFragmentShader, 633 | depthTest: false, 634 | depthWrite: false 635 | }); 636 | 637 | pathTracingMesh = new THREE.Mesh(triangleGeometry, pathTracingMaterial); 638 | pathTracingScene.add(pathTracingMesh); 639 | 640 | // the following keeps the oversized full-screen triangle right in front 641 | // of the camera at all times. This is necessary because without it, the full-screen 642 | // triangle will fall out of view and get clipped when the camera rotates past 180 degrees. 643 | worldCamera.add(pathTracingMesh); 644 | 645 | }); 646 | }); 647 | 648 | 649 | // this oversized full-screen triangle mesh copies the image output of the pathtracing shader and feeds it back in to that shader as a 'previousTexture' 650 | 651 | screenCopyUniforms = { 652 | tPathTracedImageTexture: { type: "t", value: pathTracingRenderTarget.texture } 653 | }; 654 | 655 | fileLoader.load('shaders/ScreenCopy_Fragment.glsl', function (shaderText) 656 | { 657 | 658 | screenCopyFragmentShader = shaderText; 659 | 660 | screenCopyMaterial = new THREE.ShaderMaterial({ 661 | uniforms: screenCopyUniforms, 662 | vertexShader: pathTracingVertexShader, 663 | fragmentShader: screenCopyFragmentShader, 664 | depthWrite: false, 665 | depthTest: false 666 | }); 667 | 668 | screenCopyMesh = new THREE.Mesh(triangleGeometry, screenCopyMaterial); 669 | screenCopyScene.add(screenCopyMesh); 670 | }); 671 | 672 | 673 | // this oversized full-screen triangle mesh takes the image output of the path tracing shader (which is a continuous blend of the previous frame and current frame), 674 | // and applies gamma correction (which brightens the entire image), and then displays the final accumulated rendering to the screen 675 | 676 | screenOutputUniforms = { 677 | tPathTracedImageTexture: { type: "t", value: pathTracingRenderTarget.texture }, 678 | uSampleCounter: { type: "f", value: 0.0 }, 679 | uOneOverSampleCounter: { type: "f", value: 0.0 }, 680 | uPixelEdgeSharpness: { type: "f", value: pixelEdgeSharpness }, 681 | uEdgeSharpenSpeed: { type: "f", value: edgeSharpenSpeed }, 682 | //uFilterDecaySpeed: { type: "f", value: filterDecaySpeed }, 683 | uCameraIsMoving: { type: "b1", value: false }, 684 | uSceneIsDynamic: { type: "b1", value: sceneIsDynamic }, 685 | uUseToneMapping: { type: "b1", value: useToneMapping } 686 | }; 687 | 688 | fileLoader.load('shaders/ScreenOutput_Fragment.glsl', function (shaderText) 689 | { 690 | 691 | screenOutputFragmentShader = shaderText; 692 | 693 | screenOutputMaterial = new THREE.ShaderMaterial({ 694 | uniforms: screenOutputUniforms, 695 | vertexShader: pathTracingVertexShader, 696 | fragmentShader: screenOutputFragmentShader, 697 | depthWrite: false, 698 | depthTest: false 699 | }); 700 | 701 | screenOutputMesh = new THREE.Mesh(triangleGeometry, screenOutputMaterial); 702 | screenOutputScene.add(screenOutputMesh); 703 | }); 704 | 705 | 706 | // this 'jumpstarts' the initial dimensions and parameters for the window and renderer 707 | onWindowResize(); 708 | 709 | // everything is set up, now we can start animating 710 | animate(); 711 | 712 | } // end function initTHREEjs() 713 | 714 | 715 | 716 | 717 | function animate() 718 | { 719 | 720 | frameTime = clock.getDelta(); 721 | 722 | elapsedTime = clock.getElapsedTime() % 1000; 723 | 724 | // reset flags 725 | cameraIsMoving = false; 726 | 727 | // if GUI has been used, update 728 | if (needChangePixelResolution) 729 | { 730 | pixelRatio = pixel_ResolutionController.getValue(); 731 | onWindowResize(); 732 | needChangePixelResolution = false; 733 | } 734 | 735 | if (windowIsBeingResized) 736 | { 737 | cameraIsMoving = true; 738 | windowIsBeingResized = false; 739 | } 740 | 741 | // check user controls 742 | if (mouseControl) 743 | { 744 | // movement detected 745 | if (oldYawRotation != inputRotationHorizontal || 746 | oldPitchRotation != inputRotationVertical) 747 | { 748 | cameraIsMoving = true; 749 | } 750 | 751 | // save state for next frame 752 | oldYawRotation = inputRotationHorizontal; 753 | oldPitchRotation = inputRotationVertical; 754 | 755 | } // end if (mouseControl) 756 | 757 | // if on mobile device, get input from the mobileJoystickControls 758 | if (!mouseControl) 759 | { 760 | newDeltaX = joystickDeltaX * cameraRotationSpeed; 761 | 762 | if (newDeltaX) 763 | { 764 | cameraIsMoving = true; 765 | // the ' || 0 ' prevents NaNs from creeping into inputRotationHorizontal calc below 766 | inputMovementHorizontal = ((oldDeltaX - newDeltaX) * 0.01) || 0; 767 | // mobileJoystick X movement (left and right) affects camera rotation around the Y axis 768 | inputRotationHorizontal += inputMovementHorizontal; 769 | } 770 | 771 | newDeltaY = joystickDeltaY * cameraRotationSpeed; 772 | 773 | if (newDeltaY) 774 | { 775 | cameraIsMoving = true; 776 | // the ' || 0 ' prevents NaNs from creeping into inputRotationVertical calc below 777 | inputMovementVertical = ((oldDeltaY - newDeltaY) * 0.01) || 0; 778 | // mobileJoystick Y movement (up and down) affects camera rotation around the X axis 779 | inputRotationVertical += inputMovementVertical; 780 | } 781 | 782 | // clamp the camera's vertical movement (around the x-axis) to the scene's 'ceiling' and 'floor', 783 | // so you can't accidentally flip the camera upside down 784 | inputRotationVertical = Math.max(-PI_2, Math.min(PI_2, inputRotationVertical)); 785 | 786 | // save state for next frame 787 | oldDeltaX = newDeltaX; 788 | oldDeltaY = newDeltaY; 789 | 790 | newPinchWidthX = pinchWidthX; 791 | newPinchWidthY = pinchWidthY; 792 | pinchDeltaX = newPinchWidthX - oldPinchWidthX; 793 | pinchDeltaY = newPinchWidthY - oldPinchWidthY; 794 | 795 | if (Math.abs(pinchDeltaX) > Math.abs(pinchDeltaY)) 796 | { 797 | if (pinchDeltaX < -1) 798 | { 799 | increaseFOV = true; 800 | dollyCameraOut = true; 801 | } 802 | if (pinchDeltaX > 1) 803 | { 804 | decreaseFOV = true; 805 | dollyCameraIn = true; 806 | } 807 | } 808 | 809 | if (Math.abs(pinchDeltaY) >= Math.abs(pinchDeltaX)) 810 | { 811 | if (pinchDeltaY > 1) 812 | { 813 | increaseAperture = true; 814 | } 815 | if (pinchDeltaY < -1) 816 | { 817 | decreaseAperture = true; 818 | } 819 | } 820 | 821 | // save state for next frame 822 | oldPinchWidthX = newPinchWidthX; 823 | oldPinchWidthY = newPinchWidthY; 824 | 825 | } // end if ( !mouseControl ) 826 | 827 | 828 | cameraControlsYawObject.rotateY(inputMovementHorizontal); 829 | cameraControlsPitchObject.rotateX(inputMovementVertical); 830 | 831 | // this gives us a vector in the direction that the camera is pointing, 832 | // which will be useful for moving the camera 'forward' and shooting projectiles in that direction 833 | controls.getDirection(cameraDirectionVector); 834 | cameraDirectionVector.normalize(); 835 | controls.getUpVector(cameraUpVector); 836 | cameraUpVector.normalize(); 837 | controls.getRightVector(cameraRightVector); 838 | cameraRightVector.normalize(); 839 | 840 | 841 | if (useGenericInput) 842 | { 843 | if (!isPaused) 844 | { 845 | if ((keyPressed('KeyW') || button3Pressed) && !(keyPressed('KeyS') || button4Pressed)) 846 | { 847 | cameraControlsObject.position.add(cameraDirectionVector.multiplyScalar(cameraFlightSpeed * frameTime)); 848 | cameraIsMoving = true; 849 | } 850 | if ((keyPressed('KeyS') || button4Pressed) && !(keyPressed('KeyW') || button3Pressed)) 851 | { 852 | cameraControlsObject.position.sub(cameraDirectionVector.multiplyScalar(cameraFlightSpeed * frameTime)); 853 | cameraIsMoving = true; 854 | } 855 | if ((keyPressed('KeyA') || button1Pressed) && !(keyPressed('KeyD') || button2Pressed)) 856 | { 857 | cameraControlsObject.position.sub(cameraRightVector.multiplyScalar(cameraFlightSpeed * frameTime)); 858 | cameraIsMoving = true; 859 | } 860 | if ((keyPressed('KeyD') || button2Pressed) && !(keyPressed('KeyA') || button1Pressed)) 861 | { 862 | cameraControlsObject.position.add(cameraRightVector.multiplyScalar(cameraFlightSpeed * frameTime)); 863 | cameraIsMoving = true; 864 | } 865 | if (keyPressed('KeyQ') && !keyPressed('KeyZ')) 866 | { 867 | cameraControlsObject.position.add(cameraUpVector.multiplyScalar(cameraFlightSpeed * frameTime)); 868 | cameraIsMoving = true; 869 | } 870 | if (keyPressed('KeyZ') && !keyPressed('KeyQ')) 871 | { 872 | cameraControlsObject.position.sub(cameraUpVector.multiplyScalar(cameraFlightSpeed * frameTime)); 873 | cameraIsMoving = true; 874 | } 875 | if ((keyPressed('ArrowUp') || button5Pressed) && !(keyPressed('ArrowDown') || button6Pressed)) 876 | { 877 | increaseFocusDist = true; 878 | } 879 | if ((keyPressed('ArrowDown') || button6Pressed) && !(keyPressed('ArrowUp') || button5Pressed)) 880 | { 881 | decreaseFocusDist = true; 882 | } 883 | if (keyPressed('ArrowRight') && !keyPressed('ArrowLeft')) 884 | { 885 | increaseAperture = true; 886 | } 887 | if (keyPressed('ArrowLeft') && !keyPressed('ArrowRight')) 888 | { 889 | decreaseAperture = true; 890 | } 891 | if (keyPressed('KeyO') && canPress_O) 892 | { 893 | changeToOrthographicCamera = true; 894 | canPress_O = false; 895 | } 896 | if (!keyPressed('KeyO')) 897 | canPress_O = true; 898 | 899 | if (keyPressed('KeyP') && canPress_P) 900 | { 901 | changeToPerspectiveCamera = true; 902 | canPress_P = false; 903 | } 904 | if (!keyPressed('KeyP')) 905 | canPress_P = true; 906 | } // end if (!isPaused) 907 | 908 | } // end if (useGenericInput) 909 | 910 | 911 | 912 | // update scene/demo-specific input(if custom), variables and uniforms every animation frame 913 | updateVariablesAndUniforms(); 914 | 915 | //reset controls movement 916 | inputMovementHorizontal = inputMovementVertical = 0; 917 | 918 | 919 | if (increaseFOV) 920 | { 921 | worldCamera.fov++; 922 | if (worldCamera.fov > 179) 923 | worldCamera.fov = 179; 924 | fovScale = worldCamera.fov * 0.5 * (Math.PI / 180.0); 925 | pathTracingUniforms.uVLen.value = Math.tan(fovScale); 926 | pathTracingUniforms.uULen.value = pathTracingUniforms.uVLen.value * worldCamera.aspect; 927 | 928 | cameraIsMoving = true; 929 | increaseFOV = false; 930 | } 931 | if (decreaseFOV) 932 | { 933 | worldCamera.fov--; 934 | if (worldCamera.fov < 1) 935 | worldCamera.fov = 1; 936 | fovScale = worldCamera.fov * 0.5 * (Math.PI / 180.0); 937 | pathTracingUniforms.uVLen.value = Math.tan(fovScale); 938 | pathTracingUniforms.uULen.value = pathTracingUniforms.uVLen.value * worldCamera.aspect; 939 | 940 | cameraIsMoving = true; 941 | decreaseFOV = false; 942 | } 943 | 944 | if (increaseFocusDist) 945 | { 946 | focusDistance += 1; 947 | pathTracingUniforms.uFocusDistance.value = focusDistance; 948 | cameraIsMoving = true; 949 | increaseFocusDist = false; 950 | } 951 | if (decreaseFocusDist) 952 | { 953 | focusDistance -= 1; 954 | if (focusDistance < 1) 955 | focusDistance = 1; 956 | pathTracingUniforms.uFocusDistance.value = focusDistance; 957 | cameraIsMoving = true; 958 | decreaseFocusDist = false; 959 | } 960 | 961 | if (increaseAperture) 962 | { 963 | apertureSize += (0.1 * apertureChangeSpeed); 964 | if (apertureSize > 10000.0) 965 | apertureSize = 10000.0; 966 | 967 | cameraIsMoving = true; 968 | increaseAperture = false; 969 | } 970 | if (decreaseAperture) 971 | { 972 | apertureSize -= (0.1 * apertureChangeSpeed); 973 | if (apertureSize < 0.0) 974 | apertureSize = 0.0; 975 | 976 | cameraIsMoving = true; 977 | decreaseAperture = false; 978 | } 979 | if (allowOrthographicCamera && changeToOrthographicCamera) 980 | { 981 | storedFOV = worldCamera.fov; // save current perspective camera's FOV 982 | 983 | worldCamera.fov = 90; // good default for Ortho camera - lets user see most of the scene 984 | fovScale = worldCamera.fov * 0.5 * (Math.PI / 180.0); 985 | pathTracingUniforms.uVLen.value = Math.tan(fovScale); 986 | pathTracingUniforms.uULen.value = pathTracingUniforms.uVLen.value * worldCamera.aspect; 987 | 988 | pathTracingUniforms.uUseOrthographicCamera.value = true; 989 | cameraIsMoving = true; 990 | changeToOrthographicCamera = false; 991 | } 992 | if (allowOrthographicCamera && changeToPerspectiveCamera) 993 | { 994 | worldCamera.fov = storedFOV; // return to prior perspective camera's FOV 995 | fovScale = worldCamera.fov * 0.5 * (Math.PI / 180.0); 996 | pathTracingUniforms.uVLen.value = Math.tan(fovScale); 997 | pathTracingUniforms.uULen.value = pathTracingUniforms.uVLen.value * worldCamera.aspect; 998 | 999 | pathTracingUniforms.uUseOrthographicCamera.value = false; 1000 | cameraIsMoving = true; 1001 | changeToPerspectiveCamera = false; 1002 | } 1003 | 1004 | // now update uniforms that are common to all scenes 1005 | if (!cameraIsMoving) 1006 | { 1007 | if (sceneIsDynamic) 1008 | sampleCounter = 1.0; // reset for continuous updating of image 1009 | else sampleCounter += 1.0; // for progressive refinement of image 1010 | 1011 | frameCounter += 1.0; 1012 | 1013 | cameraRecentlyMoving = false; 1014 | } 1015 | 1016 | if (cameraIsMoving) 1017 | { 1018 | frameCounter += 1.0; 1019 | 1020 | if (!cameraRecentlyMoving) 1021 | { 1022 | // record current sampleCounter before it gets set to 1.0 below 1023 | pathTracingUniforms.uPreviousSampleCount.value = sampleCounter; 1024 | frameCounter = 1.0; 1025 | cameraRecentlyMoving = true; 1026 | } 1027 | 1028 | sampleCounter = 1.0; 1029 | } 1030 | 1031 | pathTracingUniforms.uTime.value = elapsedTime; 1032 | pathTracingUniforms.uCameraIsMoving.value = cameraIsMoving; 1033 | pathTracingUniforms.uSampleCounter.value = sampleCounter; 1034 | pathTracingUniforms.uFrameCounter.value = frameCounter; 1035 | pathTracingUniforms.uRandomVec2.value.set(Math.random(), Math.random()); 1036 | 1037 | // CAMERA 1038 | 1039 | cameraControlsObject.updateMatrixWorld(true); 1040 | worldCamera.updateMatrixWorld(true); 1041 | pathTracingUniforms.uCameraMatrix.value.copy(worldCamera.matrixWorld); 1042 | pathTracingUniforms.uApertureSize.value = apertureSize; 1043 | 1044 | screenOutputUniforms.uCameraIsMoving.value = cameraIsMoving; 1045 | screenOutputUniforms.uSampleCounter.value = sampleCounter; 1046 | // PROGRESSIVE SAMPLE WEIGHT (reduces intensity of each successive animation frame's image) 1047 | screenOutputUniforms.uOneOverSampleCounter.value = 1.0 / sampleCounter; 1048 | 1049 | 1050 | // RENDERING in 3 steps 1051 | 1052 | // STEP 1 1053 | // Perform PathTracing and Render(save) into pathTracingRenderTarget, a full-screen texture (on the oversized triangle). 1054 | // Read previous screenCopyRenderTarget(via texelFetch inside fragment shader) to use as a new starting point to blend with 1055 | renderer.setRenderTarget(pathTracingRenderTarget); 1056 | renderer.render(pathTracingScene, worldCamera); 1057 | 1058 | // STEP 2 1059 | // Render(copy) the pathTracingScene output(pathTracingRenderTarget above) into screenCopyRenderTarget. 1060 | // This will be used as a new starting point for Step 1 above (essentially creating ping-pong buffers) 1061 | renderer.setRenderTarget(screenCopyRenderTarget); 1062 | renderer.render(screenCopyScene, orthoCamera); 1063 | 1064 | // STEP 3 1065 | // Render to the oversized full-screen triangle with generated pathTracingRenderTarget in STEP 1 above. 1066 | // After applying tonemapping and gamma-correction to the image, it will be shown on the screen as the final accumulated output 1067 | renderer.setRenderTarget(null); 1068 | renderer.render(screenOutputScene, orthoCamera); 1069 | 1070 | stats.update(); 1071 | 1072 | requestAnimationFrame(animate); 1073 | 1074 | } // end function animate() 1075 | -------------------------------------------------------------------------------- /js/MobileJoystickControls.js: -------------------------------------------------------------------------------- 1 | // exposed global variables/elements that your program can access 2 | let joystickDeltaX = 0; 3 | let joystickDeltaY = 0; 4 | let pinchWidthX = 0; 5 | let pinchWidthY = 0; 6 | let button1Pressed = false; 7 | let button2Pressed = false; 8 | let button3Pressed = false; 9 | let button4Pressed = false; 10 | let button5Pressed = false; 11 | let button6Pressed = false; 12 | 13 | let stickElement = null; 14 | let baseElement = null; 15 | let button1Element = null; 16 | let button2Element = null; 17 | let button3Element = null; 18 | let button4Element = null; 19 | let button5Element = null; 20 | let button6Element = null; 21 | 22 | // the following variables marked with an underscore ( _ ) are for internal use 23 | let _touches = []; 24 | let _eventTarget; 25 | let _stickDistance; 26 | let _stickNormalizedX; 27 | let _stickNormalizedY; 28 | let _buttonCanvasWidth = 70; 29 | let _buttonCanvasReducedWidth = 50; 30 | let _buttonCanvasHalfWidth = _buttonCanvasWidth * 0.5; 31 | let _smallButtonCanvasWidth = 40; 32 | let _smallButtonCanvasReducedWidth = 28; 33 | let _smallButtonCanvasHalfWidth = _smallButtonCanvasWidth * 0.5; 34 | let _showJoystick; 35 | let _showButtons; 36 | let _useDarkButtons; 37 | let _limitStickTravel; 38 | let _stickRadius; 39 | let _baseX; 40 | let _baseY; 41 | let _stickX; 42 | let _stickY; 43 | let _container; 44 | let _pinchWasActive = false; 45 | 46 | 47 | 48 | let MobileJoystickControls = function(opts) 49 | { 50 | opts = opts || {}; 51 | // grab the options passed into this constructor function 52 | _showJoystick = opts.showJoystick || false; 53 | _showButtons = opts.showButtons || true; 54 | _useDarkButtons = opts.useDarkButtons || false; 55 | 56 | _baseX = _stickX = opts.baseX || 100; 57 | _baseY = _stickY = opts.baseY || 200; 58 | 59 | _limitStickTravel = opts.limitStickTravel || false; 60 | if (_limitStickTravel) _showJoystick = true; 61 | _stickRadius = opts.stickRadius || 50; 62 | if (_stickRadius > 100) _stickRadius = 100; 63 | 64 | 65 | _container = document.body; 66 | 67 | 68 | //create joystick Base 69 | baseElement = document.createElement('canvas'); 70 | baseElement.width = 126; 71 | baseElement.height = 126; 72 | _container.appendChild(baseElement); 73 | baseElement.style.position = "absolute"; 74 | baseElement.style.display = "none"; 75 | 76 | _Base_ctx = baseElement.getContext('2d'); 77 | _Base_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 78 | _Base_ctx.lineWidth = 2; 79 | _Base_ctx.beginPath(); 80 | _Base_ctx.arc(baseElement.width / 2, baseElement.width / 2, 40, 0, Math.PI * 2, true); 81 | _Base_ctx.stroke(); 82 | 83 | //create joystick Stick 84 | stickElement = document.createElement('canvas'); 85 | stickElement.width = 86; 86 | stickElement.height = 86; 87 | _container.appendChild(stickElement); 88 | stickElement.style.position = "absolute"; 89 | stickElement.style.display = "none"; 90 | 91 | _Stick_ctx = stickElement.getContext('2d'); 92 | _Stick_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 93 | _Stick_ctx.lineWidth = 3; 94 | _Stick_ctx.beginPath(); 95 | _Stick_ctx.arc(stickElement.width / 2, stickElement.width / 2, 30, 0, Math.PI * 2, true); 96 | _Stick_ctx.stroke(); 97 | 98 | 99 | //create button1 100 | button1Element = document.createElement('canvas'); 101 | button1Element.width = _buttonCanvasReducedWidth; // for Triangle Button 102 | //button1Element.width = _buttonCanvasWidth; // for Circle Button 103 | button1Element.height = _buttonCanvasWidth; 104 | 105 | _container.appendChild(button1Element); 106 | button1Element.style.position = "absolute"; 107 | button1Element.style.display = "none"; 108 | button1Element.style.zIndex = "10"; 109 | button1Pressed = false; 110 | 111 | _Button1_ctx = button1Element.getContext('2d'); 112 | _Button1_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 113 | _Button1_ctx.lineWidth = 3; 114 | 115 | // Triangle Button 116 | _Button1_ctx.beginPath(); 117 | _Button1_ctx.moveTo(0, _buttonCanvasHalfWidth); 118 | _Button1_ctx.lineTo(_buttonCanvasReducedWidth, _buttonCanvasWidth); 119 | _Button1_ctx.lineTo(_buttonCanvasReducedWidth, 0); 120 | _Button1_ctx.closePath(); 121 | _Button1_ctx.stroke(); 122 | 123 | /* 124 | // Circle Button 125 | _Button1_ctx.beginPath(); 126 | _Button1_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 127 | _Button1_ctx.stroke(); 128 | _Button1_ctx.lineWidth = 1; 129 | _Button1_ctx.beginPath(); 130 | _Button1_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 131 | _Button1_ctx.stroke(); 132 | */ 133 | 134 | //create button2 135 | button2Element = document.createElement('canvas'); 136 | button2Element.width = _buttonCanvasReducedWidth; // for Triangle Button 137 | //button2Element.width = _buttonCanvasWidth; // for Circle Button 138 | button2Element.height = _buttonCanvasWidth; 139 | 140 | _container.appendChild(button2Element); 141 | button2Element.style.position = "absolute"; 142 | button2Element.style.display = "none"; 143 | button2Element.style.zIndex = "10"; 144 | button2Pressed = false; 145 | 146 | _Button2_ctx = button2Element.getContext('2d'); 147 | _Button2_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 148 | _Button2_ctx.lineWidth = 3; 149 | 150 | // Triangle Button 151 | _Button2_ctx.beginPath(); 152 | _Button2_ctx.moveTo(_buttonCanvasReducedWidth, _buttonCanvasHalfWidth); 153 | _Button2_ctx.lineTo(0, 0); 154 | _Button2_ctx.lineTo(0, _buttonCanvasWidth); 155 | _Button2_ctx.closePath(); 156 | _Button2_ctx.stroke(); 157 | 158 | /* 159 | // Circle Button 160 | _Button2_ctx.beginPath(); 161 | _Button2_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 162 | _Button2_ctx.stroke(); 163 | _Button2_ctx.lineWidth = 1; 164 | _Button2_ctx.beginPath(); 165 | _Button2_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 166 | _Button2_ctx.stroke(); 167 | */ 168 | 169 | //create button3 170 | button3Element = document.createElement('canvas'); 171 | button3Element.width = _buttonCanvasWidth; 172 | button3Element.height = _buttonCanvasReducedWidth; // for Triangle Button 173 | //button3Element.height = _buttonCanvasWidth; // for Circle Button 174 | 175 | _container.appendChild(button3Element); 176 | button3Element.style.position = "absolute"; 177 | button3Element.style.display = "none"; 178 | button3Element.style.zIndex = "10"; 179 | button3Pressed = false; 180 | 181 | _Button3_ctx = button3Element.getContext('2d'); 182 | _Button3_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 183 | _Button3_ctx.lineWidth = 3; 184 | 185 | // Triangle Button 186 | _Button3_ctx.beginPath(); 187 | _Button3_ctx.moveTo(_buttonCanvasHalfWidth, 0); 188 | _Button3_ctx.lineTo(0, _buttonCanvasReducedWidth); 189 | _Button3_ctx.lineTo(_buttonCanvasWidth, _buttonCanvasReducedWidth); 190 | _Button3_ctx.closePath(); 191 | _Button3_ctx.stroke(); 192 | 193 | /* 194 | // Circle Button 195 | _Button3_ctx.beginPath(); 196 | _Button3_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 197 | _Button3_ctx.stroke(); 198 | _Button3_ctx.lineWidth = 1; 199 | _Button3_ctx.beginPath(); 200 | _Button3_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 201 | _Button3_ctx.stroke(); 202 | */ 203 | 204 | //create button4 205 | button4Element = document.createElement('canvas'); 206 | button4Element.width = _buttonCanvasWidth; 207 | button4Element.height = _buttonCanvasReducedWidth; // for Triangle Button 208 | //button4Element.height = _buttonCanvasWidth; // for Circle Button 209 | 210 | _container.appendChild(button4Element); 211 | button4Element.style.position = "absolute"; 212 | button4Element.style.display = "none"; 213 | button4Element.style.zIndex = "10"; 214 | button4Pressed = false; 215 | 216 | _Button4_ctx = button4Element.getContext('2d'); 217 | _Button4_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 218 | _Button4_ctx.lineWidth = 3; 219 | 220 | // Triangle Button 221 | _Button4_ctx.beginPath(); 222 | _Button4_ctx.moveTo(_buttonCanvasHalfWidth, _buttonCanvasReducedWidth); 223 | _Button4_ctx.lineTo(_buttonCanvasWidth, 0); 224 | _Button4_ctx.lineTo(0, 0); 225 | _Button4_ctx.closePath(); 226 | _Button4_ctx.stroke(); 227 | 228 | /* 229 | // Circle Button 230 | _Button4_ctx.beginPath(); 231 | _Button4_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 232 | _Button4_ctx.stroke(); 233 | _Button4_ctx.lineWidth = 1; 234 | _Button4_ctx.beginPath(); 235 | _Button4_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 236 | _Button4_ctx.stroke(); 237 | */ 238 | 239 | //create button5 240 | button5Element = document.createElement('canvas'); 241 | button5Element.width = _smallButtonCanvasWidth; 242 | button5Element.height = _smallButtonCanvasReducedWidth; // for Triangle Button 243 | //button5Element.height = _smallButtonCanvasWidth; // for Circle Button 244 | 245 | _container.appendChild(button5Element); 246 | button5Element.style.position = "absolute"; 247 | button5Element.style.display = "none"; 248 | button5Element.style.zIndex = "10"; 249 | button5Pressed = false; 250 | 251 | _Button5_ctx = button5Element.getContext('2d'); 252 | _Button5_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 253 | _Button5_ctx.lineWidth = 3; 254 | 255 | // Triangle Button 256 | _Button5_ctx.beginPath(); 257 | _Button5_ctx.moveTo(_smallButtonCanvasHalfWidth, 0); 258 | _Button5_ctx.lineTo(0, _smallButtonCanvasReducedWidth); 259 | _Button5_ctx.lineTo(_smallButtonCanvasWidth, _smallButtonCanvasReducedWidth); 260 | _Button5_ctx.closePath(); 261 | _Button5_ctx.stroke(); 262 | 263 | /* 264 | // Circle Button 265 | _Button5_ctx.beginPath(); 266 | _Button5_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 8, 0, Math.PI * 2); 267 | _Button5_ctx.stroke(); 268 | _Button5_ctx.lineWidth = 1; 269 | _Button5_ctx.beginPath(); 270 | _Button5_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 1, 0, Math.PI * 2); 271 | _Button5_ctx.stroke(); 272 | */ 273 | 274 | //create button6 275 | button6Element = document.createElement('canvas'); 276 | button6Element.width = _smallButtonCanvasWidth; 277 | button6Element.height = _smallButtonCanvasReducedWidth; // for Triangle Button 278 | //button6Element.height = _buttonCanvasWidth; // for Circle Button 279 | 280 | _container.appendChild(button6Element); 281 | button6Element.style.position = "absolute"; 282 | button6Element.style.display = "none"; 283 | button6Element.style.zIndex = "10"; 284 | button6Pressed = false; 285 | 286 | _Button6_ctx = button6Element.getContext('2d'); 287 | _Button6_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 288 | _Button6_ctx.lineWidth = 3; 289 | 290 | // Triangle Button 291 | _Button6_ctx.beginPath(); 292 | _Button6_ctx.moveTo(_smallButtonCanvasHalfWidth, _smallButtonCanvasReducedWidth); 293 | _Button6_ctx.lineTo(_smallButtonCanvasWidth, 0); 294 | _Button6_ctx.lineTo(0, 0); 295 | _Button6_ctx.closePath(); 296 | _Button6_ctx.stroke(); 297 | 298 | /* 299 | // Circle Button 300 | _Button6_ctx.beginPath(); 301 | _Button6_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 8, 0, Math.PI * 2); 302 | _Button6_ctx.stroke(); 303 | _Button6_ctx.lineWidth = 1; 304 | _Button6_ctx.beginPath(); 305 | _Button6_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 1, 0, Math.PI * 2); 306 | _Button6_ctx.stroke(); 307 | */ 308 | 309 | 310 | // the following listeners are for 1-finger touch detection to emulate mouse-click and mouse-drag operations 311 | _container.addEventListener('pointerdown', _onPointerDown, false); 312 | _container.addEventListener('pointermove', _onPointerMove, false); 313 | _container.addEventListener('pointerup', _onPointerUp, false); 314 | // the following listener is for 2-finger pinch gesture detection 315 | _container.addEventListener('touchmove', _onTouchMove, false); 316 | 317 | }; // end let MobileJoystickControls = function (opts) 318 | 319 | 320 | function _move(style, x, y) 321 | { 322 | style.left = x + 'px'; 323 | style.top = y + 'px'; 324 | } 325 | 326 | function _onButton1Down() 327 | { 328 | button1Pressed = true; 329 | 330 | _Button1_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.5)' : 'rgba(255,255,255,0.5)'; 331 | _Button1_ctx.lineWidth = 3; 332 | 333 | // Triangle Button 334 | _Button1_ctx.beginPath(); 335 | _Button1_ctx.moveTo(0, _buttonCanvasHalfWidth); 336 | _Button1_ctx.lineTo(_buttonCanvasReducedWidth, _buttonCanvasWidth); 337 | _Button1_ctx.lineTo(_buttonCanvasReducedWidth, 0); 338 | _Button1_ctx.closePath(); 339 | _Button1_ctx.stroke(); 340 | 341 | /* 342 | // Circle Button 343 | _Button1_ctx.beginPath(); 344 | _Button1_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 345 | _Button1_ctx.stroke(); 346 | _Button1_ctx.strokeStyle = 'rgba(255,255,255,0.2)'; 347 | _Button1_ctx.lineWidth = 1; 348 | _Button1_ctx.beginPath(); 349 | _Button1_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 350 | _Button1_ctx.stroke(); 351 | */ 352 | } 353 | 354 | function _onButton1Up() 355 | { 356 | button1Pressed = false; 357 | 358 | _Button1_ctx.clearRect(0, 0, _buttonCanvasWidth, _buttonCanvasWidth); 359 | _Button1_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 360 | _Button1_ctx.lineWidth = 3; 361 | 362 | // Triangle Button 363 | _Button1_ctx.beginPath(); 364 | _Button1_ctx.moveTo(0, _buttonCanvasHalfWidth); 365 | _Button1_ctx.lineTo(_buttonCanvasReducedWidth, _buttonCanvasWidth); 366 | _Button1_ctx.lineTo(_buttonCanvasReducedWidth, 0); 367 | _Button1_ctx.closePath(); 368 | _Button1_ctx.stroke(); 369 | 370 | /* 371 | // Circle Button 372 | _Button1_ctx.beginPath(); 373 | _Button1_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 374 | _Button1_ctx.stroke(); 375 | _Button1_ctx.lineWidth = 1; 376 | _Button1_ctx.beginPath(); 377 | _Button1_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 378 | _Button1_ctx.stroke(); 379 | */ 380 | } 381 | 382 | function _onButton2Down() 383 | { 384 | button2Pressed = true; 385 | 386 | _Button2_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.5)' : 'rgba(255,255,255,0.5)'; 387 | _Button2_ctx.lineWidth = 3; 388 | 389 | // Triangle Button 390 | _Button2_ctx.beginPath(); 391 | _Button2_ctx.moveTo(_buttonCanvasReducedWidth, _buttonCanvasHalfWidth); 392 | _Button2_ctx.lineTo(0, 0); 393 | _Button2_ctx.lineTo(0, _buttonCanvasWidth); 394 | _Button2_ctx.closePath(); 395 | _Button2_ctx.stroke(); 396 | 397 | /* 398 | // Circle Button 399 | _Button2_ctx.beginPath(); 400 | _Button2_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 401 | _Button2_ctx.stroke(); 402 | _Button2_ctx.strokeStyle = 'rgba(255,255,255,0.2)'; 403 | _Button2_ctx.lineWidth = 1; 404 | _Button2_ctx.beginPath(); 405 | _Button2_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 406 | _Button2_ctx.stroke(); 407 | */ 408 | } 409 | 410 | function _onButton2Up() 411 | { 412 | button2Pressed = false; 413 | 414 | _Button2_ctx.clearRect(0, 0, _buttonCanvasWidth, _buttonCanvasWidth); 415 | _Button2_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 416 | _Button2_ctx.lineWidth = 3; 417 | 418 | // Triangle Button 419 | _Button2_ctx.beginPath(); 420 | _Button2_ctx.moveTo(_buttonCanvasReducedWidth, _buttonCanvasHalfWidth); 421 | _Button2_ctx.lineTo(0, 0); 422 | _Button2_ctx.lineTo(0, _buttonCanvasWidth); 423 | _Button2_ctx.closePath(); 424 | _Button2_ctx.stroke(); 425 | 426 | /* 427 | // Circle Button 428 | _Button2_ctx.beginPath(); 429 | _Button2_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 430 | _Button2_ctx.stroke(); 431 | _Button2_ctx.lineWidth = 1; 432 | _Button2_ctx.beginPath(); 433 | _Button2_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 434 | _Button2_ctx.stroke(); 435 | */ 436 | } 437 | 438 | function _onButton3Down() 439 | { 440 | button3Pressed = true; 441 | 442 | _Button3_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.5)' : 'rgba(255,255,255,0.5)'; 443 | _Button3_ctx.lineWidth = 3; 444 | 445 | // Triangle Button 446 | _Button3_ctx.beginPath(); 447 | _Button3_ctx.moveTo(_buttonCanvasHalfWidth, 0); 448 | _Button3_ctx.lineTo(0, _buttonCanvasReducedWidth); 449 | _Button3_ctx.lineTo(_buttonCanvasWidth, _buttonCanvasReducedWidth); 450 | _Button3_ctx.closePath(); 451 | _Button3_ctx.stroke(); 452 | 453 | /* 454 | // Circle Button 455 | _Button3_ctx.beginPath(); 456 | _Button3_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 457 | _Button3_ctx.stroke(); 458 | _Button3_ctx.strokeStyle = 'rgba(255,255,255,0.2)'; 459 | _Button3_ctx.lineWidth = 1; 460 | _Button3_ctx.beginPath(); 461 | _Button3_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 462 | _Button3_ctx.stroke(); 463 | */ 464 | } 465 | 466 | function _onButton3Up() 467 | { 468 | button3Pressed = false; 469 | 470 | _Button3_ctx.clearRect(0, 0, _buttonCanvasWidth, _buttonCanvasWidth); 471 | _Button3_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 472 | _Button3_ctx.lineWidth = 3; 473 | 474 | // Triangle Button 475 | _Button3_ctx.beginPath(); 476 | _Button3_ctx.moveTo(_buttonCanvasHalfWidth, 0); 477 | _Button3_ctx.lineTo(0, _buttonCanvasReducedWidth); 478 | _Button3_ctx.lineTo(_buttonCanvasWidth, _buttonCanvasReducedWidth); 479 | _Button3_ctx.closePath(); 480 | _Button3_ctx.stroke(); 481 | 482 | /* 483 | // Circle Button 484 | _Button3_ctx.beginPath(); 485 | _Button3_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 486 | _Button3_ctx.stroke(); 487 | _Button3_ctx.lineWidth = 1; 488 | _Button3_ctx.beginPath(); 489 | _Button3_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 490 | _Button3_ctx.stroke(); 491 | */ 492 | } 493 | 494 | function _onButton4Down() 495 | { 496 | button4Pressed = true; 497 | 498 | _Button4_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.5)' : 'rgba(255,255,255,0.5)'; 499 | _Button4_ctx.lineWidth = 3; 500 | 501 | // Triangle Button 502 | _Button4_ctx.beginPath(); 503 | _Button4_ctx.moveTo(_buttonCanvasHalfWidth, _buttonCanvasReducedWidth); 504 | _Button4_ctx.lineTo(_buttonCanvasWidth, 0); 505 | _Button4_ctx.lineTo(0, 0); 506 | _Button4_ctx.closePath(); 507 | _Button4_ctx.stroke(); 508 | 509 | /* 510 | // Circle Button 511 | _Button4_ctx.beginPath(); 512 | _Button4_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 513 | _Button4_ctx.stroke(); 514 | _Button4_ctx.strokeStyle = 'rgba(255,255,255,0.2)'; 515 | _Button4_ctx.lineWidth = 1; 516 | _Button4_ctx.beginPath(); 517 | _Button4_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 518 | _Button4_ctx.stroke(); 519 | */ 520 | } 521 | 522 | function _onButton4Up() 523 | { 524 | button4Pressed = false; 525 | 526 | _Button4_ctx.clearRect(0, 0, _buttonCanvasWidth, _buttonCanvasWidth); 527 | _Button4_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 528 | _Button4_ctx.lineWidth = 3; 529 | 530 | // Triangle Button 531 | _Button4_ctx.beginPath(); 532 | _Button4_ctx.moveTo(_buttonCanvasHalfWidth, _buttonCanvasReducedWidth); 533 | _Button4_ctx.lineTo(_buttonCanvasWidth, 0); 534 | _Button4_ctx.lineTo(0, 0); 535 | _Button4_ctx.closePath(); 536 | _Button4_ctx.stroke(); 537 | 538 | /* 539 | // Circle Button 540 | _Button4_ctx.beginPath(); 541 | _Button4_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 8, 0, Math.PI * 2); 542 | _Button4_ctx.stroke(); 543 | _Button4_ctx.lineWidth = 1; 544 | _Button4_ctx.beginPath(); 545 | _Button4_ctx.arc(_buttonCanvasHalfWidth, _buttonCanvasHalfWidth, _buttonCanvasHalfWidth - 1, 0, Math.PI * 2); 546 | _Button4_ctx.stroke(); 547 | */ 548 | } 549 | 550 | function _onButton5Down() 551 | { 552 | button5Pressed = true; 553 | 554 | _Button5_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.5)' : 'rgba(255,255,255,0.5)'; 555 | _Button5_ctx.lineWidth = 3; 556 | 557 | // Triangle Button 558 | _Button5_ctx.beginPath(); 559 | _Button5_ctx.moveTo(_smallButtonCanvasHalfWidth, 0); 560 | _Button5_ctx.lineTo(0, _smallButtonCanvasReducedWidth); 561 | _Button5_ctx.lineTo(_smallButtonCanvasWidth, _smallButtonCanvasReducedWidth); 562 | _Button5_ctx.closePath(); 563 | _Button5_ctx.stroke(); 564 | 565 | /* 566 | // Circle Button 567 | _Button5_ctx.beginPath(); 568 | _Button5_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 8, 0, Math.PI * 2); 569 | _Button5_ctx.stroke(); 570 | _Button5_ctx.lineWidth = 1; 571 | _Button5_ctx.beginPath(); 572 | _Button5_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 1, 0, Math.PI * 2); 573 | _Button5_ctx.stroke(); 574 | */ 575 | } 576 | 577 | function _onButton5Up() 578 | { 579 | button5Pressed = false; 580 | 581 | _Button5_ctx.clearRect(0, 0, _smallButtonCanvasWidth, _smallButtonCanvasWidth); 582 | _Button5_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 583 | _Button5_ctx.lineWidth = 3; 584 | 585 | // Triangle Button 586 | _Button5_ctx.beginPath(); 587 | _Button5_ctx.moveTo(_smallButtonCanvasHalfWidth, 0); 588 | _Button5_ctx.lineTo(0, _smallButtonCanvasReducedWidth); 589 | _Button5_ctx.lineTo(_smallButtonCanvasWidth, _smallButtonCanvasReducedWidth); 590 | _Button5_ctx.closePath(); 591 | _Button5_ctx.stroke(); 592 | 593 | /* 594 | // Circle Button 595 | _Button5_ctx.beginPath(); 596 | _Button5_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 8, 0, Math.PI * 2); 597 | _Button5_ctx.stroke(); 598 | _Button5_ctx.lineWidth = 1; 599 | _Button5_ctx.beginPath(); 600 | _Button5_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 1, 0, Math.PI * 2); 601 | _Button5_ctx.stroke(); 602 | */ 603 | } 604 | 605 | function _onButton6Down() 606 | { 607 | button6Pressed = true; 608 | 609 | _Button6_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.5)' : 'rgba(255,255,255,0.5)'; 610 | _Button6_ctx.lineWidth = 3; 611 | 612 | // Triangle Button 613 | _Button6_ctx.beginPath(); 614 | _Button6_ctx.moveTo(_smallButtonCanvasHalfWidth, _smallButtonCanvasReducedWidth); 615 | _Button6_ctx.lineTo(_smallButtonCanvasWidth, 0); 616 | _Button6_ctx.lineTo(0, 0); 617 | _Button6_ctx.closePath(); 618 | _Button6_ctx.stroke(); 619 | 620 | /* 621 | // Circle Button 622 | _Button6_ctx.beginPath(); 623 | _Button6_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 8, 0, Math.PI * 2); 624 | _Button6_ctx.stroke(); 625 | _Button6_ctx.lineWidth = 1; 626 | _Button6_ctx.beginPath(); 627 | _Button6_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 1, 0, Math.PI * 2); 628 | _Button6_ctx.stroke(); 629 | */ 630 | } 631 | 632 | function _onButton6Up() 633 | { 634 | button6Pressed = false; 635 | 636 | _Button6_ctx.clearRect(0, 0, _smallButtonCanvasWidth, _smallButtonCanvasWidth); 637 | _Button6_ctx.strokeStyle = _useDarkButtons ? 'rgba(55,55,55,0.2)' : 'rgba(255,255,255,0.2)'; 638 | _Button6_ctx.lineWidth = 3; 639 | 640 | // Triangle Button 641 | _Button6_ctx.beginPath(); 642 | _Button6_ctx.moveTo(_smallButtonCanvasHalfWidth, _smallButtonCanvasReducedWidth); 643 | _Button6_ctx.lineTo(_smallButtonCanvasWidth, 0); 644 | _Button6_ctx.lineTo(0, 0); 645 | _Button6_ctx.closePath(); 646 | _Button6_ctx.stroke(); 647 | 648 | /* 649 | // Circle Button 650 | _Button6_ctx.beginPath(); 651 | _Button6_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 8, 0, Math.PI * 2); 652 | _Button6_ctx.stroke(); 653 | _Button6_ctx.lineWidth = 1; 654 | _Button6_ctx.beginPath(); 655 | _Button6_ctx.arc(_smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth, _smallButtonCanvasHalfWidth - 1, 0, Math.PI * 2); 656 | _Button6_ctx.stroke(); 657 | */ 658 | } 659 | 660 | 661 | function _onPointerDown(event) 662 | { 663 | 664 | _eventTarget = event.target; 665 | 666 | if (_showButtons) 667 | { 668 | if (_eventTarget == button1Element) 669 | return _onButton1Down(); 670 | if (_eventTarget == button2Element) 671 | return _onButton2Down(); 672 | if (_eventTarget == button3Element) 673 | return _onButton3Down(); 674 | if (_eventTarget == button4Element) 675 | return _onButton4Down(); 676 | if (_eventTarget == button5Element) 677 | return _onButton5Down(); 678 | if (_eventTarget == button6Element) 679 | return _onButton6Down(); 680 | } 681 | 682 | if (_eventTarget != renderer.domElement) // target was the GUI menu 683 | return; 684 | 685 | // else target is the joystick area 686 | _stickX = event.clientX; 687 | _stickY = event.clientY; 688 | 689 | _baseX = _stickX; 690 | _baseY = _stickY; 691 | 692 | joystickDeltaX = joystickDeltaY = 0; 693 | 694 | } // end function _onPointerDown(event) 695 | 696 | 697 | function _onPointerMove(event) 698 | { 699 | 700 | _eventTarget = event.target; 701 | 702 | if (_eventTarget != renderer.domElement) // target was the GUI menu or Buttons 703 | return; 704 | 705 | _stickX = event.clientX; 706 | _stickY = event.clientY; 707 | 708 | joystickDeltaX = _stickX - _baseX; 709 | joystickDeltaY = _stickY - _baseY; 710 | 711 | if (_limitStickTravel) 712 | { 713 | _stickDistance = Math.sqrt((joystickDeltaX * joystickDeltaX) + (joystickDeltaY * joystickDeltaY)); 714 | 715 | if (_stickDistance > _stickRadius) 716 | { 717 | _stickNormalizedX = joystickDeltaX / _stickDistance; 718 | _stickNormalizedY = joystickDeltaY / _stickDistance; 719 | 720 | _stickX = _stickNormalizedX * _stickRadius + _baseX; 721 | _stickY = _stickNormalizedY * _stickRadius + _baseY; 722 | 723 | joystickDeltaX = _stickX - _baseX; 724 | joystickDeltaY = _stickY - _baseY; 725 | } 726 | } 727 | 728 | if (_pinchWasActive) 729 | { 730 | _pinchWasActive = false; 731 | 732 | _baseX = event.clientX; 733 | _baseY = event.clientY; 734 | 735 | _stickX = _baseX; 736 | _stickY = _baseY; 737 | 738 | joystickDeltaX = joystickDeltaY = 0; 739 | } 740 | 741 | if (_showJoystick) 742 | { 743 | stickElement.style.display = ""; 744 | _move(baseElement.style, (_baseX - baseElement.width / 2), (_baseY - baseElement.height / 2)); 745 | 746 | baseElement.style.display = ""; 747 | _move(stickElement.style, (_stickX - stickElement.width / 2), (_stickY - stickElement.height / 2)); 748 | } 749 | 750 | } // end function _onPointerMove(event) 751 | 752 | 753 | function _onPointerUp(event) 754 | { 755 | 756 | _eventTarget = event.target; 757 | 758 | if (_showButtons) 759 | { 760 | if (_eventTarget == button1Element) 761 | return _onButton1Up(); 762 | if (_eventTarget == button2Element) 763 | return _onButton2Up(); 764 | if (_eventTarget == button3Element) 765 | return _onButton3Up(); 766 | if (_eventTarget == button4Element) 767 | return _onButton4Up(); 768 | if (_eventTarget == button5Element) 769 | return _onButton5Up(); 770 | if (_eventTarget == button6Element) 771 | return _onButton6Up(); 772 | } 773 | 774 | if (_eventTarget != renderer.domElement) // target was the GUI menu 775 | return; 776 | 777 | joystickDeltaX = joystickDeltaY = 0; 778 | 779 | baseElement.style.display = "none"; 780 | stickElement.style.display = "none"; 781 | 782 | } // end function _onPointerUp(event) 783 | 784 | 785 | function _onTouchMove(event) 786 | { 787 | // we only want to deal with a 2-finger pinch 788 | if (event.touches.length != 2) 789 | return; 790 | 791 | _touches = event.touches; 792 | 793 | if ( (!_showButtons) || // if no show buttons, there's no need to do the following checks: 794 | (_touches[0].target != button1Element && _touches[0].target != button2Element && 795 | _touches[0].target != button3Element && _touches[0].target != button4Element && 796 | _touches[0].target != button5Element && _touches[0].target != button6Element && 797 | _touches[1].target != button1Element && _touches[1].target != button2Element && 798 | _touches[1].target != button3Element && _touches[1].target != button4Element && 799 | _touches[1].target != button5Element && _touches[1].target != button6Element) ) 800 | { 801 | pinchWidthX = Math.abs(_touches[1].pageX - _touches[0].pageX); 802 | pinchWidthY = Math.abs(_touches[1].pageY - _touches[0].pageY); 803 | 804 | _stickX = _baseX; 805 | _stickY = _baseY; 806 | 807 | joystickDeltaX = joystickDeltaY = 0; 808 | 809 | _pinchWasActive = true; 810 | 811 | baseElement.style.display = "none"; 812 | stickElement.style.display = "none"; 813 | } 814 | 815 | } // end function _onTouchMove(event) 816 | -------------------------------------------------------------------------------- /js/lil-gui.module.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lil-gui 3 | * https://lil-gui.georgealways.com 4 | * @version 0.17.0 5 | * @author George Michael Brower 6 | * @license MIT 7 | */ 8 | class t{constructor(i,e,s,n,l="div"){this.parent=i,this.object=e,this.property=s,this._disabled=!1,this._hidden=!1,this.initialValue=this.getValue(),this.domElement=document.createElement("div"),this.domElement.classList.add("controller"),this.domElement.classList.add(n),this.$name=document.createElement("div"),this.$name.classList.add("name"),t.nextNameID=t.nextNameID||0,this.$name.id="lil-gui-name-"+ ++t.nextNameID,this.$widget=document.createElement(l),this.$widget.classList.add("widget"),this.$disable=this.$widget,this.domElement.appendChild(this.$name),this.domElement.appendChild(this.$widget),this.parent.children.push(this),this.parent.controllers.push(this),this.parent.$children.appendChild(this.domElement),this._listenCallback=this._listenCallback.bind(this),this.name(s)}name(t){return this._name=t,this.$name.innerHTML=t,this}onChange(t){return this._onChange=t,this}_callOnChange(){this.parent._callOnChange(this),void 0!==this._onChange&&this._onChange.call(this,this.getValue()),this._changed=!0}onFinishChange(t){return this._onFinishChange=t,this}_callOnFinishChange(){this._changed&&(this.parent._callOnFinishChange(this),void 0!==this._onFinishChange&&this._onFinishChange.call(this,this.getValue())),this._changed=!1}reset(){return this.setValue(this.initialValue),this._callOnFinishChange(),this}enable(t=!0){return this.disable(!t)}disable(t=!0){return t===this._disabled||(this._disabled=t,this.domElement.classList.toggle("disabled",t),this.$disable.toggleAttribute("disabled",t)),this}show(t=!0){return this._hidden=!t,this.domElement.style.display=this._hidden?"none":"",this}hide(){return this.show(!1)}options(t){const i=this.parent.add(this.object,this.property,t);return i.name(this._name),this.destroy(),i}min(t){return this}max(t){return this}step(t){return this}decimals(t){return this}listen(t=!0){return this._listening=t,void 0!==this._listenCallbackID&&(cancelAnimationFrame(this._listenCallbackID),this._listenCallbackID=void 0),this._listening&&this._listenCallback(),this}_listenCallback(){this._listenCallbackID=requestAnimationFrame(this._listenCallback);const t=this.save();t!==this._listenPrevValue&&this.updateDisplay(),this._listenPrevValue=t}getValue(){return this.object[this.property]}setValue(t){return this.object[this.property]=t,this._callOnChange(),this.updateDisplay(),this}updateDisplay(){return this}load(t){return this.setValue(t),this._callOnFinishChange(),this}save(){return this.getValue()}destroy(){this.listen(!1),this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent.controllers.splice(this.parent.controllers.indexOf(this),1),this.parent.$children.removeChild(this.domElement)}}class i extends t{constructor(t,i,e){super(t,i,e,"boolean","label"),this.$input=document.createElement("input"),this.$input.setAttribute("type","checkbox"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$widget.appendChild(this.$input),this.$input.addEventListener("change",()=>{this.setValue(this.$input.checked),this._callOnFinishChange()}),this.$disable=this.$input,this.updateDisplay()}updateDisplay(){return this.$input.checked=this.getValue(),this}}function e(t){let i,e;return(i=t.match(/(#|0x)?([a-f0-9]{6})/i))?e=i[2]:(i=t.match(/rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/))?e=parseInt(i[1]).toString(16).padStart(2,0)+parseInt(i[2]).toString(16).padStart(2,0)+parseInt(i[3]).toString(16).padStart(2,0):(i=t.match(/^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i))&&(e=i[1]+i[1]+i[2]+i[2]+i[3]+i[3]),!!e&&"#"+e}const s={isPrimitive:!0,match:t=>"string"==typeof t,fromHexString:e,toHexString:e},n={isPrimitive:!0,match:t=>"number"==typeof t,fromHexString:t=>parseInt(t.substring(1),16),toHexString:t=>"#"+t.toString(16).padStart(6,0)},l={isPrimitive:!1,match:Array.isArray,fromHexString(t,i,e=1){const s=n.fromHexString(t);i[0]=(s>>16&255)/255*e,i[1]=(s>>8&255)/255*e,i[2]=(255&s)/255*e},toHexString:([t,i,e],s=1)=>n.toHexString(t*(s=255/s)<<16^i*s<<8^e*s<<0)},r={isPrimitive:!1,match:t=>Object(t)===t,fromHexString(t,i,e=1){const s=n.fromHexString(t);i.r=(s>>16&255)/255*e,i.g=(s>>8&255)/255*e,i.b=(255&s)/255*e},toHexString:({r:t,g:i,b:e},s=1)=>n.toHexString(t*(s=255/s)<<16^i*s<<8^e*s<<0)},o=[s,n,l,r];class a extends t{constructor(t,i,s,n){var l;super(t,i,s,"color"),this.$input=document.createElement("input"),this.$input.setAttribute("type","color"),this.$input.setAttribute("tabindex",-1),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$text=document.createElement("input"),this.$text.setAttribute("type","text"),this.$text.setAttribute("spellcheck","false"),this.$text.setAttribute("aria-labelledby",this.$name.id),this.$display=document.createElement("div"),this.$display.classList.add("display"),this.$display.appendChild(this.$input),this.$widget.appendChild(this.$display),this.$widget.appendChild(this.$text),this._format=(l=this.initialValue,o.find(t=>t.match(l))),this._rgbScale=n,this._initialValueHexString=this.save(),this._textFocused=!1,this.$input.addEventListener("input",()=>{this._setValueFromHexString(this.$input.value)}),this.$input.addEventListener("blur",()=>{this._callOnFinishChange()}),this.$text.addEventListener("input",()=>{const t=e(this.$text.value);t&&this._setValueFromHexString(t)}),this.$text.addEventListener("focus",()=>{this._textFocused=!0,this.$text.select()}),this.$text.addEventListener("blur",()=>{this._textFocused=!1,this.updateDisplay(),this._callOnFinishChange()}),this.$disable=this.$text,this.updateDisplay()}reset(){return this._setValueFromHexString(this._initialValueHexString),this}_setValueFromHexString(t){if(this._format.isPrimitive){const i=this._format.fromHexString(t);this.setValue(i)}else this._format.fromHexString(t,this.getValue(),this._rgbScale),this._callOnChange(),this.updateDisplay()}save(){return this._format.toHexString(this.getValue(),this._rgbScale)}load(t){return this._setValueFromHexString(t),this._callOnFinishChange(),this}updateDisplay(){return this.$input.value=this._format.toHexString(this.getValue(),this._rgbScale),this._textFocused||(this.$text.value=this.$input.value.substring(1)),this.$display.style.backgroundColor=this.$input.value,this}}class h extends t{constructor(t,i,e){super(t,i,e,"function"),this.$button=document.createElement("button"),this.$button.appendChild(this.$name),this.$widget.appendChild(this.$button),this.$button.addEventListener("click",t=>{t.preventDefault(),this.getValue().call(this.object)}),this.$button.addEventListener("touchstart",()=>{},{passive:!0}),this.$disable=this.$button}}class d extends t{constructor(t,i,e,s,n,l){super(t,i,e,"number"),this._initInput(),this.min(s),this.max(n);const r=void 0!==l;this.step(r?l:this._getImplicitStep(),r),this.updateDisplay()}decimals(t){return this._decimals=t,this.updateDisplay(),this}min(t){return this._min=t,this._onUpdateMinMax(),this}max(t){return this._max=t,this._onUpdateMinMax(),this}step(t,i=!0){return this._step=t,this._stepExplicit=i,this}updateDisplay(){const t=this.getValue();if(this._hasSlider){let i=(t-this._min)/(this._max-this._min);i=Math.max(0,Math.min(i,1)),this.$fill.style.width=100*i+"%"}return this._inputFocused||(this.$input.value=void 0===this._decimals?t:t.toFixed(this._decimals)),this}_initInput(){this.$input=document.createElement("input"),this.$input.setAttribute("type","number"),this.$input.setAttribute("step","any"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$widget.appendChild(this.$input),this.$disable=this.$input;const t=t=>{const i=parseFloat(this.$input.value);isNaN(i)||(this._snapClampSetValue(i+t),this.$input.value=this.getValue())};let i,e,s,n,l,r=!1;const o=t=>{if(r){const s=t.clientX-i,n=t.clientY-e;Math.abs(n)>5?(t.preventDefault(),this.$input.blur(),r=!1,this._setDraggingStyle(!0,"vertical")):Math.abs(s)>5&&a()}if(!r){const i=t.clientY-s;l-=i*this._step*this._arrowKeyMultiplier(t),n+l>this._max?l=this._max-n:n+l{this._setDraggingStyle(!1,"vertical"),this._callOnFinishChange(),window.removeEventListener("mousemove",o),window.removeEventListener("mouseup",a)};this.$input.addEventListener("input",()=>{let t=parseFloat(this.$input.value);isNaN(t)||(this._stepExplicit&&(t=this._snap(t)),this.setValue(this._clamp(t)))}),this.$input.addEventListener("keydown",i=>{"Enter"===i.code&&this.$input.blur(),"ArrowUp"===i.code&&(i.preventDefault(),t(this._step*this._arrowKeyMultiplier(i))),"ArrowDown"===i.code&&(i.preventDefault(),t(this._step*this._arrowKeyMultiplier(i)*-1))}),this.$input.addEventListener("wheel",i=>{this._inputFocused&&(i.preventDefault(),t(this._step*this._normalizeMouseWheel(i)))},{passive:!1}),this.$input.addEventListener("mousedown",t=>{i=t.clientX,e=s=t.clientY,r=!0,n=this.getValue(),l=0,window.addEventListener("mousemove",o),window.addEventListener("mouseup",a)}),this.$input.addEventListener("focus",()=>{this._inputFocused=!0}),this.$input.addEventListener("blur",()=>{this._inputFocused=!1,this.updateDisplay(),this._callOnFinishChange()})}_initSlider(){this._hasSlider=!0,this.$slider=document.createElement("div"),this.$slider.classList.add("slider"),this.$fill=document.createElement("div"),this.$fill.classList.add("fill"),this.$slider.appendChild(this.$fill),this.$widget.insertBefore(this.$slider,this.$input),this.domElement.classList.add("hasSlider");const t=t=>{const i=this.$slider.getBoundingClientRect();let e=(s=t,n=i.left,l=i.right,r=this._min,o=this._max,(s-n)/(l-n)*(o-r)+r);var s,n,l,r,o;this._snapClampSetValue(e)},i=i=>{t(i.clientX)},e=()=>{this._callOnFinishChange(),this._setDraggingStyle(!1),window.removeEventListener("mousemove",i),window.removeEventListener("mouseup",e)};let s,n,l=!1;const r=i=>{i.preventDefault(),this._setDraggingStyle(!0),t(i.touches[0].clientX),l=!1},o=i=>{if(l){const t=i.touches[0].clientX-s,e=i.touches[0].clientY-n;Math.abs(t)>Math.abs(e)?r(i):(window.removeEventListener("touchmove",o),window.removeEventListener("touchend",a))}else i.preventDefault(),t(i.touches[0].clientX)},a=()=>{this._callOnFinishChange(),this._setDraggingStyle(!1),window.removeEventListener("touchmove",o),window.removeEventListener("touchend",a)},h=this._callOnFinishChange.bind(this);let d;this.$slider.addEventListener("mousedown",s=>{this._setDraggingStyle(!0),t(s.clientX),window.addEventListener("mousemove",i),window.addEventListener("mouseup",e)}),this.$slider.addEventListener("touchstart",t=>{t.touches.length>1||(this._hasScrollBar?(s=t.touches[0].clientX,n=t.touches[0].clientY,l=!0):r(t),window.addEventListener("touchmove",o,{passive:!1}),window.addEventListener("touchend",a))},{passive:!1}),this.$slider.addEventListener("wheel",t=>{if(Math.abs(t.deltaX)this._max&&(t=this._max),t}_snapClampSetValue(t){this.setValue(this._clamp(this._snap(t)))}get _hasScrollBar(){const t=this.parent.root.$children;return t.scrollHeight>t.clientHeight}get _hasMin(){return void 0!==this._min}get _hasMax(){return void 0!==this._max}}class c extends t{constructor(t,i,e,s){super(t,i,e,"option"),this.$select=document.createElement("select"),this.$select.setAttribute("aria-labelledby",this.$name.id),this.$display=document.createElement("div"),this.$display.classList.add("display"),this._values=Array.isArray(s)?s:Object.values(s),this._names=Array.isArray(s)?s:Object.keys(s),this._names.forEach(t=>{const i=document.createElement("option");i.innerHTML=t,this.$select.appendChild(i)}),this.$select.addEventListener("change",()=>{this.setValue(this._values[this.$select.selectedIndex]),this._callOnFinishChange()}),this.$select.addEventListener("focus",()=>{this.$display.classList.add("focus")}),this.$select.addEventListener("blur",()=>{this.$display.classList.remove("focus")}),this.$widget.appendChild(this.$select),this.$widget.appendChild(this.$display),this.$disable=this.$select,this.updateDisplay()}updateDisplay(){const t=this.getValue(),i=this._values.indexOf(t);return this.$select.selectedIndex=i,this.$display.innerHTML=-1===i?t:this._names[i],this}}class u extends t{constructor(t,i,e){super(t,i,e,"string"),this.$input=document.createElement("input"),this.$input.setAttribute("type","text"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$input.addEventListener("input",()=>{this.setValue(this.$input.value)}),this.$input.addEventListener("keydown",t=>{"Enter"===t.code&&this.$input.blur()}),this.$input.addEventListener("blur",()=>{this._callOnFinishChange()}),this.$widget.appendChild(this.$input),this.$disable=this.$input,this.updateDisplay()}updateDisplay(){return this.$input.value=this.getValue(),this}}let p=!1;class g{constructor({parent:t,autoPlace:i=void 0===t,container:e,width:s,title:n="Controls",injectStyles:l=!0,touchStyles:r=!0}={}){if(this.parent=t,this.root=t?t.root:this,this.children=[],this.controllers=[],this.folders=[],this._closed=!1,this._hidden=!1,this.domElement=document.createElement("div"),this.domElement.classList.add("lil-gui"),this.$title=document.createElement("div"),this.$title.classList.add("title"),this.$title.setAttribute("role","button"),this.$title.setAttribute("aria-expanded",!0),this.$title.setAttribute("tabindex",0),this.$title.addEventListener("click",()=>this.openAnimated(this._closed)),this.$title.addEventListener("keydown",t=>{"Enter"!==t.code&&"Space"!==t.code||(t.preventDefault(),this.$title.click())}),this.$title.addEventListener("touchstart",()=>{},{passive:!0}),this.$children=document.createElement("div"),this.$children.classList.add("children"),this.domElement.appendChild(this.$title),this.domElement.appendChild(this.$children),this.title(n),r&&this.domElement.classList.add("allow-touch-styles"),this.parent)return this.parent.children.push(this),this.parent.folders.push(this),void this.parent.$children.appendChild(this.domElement);this.domElement.classList.add("root"),!p&&l&&(!function(t){const i=document.createElement("style");i.innerHTML=t;const e=document.querySelector("head link[rel=stylesheet], head style");e?document.head.insertBefore(i,e):document.head.appendChild(i)}('.lil-gui{--background-color:#1f1f1f;--text-color:#ebebeb;--title-background-color:#111;--title-text-color:#ebebeb;--widget-color:#424242;--hover-color:#4f4f4f;--focus-color:#595959;--number-color:#2cc9ff;--string-color:#a2db3c;--font-size:11px;--input-font-size:11px;--font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;--font-family-mono:Menlo,Monaco,Consolas,"Droid Sans Mono",monospace;--padding:4px;--spacing:4px;--widget-height:20px;--name-width:45%;--slider-knob-width:2px;--slider-input-width:27%;--color-input-width:27%;--slider-input-min-width:45px;--color-input-min-width:45px;--folder-indent:7px;--widget-padding:0 0 0 3px;--widget-border-radius:2px;--checkbox-size:calc(var(--widget-height)*0.75);--scrollbar-width:5px;background-color:var(--background-color);color:var(--text-color);font-family:var(--font-family);font-size:var(--font-size);font-style:normal;font-weight:400;line-height:1;text-align:left;touch-action:manipulation;user-select:none;-webkit-user-select:none}.lil-gui,.lil-gui *{box-sizing:border-box;margin:0;padding:0}.lil-gui.root{display:flex;flex-direction:column;width:var(--width,245px)}.lil-gui.root>.title{background:var(--title-background-color);color:var(--title-text-color)}.lil-gui.root>.children{overflow-x:hidden;overflow-y:auto}.lil-gui.root>.children::-webkit-scrollbar{background:var(--background-color);height:var(--scrollbar-width);width:var(--scrollbar-width)}.lil-gui.root>.children::-webkit-scrollbar-thumb{background:var(--focus-color);border-radius:var(--scrollbar-width)}.lil-gui.force-touch-styles{--widget-height:28px;--padding:6px;--spacing:6px;--font-size:13px;--input-font-size:16px;--folder-indent:10px;--scrollbar-width:7px;--slider-input-min-width:50px;--color-input-min-width:65px}.lil-gui.autoPlace{max-height:100%;position:fixed;right:15px;top:0;z-index:1001}.lil-gui .controller{align-items:center;display:flex;margin:var(--spacing) 0;padding:0 var(--padding)}.lil-gui .controller.disabled{opacity:.5}.lil-gui .controller.disabled,.lil-gui .controller.disabled *{pointer-events:none!important}.lil-gui .controller>.name{flex-shrink:0;line-height:var(--widget-height);min-width:var(--name-width);padding-right:var(--spacing);white-space:pre}.lil-gui .controller .widget{align-items:center;display:flex;min-height:var(--widget-height);position:relative;width:100%}.lil-gui .controller.string input{color:var(--string-color)}.lil-gui .controller.boolean .widget{cursor:pointer}.lil-gui .controller.color .display{border-radius:var(--widget-border-radius);height:var(--widget-height);position:relative;width:100%}.lil-gui .controller.color input[type=color]{cursor:pointer;height:100%;opacity:0;width:100%}.lil-gui .controller.color input[type=text]{flex-shrink:0;font-family:var(--font-family-mono);margin-left:var(--spacing);min-width:var(--color-input-min-width);width:var(--color-input-width)}.lil-gui .controller.option select{max-width:100%;opacity:0;position:absolute;width:100%}.lil-gui .controller.option .display{background:var(--widget-color);border-radius:var(--widget-border-radius);height:var(--widget-height);line-height:var(--widget-height);max-width:100%;overflow:hidden;padding-left:.55em;padding-right:1.75em;pointer-events:none;position:relative;word-break:break-all}.lil-gui .controller.option .display.active{background:var(--focus-color)}.lil-gui .controller.option .display:after{bottom:0;content:"↕";font-family:lil-gui;padding-right:.375em;position:absolute;right:0;top:0}.lil-gui .controller.option .widget,.lil-gui .controller.option select{cursor:pointer}.lil-gui .controller.number input{color:var(--number-color)}.lil-gui .controller.number.hasSlider input{flex-shrink:0;margin-left:var(--spacing);min-width:var(--slider-input-min-width);width:var(--slider-input-width)}.lil-gui .controller.number .slider{background-color:var(--widget-color);border-radius:var(--widget-border-radius);cursor:ew-resize;height:var(--widget-height);overflow:hidden;padding-right:var(--slider-knob-width);touch-action:pan-y;width:100%}.lil-gui .controller.number .slider.active{background-color:var(--focus-color)}.lil-gui .controller.number .slider.active .fill{opacity:.95}.lil-gui .controller.number .fill{border-right:var(--slider-knob-width) solid var(--number-color);box-sizing:content-box;height:100%}.lil-gui-dragging .lil-gui{--hover-color:var(--widget-color)}.lil-gui-dragging *{cursor:ew-resize!important}.lil-gui-dragging.lil-gui-vertical *{cursor:ns-resize!important}.lil-gui .title{--title-height:calc(var(--widget-height) + var(--spacing)*1.25);-webkit-tap-highlight-color:transparent;text-decoration-skip:objects;cursor:pointer;font-weight:600;height:var(--title-height);line-height:calc(var(--title-height) - 4px);outline:none;padding:0 var(--padding)}.lil-gui .title:before{content:"▾";display:inline-block;font-family:lil-gui;padding-right:2px}.lil-gui .title:active{background:var(--title-background-color);opacity:.75}.lil-gui.root>.title:focus{text-decoration:none!important}.lil-gui.closed>.title:before{content:"▸"}.lil-gui.closed>.children{opacity:0;transform:translateY(-7px)}.lil-gui.closed:not(.transition)>.children{display:none}.lil-gui.transition>.children{overflow:hidden;pointer-events:none;transition-duration:.3s;transition-property:height,opacity,transform;transition-timing-function:cubic-bezier(.2,.6,.35,1)}.lil-gui .children:empty:before{content:"Empty";display:block;font-style:italic;height:var(--widget-height);line-height:var(--widget-height);margin:var(--spacing) 0;opacity:.5;padding:0 var(--padding)}.lil-gui.root>.children>.lil-gui>.title{border-width:0;border-bottom:1px solid var(--widget-color);border-left:0 solid var(--widget-color);border-right:0 solid var(--widget-color);border-top:1px solid var(--widget-color);transition:border-color .3s}.lil-gui.root>.children>.lil-gui.closed>.title{border-bottom-color:transparent}.lil-gui+.controller{border-top:1px solid var(--widget-color);margin-top:0;padding-top:var(--spacing)}.lil-gui .lil-gui .lil-gui>.title{border:none}.lil-gui .lil-gui .lil-gui>.children{border:none;border-left:2px solid var(--widget-color);margin-left:var(--folder-indent)}.lil-gui .lil-gui .controller{border:none}.lil-gui input{-webkit-tap-highlight-color:transparent;background:var(--widget-color);border:0;border-radius:var(--widget-border-radius);color:var(--text-color);font-family:var(--font-family);font-size:var(--input-font-size);height:var(--widget-height);outline:none;width:100%}.lil-gui input:disabled{opacity:1}.lil-gui input[type=number],.lil-gui input[type=text]{padding:var(--widget-padding)}.lil-gui input[type=number]:focus,.lil-gui input[type=text]:focus{background:var(--focus-color)}.lil-gui input::-webkit-inner-spin-button,.lil-gui input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.lil-gui input[type=number]{-moz-appearance:textfield}.lil-gui input[type=checkbox]{appearance:none;-webkit-appearance:none;border-radius:var(--widget-border-radius);cursor:pointer;height:var(--checkbox-size);text-align:center;width:var(--checkbox-size)}.lil-gui input[type=checkbox]:checked:before{content:"✓";font-family:lil-gui;font-size:var(--checkbox-size);line-height:var(--checkbox-size)}.lil-gui button{-webkit-tap-highlight-color:transparent;background:var(--widget-color);border:1px solid var(--widget-color);border-radius:var(--widget-border-radius);color:var(--text-color);cursor:pointer;font-family:var(--font-family);font-size:var(--font-size);height:var(--widget-height);line-height:calc(var(--widget-height) - 4px);outline:none;text-align:center;text-transform:none;width:100%}.lil-gui button:active{background:var(--focus-color)}@font-face{font-family:lil-gui;src:url("data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUsAAsAAAAACJwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAAH4AAADAImwmYE9TLzIAAAGIAAAAPwAAAGBKqH5SY21hcAAAAcgAAAD0AAACrukyyJBnbHlmAAACvAAAAF8AAACEIZpWH2hlYWQAAAMcAAAAJwAAADZfcj2zaGhlYQAAA0QAAAAYAAAAJAC5AHhobXR4AAADXAAAABAAAABMAZAAAGxvY2EAAANsAAAAFAAAACgCEgIybWF4cAAAA4AAAAAeAAAAIAEfABJuYW1lAAADoAAAASIAAAIK9SUU/XBvc3QAAATEAAAAZgAAAJCTcMc2eJxVjbEOgjAURU+hFRBK1dGRL+ALnAiToyMLEzFpnPz/eAshwSa97517c/MwwJmeB9kwPl+0cf5+uGPZXsqPu4nvZabcSZldZ6kfyWnomFY/eScKqZNWupKJO6kXN3K9uCVoL7iInPr1X5baXs3tjuMqCtzEuagm/AAlzQgPAAB4nGNgYRBlnMDAysDAYM/gBiT5oLQBAwuDJAMDEwMrMwNWEJDmmsJwgCFeXZghBcjlZMgFCzOiKOIFAB71Bb8AeJy1kjFuwkAQRZ+DwRAwBtNQRUGKQ8OdKCAWUhAgKLhIuAsVSpWz5Bbkj3dEgYiUIszqWdpZe+Z7/wB1oCYmIoboiwiLT2WjKl/jscrHfGg/pKdMkyklC5Zs2LEfHYpjcRoPzme9MWWmk3dWbK9ObkWkikOetJ554fWyoEsmdSlt+uR0pCJR34b6t/TVg1SY3sYvdf8vuiKrpyaDXDISiegp17p7579Gp3p++y7HPAiY9pmTibljrr85qSidtlg4+l25GLCaS8e6rRxNBmsnERunKbaOObRz7N72ju5vdAjYpBXHgJylOAVsMseDAPEP8LYoUHicY2BiAAEfhiAGJgZWBgZ7RnFRdnVJELCQlBSRlATJMoLV2DK4glSYs6ubq5vbKrJLSbGrgEmovDuDJVhe3VzcXFwNLCOILB/C4IuQ1xTn5FPilBTj5FPmBAB4WwoqAHicY2BkYGAA4sk1sR/j+W2+MnAzpDBgAyEMQUCSg4EJxAEAwUgFHgB4nGNgZGBgSGFggJMhDIwMqEAYAByHATJ4nGNgAIIUNEwmAABl3AGReJxjYAACIQYlBiMGJ3wQAEcQBEV4nGNgZGBgEGZgY2BiAAEQyQWEDAz/wXwGAAsPATIAAHicXdBNSsNAHAXwl35iA0UQXYnMShfS9GPZA7T7LgIu03SSpkwzYTIt1BN4Ak/gKTyAeCxfw39jZkjymzcvAwmAW/wgwHUEGDb36+jQQ3GXGot79L24jxCP4gHzF/EIr4jEIe7wxhOC3g2TMYy4Q7+Lu/SHuEd/ivt4wJd4wPxbPEKMX3GI5+DJFGaSn4qNzk8mcbKSR6xdXdhSzaOZJGtdapd4vVPbi6rP+cL7TGXOHtXKll4bY1Xl7EGnPtp7Xy2n00zyKLVHfkHBa4IcJ2oD3cgggWvt/V/FbDrUlEUJhTn/0azVWbNTNr0Ens8de1tceK9xZmfB1CPjOmPH4kitmvOubcNpmVTN3oFJyjzCvnmrwhJTzqzVj9jiSX911FjeAAB4nG3HMRKCMBBA0f0giiKi4DU8k0V2GWbIZDOh4PoWWvq6J5V8If9NVNQcaDhyouXMhY4rPTcG7jwYmXhKq8Wz+p762aNaeYXom2n3m2dLTVgsrCgFJ7OTmIkYbwIbC6vIB7WmFfAAAA==") format("woff")}@media (pointer:coarse){.lil-gui.allow-touch-styles{--widget-height:28px;--padding:6px;--spacing:6px;--font-size:13px;--input-font-size:16px;--folder-indent:10px;--scrollbar-width:7px;--slider-input-min-width:50px;--color-input-min-width:65px}}@media (hover:hover){.lil-gui .controller.color .display:hover:before{border:1px solid #fff9;border-radius:var(--widget-border-radius);bottom:0;content:" ";display:block;left:0;position:absolute;right:0;top:0}.lil-gui .controller.option .display.focus{background:var(--focus-color)}.lil-gui .controller.option .widget:hover .display{background:var(--hover-color)}.lil-gui .controller.number .slider:hover{background-color:var(--hover-color)}body:not(.lil-gui-dragging) .lil-gui .title:hover{background:var(--title-background-color);opacity:.85}.lil-gui .title:focus{text-decoration:underline var(--focus-color)}.lil-gui input:hover{background:var(--hover-color)}.lil-gui input:active{background:var(--focus-color)}.lil-gui input[type=checkbox]:focus{box-shadow:inset 0 0 0 1px var(--focus-color)}.lil-gui button:hover{background:var(--hover-color);border-color:var(--hover-color)}.lil-gui button:focus{border-color:var(--focus-color)}}'),p=!0),e?e.appendChild(this.domElement):i&&(this.domElement.classList.add("autoPlace"),document.body.appendChild(this.domElement)),s&&this.domElement.style.setProperty("--width",s+"px"),this.domElement.addEventListener("keydown",t=>t.stopPropagation()),this.domElement.addEventListener("keyup",t=>t.stopPropagation())}add(t,e,s,n,l){if(Object(s)===s)return new c(this,t,e,s);const r=t[e];switch(typeof r){case"number":return new d(this,t,e,s,n,l);case"boolean":return new i(this,t,e);case"string":return new u(this,t,e);case"function":return new h(this,t,e)}console.error("gui.add failed\n\tproperty:",e,"\n\tobject:",t,"\n\tvalue:",r)}addColor(t,i,e=1){return new a(this,t,i,e)}addFolder(t){return new g({parent:this,title:t})}load(t,i=!0){return t.controllers&&this.controllers.forEach(i=>{i instanceof h||i._name in t.controllers&&i.load(t.controllers[i._name])}),i&&t.folders&&this.folders.forEach(i=>{i._title in t.folders&&i.load(t.folders[i._title])}),this}save(t=!0){const i={controllers:{},folders:{}};return this.controllers.forEach(t=>{if(!(t instanceof h)){if(t._name in i.controllers)throw new Error(`Cannot save GUI with duplicate property "${t._name}"`);i.controllers[t._name]=t.save()}}),t&&this.folders.forEach(t=>{if(t._title in i.folders)throw new Error(`Cannot save GUI with duplicate folder "${t._title}"`);i.folders[t._title]=t.save()}),i}open(t=!0){return this._closed=!t,this.$title.setAttribute("aria-expanded",!this._closed),this.domElement.classList.toggle("closed",this._closed),this}close(){return this.open(!1)}show(t=!0){return this._hidden=!t,this.domElement.style.display=this._hidden?"none":"",this}hide(){return this.show(!1)}openAnimated(t=!0){return this._closed=!t,this.$title.setAttribute("aria-expanded",!this._closed),requestAnimationFrame(()=>{const i=this.$children.clientHeight;this.$children.style.height=i+"px",this.domElement.classList.add("transition");const e=t=>{t.target===this.$children&&(this.$children.style.height="",this.domElement.classList.remove("transition"),this.$children.removeEventListener("transitionend",e))};this.$children.addEventListener("transitionend",e);const s=t?this.$children.scrollHeight:0;this.domElement.classList.toggle("closed",!t),requestAnimationFrame(()=>{this.$children.style.height=s+"px"})}),this}title(t){return this._title=t,this.$title.innerHTML=t,this}reset(t=!0){return(t?this.controllersRecursive():this.controllers).forEach(t=>t.reset()),this}onChange(t){return this._onChange=t,this}_callOnChange(t){this.parent&&this.parent._callOnChange(t),void 0!==this._onChange&&this._onChange.call(this,{object:t.object,property:t.property,value:t.getValue(),controller:t})}onFinishChange(t){return this._onFinishChange=t,this}_callOnFinishChange(t){this.parent&&this.parent._callOnFinishChange(t),void 0!==this._onFinishChange&&this._onFinishChange.call(this,{object:t.object,property:t.property,value:t.getValue(),controller:t})}destroy(){this.parent&&(this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent.folders.splice(this.parent.folders.indexOf(this),1)),this.domElement.parentElement&&this.domElement.parentElement.removeChild(this.domElement),Array.from(this.children).forEach(t=>t.destroy())}controllersRecursive(){let t=Array.from(this.controllers);return this.folders.forEach(i=>{t=t.concat(i.controllersRecursive())}),t}foldersRecursive(){let t=Array.from(this.folders);return this.folders.forEach(i=>{t=t.concat(i.foldersRecursive())}),t}}export default g;export{i as BooleanController,a as ColorController,t as Controller,h as FunctionController,g as GUI,d as NumberController,c as OptionController,u as StringController}; 9 | -------------------------------------------------------------------------------- /js/stats.module.js: -------------------------------------------------------------------------------- 1 | var Stats = function () { 2 | 3 | var mode = 0; 4 | 5 | var container = document.createElement( 'div' ); 6 | container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000'; 7 | container.addEventListener( 'click', function ( event ) { 8 | 9 | event.preventDefault(); 10 | showPanel( ++ mode % container.children.length ); 11 | 12 | }, false ); 13 | 14 | // 15 | 16 | function addPanel( panel ) { 17 | 18 | container.appendChild( panel.dom ); 19 | return panel; 20 | 21 | } 22 | 23 | function showPanel( id ) { 24 | 25 | for ( var i = 0; i < container.children.length; i ++ ) { 26 | 27 | container.children[ i ].style.display = i === id ? 'block' : 'none'; 28 | 29 | } 30 | 31 | mode = id; 32 | 33 | } 34 | 35 | // 36 | 37 | var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0; 38 | 39 | var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) ); 40 | var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) ); 41 | 42 | if ( self.performance && self.performance.memory ) { 43 | 44 | var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) ); 45 | 46 | } 47 | 48 | showPanel( 0 ); 49 | 50 | return { 51 | 52 | REVISION: 16, 53 | 54 | dom: container, 55 | 56 | addPanel: addPanel, 57 | showPanel: showPanel, 58 | 59 | begin: function () { 60 | 61 | beginTime = ( performance || Date ).now(); 62 | 63 | }, 64 | 65 | end: function () { 66 | 67 | frames ++; 68 | 69 | var time = ( performance || Date ).now(); 70 | 71 | msPanel.update( time - beginTime, 200 ); 72 | 73 | if ( time >= prevTime + 1000 ) { 74 | 75 | fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 ); 76 | 77 | prevTime = time; 78 | frames = 0; 79 | 80 | if ( memPanel ) { 81 | 82 | var memory = performance.memory; 83 | memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 ); 84 | 85 | } 86 | 87 | } 88 | 89 | return time; 90 | 91 | }, 92 | 93 | update: function () { 94 | 95 | beginTime = this.end(); 96 | 97 | }, 98 | 99 | // Backwards Compatibility 100 | 101 | domElement: container, 102 | setMode: showPanel 103 | 104 | }; 105 | 106 | }; 107 | 108 | Stats.Panel = function ( name, fg, bg ) { 109 | 110 | var min = Infinity, max = 0, round = Math.round; 111 | var PR = round( window.devicePixelRatio || 1 ); 112 | 113 | var WIDTH = 80 * PR, HEIGHT = 48 * PR, 114 | TEXT_X = 3 * PR, TEXT_Y = 2 * PR, 115 | GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR, 116 | GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR; 117 | 118 | var canvas = document.createElement( 'canvas' ); 119 | canvas.width = WIDTH; 120 | canvas.height = HEIGHT; 121 | canvas.style.cssText = 'width:80px;height:48px'; 122 | 123 | var context = canvas.getContext( '2d' ); 124 | context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif'; 125 | context.textBaseline = 'top'; 126 | 127 | context.fillStyle = bg; 128 | context.fillRect( 0, 0, WIDTH, HEIGHT ); 129 | 130 | context.fillStyle = fg; 131 | context.fillText( name, TEXT_X, TEXT_Y ); 132 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 133 | 134 | context.fillStyle = bg; 135 | context.globalAlpha = 0.9; 136 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 137 | 138 | return { 139 | 140 | dom: canvas, 141 | 142 | update: function ( value, maxValue ) { 143 | 144 | min = Math.min( min, value ); 145 | max = Math.max( max, value ); 146 | 147 | context.fillStyle = bg; 148 | context.globalAlpha = 1; 149 | context.fillRect( 0, 0, WIDTH, GRAPH_Y ); 150 | context.fillStyle = fg; 151 | context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y ); 152 | 153 | context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT ); 154 | 155 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT ); 156 | 157 | context.fillStyle = bg; 158 | context.globalAlpha = 0.9; 159 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) ); 160 | 161 | } 162 | 163 | }; 164 | 165 | }; 166 | 167 | export default Stats; 168 | -------------------------------------------------------------------------------- /shaders/AntiGravityPool_Fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | precision highp sampler2D; 4 | 5 | #include 6 | 7 | uniform vec3 uBallPositions[24]; 8 | uniform bool uShotIsInProgress; 9 | 10 | #define N_LIGHTS 8.0 11 | #define N_SPHERES 8 12 | #define N_BOXES 1 13 | 14 | //----------------------------------------------------------------------- 15 | 16 | vec3 rayOrigin, rayDirection; 17 | // recorded intersection data: 18 | vec3 hitNormal, hitEmission, hitColor; 19 | vec2 hitUV; 20 | float hitObjectID = -INFINITY; 21 | int hitType = -100; 22 | 23 | struct Sphere { float radius; vec3 position; vec3 emission; vec3 color; int type; }; 24 | struct Box { vec3 minCorner; vec3 maxCorner; vec3 emission; vec3 color; int type; }; 25 | 26 | Sphere spheres[N_SPHERES]; 27 | Box boxes[N_BOXES]; 28 | 29 | 30 | #include 31 | 32 | #include 33 | 34 | #include 35 | 36 | #include 37 | 38 | 39 | 40 | //--------------------------------------------------------------------------------------- 41 | float SceneIntersect() 42 | //--------------------------------------------------------------------------------------- 43 | { 44 | float d = INFINITY; 45 | float t = INFINITY; 46 | vec3 n; 47 | int objectCount = 0; 48 | 49 | d = BoxInteriorIntersect( boxes[0].minCorner, boxes[0].maxCorner, rayOrigin, rayDirection, n ); 50 | if (d < t) 51 | { 52 | t = d; 53 | hitNormal = n; 54 | hitEmission = boxes[0].emission; 55 | hitColor = boxes[0].color; 56 | hitType = boxes[0].type; 57 | hitObjectID = float(objectCount); 58 | } 59 | objectCount++; 60 | 61 | // white cueball / glass aiming ball 62 | d = SphereIntersect( 2.0, uBallPositions[0], rayOrigin, rayDirection ); 63 | if (d < t) 64 | { 65 | t = d; 66 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[0]; 67 | hitEmission = vec3(0); 68 | //hitColor = uShotIsInProgress ? vec3(1) : vec3(2); 69 | hitColor = vec3(1); 70 | hitType = uShotIsInProgress ? COAT : REFR; 71 | hitObjectID = float(objectCount); 72 | } 73 | objectCount++; 74 | 75 | // black ball 76 | d = SphereIntersect( 2.0, uBallPositions[1], rayOrigin, rayDirection ); 77 | if (d < t) 78 | { 79 | t = d; 80 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[1]; 81 | hitEmission = vec3(0); 82 | hitColor = vec3(0.005); 83 | hitType = COAT; 84 | hitObjectID = float(objectCount); 85 | } 86 | objectCount++; 87 | 88 | // red balls 89 | 90 | d = SphereIntersect( 2.0, uBallPositions[2], rayOrigin, rayDirection ); 91 | if (d < t) 92 | { 93 | t = d; 94 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[2]; 95 | hitEmission = vec3(0); 96 | hitColor = vec3(1.0, 0.0, 0.0); 97 | hitType = COAT; 98 | hitObjectID = float(objectCount); 99 | } 100 | objectCount++; 101 | 102 | d = SphereIntersect( 2.0, uBallPositions[3], rayOrigin, rayDirection ); 103 | if (d < t) 104 | { 105 | t = d; 106 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[3]; 107 | hitEmission = vec3(0); 108 | hitColor = vec3(1.0, 0.0, 0.0); 109 | hitType = COAT; 110 | hitObjectID = float(objectCount); 111 | } 112 | objectCount++; 113 | 114 | d = SphereIntersect( 2.0, uBallPositions[4], rayOrigin, rayDirection ); 115 | if (d < t) 116 | { 117 | t = d; 118 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[4]; 119 | hitEmission = vec3(0); 120 | hitColor = vec3(1.0, 0.0, 0.0); 121 | hitType = COAT; 122 | hitObjectID = float(objectCount); 123 | } 124 | objectCount++; 125 | 126 | d = SphereIntersect( 2.0, uBallPositions[5], rayOrigin, rayDirection ); 127 | if (d < t) 128 | { 129 | t = d; 130 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[5]; 131 | hitEmission = vec3(0); 132 | hitColor = vec3(1.0, 0.0, 0.0); 133 | hitType = COAT; 134 | hitObjectID = float(objectCount); 135 | } 136 | objectCount++; 137 | 138 | d = SphereIntersect( 2.0, uBallPositions[6], rayOrigin, rayDirection ); 139 | if (d < t) 140 | { 141 | t = d; 142 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[6]; 143 | hitEmission = vec3(0); 144 | hitColor = vec3(1.0, 0.0, 0.0); 145 | hitType = COAT; 146 | hitObjectID = float(objectCount); 147 | } 148 | objectCount++; 149 | 150 | d = SphereIntersect( 2.0, uBallPositions[7], rayOrigin, rayDirection ); 151 | if (d < t) 152 | { 153 | t = d; 154 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[7]; 155 | hitEmission = vec3(0); 156 | hitColor = vec3(1.0, 0.0, 0.0); 157 | hitType = COAT; 158 | hitObjectID = float(objectCount); 159 | } 160 | objectCount++; 161 | 162 | d = SphereIntersect( 2.0, uBallPositions[8], rayOrigin, rayDirection ); 163 | if (d < t) 164 | { 165 | t = d; 166 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[8]; 167 | hitEmission = vec3(0); 168 | hitColor = vec3(1.0, 0.0, 0.0); 169 | hitType = COAT; 170 | hitObjectID = float(objectCount); 171 | } 172 | objectCount++; 173 | 174 | // yellow balls 175 | 176 | d = SphereIntersect( 2.0, uBallPositions[9], rayOrigin, rayDirection ); 177 | if (d < t) 178 | { 179 | t = d; 180 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[9]; 181 | hitEmission = vec3(0); 182 | hitColor = vec3(1.0, 1.0, 0.0); 183 | hitType = COAT; 184 | hitObjectID = float(objectCount); 185 | } 186 | objectCount++; 187 | 188 | d = SphereIntersect( 2.0, uBallPositions[10], rayOrigin, rayDirection ); 189 | if (d < t) 190 | { 191 | t = d; 192 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[10]; 193 | hitEmission = vec3(0); 194 | hitColor = vec3(1.0, 1.0, 0.0); 195 | hitType = COAT; 196 | hitObjectID = float(objectCount); 197 | } 198 | objectCount++; 199 | 200 | d = SphereIntersect( 2.0, uBallPositions[11], rayOrigin, rayDirection ); 201 | if (d < t) 202 | { 203 | t = d; 204 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[11]; 205 | hitEmission = vec3(0); 206 | hitColor = vec3(1.0, 1.0, 0.0); 207 | hitType = COAT; 208 | hitObjectID = float(objectCount); 209 | } 210 | objectCount++; 211 | 212 | d = SphereIntersect( 2.0, uBallPositions[12], rayOrigin, rayDirection ); 213 | if (d < t) 214 | { 215 | t = d; 216 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[12]; 217 | hitEmission = vec3(0); 218 | hitColor = vec3(1.0, 1.0, 0.0); 219 | hitType = COAT; 220 | hitObjectID = float(objectCount); 221 | } 222 | objectCount++; 223 | 224 | d = SphereIntersect( 2.0, uBallPositions[13], rayOrigin, rayDirection ); 225 | if (d < t) 226 | { 227 | t = d; 228 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[13]; 229 | hitEmission = vec3(0); 230 | hitColor = vec3(1.0, 1.0, 0.0); 231 | hitType = COAT; 232 | hitObjectID = float(objectCount); 233 | } 234 | objectCount++; 235 | 236 | d = SphereIntersect( 2.0, uBallPositions[14], rayOrigin, rayDirection ); 237 | if (d < t) 238 | { 239 | t = d; 240 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[14]; 241 | hitEmission = vec3(0); 242 | hitColor = vec3(1.0, 1.0, 0.0); 243 | hitType = COAT; 244 | hitObjectID = float(objectCount); 245 | } 246 | objectCount++; 247 | 248 | d = SphereIntersect( 2.0, uBallPositions[15], rayOrigin, rayDirection ); 249 | if (d < t) 250 | { 251 | t = d; 252 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[15]; 253 | hitEmission = vec3(0); 254 | hitColor = vec3(1.0, 1.0, 0.0); 255 | hitType = COAT; 256 | hitObjectID = float(objectCount); 257 | } 258 | objectCount++; 259 | 260 | // pockets / lights 261 | 262 | d = SphereIntersect( spheres[0].radius, spheres[0].position, rayOrigin, rayDirection ); 263 | if (d < t) 264 | { 265 | t = d; 266 | hitNormal = (rayOrigin + rayDirection * t) - spheres[0].position; 267 | hitEmission = spheres[0].emission; 268 | hitColor = spheres[0].color; 269 | hitType = spheres[0].type; 270 | hitObjectID = float(objectCount); 271 | } 272 | objectCount++; 273 | 274 | d = SphereIntersect( spheres[1].radius, spheres[1].position, rayOrigin, rayDirection ); 275 | if (d < t) 276 | { 277 | t = d; 278 | hitNormal = (rayOrigin + rayDirection * t) - spheres[1].position; 279 | hitEmission = spheres[1].emission; 280 | hitColor = spheres[1].color; 281 | hitType = spheres[1].type; 282 | hitObjectID = float(objectCount); 283 | } 284 | objectCount++; 285 | 286 | d = SphereIntersect( spheres[2].radius, spheres[2].position, rayOrigin, rayDirection ); 287 | if (d < t) 288 | { 289 | t = d; 290 | hitNormal = (rayOrigin + rayDirection * t) - spheres[2].position; 291 | hitEmission = spheres[2].emission; 292 | hitColor = spheres[2].color; 293 | hitType = spheres[2].type; 294 | hitObjectID = float(objectCount); 295 | } 296 | objectCount++; 297 | 298 | d = SphereIntersect( spheres[3].radius, spheres[3].position, rayOrigin, rayDirection ); 299 | if (d < t) 300 | { 301 | t = d; 302 | hitNormal = (rayOrigin + rayDirection * t) - spheres[3].position; 303 | hitEmission = spheres[3].emission; 304 | hitColor = spheres[3].color; 305 | hitType = spheres[3].type; 306 | hitObjectID = float(objectCount); 307 | } 308 | objectCount++; 309 | 310 | d = SphereIntersect( spheres[4].radius, spheres[4].position, rayOrigin, rayDirection ); 311 | if (d < t) 312 | { 313 | t = d; 314 | hitNormal = (rayOrigin + rayDirection * t) - spheres[4].position; 315 | hitEmission = spheres[4].emission; 316 | hitColor = spheres[4].color; 317 | hitType = spheres[4].type; 318 | hitObjectID = float(objectCount); 319 | } 320 | objectCount++; 321 | 322 | d = SphereIntersect( spheres[5].radius, spheres[5].position, rayOrigin, rayDirection ); 323 | if (d < t) 324 | { 325 | t = d; 326 | hitNormal = (rayOrigin + rayDirection * t) - spheres[5].position; 327 | hitEmission = spheres[5].emission; 328 | hitColor = spheres[5].color; 329 | hitType = spheres[5].type; 330 | hitObjectID = float(objectCount); 331 | } 332 | objectCount++; 333 | 334 | d = SphereIntersect( spheres[6].radius, spheres[6].position, rayOrigin, rayDirection ); 335 | if (d < t) 336 | { 337 | t = d; 338 | hitNormal = (rayOrigin + rayDirection * t) - spheres[6].position; 339 | hitEmission = spheres[6].emission; 340 | hitColor = spheres[6].color; 341 | hitType = spheres[6].type; 342 | hitObjectID = float(objectCount); 343 | } 344 | objectCount++; 345 | 346 | d = SphereIntersect( spheres[7].radius, spheres[7].position, rayOrigin, rayDirection ); 347 | if (d < t) 348 | { 349 | t = d; 350 | hitNormal = (rayOrigin + rayDirection * t) - spheres[7].position; 351 | hitEmission = spheres[7].emission; 352 | hitColor = spheres[7].color; 353 | hitType = spheres[7].type; 354 | hitObjectID = float(objectCount); 355 | } 356 | objectCount++; 357 | 358 | 359 | return t; 360 | 361 | } // end float SceneIntersect( ) 362 | 363 | 364 | //----------------------------------------------------------------------------------------------------------------------------- 365 | vec3 CalculateRadiance(out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness ) 366 | //----------------------------------------------------------------------------------------------------------------------------- 367 | { 368 | Sphere lightChoice; 369 | 370 | vec3 accumCol = vec3(0); 371 | vec3 mask = vec3(1); 372 | vec3 reflectionMask = vec3(1); 373 | vec3 reflectionRayOrigin = vec3(0); 374 | vec3 reflectionRayDirection = vec3(0); 375 | vec3 diffuseBounceMask = vec3(1); 376 | vec3 diffuseBounceRayOrigin = vec3(0); 377 | vec3 diffuseBounceRayDirection = vec3(0); 378 | vec3 dirToLight; 379 | vec3 x, n, nl; 380 | 381 | float t; 382 | float nc, nt, ratioIoR, Re, Tr; 383 | float weight; 384 | float previousObjectID; 385 | 386 | int reflectionBounces = -1; 387 | int diffuseCount = 0; 388 | int intBest = 0; 389 | int previousIntersecType = -100; 390 | hitType = -100; 391 | 392 | int bounceIsSpecular = TRUE; 393 | int sampleLight = FALSE; 394 | int willNeedReflectionRay = FALSE; 395 | int isReflectionTime = FALSE; 396 | int reflectionNeedsToBeSharp = FALSE; 397 | int willNeedDiffuseBounceRay = FALSE; 398 | int isDiffuseBounceTime = FALSE; 399 | 400 | 401 | for (int bounces = 0; bounces < 10; bounces++) 402 | { 403 | if (isReflectionTime == TRUE) 404 | reflectionBounces++; 405 | 406 | previousIntersecType = hitType; 407 | previousObjectID = hitObjectID; 408 | 409 | t = SceneIntersect(); 410 | 411 | // //not used in this scene because we are inside a large box shape - no rays can escape 412 | if (t == INFINITY) 413 | { 414 | break; 415 | } 416 | 417 | 418 | // useful data 419 | n = normalize(hitNormal); 420 | nl = dot(n, rayDirection) < 0.0 ? n : -n; 421 | x = rayOrigin + rayDirection * t; 422 | 423 | if (bounces == 0) 424 | { 425 | objectID = hitObjectID; 426 | } 427 | if (isReflectionTime == FALSE && diffuseCount == 0 && hitObjectID != previousObjectID) 428 | { 429 | objectNormal += n; 430 | objectColor += hitColor; 431 | } 432 | 433 | 434 | 435 | if (hitType == LIGHT) 436 | { 437 | if (diffuseCount == 0 && isReflectionTime == FALSE) 438 | { 439 | pixelSharpness = 1.0; 440 | accumCol += mask * clamp(hitEmission, 0.0, 2.0); 441 | } 442 | 443 | else if (isReflectionTime == TRUE && bounceIsSpecular == TRUE) 444 | { 445 | objectNormal += nl; 446 | //objectColor = hitColor; 447 | objectID += hitObjectID; 448 | accumCol += mask * hitEmission; 449 | } 450 | else if (sampleLight == TRUE) 451 | { 452 | accumCol += mask * clamp(hitEmission, 0.0, 8.0); 453 | } 454 | 455 | if (willNeedDiffuseBounceRay == TRUE) 456 | { 457 | mask = diffuseBounceMask; 458 | rayOrigin = diffuseBounceRayOrigin; 459 | rayDirection = diffuseBounceRayDirection; 460 | 461 | willNeedDiffuseBounceRay = FALSE; 462 | bounceIsSpecular = FALSE; 463 | sampleLight = FALSE; 464 | isDiffuseBounceTime = TRUE; 465 | isReflectionTime = FALSE; 466 | diffuseCount = 1; 467 | continue; 468 | } 469 | 470 | if (willNeedReflectionRay == TRUE) 471 | { 472 | mask = reflectionMask; 473 | rayOrigin = reflectionRayOrigin; 474 | rayDirection = reflectionRayDirection; 475 | 476 | willNeedReflectionRay = FALSE; 477 | bounceIsSpecular = TRUE; 478 | sampleLight = FALSE; 479 | isReflectionTime = TRUE; 480 | isDiffuseBounceTime = FALSE; 481 | continue; 482 | } 483 | 484 | //reached a light, so we can exit 485 | break; 486 | } 487 | 488 | 489 | // if we get here and sampleLight is still true, shadow ray failed to find the light source 490 | // the ray hit an occluding object along its way to the light 491 | if (sampleLight == TRUE) 492 | { 493 | if (willNeedDiffuseBounceRay == TRUE) 494 | { 495 | mask = diffuseBounceMask; 496 | rayOrigin = diffuseBounceRayOrigin; 497 | rayDirection = diffuseBounceRayDirection; 498 | 499 | willNeedDiffuseBounceRay = FALSE; 500 | bounceIsSpecular = FALSE; 501 | sampleLight = FALSE; 502 | isDiffuseBounceTime = TRUE; 503 | isReflectionTime = FALSE; 504 | diffuseCount = 1; 505 | continue; 506 | } 507 | 508 | if (willNeedReflectionRay == TRUE) 509 | { 510 | mask = reflectionMask; 511 | rayOrigin = reflectionRayOrigin; 512 | rayDirection = reflectionRayDirection; 513 | 514 | willNeedReflectionRay = FALSE; 515 | bounceIsSpecular = TRUE; 516 | sampleLight = FALSE; 517 | isReflectionTime = TRUE; 518 | isDiffuseBounceTime = FALSE; 519 | continue; 520 | } 521 | 522 | break; 523 | } 524 | 525 | 526 | if (hitType == DIFF) // Ideal DIFFUSE reflection 527 | { 528 | diffuseCount++; 529 | 530 | mask *= hitColor; 531 | 532 | bounceIsSpecular = FALSE; 533 | 534 | rayOrigin = x + nl * uEPS_intersect; 535 | 536 | if (diffuseCount == 1) 537 | { 538 | diffuseBounceMask = mask; 539 | diffuseBounceRayOrigin = rayOrigin; 540 | diffuseBounceRayDirection = randomCosWeightedDirectionInHemisphere(nl); 541 | willNeedDiffuseBounceRay = TRUE; 542 | } 543 | 544 | // loop through the 8 sphere lights and find the best one to sample 545 | for (int i = 0; i < N_SPHERES; i++) 546 | { 547 | intBest = rng() < dot(nl, normalize(spheres[i].position - x)) ? i : intBest; 548 | } 549 | lightChoice = spheres[intBest]; 550 | 551 | dirToLight = randomDirectionInSpecularLobe(normalize(lightChoice.position - x), 0.15); 552 | mask *= N_LIGHTS; 553 | mask *= max(0.0, dot(nl, dirToLight)) * 0.005; 554 | 555 | rayDirection = dirToLight; 556 | sampleLight = TRUE; 557 | continue; 558 | 559 | } // end if (hitType == DIFF) 560 | 561 | 562 | if (hitType == REFR) // Ideal dielectric REFRACTION 563 | { 564 | nc = 1.0; // IOR of Air 565 | nt = 1.3; // IOR of special Glass aiming cueball for this game 566 | 567 | // use 'nl' instead of 'n' in below function arguments for non-ray-bending clear materials 568 | Re = calcFresnelReflectance(rayDirection, n, nc, nt, ratioIoR); 569 | Tr = 1.0 - Re; 570 | 571 | if (Re == 1.0) 572 | { 573 | rayDirection = reflect(rayDirection, nl); 574 | rayOrigin = x + nl * uEPS_intersect; 575 | continue; 576 | } 577 | 578 | if (bounces == 0) 579 | { 580 | reflectionMask = mask * Re; 581 | reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface 582 | reflectionRayOrigin = x + nl * uEPS_intersect; 583 | willNeedReflectionRay = TRUE; 584 | } 585 | 586 | // make glass aiming cueball brighter 587 | // if (diffuseCount == 0 && bounces == 1) 588 | // mask = uEPS_intersect == 1.0 ? vec3(6) : vec3(2); // make even brighter on mobile 589 | 590 | mask *= hitColor; 591 | mask *= Tr; 592 | 593 | // transmit ray through surface 594 | rayDirection = rayDirection; // this lets the viewing ray pass through without bending due to refraction 595 | rayOrigin = x - nl * uEPS_intersect; 596 | 597 | continue; 598 | 599 | } // end if (hitType == REFR) 600 | 601 | 602 | if (hitType == COAT) // Diffuse object underneath with ClearCoat on top 603 | { 604 | nc = 1.0; // IOR of Air 605 | nt = 1.8; // IOR of very thick ClearCoat for pool balls 606 | Re = calcFresnelReflectance(rayDirection, nl, nc, nt, ratioIoR); 607 | Tr = 1.0 - Re; 608 | 609 | if (diffuseCount == 0 && hitObjectID != previousObjectID) 610 | { 611 | reflectionMask = mask * Re; 612 | reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface 613 | reflectionRayOrigin = x + nl * uEPS_intersect; 614 | willNeedReflectionRay = TRUE; 615 | } 616 | 617 | diffuseCount++; 618 | 619 | mask *= Tr; 620 | mask *= hitColor; 621 | 622 | bounceIsSpecular = FALSE; 623 | 624 | rayOrigin = x + nl * uEPS_intersect; 625 | 626 | if (diffuseCount == 1) 627 | { 628 | diffuseBounceMask = mask; 629 | diffuseBounceRayOrigin = rayOrigin; 630 | diffuseBounceRayDirection = randomCosWeightedDirectionInHemisphere(nl); 631 | willNeedDiffuseBounceRay = TRUE; 632 | } 633 | 634 | // loop through the 8 sphere lights and find the best one to sample 635 | for (int i = 0; i < N_SPHERES; i++) 636 | { 637 | intBest = rng() * 1.5 < dot(nl, normalize(spheres[i].position - x)) ? i : intBest; 638 | } 639 | lightChoice = spheres[intBest]; 640 | 641 | dirToLight = randomDirectionInSpecularLobe(normalize(lightChoice.position - x), 0.2); 642 | 643 | mask *= N_LIGHTS; 644 | mask *= max(0.0, dot(nl, dirToLight)) * 0.03;//0.01; 645 | 646 | rayDirection = dirToLight; 647 | sampleLight = TRUE; 648 | continue; 649 | 650 | } //end if (hitType == COAT) 651 | 652 | } // end for (int bounces = 0; bounces < 10; bounces++) 653 | 654 | return max(vec3(0), accumCol); 655 | 656 | } // end vec3 CalculateRadiance( out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness ) 657 | 658 | 659 | //----------------------------------------------------------------------- 660 | void SetupScene(void) 661 | //----------------------------------------------------------------------- 662 | { 663 | vec3 z = vec3(0); 664 | vec3 L = vec3(1, 1, 1) * 10.0; // bright White light 665 | 666 | spheres[0] = Sphere(10.0, uBallPositions[16], L, z, LIGHT); // bottom left front spherical light 667 | spheres[1] = Sphere(10.0, uBallPositions[17], L, z, LIGHT); // bottom right front spherical light 668 | spheres[2] = Sphere(10.0, uBallPositions[18], L, z, LIGHT); // top left front spherical light 669 | spheres[3] = Sphere(10.0, uBallPositions[19], L, z, LIGHT); // top right front spherical light 670 | 671 | spheres[4] = Sphere(10.0, uBallPositions[20], L, z, LIGHT); // bottom left back spherical light 672 | spheres[5] = Sphere(10.0, uBallPositions[21], L, z, LIGHT); // bottom right back spherical light 673 | spheres[6] = Sphere(10.0, uBallPositions[22], L, z, LIGHT); // top left back spherical light 674 | spheres[7] = Sphere(10.0, uBallPositions[23], L, z, LIGHT); // top right back spherical light 675 | 676 | boxes[0] = Box(vec3(-50.5), vec3(50.5), z, vec3(0.0, 0.05, 0.99), DIFF); // Diffuse Box 677 | } 678 | 679 | 680 | 681 | 682 | // tentFilter from Peter Shirley's 'Realistic Ray Tracing (2nd Edition)' book, pg. 60 683 | float tentFilter(float x) 684 | { 685 | return (x < 0.5) ? sqrt(2.0 * x) - 1.0 : 1.0 - sqrt(2.0 - (2.0 * x)); 686 | } 687 | 688 | 689 | void main( void ) 690 | { 691 | vec3 camRight = vec3( uCameraMatrix[0][0], uCameraMatrix[0][1], uCameraMatrix[0][2]); 692 | vec3 camUp = vec3( uCameraMatrix[1][0], uCameraMatrix[1][1], uCameraMatrix[1][2]); 693 | vec3 camForward = vec3(-uCameraMatrix[2][0], -uCameraMatrix[2][1], -uCameraMatrix[2][2]); 694 | // the following is not needed - three.js has a built-in uniform named cameraPosition 695 | //vec3 camPos = vec3( uCameraMatrix[3][0], uCameraMatrix[3][1], uCameraMatrix[3][2]); 696 | 697 | // calculate unique seed for rng() function 698 | seed = uvec2(uFrameCounter, uFrameCounter + 1.0) * uvec2(gl_FragCoord); 699 | // initialize rand() variables 700 | randNumber = 0.0; // the final randomly-generated number (range: 0.0 to 1.0) 701 | blueNoise = texelFetch(tBlueNoiseTexture, ivec2(mod(floor(gl_FragCoord.xy), 128.0)), 0).r; 702 | 703 | vec2 pixelOffset = vec2( tentFilter(rand()), tentFilter(rand()) ); 704 | pixelOffset *= uCameraIsMoving ? 0.5 : 1.5; //1.5 needed to smooth out edges of pool balls 705 | 706 | // we must map pixelPos into the range -1.0 to +1.0 707 | vec2 pixelPos = ((gl_FragCoord.xy + vec2(0.5) + pixelOffset) / uResolution) * 2.0 - 1.0; 708 | 709 | vec3 rayDir = normalize( pixelPos.x * camRight * uULen + pixelPos.y * camUp * uVLen + camForward ); 710 | 711 | // depth of field 712 | vec3 focalPoint = uFocusDistance * rayDir; 713 | float randomAngle = rng() * TWO_PI; // pick random point on aperture 714 | float randomRadius = rng() * uApertureSize; 715 | vec3 randomAperturePos = ( cos(randomAngle) * camRight + sin(randomAngle) * camUp ) * sqrt(randomRadius); 716 | // point on aperture to focal point 717 | vec3 finalRayDir = normalize(focalPoint - randomAperturePos); 718 | 719 | rayOrigin = cameraPosition + randomAperturePos; 720 | rayDirection = finalRayDir; 721 | 722 | SetupScene(); 723 | 724 | // Edge Detection - don't want to blur edges where either surface normals change abruptly (i.e. room wall corners), objects overlap each other (i.e. edge of a foreground sphere in front of another sphere right behind it), 725 | // or an abrupt color variation on the same smooth surface, even if it has similar surface normals (i.e. checkerboard pattern). Want to keep all of these cases as sharp as possible - no blur filter will be applied. 726 | vec3 objectNormal, objectColor; 727 | float objectID = -INFINITY; 728 | float pixelSharpness = 0.0; 729 | 730 | // perform path tracing and get resulting pixel color 731 | vec4 currentPixel = vec4( vec3(CalculateRadiance(objectNormal, objectColor, objectID, pixelSharpness)), 0.0 ); 732 | 733 | // if difference between normals of neighboring pixels is less than the first edge0 threshold, the white edge line effect is considered off (0.0) 734 | float edge0 = 0.2; // edge0 is the minimum difference required between normals of neighboring pixels to start becoming a white edge line 735 | // any difference between normals of neighboring pixels that is between edge0 and edge1 smoothly ramps up the white edge line brightness (smoothstep 0.0-1.0) 736 | float edge1 = 0.6; // once the difference between normals of neighboring pixels is >= this edge1 threshold, the white edge line is considered fully bright (1.0) 737 | float difference_Nx = fwidth(objectNormal.x); 738 | float difference_Ny = fwidth(objectNormal.y); 739 | float difference_Nz = fwidth(objectNormal.z); 740 | float normalDifference = smoothstep(edge0, edge1, difference_Nx) + smoothstep(edge0, edge1, difference_Ny) + smoothstep(edge0, edge1, difference_Nz); 741 | 742 | float objectDifference = min(fwidth(objectID), 1.0); 743 | 744 | float colorDifference = (fwidth(objectColor.r) + fwidth(objectColor.g) + fwidth(objectColor.b)) > 0.0 ? 1.0 : 0.0; 745 | // white-line debug visualization for normal difference 746 | //currentPixel.rgb += (rng() * 1.5) * vec3(normalDifference); 747 | // white-line debug visualization for object difference 748 | //currentPixel.rgb += (rng() * 1.5) * vec3(objectDifference); 749 | // white-line debug visualization for color difference 750 | //currentPixel.rgb += (rng() * 1.5) * vec3(colorDifference); 751 | // white-line debug visualization for all 3 differences 752 | //currentPixel.rgb += (rng() * 1.5) * vec3( clamp(max(normalDifference, max(objectDifference, colorDifference)), 0.0, 1.0) ); 753 | 754 | vec4 previousPixel = texelFetch(tPreviousTexture, ivec2(gl_FragCoord.xy), 0); 755 | 756 | 757 | if (uCameraIsMoving) // camera is currently moving 758 | { 759 | previousPixel.rgb *= 0.5; // motion-blur trail amount (old image) 760 | currentPixel.rgb *= 0.5; // brightness of new image (noisy) 761 | 762 | previousPixel.a = 0.0; 763 | } 764 | else 765 | { 766 | previousPixel.rgb *= 0.9; // motion-blur trail amount (old image) 767 | currentPixel.rgb *= 0.1; // brightness of new image (noisy) 768 | } 769 | 770 | currentPixel.a = pixelSharpness; 771 | 772 | // check for all edges that are not light sources 773 | if (pixelSharpness < 1.01 && (colorDifference >= 1.0 || normalDifference >= 0.9 || objectDifference >= 1.0)) // all other edges 774 | currentPixel.a = pixelSharpness = 1.0; 775 | 776 | // makes light source edges (shape boundaries) more stable 777 | // if (previousPixel.a == 1.01) 778 | // currentPixel.a = 1.01; 779 | 780 | // makes sharp edges more stable 781 | if (previousPixel.a == 1.0) 782 | currentPixel.a = 1.0; 783 | 784 | // for dynamic scenes (to clear out old, dark, sharp pixel trails left behind from moving objects) 785 | if (previousPixel.a == 1.0 && rng() < 0.05) 786 | currentPixel.a = 0.0; 787 | 788 | 789 | pc_fragColor = vec4(previousPixel.rgb + currentPixel.rgb, currentPixel.a); 790 | } 791 | -------------------------------------------------------------------------------- /shaders/AntiGravityPool_Fragment_Mobile.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | precision highp sampler2D; 4 | 5 | #include 6 | 7 | uniform vec3 uBallPositions[24]; 8 | uniform bool uShotIsInProgress; 9 | 10 | #define N_LIGHTS 8.0 11 | #define N_SPHERES 8 12 | #define N_BOXES 1 13 | 14 | //----------------------------------------------------------------------- 15 | 16 | vec3 rayOrigin, rayDirection; 17 | // recorded intersection data: 18 | vec3 hitNormal, hitEmission, hitColor; 19 | vec2 hitUV; 20 | float hitObjectID = -INFINITY; 21 | int hitType = -100; 22 | 23 | struct Sphere { float radius; vec3 position; vec3 emission; vec3 color; int type; }; 24 | struct Box { vec3 minCorner; vec3 maxCorner; vec3 emission; vec3 color; int type; }; 25 | 26 | Sphere spheres[N_SPHERES]; 27 | Box boxes[N_BOXES]; 28 | 29 | 30 | #include 31 | 32 | #include 33 | 34 | #include 35 | 36 | #include 37 | 38 | 39 | 40 | //--------------------------------------------------------------------------------------- 41 | float SceneIntersect() 42 | //--------------------------------------------------------------------------------------- 43 | { 44 | float d = INFINITY; 45 | float t = INFINITY; 46 | vec3 n; 47 | int objectCount = 0; 48 | 49 | d = BoxInteriorIntersect( boxes[0].minCorner, boxes[0].maxCorner, rayOrigin, rayDirection, n ); 50 | if (d < t) 51 | { 52 | t = d; 53 | hitNormal = n; 54 | hitEmission = boxes[0].emission; 55 | hitColor = boxes[0].color; 56 | hitType = boxes[0].type; 57 | hitObjectID = float(objectCount); 58 | } 59 | objectCount++; 60 | 61 | // white cueball / glass aiming ball 62 | d = SphereIntersect( 2.0, uBallPositions[0], rayOrigin, rayDirection ); 63 | if (d < t) 64 | { 65 | t = d; 66 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[0]; 67 | hitEmission = vec3(0); 68 | //hitColor = uShotIsInProgress ? vec3(1) : vec3(2); 69 | hitColor = vec3(1); 70 | hitType = uShotIsInProgress ? COAT : REFR; 71 | hitObjectID = float(objectCount); 72 | } 73 | objectCount++; 74 | 75 | // black ball 76 | d = SphereIntersect( 2.0, uBallPositions[1], rayOrigin, rayDirection ); 77 | if (d < t) 78 | { 79 | t = d; 80 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[1]; 81 | hitEmission = vec3(0); 82 | hitColor = vec3(0.005); 83 | hitType = COAT; 84 | hitObjectID = float(objectCount); 85 | } 86 | objectCount++; 87 | 88 | // red balls 89 | 90 | d = SphereIntersect( 2.0, uBallPositions[2], rayOrigin, rayDirection ); 91 | if (d < t) 92 | { 93 | t = d; 94 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[2]; 95 | hitEmission = vec3(0); 96 | hitColor = vec3(1.0, 0.0, 0.0); 97 | hitType = COAT; 98 | hitObjectID = float(objectCount); 99 | } 100 | objectCount++; 101 | 102 | d = SphereIntersect( 2.0, uBallPositions[3], rayOrigin, rayDirection ); 103 | if (d < t) 104 | { 105 | t = d; 106 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[3]; 107 | hitEmission = vec3(0); 108 | hitColor = vec3(1.0, 0.0, 0.0); 109 | hitType = COAT; 110 | hitObjectID = float(objectCount); 111 | } 112 | objectCount++; 113 | 114 | d = SphereIntersect( 2.0, uBallPositions[4], rayOrigin, rayDirection ); 115 | if (d < t) 116 | { 117 | t = d; 118 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[4]; 119 | hitEmission = vec3(0); 120 | hitColor = vec3(1.0, 0.0, 0.0); 121 | hitType = COAT; 122 | hitObjectID = float(objectCount); 123 | } 124 | objectCount++; 125 | 126 | d = SphereIntersect( 2.0, uBallPositions[5], rayOrigin, rayDirection ); 127 | if (d < t) 128 | { 129 | t = d; 130 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[5]; 131 | hitEmission = vec3(0); 132 | hitColor = vec3(1.0, 0.0, 0.0); 133 | hitType = COAT; 134 | hitObjectID = float(objectCount); 135 | } 136 | objectCount++; 137 | 138 | d = SphereIntersect( 2.0, uBallPositions[6], rayOrigin, rayDirection ); 139 | if (d < t) 140 | { 141 | t = d; 142 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[6]; 143 | hitEmission = vec3(0); 144 | hitColor = vec3(1.0, 0.0, 0.0); 145 | hitType = COAT; 146 | hitObjectID = float(objectCount); 147 | } 148 | objectCount++; 149 | 150 | d = SphereIntersect( 2.0, uBallPositions[7], rayOrigin, rayDirection ); 151 | if (d < t) 152 | { 153 | t = d; 154 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[7]; 155 | hitEmission = vec3(0); 156 | hitColor = vec3(1.0, 0.0, 0.0); 157 | hitType = COAT; 158 | hitObjectID = float(objectCount); 159 | } 160 | objectCount++; 161 | 162 | d = SphereIntersect( 2.0, uBallPositions[8], rayOrigin, rayDirection ); 163 | if (d < t) 164 | { 165 | t = d; 166 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[8]; 167 | hitEmission = vec3(0); 168 | hitColor = vec3(1.0, 0.0, 0.0); 169 | hitType = COAT; 170 | hitObjectID = float(objectCount); 171 | } 172 | objectCount++; 173 | 174 | // yellow balls 175 | 176 | d = SphereIntersect( 2.0, uBallPositions[9], rayOrigin, rayDirection ); 177 | if (d < t) 178 | { 179 | t = d; 180 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[9]; 181 | hitEmission = vec3(0); 182 | hitColor = vec3(1.0, 1.0, 0.0); 183 | hitType = COAT; 184 | hitObjectID = float(objectCount); 185 | } 186 | objectCount++; 187 | 188 | d = SphereIntersect( 2.0, uBallPositions[10], rayOrigin, rayDirection ); 189 | if (d < t) 190 | { 191 | t = d; 192 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[10]; 193 | hitEmission = vec3(0); 194 | hitColor = vec3(1.0, 1.0, 0.0); 195 | hitType = COAT; 196 | hitObjectID = float(objectCount); 197 | } 198 | objectCount++; 199 | 200 | d = SphereIntersect( 2.0, uBallPositions[11], rayOrigin, rayDirection ); 201 | if (d < t) 202 | { 203 | t = d; 204 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[11]; 205 | hitEmission = vec3(0); 206 | hitColor = vec3(1.0, 1.0, 0.0); 207 | hitType = COAT; 208 | hitObjectID = float(objectCount); 209 | } 210 | objectCount++; 211 | 212 | d = SphereIntersect( 2.0, uBallPositions[12], rayOrigin, rayDirection ); 213 | if (d < t) 214 | { 215 | t = d; 216 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[12]; 217 | hitEmission = vec3(0); 218 | hitColor = vec3(1.0, 1.0, 0.0); 219 | hitType = COAT; 220 | hitObjectID = float(objectCount); 221 | } 222 | objectCount++; 223 | 224 | d = SphereIntersect( 2.0, uBallPositions[13], rayOrigin, rayDirection ); 225 | if (d < t) 226 | { 227 | t = d; 228 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[13]; 229 | hitEmission = vec3(0); 230 | hitColor = vec3(1.0, 1.0, 0.0); 231 | hitType = COAT; 232 | hitObjectID = float(objectCount); 233 | } 234 | objectCount++; 235 | 236 | d = SphereIntersect( 2.0, uBallPositions[14], rayOrigin, rayDirection ); 237 | if (d < t) 238 | { 239 | t = d; 240 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[14]; 241 | hitEmission = vec3(0); 242 | hitColor = vec3(1.0, 1.0, 0.0); 243 | hitType = COAT; 244 | hitObjectID = float(objectCount); 245 | } 246 | objectCount++; 247 | 248 | d = SphereIntersect( 2.0, uBallPositions[15], rayOrigin, rayDirection ); 249 | if (d < t) 250 | { 251 | t = d; 252 | hitNormal = (rayOrigin + rayDirection * t) - uBallPositions[15]; 253 | hitEmission = vec3(0); 254 | hitColor = vec3(1.0, 1.0, 0.0); 255 | hitType = COAT; 256 | hitObjectID = float(objectCount); 257 | } 258 | objectCount++; 259 | 260 | // pockets / lights 261 | 262 | d = SphereIntersect( spheres[0].radius, spheres[0].position, rayOrigin, rayDirection ); 263 | if (d < t) 264 | { 265 | t = d; 266 | hitNormal = (rayOrigin + rayDirection * t) - spheres[0].position; 267 | hitEmission = spheres[0].emission; 268 | hitColor = spheres[0].color; 269 | hitType = spheres[0].type; 270 | hitObjectID = float(objectCount); 271 | } 272 | objectCount++; 273 | 274 | d = SphereIntersect( spheres[1].radius, spheres[1].position, rayOrigin, rayDirection ); 275 | if (d < t) 276 | { 277 | t = d; 278 | hitNormal = (rayOrigin + rayDirection * t) - spheres[1].position; 279 | hitEmission = spheres[1].emission; 280 | hitColor = spheres[1].color; 281 | hitType = spheres[1].type; 282 | hitObjectID = float(objectCount); 283 | } 284 | objectCount++; 285 | 286 | d = SphereIntersect( spheres[2].radius, spheres[2].position, rayOrigin, rayDirection ); 287 | if (d < t) 288 | { 289 | t = d; 290 | hitNormal = (rayOrigin + rayDirection * t) - spheres[2].position; 291 | hitEmission = spheres[2].emission; 292 | hitColor = spheres[2].color; 293 | hitType = spheres[2].type; 294 | hitObjectID = float(objectCount); 295 | } 296 | objectCount++; 297 | 298 | d = SphereIntersect( spheres[3].radius, spheres[3].position, rayOrigin, rayDirection ); 299 | if (d < t) 300 | { 301 | t = d; 302 | hitNormal = (rayOrigin + rayDirection * t) - spheres[3].position; 303 | hitEmission = spheres[3].emission; 304 | hitColor = spheres[3].color; 305 | hitType = spheres[3].type; 306 | hitObjectID = float(objectCount); 307 | } 308 | objectCount++; 309 | 310 | d = SphereIntersect( spheres[4].radius, spheres[4].position, rayOrigin, rayDirection ); 311 | if (d < t) 312 | { 313 | t = d; 314 | hitNormal = (rayOrigin + rayDirection * t) - spheres[4].position; 315 | hitEmission = spheres[4].emission; 316 | hitColor = spheres[4].color; 317 | hitType = spheres[4].type; 318 | hitObjectID = float(objectCount); 319 | } 320 | objectCount++; 321 | 322 | d = SphereIntersect( spheres[5].radius, spheres[5].position, rayOrigin, rayDirection ); 323 | if (d < t) 324 | { 325 | t = d; 326 | hitNormal = (rayOrigin + rayDirection * t) - spheres[5].position; 327 | hitEmission = spheres[5].emission; 328 | hitColor = spheres[5].color; 329 | hitType = spheres[5].type; 330 | hitObjectID = float(objectCount); 331 | } 332 | objectCount++; 333 | 334 | d = SphereIntersect( spheres[6].radius, spheres[6].position, rayOrigin, rayDirection ); 335 | if (d < t) 336 | { 337 | t = d; 338 | hitNormal = (rayOrigin + rayDirection * t) - spheres[6].position; 339 | hitEmission = spheres[6].emission; 340 | hitColor = spheres[6].color; 341 | hitType = spheres[6].type; 342 | hitObjectID = float(objectCount); 343 | } 344 | objectCount++; 345 | 346 | d = SphereIntersect( spheres[7].radius, spheres[7].position, rayOrigin, rayDirection ); 347 | if (d < t) 348 | { 349 | t = d; 350 | hitNormal = (rayOrigin + rayDirection * t) - spheres[7].position; 351 | hitEmission = spheres[7].emission; 352 | hitColor = spheres[7].color; 353 | hitType = spheres[7].type; 354 | hitObjectID = float(objectCount); 355 | } 356 | objectCount++; 357 | 358 | 359 | return t; 360 | 361 | } // end float SceneIntersect( ) 362 | 363 | 364 | //----------------------------------------------------------------------------------------------------------------------------- 365 | vec3 CalculateRadiance(out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness ) 366 | //----------------------------------------------------------------------------------------------------------------------------- 367 | { 368 | Sphere lightChoice; 369 | 370 | vec3 accumCol = vec3(0); 371 | vec3 mask = vec3(1); 372 | vec3 reflectionMask = vec3(1); 373 | vec3 reflectionRayOrigin = vec3(0); 374 | vec3 reflectionRayDirection = vec3(0); 375 | vec3 dirToLight; 376 | vec3 x, n, nl; 377 | 378 | float t; 379 | float nc, nt, ratioIoR, Re, Tr; 380 | float weight; 381 | float previousObjectID; 382 | 383 | int reflectionBounces = -1; 384 | int diffuseCount = 0; 385 | int intBest = 0; 386 | int previousIntersecType = -100; 387 | hitType = -100; 388 | 389 | int bounceIsSpecular = TRUE; 390 | int sampleLight = FALSE; 391 | int willNeedReflectionRay = FALSE; 392 | int isReflectionTime = FALSE; 393 | int reflectionNeedsToBeSharp = FALSE; 394 | 395 | 396 | for (int bounces = 0; bounces < 8; bounces++) 397 | { 398 | if (isReflectionTime == TRUE) 399 | reflectionBounces++; 400 | 401 | previousIntersecType = hitType; 402 | previousObjectID = hitObjectID; 403 | 404 | t = SceneIntersect(); 405 | 406 | // //not used in this scene because we are inside a large box shape - no rays can escape 407 | if (t == INFINITY) 408 | { 409 | break; 410 | } 411 | 412 | 413 | // useful data 414 | n = normalize(hitNormal); 415 | nl = dot(n, rayDirection) < 0.0 ? n : -n; 416 | x = rayOrigin + rayDirection * t; 417 | 418 | if (bounces == 0) 419 | { 420 | objectID = hitObjectID; 421 | } 422 | if (isReflectionTime == FALSE && diffuseCount == 0 && hitObjectID != previousObjectID) 423 | { 424 | objectNormal += n; 425 | objectColor += hitColor; 426 | } 427 | 428 | 429 | 430 | if (hitType == LIGHT) 431 | { 432 | if (diffuseCount == 0 && isReflectionTime == FALSE) 433 | { 434 | pixelSharpness = 1.0; 435 | accumCol += mask * clamp(hitEmission, 0.0, 2.0); 436 | } 437 | 438 | else if (isReflectionTime == TRUE && bounceIsSpecular == TRUE) 439 | { 440 | objectNormal += nl; 441 | //objectColor = hitColor; 442 | objectID += hitObjectID; 443 | accumCol += mask * hitEmission; 444 | } 445 | else if (sampleLight == TRUE) 446 | { 447 | accumCol += mask * clamp(hitEmission, 0.0, 8.0); 448 | } 449 | 450 | if (willNeedReflectionRay == TRUE) 451 | { 452 | mask = reflectionMask; 453 | rayOrigin = reflectionRayOrigin; 454 | rayDirection = reflectionRayDirection; 455 | 456 | willNeedReflectionRay = FALSE; 457 | bounceIsSpecular = TRUE; 458 | sampleLight = FALSE; 459 | isReflectionTime = TRUE; 460 | continue; 461 | } 462 | 463 | //reached a light, so we can exit 464 | break; 465 | } 466 | 467 | 468 | // if we get here and sampleLight is still true, shadow ray failed to find the light source 469 | // the ray hit an occluding object along its way to the light 470 | if (sampleLight == TRUE) 471 | { 472 | if (willNeedReflectionRay == TRUE) 473 | { 474 | mask = reflectionMask; 475 | rayOrigin = reflectionRayOrigin; 476 | rayDirection = reflectionRayDirection; 477 | 478 | willNeedReflectionRay = FALSE; 479 | bounceIsSpecular = TRUE; 480 | sampleLight = FALSE; 481 | isReflectionTime = TRUE; 482 | continue; 483 | } 484 | 485 | break; 486 | } 487 | 488 | 489 | if (hitType == DIFF) // Ideal DIFFUSE reflection 490 | { 491 | diffuseCount++; 492 | 493 | mask *= hitColor; 494 | 495 | bounceIsSpecular = FALSE; 496 | 497 | if (diffuseCount == 1 && rand() < 0.5) 498 | { 499 | mask *= 2.0; 500 | // choose random Diffuse sample vector 501 | rayDirection = randomCosWeightedDirectionInHemisphere(nl); 502 | rayOrigin = x + nl * uEPS_intersect; 503 | continue; 504 | } 505 | 506 | // loop through the 8 sphere lights and find the best one to sample 507 | for (int i = 0; i < N_SPHERES; i++) 508 | { 509 | intBest = rng() < dot(nl, normalize(spheres[i].position - x)) ? i : intBest; 510 | } 511 | lightChoice = spheres[intBest]; 512 | 513 | dirToLight = randomDirectionInSpecularLobe(normalize(lightChoice.position - x), 0.15); 514 | mask *= diffuseCount == 1 ? 2.0 : 1.0; 515 | mask *= N_LIGHTS; 516 | mask *= max(0.0, dot(nl, dirToLight)) * 0.005; 517 | 518 | rayDirection = dirToLight; 519 | rayOrigin = x + nl * uEPS_intersect; 520 | 521 | sampleLight = TRUE; 522 | continue; 523 | 524 | } // end if (hitType == DIFF) 525 | 526 | 527 | if (hitType == REFR) // Ideal dielectric REFRACTION 528 | { 529 | nc = 1.0; // IOR of Air 530 | nt = 1.3; // IOR of special Glass aiming cueball for this game 531 | 532 | // use 'nl' instead of 'n' in below function arguments for non-ray-bending clear materials 533 | Re = calcFresnelReflectance(rayDirection, n, nc, nt, ratioIoR); 534 | Tr = 1.0 - Re; 535 | 536 | if (Re == 1.0) 537 | { 538 | rayDirection = reflect(rayDirection, nl); 539 | rayOrigin = x + nl * uEPS_intersect; 540 | continue; 541 | } 542 | 543 | if (bounces == 0) 544 | { 545 | reflectionMask = mask * Re; 546 | reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface 547 | reflectionRayOrigin = x + nl * uEPS_intersect; 548 | willNeedReflectionRay = TRUE; 549 | } 550 | 551 | // make glass aiming cueball brighter 552 | // if (diffuseCount == 0 && bounces == 1) 553 | // mask = uEPS_intersect == 1.0 ? vec3(6) : vec3(2); // make even brighter on mobile 554 | 555 | mask *= hitColor; 556 | mask *= Tr; 557 | 558 | // transmit ray through surface 559 | rayDirection = rayDirection; // this lets the viewing ray pass through without bending due to refraction 560 | rayOrigin = x - nl * uEPS_intersect; 561 | 562 | continue; 563 | 564 | } // end if (hitType == REFR) 565 | 566 | 567 | if (hitType == COAT) // Diffuse object underneath with ClearCoat on top 568 | { 569 | nc = 1.0; // IOR of Air 570 | nt = 1.8; // IOR of very thick ClearCoat for pool balls 571 | Re = calcFresnelReflectance(rayDirection, nl, nc, nt, ratioIoR); 572 | Tr = 1.0 - Re; 573 | 574 | if (diffuseCount == 0 && hitObjectID != previousObjectID) 575 | { 576 | reflectionMask = mask * Re; 577 | reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface 578 | reflectionRayOrigin = x + nl * uEPS_intersect; 579 | willNeedReflectionRay = TRUE; 580 | } 581 | 582 | diffuseCount++; 583 | 584 | mask *= Tr; 585 | mask *= hitColor; 586 | 587 | bounceIsSpecular = FALSE; 588 | 589 | // loop through the 8 sphere lights and find the best one to sample 590 | for (int i = 0; i < N_SPHERES; i++) 591 | { 592 | intBest = rng() * 1.5 < dot(nl, normalize(spheres[i].position - x)) ? i : intBest; 593 | } 594 | lightChoice = spheres[intBest]; 595 | 596 | dirToLight = randomDirectionInSpecularLobe(normalize(lightChoice.position - x), 0.2); 597 | 598 | mask *= N_LIGHTS; 599 | mask *= max(0.0, dot(nl, dirToLight)) * 0.03;//0.01; 600 | 601 | rayDirection = dirToLight; 602 | rayOrigin = x + nl * uEPS_intersect; 603 | 604 | sampleLight = TRUE; 605 | continue; 606 | 607 | } //end if (hitType == COAT) 608 | 609 | } // end for (int bounces = 0; bounces < 6; bounces++) 610 | 611 | return max(vec3(0), accumCol); 612 | 613 | } // end vec3 CalculateRadiance( out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness ) 614 | 615 | 616 | //----------------------------------------------------------------------- 617 | void SetupScene(void) 618 | //----------------------------------------------------------------------- 619 | { 620 | vec3 z = vec3(0); 621 | vec3 L = vec3(1, 1, 1) * 10.0; // bright White light 622 | 623 | spheres[0] = Sphere(10.0, uBallPositions[16], L, z, LIGHT); // bottom left front spherical light 624 | spheres[1] = Sphere(10.0, uBallPositions[17], L, z, LIGHT); // bottom right front spherical light 625 | spheres[2] = Sphere(10.0, uBallPositions[18], L, z, LIGHT); // top left front spherical light 626 | spheres[3] = Sphere(10.0, uBallPositions[19], L, z, LIGHT); // top right front spherical light 627 | 628 | spheres[4] = Sphere(10.0, uBallPositions[20], L, z, LIGHT); // bottom left back spherical light 629 | spheres[5] = Sphere(10.0, uBallPositions[21], L, z, LIGHT); // bottom right back spherical light 630 | spheres[6] = Sphere(10.0, uBallPositions[22], L, z, LIGHT); // top left back spherical light 631 | spheres[7] = Sphere(10.0, uBallPositions[23], L, z, LIGHT); // top right back spherical light 632 | 633 | boxes[0] = Box(vec3(-50.5), vec3(50.5), z, vec3(0.0, 0.05, 0.99), DIFF); // Diffuse Box 634 | } 635 | 636 | 637 | 638 | 639 | // tentFilter from Peter Shirley's 'Realistic Ray Tracing (2nd Edition)' book, pg. 60 640 | float tentFilter(float x) 641 | { 642 | return (x < 0.5) ? sqrt(2.0 * x) - 1.0 : 1.0 - sqrt(2.0 - (2.0 * x)); 643 | } 644 | 645 | 646 | void main( void ) 647 | { 648 | vec3 camRight = vec3( uCameraMatrix[0][0], uCameraMatrix[0][1], uCameraMatrix[0][2]); 649 | vec3 camUp = vec3( uCameraMatrix[1][0], uCameraMatrix[1][1], uCameraMatrix[1][2]); 650 | vec3 camForward = vec3(-uCameraMatrix[2][0], -uCameraMatrix[2][1], -uCameraMatrix[2][2]); 651 | // the following is not needed - three.js has a built-in uniform named cameraPosition 652 | //vec3 camPos = vec3( uCameraMatrix[3][0], uCameraMatrix[3][1], uCameraMatrix[3][2]); 653 | 654 | // calculate unique seed for rng() function 655 | seed = uvec2(uFrameCounter, uFrameCounter + 1.0) * uvec2(gl_FragCoord); 656 | // initialize rand() variables 657 | randNumber = 0.0; // the final randomly-generated number (range: 0.0 to 1.0) 658 | blueNoise = texelFetch(tBlueNoiseTexture, ivec2(mod(floor(gl_FragCoord.xy), 128.0)), 0).r; 659 | 660 | vec2 pixelOffset = vec2( tentFilter(rand()), tentFilter(rand()) ); 661 | pixelOffset *= uCameraIsMoving ? 0.5 : 1.5; //1.5 needed to smooth out edges of pool balls 662 | 663 | // we must map pixelPos into the range -1.0 to +1.0 664 | vec2 pixelPos = ((gl_FragCoord.xy + vec2(0.5) + pixelOffset) / uResolution) * 2.0 - 1.0; 665 | 666 | vec3 rayDir = normalize( pixelPos.x * camRight * uULen + pixelPos.y * camUp * uVLen + camForward ); 667 | 668 | // depth of field 669 | vec3 focalPoint = uFocusDistance * rayDir; 670 | float randomAngle = rng() * TWO_PI; // pick random point on aperture 671 | float randomRadius = rng() * uApertureSize; 672 | vec3 randomAperturePos = ( cos(randomAngle) * camRight + sin(randomAngle) * camUp ) * sqrt(randomRadius); 673 | // point on aperture to focal point 674 | vec3 finalRayDir = normalize(focalPoint - randomAperturePos); 675 | 676 | rayOrigin = cameraPosition + randomAperturePos; 677 | rayDirection = finalRayDir; 678 | 679 | SetupScene(); 680 | 681 | // Edge Detection - don't want to blur edges where either surface normals change abruptly (i.e. room wall corners), objects overlap each other (i.e. edge of a foreground sphere in front of another sphere right behind it), 682 | // or an abrupt color variation on the same smooth surface, even if it has similar surface normals (i.e. checkerboard pattern). Want to keep all of these cases as sharp as possible - no blur filter will be applied. 683 | vec3 objectNormal, objectColor; 684 | float objectID = -INFINITY; 685 | float pixelSharpness = 0.0; 686 | 687 | // perform path tracing and get resulting pixel color 688 | vec4 currentPixel = vec4( vec3(CalculateRadiance(objectNormal, objectColor, objectID, pixelSharpness)), 0.0 ); 689 | 690 | // if difference between normals of neighboring pixels is less than the first edge0 threshold, the white edge line effect is considered off (0.0) 691 | float edge0 = 0.2; // edge0 is the minimum difference required between normals of neighboring pixels to start becoming a white edge line 692 | // any difference between normals of neighboring pixels that is between edge0 and edge1 smoothly ramps up the white edge line brightness (smoothstep 0.0-1.0) 693 | float edge1 = 0.6; // once the difference between normals of neighboring pixels is >= this edge1 threshold, the white edge line is considered fully bright (1.0) 694 | float difference_Nx = fwidth(objectNormal.x); 695 | float difference_Ny = fwidth(objectNormal.y); 696 | float difference_Nz = fwidth(objectNormal.z); 697 | float normalDifference = smoothstep(edge0, edge1, difference_Nx) + smoothstep(edge0, edge1, difference_Ny) + smoothstep(edge0, edge1, difference_Nz); 698 | 699 | float objectDifference = min(fwidth(objectID), 1.0); 700 | 701 | float colorDifference = (fwidth(objectColor.r) + fwidth(objectColor.g) + fwidth(objectColor.b)) > 0.0 ? 1.0 : 0.0; 702 | // white-line debug visualization for normal difference 703 | //currentPixel.rgb += (rng() * 1.5) * vec3(normalDifference); 704 | // white-line debug visualization for object difference 705 | //currentPixel.rgb += (rng() * 1.5) * vec3(objectDifference); 706 | // white-line debug visualization for color difference 707 | //currentPixel.rgb += (rng() * 1.5) * vec3(colorDifference); 708 | // white-line debug visualization for all 3 differences 709 | //currentPixel.rgb += (rng() * 1.5) * vec3( clamp(max(normalDifference, max(objectDifference, colorDifference)), 0.0, 1.0) ); 710 | 711 | vec4 previousPixel = texelFetch(tPreviousTexture, ivec2(gl_FragCoord.xy), 0); 712 | 713 | 714 | if (uCameraIsMoving) // camera is currently moving 715 | { 716 | previousPixel.rgb *= 0.5; // motion-blur trail amount (old image) 717 | currentPixel.rgb *= 0.5; // brightness of new image (noisy) 718 | 719 | previousPixel.a = 0.0; 720 | } 721 | else 722 | { 723 | previousPixel.rgb *= 0.9; // motion-blur trail amount (old image) 724 | currentPixel.rgb *= 0.1; // brightness of new image (noisy) 725 | } 726 | 727 | currentPixel.a = pixelSharpness; 728 | 729 | // check for all edges that are not light sources 730 | if (pixelSharpness < 1.01 && (colorDifference >= 1.0 || normalDifference >= 0.9 || objectDifference >= 1.0)) // all other edges 731 | currentPixel.a = pixelSharpness = 1.0; 732 | 733 | // makes light source edges (shape boundaries) more stable 734 | // if (previousPixel.a == 1.01) 735 | // currentPixel.a = 1.01; 736 | 737 | // makes sharp edges more stable 738 | if (previousPixel.a == 1.0) 739 | currentPixel.a = 1.0; 740 | 741 | // for dynamic scenes (to clear out old, dark, sharp pixel trails left behind from moving objects) 742 | if (previousPixel.a == 1.0 && rng() < 0.05) 743 | currentPixel.a = 0.0; 744 | 745 | 746 | pc_fragColor = vec4(previousPixel.rgb + currentPixel.rgb, currentPixel.a); 747 | } -------------------------------------------------------------------------------- /shaders/ScreenCopy_Fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | precision highp sampler2D; 4 | 5 | uniform sampler2D tPathTracedImageTexture; 6 | 7 | void main() 8 | { 9 | pc_fragColor = texelFetch(tPathTracedImageTexture, ivec2(gl_FragCoord.xy), 0); 10 | } -------------------------------------------------------------------------------- /shaders/ScreenOutput_Fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | precision highp sampler2D; 4 | 5 | uniform sampler2D tPathTracedImageTexture; 6 | uniform float uSampleCounter; 7 | uniform float uOneOverSampleCounter; 8 | uniform float uPixelEdgeSharpness; 9 | uniform float uEdgeSharpenSpeed; 10 | //uniform float uFilterDecaySpeed; 11 | uniform bool uCameraIsMoving; 12 | uniform bool uSceneIsDynamic; 13 | uniform bool uUseToneMapping; 14 | 15 | #define TRUE 1 16 | #define FALSE 0 17 | 18 | void main() 19 | { 20 | // First, start with a large blur kernel, which will be used on all diffuse 21 | // surfaces. It will blur out the noise, giving a smoother, more uniform color. 22 | // Starting at the current pixel (centerPixel), the algorithm performs an outward search/walk 23 | // moving to the immediate neighbor pixels around the center pixel, and then out farther to 24 | // more distant neighbors. If the outward walk doesn't encounter any 'edge' pixels, it will continue 25 | // until it reaches the maximum extents of the large kernel (a little less than 7x7 pixels, minus the 4 26 | // corners to give a more rounded kernel filter shape). However, while walking/searching outward from 27 | // the center pixel, if the walk encounters an 'edge' boundary pixel, it will not blend (average in) with 28 | // that pixel, and will stop the search/walk from going any further in that direction. This keeps the edge 29 | // boundary pixels non-blurred, and these edges remain sharp in the final image. 30 | 31 | vec4 m37[37]; 32 | 33 | vec2 glFragCoord_xy = gl_FragCoord.xy; 34 | 35 | 36 | m37[ 0] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1, 3)), 0); 37 | m37[ 1] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0, 3)), 0); 38 | m37[ 2] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1, 3)), 0); 39 | m37[ 3] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-2, 2)), 0); 40 | m37[ 4] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1, 2)), 0); 41 | m37[ 5] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0, 2)), 0); 42 | m37[ 6] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1, 2)), 0); 43 | m37[ 7] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 2, 2)), 0); 44 | m37[ 8] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-3, 1)), 0); 45 | m37[ 9] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-2, 1)), 0); 46 | m37[10] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1, 1)), 0); 47 | m37[11] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0, 1)), 0); 48 | m37[12] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1, 1)), 0); 49 | m37[13] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 2, 1)), 0); 50 | m37[14] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 3, 1)), 0); 51 | m37[15] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-3, 0)), 0); 52 | m37[16] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-2, 0)), 0); 53 | m37[17] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1, 0)), 0); 54 | m37[18] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0, 0)), 0); // center pixel 55 | m37[19] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1, 0)), 0); 56 | m37[20] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 2, 0)), 0); 57 | m37[21] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 3, 0)), 0); 58 | m37[22] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-3,-1)), 0); 59 | m37[23] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-2,-1)), 0); 60 | m37[24] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1,-1)), 0); 61 | m37[25] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0,-1)), 0); 62 | m37[26] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1,-1)), 0); 63 | m37[27] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 2,-1)), 0); 64 | m37[28] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 3,-1)), 0); 65 | m37[29] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-2,-2)), 0); 66 | m37[30] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1,-2)), 0); 67 | m37[31] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0,-2)), 0); 68 | m37[32] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1,-2)), 0); 69 | m37[33] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 2,-2)), 0); 70 | m37[34] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2(-1,-3)), 0); 71 | m37[35] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 0,-3)), 0); 72 | m37[36] = texelFetch(tPathTracedImageTexture, ivec2(glFragCoord_xy + vec2( 1,-3)), 0); 73 | 74 | 75 | vec4 centerPixel = m37[18]; 76 | vec3 filteredPixelColor, edgePixelColor; 77 | float threshold = 1.0; 78 | int count = 1; 79 | int nextToAnEdgePixel = FALSE; 80 | 81 | // start with center pixel rgb color 82 | filteredPixelColor = centerPixel.rgb; 83 | 84 | // search above 85 | if (m37[11].a < threshold) 86 | { 87 | filteredPixelColor += m37[11].rgb; 88 | count++; 89 | if (m37[5].a < threshold) 90 | { 91 | filteredPixelColor += m37[5].rgb; 92 | count++; 93 | if (m37[1].a < threshold) 94 | { 95 | filteredPixelColor += m37[1].rgb; 96 | count++; 97 | if (m37[0].a < threshold) 98 | { 99 | filteredPixelColor += m37[0].rgb; 100 | count++; 101 | } 102 | if (m37[2].a < threshold) 103 | { 104 | filteredPixelColor += m37[2].rgb; 105 | count++; 106 | } 107 | } 108 | } 109 | } 110 | else 111 | { 112 | nextToAnEdgePixel = TRUE; 113 | } 114 | 115 | 116 | 117 | // search left 118 | if (m37[17].a < threshold) 119 | { 120 | filteredPixelColor += m37[17].rgb; 121 | count++; 122 | if (m37[16].a < threshold) 123 | { 124 | filteredPixelColor += m37[16].rgb; 125 | count++; 126 | if (m37[15].a < threshold) 127 | { 128 | filteredPixelColor += m37[15].rgb; 129 | count++; 130 | if (m37[8].a < threshold) 131 | { 132 | filteredPixelColor += m37[8].rgb; 133 | count++; 134 | } 135 | if (m37[22].a < threshold) 136 | { 137 | filteredPixelColor += m37[22].rgb; 138 | count++; 139 | } 140 | } 141 | } 142 | } 143 | else 144 | { 145 | nextToAnEdgePixel = TRUE; 146 | } 147 | 148 | // search right 149 | if (m37[19].a < threshold) 150 | { 151 | filteredPixelColor += m37[19].rgb; 152 | count++; 153 | if (m37[20].a < threshold) 154 | { 155 | filteredPixelColor += m37[20].rgb; 156 | count++; 157 | if (m37[21].a < threshold) 158 | { 159 | filteredPixelColor += m37[21].rgb; 160 | count++; 161 | if (m37[14].a < threshold) 162 | { 163 | filteredPixelColor += m37[14].rgb; 164 | count++; 165 | } 166 | if (m37[28].a < threshold) 167 | { 168 | filteredPixelColor += m37[28].rgb; 169 | count++; 170 | } 171 | } 172 | } 173 | } 174 | else 175 | { 176 | nextToAnEdgePixel = TRUE; 177 | } 178 | 179 | // search below 180 | if (m37[25].a < threshold) 181 | { 182 | filteredPixelColor += m37[25].rgb; 183 | count++; 184 | if (m37[31].a < threshold) 185 | { 186 | filteredPixelColor += m37[31].rgb; 187 | count++; 188 | if (m37[35].a < threshold) 189 | { 190 | filteredPixelColor += m37[35].rgb; 191 | count++; 192 | if (m37[34].a < threshold) 193 | { 194 | filteredPixelColor += m37[34].rgb; 195 | count++; 196 | } 197 | if (m37[36].a < threshold) 198 | { 199 | filteredPixelColor += m37[36].rgb; 200 | count++; 201 | } 202 | } 203 | } 204 | } 205 | else 206 | { 207 | nextToAnEdgePixel = TRUE; 208 | } 209 | 210 | // search upper-left diagonal 211 | if (m37[10].a < threshold) 212 | { 213 | filteredPixelColor += m37[10].rgb; 214 | count++; 215 | if (m37[3].a < threshold) 216 | { 217 | filteredPixelColor += m37[3].rgb; 218 | count++; 219 | } 220 | if (m37[4].a < threshold) 221 | { 222 | filteredPixelColor += m37[4].rgb; 223 | count++; 224 | } 225 | if (m37[9].a < threshold) 226 | { 227 | filteredPixelColor += m37[9].rgb; 228 | count++; 229 | } 230 | } 231 | 232 | // search upper-right diagonal 233 | if (m37[12].a < threshold) 234 | { 235 | filteredPixelColor += m37[12].rgb; 236 | count++; 237 | if (m37[6].a < threshold) 238 | { 239 | filteredPixelColor += m37[6].rgb; 240 | count++; 241 | } 242 | if (m37[7].a < threshold) 243 | { 244 | filteredPixelColor += m37[7].rgb; 245 | count++; 246 | } 247 | if (m37[13].a < threshold) 248 | { 249 | filteredPixelColor += m37[13].rgb; 250 | count++; 251 | } 252 | } 253 | 254 | // search lower-left diagonal 255 | if (m37[24].a < threshold) 256 | { 257 | filteredPixelColor += m37[24].rgb; 258 | count++; 259 | if (m37[23].a < threshold) 260 | { 261 | filteredPixelColor += m37[23].rgb; 262 | count++; 263 | } 264 | if (m37[29].a < threshold) 265 | { 266 | filteredPixelColor += m37[29].rgb; 267 | count++; 268 | } 269 | if (m37[30].a < threshold) 270 | { 271 | filteredPixelColor += m37[30].rgb; 272 | count++; 273 | } 274 | } 275 | 276 | // search lower-right diagonal 277 | if (m37[26].a < threshold) 278 | { 279 | filteredPixelColor += m37[26].rgb; 280 | count++; 281 | if (m37[27].a < threshold) 282 | { 283 | filteredPixelColor += m37[27].rgb; 284 | count++; 285 | } 286 | if (m37[32].a < threshold) 287 | { 288 | filteredPixelColor += m37[32].rgb; 289 | count++; 290 | } 291 | if (m37[33].a < threshold) 292 | { 293 | filteredPixelColor += m37[33].rgb; 294 | count++; 295 | } 296 | } 297 | 298 | 299 | // divide by total count to get the average 300 | filteredPixelColor *= (1.0 / float(count)); 301 | 302 | 303 | 304 | // next, use a smaller blur kernel (13 pixels in roughly circular shape), to help blend the noisy, sharp edge pixels 305 | 306 | // m37[18] is the center pixel 307 | edgePixelColor = m37[ 5].rgb + 308 | m37[10].rgb + m37[11].rgb + m37[12].rgb + 309 | m37[16].rgb + m37[17].rgb + m37[18].rgb + m37[19].rgb + m37[20].rgb + 310 | m37[24].rgb + m37[25].rgb + m37[26].rgb + 311 | m37[31].rgb; 312 | 313 | // if not averaged, the above additions produce white outlines along edges 314 | edgePixelColor *= 0.0769230769; // same as dividing by 13 pixels (1 / 13), to get the average 315 | 316 | if (uSceneIsDynamic) // dynamic scene with moving objects and camera (i.e. a game) 317 | { 318 | if (uCameraIsMoving) 319 | { 320 | if (nextToAnEdgePixel == TRUE) 321 | filteredPixelColor = mix(edgePixelColor, centerPixel.rgb, 0.25); 322 | } 323 | else if (centerPixel.a == 1.0 || nextToAnEdgePixel == TRUE) 324 | filteredPixelColor = mix(edgePixelColor, centerPixel.rgb, 0.5); 325 | 326 | } 327 | if (!uSceneIsDynamic) // static scene (only camera can move) 328 | { 329 | if (uCameraIsMoving) 330 | { 331 | if (nextToAnEdgePixel == TRUE) 332 | filteredPixelColor = mix(edgePixelColor, centerPixel.rgb, 0.25); 333 | } 334 | else if (centerPixel.a == 1.0) 335 | filteredPixelColor = mix(filteredPixelColor, centerPixel.rgb, clamp(uSampleCounter * uEdgeSharpenSpeed, 0.0, 1.0)); 336 | // the following statement helps smooth out jagged stairstepping where the blurred filteredPixelColor pixels meet the sharp edges 337 | else if (uSampleCounter > 500.0 && nextToAnEdgePixel == TRUE) 338 | filteredPixelColor = centerPixel.rgb; 339 | 340 | } 341 | 342 | // if the .a value comes into this shader as 1.01, this is an outdoor raymarching demo, and no denoising/blended is needed 343 | if (centerPixel.a == 1.01) 344 | filteredPixelColor = centerPixel.rgb; // no blending, maximum sharpness 345 | 346 | 347 | // final filteredPixelColor processing //////////////////////////////////// 348 | 349 | // average accumulation buffer 350 | filteredPixelColor *= uOneOverSampleCounter; 351 | 352 | // apply tone mapping (brings pixel into 0.0-1.0 rgb color range) 353 | filteredPixelColor = uUseToneMapping ? ReinhardToneMapping(filteredPixelColor) : filteredPixelColor; 354 | 355 | // lastly, apply gamma correction (gives more intensity/brightness range where it's needed) 356 | pc_fragColor = vec4(sqrt(filteredPixelColor), 1.0); 357 | } 358 | -------------------------------------------------------------------------------- /shaders/common_PathTracing_Vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | 4 | void main() 5 | { 6 | gl_Position = vec4( position, 1.0 ); 7 | } 8 | -------------------------------------------------------------------------------- /sounds/chalk.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/chalk.mp3 -------------------------------------------------------------------------------- /sounds/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/click.mp3 -------------------------------------------------------------------------------- /sounds/click2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/click2.mp3 -------------------------------------------------------------------------------- /sounds/cuestick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/cuestick.mp3 -------------------------------------------------------------------------------- /sounds/ping_pong.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/ping_pong.mp3 -------------------------------------------------------------------------------- /sounds/pocket.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/pocket.mp3 -------------------------------------------------------------------------------- /sounds/rack.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/rack.mp3 -------------------------------------------------------------------------------- /sounds/rail.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/sounds/rail.mp3 -------------------------------------------------------------------------------- /textures/BlueNoise_R_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/AntiGravity-Pool/e61e238084c8eee8da74597a31f03622b2b6a29f/textures/BlueNoise_R_128.png --------------------------------------------------------------------------------