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