Desktop Controls (update 7/30/22: Mobile GUI/Controls coming soon, now that Mobile rendering is working!)
7 |
8 | * Click anywhere to capture mouse
9 | * move Mouse to free-look
10 | * Right/Left Arrows to open/close camera aperture (depth of field effect)
11 | * WASD,QZ to fly camera in Landscape selection mode (disabled when entering your Robot in game mode)
12 | * Press SPACEBAR to generate a new random landscape (cycles through 4 color themes)
13 | * Press E to enter the player's starting Robot (this is where you start each game level)
14 | * With a checkerboard tile (or boulder) selected, Press R to create another Robot
15 | * With that new robot selected (or its checkerboard tile selected), Press E to Enter that other robot
16 | * With a checkerboard tile (or another boulder) selected, Press B to create a Boulder (a base on which more Boulders or another Robot can be stacked)
17 | * With a checkerboard tile (or boulder) selected, Press T to create a Tree (a technique to partially hide your player Robot from the Sentinel's gaze!)
18 | * MouseClick to absorb an item. Note: game rules state that you must be able to see the checkerboard tile on which the item sits (will not work if the item is too high). Does not apply to Boulder bases or player Robots - they can be clicked/absorbed from anywhere on the level.
19 | * Press H to Hyperspace to a new random tile location, at or below current player height - Warning: Hyperspacing costs 3 energy units!
20 | * If player energy drops below 0, the level is lost and the player restarts the same type of level
21 | * To Win a level, Press H to Hyperspace while standing on top of the Sentinel's pedestal (after you absorb her and place your robot where she was!)
22 |
23 |
24 |
25 |
TODO
26 |
27 | * Make the Sentinel Game! lol - now that the rendering is mostly worked out, add the actual gameplay and game logic.
28 | * Upon seeing an object with more energy than a natural tree, make Head Sentinel and her lower Sentries dissolve and break down the object.
29 | * Add simple GUI showing player assets as well as player visibility to The Sentinel and lower Sentries.
30 | * Add the classic original sound effects and short recurring melodic theme.
31 | * Now that Mobile rendering is working, add mobile GUI/buttons and increase mobile performance (framerate)
32 |
33 |
ABOUT
34 |
35 | * Following my Path Traced Pong [game](https://github.com/erichlof/PathTracedPong), this is the third in a series of real-time fully path traced games for all devices with a browser. The technology behind this game is a combination of my three.js path tracing [project](https://github.com/erichlof/THREE.js-PathTracing-Renderer) and the WebAudio API for sound effects. The goal of this game project and others like it is to enable path traced real-time games in the browser for all players, regardless of their system specs or GPU power.
36 |
37 |
38 | This is a remake of the iconic classic 1986 game, The Sentinel https://en.wikipedia.org/wiki/The_Sentinel_(video_game) by Geoff Crammond (Amiga screenshots) http://www.grospixels.com/site/sentinel.php . Originally for the BBC Micro, it was soon ported to different systems of the day, including the Commodore 64, which I owned. I'll always remember - I was 13 years old, strolling through the mall with my parents (yeah I wasn't so cool), when I saw The Sentinel game for the first time - a demo running on a Commodore 64 monitor in Babbage's (an old computer software retailer) store front display to all mall pedestrians. I stopped in my tracks. This was the first true 3D filled polygon game running on the Commodore 64! (albeit with a really low framerate, understandably!). I immediately walked in the software store and bought it that day - I still remember how the big black Sentinel game box looked and felt.
39 |
40 | Later when I got home and for many months afterward, I not only enjoyed endless hours playing it, but it actually had an effect on me that no other game has since. It showed me what a computer game is capable of: not just a toy to dump quarters into at the mall arcade, but an entire world (well, landscape) that pulls you in, surrounds you completely, and makes you feel that you are really there. I don't know how the creator/programmer, Geoff Crammond, came up with this amazing idea, nor do I even know how he got 3D filled polygon graphics to run at all on such underpowered systems, but what I do know is that this game and its gameplay is like no other. Its sterile, haunting atmosphere sticks with you (or at least it did back when I was a teen). It rises above how society regarded computer games to the level of true art. Geoff Crammond was able not only to convey his other-worldy vision on 1986 hardware, but in my humble opinion he created a truly unique real-time masterpiece that needs to be documented and cherished.
41 |
42 | My remake of the game, which I call The Sentinel: 2nd Look, hopefully brings the classic into modern times so that a wider audience can enjoy it at 30-60fps on any desktop/laptop with a browser. What better way to bring together my favorite game of all time and my favorite graphic technique (path tracing!) into one project! Now I know that there are a few modern fan remakes out there already, but I wanted to do something slightly different with my version and add my path tracing touch to it. Since I can't wade through Geoff Crammond's original assembly source code, I had to reverse-engineer the game's landscape generation process. This alone took months because it's not too obvious how he coded it and I went down a couple of wrong/dead-end paths on my journey to faithfully recreate the look and feel of his cool landscapes. Graphics-wise, I added a day cycle/sun light source to the environment, added full path tracing to cast pixel-perfect shadows on the terrain, self-shadowing of game objects, and providing true ray-traced reflections in the white/black connector panels of the landscape. It's really rewarding to fly high and low and explore the environment, watching the setting sun cast eerie shadows on the landscape and game objects. I hope you enjoy my homage to this iconic game, and I hope it gives you that magical sense of immersion like I had when I was 13 at my Commodore 64! :)
43 |
44 |
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 > 250.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 |
--------------------------------------------------------------------------------
/js/BVH_Acc_Structure_Iterative_Fast_Builder.js:
--------------------------------------------------------------------------------
1 | /* BVH (Bounding Volume Hierarchy) Iterative Fast Builder */
2 | /*
3 | Inspired by: Thanassis Tsiodras (ttsiodras on GitHub)
4 | https://github.com/ttsiodras/renderer-cuda/blob/master/src/BVH.cpp
5 | Edited and Ported from C++ to Javascript by: Erich Loftis (erichlof on GitHub)
6 | https://github.com/erichlof/THREE.js-PathTracing-Renderer
7 | */
8 |
9 |
10 | let stackptr = 0;
11 | let buildnodes = [];
12 | let leftWorkLists = [];
13 | let rightWorkLists = [];
14 | let parentList = [];
15 | let currentList, aabb_array_copy;
16 | let k, value, side0, side1, side2;
17 | let bestSplit, goodSplit, okaySplit;
18 | let bestAxis, goodAxis, okayAxis;
19 | let leftWorkCount = 0;
20 | let rightWorkCount = 0;
21 | let currentMinCorner = new THREE.Vector3();
22 | let currentMaxCorner = new THREE.Vector3();
23 | let testMinCorner = new THREE.Vector3();
24 | let testMaxCorner = new THREE.Vector3();
25 | let testCentroid = new THREE.Vector3();
26 | let currentCentroid = new THREE.Vector3();
27 | let spatialAverage = new THREE.Vector3();
28 |
29 |
30 | function BVH_FlatNode()
31 | {
32 | this.idSelf = 0;
33 | this.idPrimitive = -1; // a negative primitive id means that this is another inner node
34 | this.idRightChild = 0;
35 | this.idParent = 0;
36 | this.minCorner = new THREE.Vector3();
37 | this.maxCorner = new THREE.Vector3();
38 | }
39 |
40 |
41 | function BVH_Create_Node(workList, idParent, isRightBranch)
42 | {
43 |
44 | // re-initialize bounding box extents
45 | currentMinCorner.set(Infinity, Infinity, Infinity);
46 | currentMaxCorner.set(-Infinity, -Infinity, -Infinity);
47 |
48 | if (workList.length < 1)
49 | { // should never happen, but just in case...
50 | return;
51 | }
52 | else if (workList.length == 1)
53 | {
54 | // if we're down to 1 primitive aabb, quickly create a leaf node and return.
55 | k = workList[0];
56 | // create leaf node
57 | let flatLeafNode = new BVH_FlatNode();
58 | flatLeafNode.idSelf = buildnodes.length;
59 | flatLeafNode.idPrimitive = k; // id of primitive (usually a triangle) that is stored inside this AABB leaf node
60 | flatLeafNode.idRightChild = -1; // leaf nodes do not have children
61 | flatLeafNode.idParent = idParent;
62 | flatLeafNode.minCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
63 | flatLeafNode.maxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
64 | buildnodes.push(flatLeafNode);
65 |
66 | // if this is a right branch, fill in parent's missing link to this right child,
67 | // now that we have assigned this right child an ID
68 | if (isRightBranch)
69 | buildnodes[idParent].idRightChild = flatLeafNode.idSelf;
70 |
71 | return;
72 | } // end else if (workList.length == 1)
73 |
74 | else if (workList.length > 1)
75 | {
76 | // this is where the real work happens: we must sort an arbitrary number of primitive (usually triangles) AABBs.
77 | // to get a balanced tree, we hope for about half to be placed in left child, half to be placed in right child.
78 |
79 | // construct/grow bounding box around all of the current workList's primitive(triangle) AABBs
80 | for (let i = 0; i < workList.length; i++)
81 | {
82 | k = workList[i];
83 | testMinCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
84 | testMaxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
85 | currentMinCorner.min(testMinCorner);
86 | currentMaxCorner.max(testMaxCorner);
87 | }
88 |
89 | // create an inner node to represent this newly grown bounding box
90 | let flatnode = new BVH_FlatNode();
91 | flatnode.idSelf = buildnodes.length; // its own id matches the number of nodes we've created so far
92 | flatnode.idPrimitive = -1; // a negative primitive id means that this is just another inner node (with pointers to children), no triangle
93 | flatnode.idRightChild = 0; // missing RightChild link will be filled in soon; don't know how deep the left branches will go while constructing top-to-bottom
94 | flatnode.idParent = idParent;
95 | flatnode.minCorner.copy(currentMinCorner);
96 | flatnode.maxCorner.copy(currentMaxCorner);
97 | buildnodes.push(flatnode);
98 |
99 | // if this is a right branch, fill in parent's missing link to this right child,
100 | // now that we have assigned this right child an ID
101 | if (isRightBranch)
102 | buildnodes[idParent].idRightChild = flatnode.idSelf;
103 |
104 |
105 | // Begin Spatial Median split plane determination and primitive AABB sorting
106 |
107 | side0 = currentMaxCorner.x - currentMinCorner.x; // length along X-axis
108 | side1 = currentMaxCorner.y - currentMinCorner.y; // length along Y-axis
109 | side2 = currentMaxCorner.z - currentMinCorner.z; // length along Z-axis
110 |
111 | // calculate the middle point of this newly-grown bounding box (aka the 'spatial median')
112 | // this simply uses the spatial average of the longest box extent to determine the split plane,
113 | // which is very fast and results in a fair quality, fairly balanced binary tree structure
114 | spatialAverage.copy(currentMinCorner).add(currentMaxCorner).multiplyScalar(0.5);
115 |
116 | // initialize variables
117 | bestAxis = 0; goodAxis = 1; okayAxis = 2;
118 | bestSplit = spatialAverage.x; goodSplit = spatialAverage.y; okaySplit = spatialAverage.z;
119 |
120 | // determine the longest extent of the box, and start with that as splitting dimension
121 | if (side0 >= side1 && side0 >= side2)
122 | {
123 | bestAxis = 0;
124 | bestSplit = spatialAverage.x;
125 | if (side1 >= side2)
126 | {
127 | goodAxis = 1;
128 | goodSplit = spatialAverage.y;
129 | okayAxis = 2;
130 | okaySplit = spatialAverage.z;
131 | }
132 | else
133 | {
134 | goodAxis = 2;
135 | goodSplit = spatialAverage.z;
136 | okayAxis = 1;
137 | okaySplit = spatialAverage.y;
138 | }
139 | }
140 | else if (side1 >= side0 && side1 >= side2)
141 | {
142 | bestAxis = 1;
143 | bestSplit = spatialAverage.y;
144 | if (side0 >= side2)
145 | {
146 | goodAxis = 0;
147 | goodSplit = spatialAverage.x;
148 | okayAxis = 2;
149 | okaySplit = spatialAverage.z;
150 | }
151 | else
152 | {
153 | goodAxis = 2;
154 | goodSplit = spatialAverage.z;
155 | okayAxis = 0;
156 | okaySplit = spatialAverage.x;
157 | }
158 | }
159 | else// if (side2 >= side0 && side2 >= side1)
160 | {
161 | bestAxis = 2;
162 | bestSplit = spatialAverage.z;
163 | if (side0 >= side1)
164 | {
165 | goodAxis = 0;
166 | goodSplit = spatialAverage.x;
167 | okayAxis = 1;
168 | okaySplit = spatialAverage.y;
169 | }
170 | else
171 | {
172 | goodAxis = 1;
173 | goodSplit = spatialAverage.y;
174 | okayAxis = 0;
175 | okaySplit = spatialAverage.x;
176 | }
177 | }
178 |
179 | // try best axis first, then try the other two if necessary
180 | for (let axis = 0; axis < 3; axis++)
181 | {
182 | // distribute the triangle AABBs in either the left child or right child
183 | // reset counters for the loop coming up
184 | leftWorkCount = 0;
185 | rightWorkCount = 0;
186 |
187 | // this loop is to count how many elements we will need for the left branch and the right branch
188 | for (let i = 0; i < workList.length; i++)
189 | {
190 | k = workList[i];
191 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
192 |
193 | // get bbox center
194 | if (bestAxis == 0) value = testCentroid.x; // X-axis
195 | else if (bestAxis == 1) value = testCentroid.y; // Y-axis
196 | else value = testCentroid.z; // Z-axis
197 |
198 | if (value < bestSplit)
199 | {
200 | leftWorkCount++;
201 | } else
202 | {
203 | rightWorkCount++;
204 | }
205 | }
206 |
207 | if (leftWorkCount > 0 && rightWorkCount > 0)
208 | {
209 | break; // success, move on to the next part
210 | }
211 | else// if (leftWorkCount == 0 || rightWorkCount == 0)
212 | {
213 | // try another axis
214 | if (axis == 0)
215 | {
216 | bestAxis = goodAxis;
217 | bestSplit = goodSplit;
218 | }
219 | else if (axis == 1)
220 | {
221 | bestAxis = okayAxis;
222 | bestSplit = okaySplit;
223 | }
224 |
225 | continue;
226 | }
227 |
228 | } // end for (let axis = 0; axis < 3; axis++)
229 |
230 |
231 | // if the below if statement is true, then we have successfully sorted the primitive(triangle) AABBs
232 | if (leftWorkCount > 0 && rightWorkCount > 0)
233 | {
234 | // now that the size of each branch is known, we can initialize the left and right arrays
235 | leftWorkLists[stackptr] = new Uint32Array(leftWorkCount);
236 | rightWorkLists[stackptr] = new Uint32Array(rightWorkCount);
237 |
238 | // reset counters for the loop coming up
239 | leftWorkCount = 0;
240 | rightWorkCount = 0;
241 |
242 | // sort the primitives and populate the current leftWorkLists and rightWorklists
243 | for (let i = 0; i < workList.length; i++)
244 | {
245 | k = workList[i];
246 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
247 |
248 | // get bbox center
249 | if (bestAxis == 0) value = testCentroid.x; // X-axis
250 | else if (bestAxis == 1) value = testCentroid.y; // Y-axis
251 | else value = testCentroid.z; // Z-axis
252 |
253 | if (value < bestSplit)
254 | {
255 | leftWorkLists[stackptr][leftWorkCount] = k;
256 | leftWorkCount++;
257 | } else
258 | {
259 | rightWorkLists[stackptr][rightWorkCount] = k;
260 | rightWorkCount++;
261 | }
262 | }
263 |
264 | return; // success!
265 |
266 | } // end if (leftWorkCount > 0 && rightWorkCount > 0)
267 |
268 |
269 | // if we reached this point, the builder failed to find a decent splitting plane axis, so
270 | // manually populate the current leftWorkLists and rightWorklists.
271 | // reset counters to 0
272 | leftWorkCount = 0;
273 | rightWorkCount = 0;
274 |
275 | // this loop is to count how many elements we will need for the left branch and the right branch
276 | for (let i = 0; i < workList.length; i++)
277 | {
278 | if (i % 2 == 0)
279 | {
280 | leftWorkCount++;
281 | } else
282 | {
283 | rightWorkCount++;
284 | }
285 | }
286 |
287 | // now that the size of each branch is known, we can initialize the left and right arrays
288 | leftWorkLists[stackptr] = new Uint32Array(leftWorkCount);
289 | rightWorkLists[stackptr] = new Uint32Array(rightWorkCount);
290 |
291 | // reset counters for the loop coming up
292 | leftWorkCount = 0;
293 | rightWorkCount = 0;
294 |
295 | for (let i = 0; i < workList.length; i++)
296 | {
297 | k = workList[i];
298 |
299 | if (i % 2 == 0)
300 | {
301 | leftWorkLists[stackptr][leftWorkCount] = k;
302 | leftWorkCount++;
303 | } else
304 | {
305 | rightWorkLists[stackptr][rightWorkCount] = k;
306 | rightWorkCount++;
307 | }
308 | }
309 |
310 | } // end else if (workList.length > 1)
311 |
312 | } // end function BVH_Create_Node(workList, idParent, isRightBranch)
313 |
314 |
315 |
316 | function BVH_Build_Iterative(workList, aabb_array)
317 | {
318 |
319 | currentList = workList;
320 | // save a global copy of the supplied aabb_array, so that it can be used by the various functions in this file
321 | aabb_array_copy = new Float32Array(aabb_array);
322 |
323 | // reset BVH builder arrays;
324 | buildnodes = [];
325 | leftWorkLists = [];
326 | rightWorkLists = [];
327 | parentList = [];
328 |
329 | // initialize variables
330 | stackptr = 0;
331 |
332 | // parent id of -1, meaning this is the root node, which has no parent
333 | parentList.push(-1);
334 | BVH_Create_Node(currentList, -1, false); // build root node
335 |
336 | // build the tree using the "go down left branches until done, then ascend back up right branches" approach
337 | while (stackptr > -1)
338 | {
339 | // pop the next node off of the left-side stack
340 | currentList = leftWorkLists[stackptr];
341 |
342 | if (currentList != undefined)
343 | { // left side of tree
344 |
345 | leftWorkLists[stackptr] = null; // mark as processed
346 | stackptr++;
347 |
348 | parentList.push(buildnodes.length - 1);
349 |
350 | // build the left node
351 | BVH_Create_Node(currentList, buildnodes.length - 1, false);
352 | }
353 | else
354 | { // right side of tree
355 | // pop the next node off of the right-side stack
356 | currentList = rightWorkLists[stackptr];
357 |
358 | if (currentList != undefined)
359 | {
360 | rightWorkLists[stackptr] = null; // mark as processed
361 | stackptr++;
362 |
363 | // build the right node
364 | BVH_Create_Node(currentList, parentList.pop(), true);
365 | }
366 | else
367 | {
368 | stackptr--;
369 | }
370 | }
371 |
372 | } // end while (stackptr > -1)
373 |
374 |
375 | // Copy the buildnodes array into the aabb_array
376 | for (let n = 0; n < buildnodes.length; n++)
377 | {
378 | // slot 0
379 | aabb_array[8 * n + 0] = buildnodes[n].idPrimitive; // r or x component
380 | aabb_array[8 * n + 1] = buildnodes[n].minCorner.x; // g or y component
381 | aabb_array[8 * n + 2] = buildnodes[n].minCorner.y; // b or z component
382 | aabb_array[8 * n + 3] = buildnodes[n].minCorner.z; // a or w component
383 |
384 | // slot 1
385 | aabb_array[8 * n + 4] = buildnodes[n].idRightChild; // r or x component
386 | aabb_array[8 * n + 5] = buildnodes[n].maxCorner.x; // g or y component
387 | aabb_array[8 * n + 6] = buildnodes[n].maxCorner.y; // b or z component
388 | aabb_array[8 * n + 7] = buildnodes[n].maxCorner.z; // a or w component
389 | }
390 |
391 | } // end function BVH_Build_Iterative(workList, aabb_array)
392 |
--------------------------------------------------------------------------------
/js/SimplexNoise.js:
--------------------------------------------------------------------------------
1 | // Ported from Stefan Gustavson's java implementation
2 | // http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
3 | // Read Stefan's excellent paper for details on how this code works.
4 | //
5 | // Sean McCullough banksean@gmail.com
6 | //
7 | // Added 4D noise
8 |
9 | /**
10 | * You can pass in a random number generator object if you like.
11 | * It is assumed to have a random() method.
12 | */
13 | SimplexNoise = function (r)
14 | {
15 |
16 | if (r == undefined) r = Math;
17 | this.grad3 = [[1, 1, 0], [- 1, 1, 0], [1, - 1, 0], [- 1, - 1, 0],
18 | [1, 0, 1], [- 1, 0, 1], [1, 0, - 1], [- 1, 0, - 1],
19 | [0, 1, 1], [0, - 1, 1], [0, 1, - 1], [0, - 1, - 1]];
20 |
21 | this.grad4 = [[0, 1, 1, 1], [0, 1, 1, - 1], [0, 1, - 1, 1], [0, 1, - 1, - 1],
22 | [0, - 1, 1, 1], [0, - 1, 1, - 1], [0, - 1, - 1, 1], [0, - 1, - 1, - 1],
23 | [1, 0, 1, 1], [1, 0, 1, - 1], [1, 0, - 1, 1], [1, 0, - 1, - 1],
24 | [- 1, 0, 1, 1], [- 1, 0, 1, - 1], [- 1, 0, - 1, 1], [- 1, 0, - 1, - 1],
25 | [1, 1, 0, 1], [1, 1, 0, - 1], [1, - 1, 0, 1], [1, - 1, 0, - 1],
26 | [- 1, 1, 0, 1], [- 1, 1, 0, - 1], [- 1, - 1, 0, 1], [- 1, - 1, 0, - 1],
27 | [1, 1, 1, 0], [1, 1, - 1, 0], [1, - 1, 1, 0], [1, - 1, - 1, 0],
28 | [- 1, 1, 1, 0], [- 1, 1, - 1, 0], [- 1, - 1, 1, 0], [- 1, - 1, - 1, 0]];
29 |
30 | this.p = [];
31 |
32 | for (var i = 0; i < 256; i++)
33 | {
34 |
35 | this.p[i] = Math.floor(r.random() * 256);
36 |
37 | }
38 |
39 | // To remove the need for index wrapping, double the permutation table length
40 | this.perm = [];
41 |
42 | for (var i = 0; i < 512; i++)
43 | {
44 |
45 | this.perm[i] = this.p[i & 255];
46 |
47 | }
48 |
49 | // A lookup table to traverse the simplex around a given point in 4D.
50 | // Details can be found where this table is used, in the 4D noise method.
51 | this.simplex = [
52 | [0, 1, 2, 3], [0, 1, 3, 2], [0, 0, 0, 0], [0, 2, 3, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [1, 2, 3, 0],
53 | [0, 2, 1, 3], [0, 0, 0, 0], [0, 3, 1, 2], [0, 3, 2, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [1, 3, 2, 0],
54 | [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
55 | [1, 2, 0, 3], [0, 0, 0, 0], [1, 3, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [2, 3, 0, 1], [2, 3, 1, 0],
56 | [1, 0, 2, 3], [1, 0, 3, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [2, 0, 3, 1], [0, 0, 0, 0], [2, 1, 3, 0],
57 | [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
58 | [2, 0, 1, 3], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 1, 2], [3, 0, 2, 1], [0, 0, 0, 0], [3, 1, 2, 0],
59 | [2, 1, 0, 3], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [3, 1, 0, 2], [0, 0, 0, 0], [3, 2, 0, 1], [3, 2, 1, 0]];
60 |
61 | };
62 |
63 | SimplexNoise.prototype.dot = function (g, x, y)
64 | {
65 |
66 | return g[0] * x + g[1] * y;
67 |
68 | };
69 |
70 | SimplexNoise.prototype.dot3 = function (g, x, y, z)
71 | {
72 |
73 | return g[0] * x + g[1] * y + g[2] * z;
74 |
75 | };
76 |
77 | SimplexNoise.prototype.dot4 = function (g, x, y, z, w)
78 | {
79 |
80 | return g[0] * x + g[1] * y + g[2] * z + g[3] * w;
81 |
82 | };
83 |
84 | SimplexNoise.prototype.noise = function (xin, yin)
85 | {
86 |
87 | var n0, n1, n2; // Noise contributions from the three corners
88 | // Skew the input space to determine which simplex cell we're in
89 | var F2 = 0.5 * (Math.sqrt(3.0) - 1.0);
90 | var s = (xin + yin) * F2; // Hairy factor for 2D
91 | var i = Math.floor(xin + s);
92 | var j = Math.floor(yin + s);
93 | var G2 = (3.0 - Math.sqrt(3.0)) / 6.0;
94 | var t = (i + j) * G2;
95 | var X0 = i - t; // Unskew the cell origin back to (x,y) space
96 | var Y0 = j - t;
97 | var x0 = xin - X0; // The x,y distances from the cell origin
98 | var y0 = yin - Y0;
99 | // For the 2D case, the simplex shape is an equilateral triangle.
100 | // Determine which simplex we are in.
101 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
102 | if (x0 > y0)
103 | {
104 |
105 | i1 = 1; j1 = 0;
106 |
107 | // lower triangle, XY order: (0,0)->(1,0)->(1,1)
108 |
109 | } else
110 | {
111 |
112 | i1 = 0; j1 = 1;
113 |
114 | } // upper triangle, YX order: (0,0)->(0,1)->(1,1)
115 |
116 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
117 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
118 | // c = (3-sqrt(3))/6
119 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
120 | var y1 = y0 - j1 + G2;
121 | var x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords
122 | var y2 = y0 - 1.0 + 2.0 * G2;
123 | // Work out the hashed gradient indices of the three simplex corners
124 | var ii = i & 255;
125 | var jj = j & 255;
126 | var gi0 = this.perm[ii + this.perm[jj]] % 12;
127 | var gi1 = this.perm[ii + i1 + this.perm[jj + j1]] % 12;
128 | var gi2 = this.perm[ii + 1 + this.perm[jj + 1]] % 12;
129 | // Calculate the contribution from the three corners
130 | var t0 = 0.5 - x0 * x0 - y0 * y0;
131 | if (t0 < 0) n0 = 0.0;
132 | else
133 | {
134 |
135 | t0 *= t0;
136 | n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0); // (x,y) of grad3 used for 2D gradient
137 |
138 | }
139 |
140 | var t1 = 0.5 - x1 * x1 - y1 * y1;
141 | if (t1 < 0) n1 = 0.0;
142 | else
143 | {
144 |
145 | t1 *= t1;
146 | n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1);
147 |
148 | }
149 |
150 | var t2 = 0.5 - x2 * x2 - y2 * y2;
151 | if (t2 < 0) n2 = 0.0;
152 | else
153 | {
154 |
155 | t2 *= t2;
156 | n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2);
157 |
158 | }
159 |
160 | // Add contributions from each corner to get the final noise value.
161 | // The result is scaled to return values in the interval [-1,1].
162 | return 70.0 * (n0 + n1 + n2);
163 |
164 | };
165 |
166 | // 3D simplex noise
167 | SimplexNoise.prototype.noise3d = function (xin, yin, zin)
168 | {
169 |
170 | var n0, n1, n2, n3; // Noise contributions from the four corners
171 | // Skew the input space to determine which simplex cell we're in
172 | var F3 = 1.0 / 3.0;
173 | var s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D
174 | var i = Math.floor(xin + s);
175 | var j = Math.floor(yin + s);
176 | var k = Math.floor(zin + s);
177 | var G3 = 1.0 / 6.0; // Very nice and simple unskew factor, too
178 | var t = (i + j + k) * G3;
179 | var X0 = i - t; // Unskew the cell origin back to (x,y,z) space
180 | var Y0 = j - t;
181 | var Z0 = k - t;
182 | var x0 = xin - X0; // The x,y,z distances from the cell origin
183 | var y0 = yin - Y0;
184 | var z0 = zin - Z0;
185 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron.
186 | // Determine which simplex we are in.
187 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords
188 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords
189 | if (x0 >= y0)
190 | {
191 |
192 | if (y0 >= z0)
193 | {
194 |
195 | i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0;
196 |
197 | // X Y Z order
198 |
199 | } else if (x0 >= z0)
200 | {
201 |
202 | i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1;
203 |
204 | // X Z Y order
205 |
206 | } else
207 | {
208 |
209 | i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1;
210 |
211 | } // Z X Y order
212 |
213 | } else
214 | { // x0 y0) ? 32 : 0;
345 | var c2 = (x0 > z0) ? 16 : 0;
346 | var c3 = (y0 > z0) ? 8 : 0;
347 | var c4 = (x0 > w0) ? 4 : 0;
348 | var c5 = (y0 > w0) ? 2 : 0;
349 | var c6 = (z0 > w0) ? 1 : 0;
350 | var c = c1 + c2 + c3 + c4 + c5 + c6;
351 | var i1, j1, k1, l1; // The integer offsets for the second simplex corner
352 | var i2, j2, k2, l2; // The integer offsets for the third simplex corner
353 | var i3, j3, k3, l3; // The integer offsets for the fourth simplex corner
354 | // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order.
355 | // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0;
360 | j1 = simplex[c][1] >= 3 ? 1 : 0;
361 | k1 = simplex[c][2] >= 3 ? 1 : 0;
362 | l1 = simplex[c][3] >= 3 ? 1 : 0;
363 | // The number 2 in the "simplex" array is at the second largest coordinate.
364 | i2 = simplex[c][0] >= 2 ? 1 : 0;
365 | j2 = simplex[c][1] >= 2 ? 1 : 0; k2 = simplex[c][2] >= 2 ? 1 : 0;
366 | l2 = simplex[c][3] >= 2 ? 1 : 0;
367 | // The number 1 in the "simplex" array is at the second smallest coordinate.
368 | i3 = simplex[c][0] >= 1 ? 1 : 0;
369 | j3 = simplex[c][1] >= 1 ? 1 : 0;
370 | k3 = simplex[c][2] >= 1 ? 1 : 0;
371 | l3 = simplex[c][3] >= 1 ? 1 : 0;
372 | // The fifth corner has all coordinate offsets = 1, so no need to look that up.
373 | var x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords
374 | var y1 = y0 - j1 + G4;
375 | var z1 = z0 - k1 + G4;
376 | var w1 = w0 - l1 + G4;
377 | var x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords
378 | var y2 = y0 - j2 + 2.0 * G4;
379 | var z2 = z0 - k2 + 2.0 * G4;
380 | var w2 = w0 - l2 + 2.0 * G4;
381 | var x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords
382 | var y3 = y0 - j3 + 3.0 * G4;
383 | var z3 = z0 - k3 + 3.0 * G4;
384 | var w3 = w0 - l3 + 3.0 * G4;
385 | var x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords
386 | var y4 = y0 - 1.0 + 4.0 * G4;
387 | var z4 = z0 - 1.0 + 4.0 * G4;
388 | var w4 = w0 - 1.0 + 4.0 * G4;
389 | // Work out the hashed gradient indices of the five simplex corners
390 | var ii = i & 255;
391 | var jj = j & 255;
392 | var kk = k & 255;
393 | var ll = l & 255;
394 | var gi0 = perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32;
395 | var gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32;
396 | var gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32;
397 | var gi3 = perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32;
398 | var gi4 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32;
399 | // Calculate the contribution from the five corners
400 | var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0;
401 | if (t0 < 0) n0 = 0.0;
402 | else
403 | {
404 |
405 | t0 *= t0;
406 | n0 = t0 * t0 * this.dot4(grad4[gi0], x0, y0, z0, w0);
407 |
408 | }
409 |
410 | var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1;
411 | if (t1 < 0) n1 = 0.0;
412 | else
413 | {
414 |
415 | t1 *= t1;
416 | n1 = t1 * t1 * this.dot4(grad4[gi1], x1, y1, z1, w1);
417 |
418 | }
419 |
420 | var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2;
421 | if (t2 < 0) n2 = 0.0;
422 | else
423 | {
424 |
425 | t2 *= t2;
426 | n2 = t2 * t2 * this.dot4(grad4[gi2], x2, y2, z2, w2);
427 |
428 | }
429 |
430 | var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3;
431 | if (t3 < 0) n3 = 0.0;
432 | else
433 | {
434 |
435 | t3 *= t3;
436 | n3 = t3 * t3 * this.dot4(grad4[gi3], x3, y3, z3, w3);
437 |
438 | }
439 |
440 | var t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4;
441 | if (t4 < 0) n4 = 0.0;
442 | else
443 | {
444 |
445 | t4 *= t4;
446 | n4 = t4 * t4 * this.dot4(grad4[gi4], x4, y4, z4, w4);
447 |
448 | }
449 |
450 | // Sum up and scale the result to cover the range [-1,1]
451 | return 27.0 * (n0 + n1 + n2 + n3 + n4);
452 |
453 | };
454 |
--------------------------------------------------------------------------------
/js/BVH_Acc_Structure_Iterative_SAH_Builder.js:
--------------------------------------------------------------------------------
1 | /* BVH (Bounding Volume Hierarchy) Iterative SAH (Surface Area Heuristic) Builder */
2 | /*
3 | Inspired by: Thanassis Tsiodras (ttsiodras on GitHub)
4 | https://github.com/ttsiodras/renderer-cuda/blob/master/src/BVH.cpp
5 | Edited and Ported from C++ to Javascript by: Erich Loftis (erichlof on GitHub)
6 | https://github.com/erichlof/THREE.js-PathTracing-Renderer
7 | */
8 |
9 |
10 | let stackptr = 0;
11 | let buildnodes = [];
12 | let leftWorkLists = [];
13 | let rightWorkLists = [];
14 | let parentList = [];
15 | let currentList, aabb_array_copy;
16 | let bestSplit = null;
17 | let bestAxis = null;
18 | let leftWorkCount = 0;
19 | let rightWorkCount = 0;
20 | let bestSplitHasBeenFound = false;
21 | let currentMinCorner = new THREE.Vector3();
22 | let currentMaxCorner = new THREE.Vector3();
23 | let testMinCorner = new THREE.Vector3();
24 | let testMaxCorner = new THREE.Vector3();
25 | let testCentroid = new THREE.Vector3();
26 | let currentCentroid = new THREE.Vector3();
27 | let minCentroid = new THREE.Vector3();
28 | let maxCentroid = new THREE.Vector3();
29 | let centroidAverage = new THREE.Vector3();
30 | let spatialAverage = new THREE.Vector3();
31 | let LBottomCorner = new THREE.Vector3();
32 | let LTopCorner = new THREE.Vector3();
33 | let RBottomCorner = new THREE.Vector3();
34 | let RTopCorner = new THREE.Vector3();
35 | let k, value, side0, side1, side2, minCost, testSplit, testStep;
36 | let countLeft, countRight;
37 | let currentAxis, longestAxis, mediumAxis, shortestAxis;
38 | let lside0, lside1, lside2, rside0, rside1, rside2;
39 | let surfaceLeft, surfaceRight, totalCost;
40 | let numBins = 4; // must be 2 or higher for the BVH to work properly
41 |
42 |
43 |
44 | function BVH_FlatNode()
45 | {
46 | this.idSelf = 0;
47 | this.idPrimitive = -1; // a negative primitive id means that this is another inner node
48 | this.idRightChild = 0;
49 | this.idParent = 0;
50 | this.minCorner = new THREE.Vector3();
51 | this.maxCorner = new THREE.Vector3();
52 | }
53 |
54 |
55 | function BVH_Create_Node(workList, idParent, isRightBranch)
56 | {
57 | // reset flag
58 | bestSplitHasBeenFound = false;
59 |
60 | // re-initialize bounding box extents
61 | currentMinCorner.set(Infinity, Infinity, Infinity);
62 | currentMaxCorner.set(-Infinity, -Infinity, -Infinity);
63 |
64 | if (workList.length < 1)
65 | { // should never happen, but just in case...
66 | return;
67 | }
68 | else if (workList.length == 1)
69 | {
70 | // if we're down to 1 primitive aabb, quickly create a leaf node and return.
71 | k = workList[0];
72 | // create leaf node
73 | let flatLeafNode = new BVH_FlatNode();
74 | flatLeafNode.idSelf = buildnodes.length;
75 | flatLeafNode.idPrimitive = k; // id of primitive (usually a triangle) that is stored inside this AABB leaf node
76 | flatLeafNode.idRightChild = -1; // leaf nodes do not have children
77 | flatLeafNode.idParent = idParent;
78 | flatLeafNode.minCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
79 | flatLeafNode.maxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
80 | buildnodes.push(flatLeafNode);
81 |
82 | // if this is a right branch, fill in parent's missing link to this right child,
83 | // now that we have assigned this right child an ID
84 | if (isRightBranch)
85 | buildnodes[idParent].idRightChild = flatLeafNode.idSelf;
86 |
87 | return;
88 | } // end else if (workList.length == 1)
89 |
90 | else if (workList.length == 2)
91 | {
92 | // if we're down to 2 primitive AABBs, quickly create 1 interior node (that holds both), and 2 leaf nodes, then return.
93 |
94 | // construct bounding box around the current workList's triangle AABBs
95 | for (let i = 0; i < 2; i++)
96 | {
97 | k = workList[i];
98 | testMinCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
99 | testMaxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
100 | currentMinCorner.min(testMinCorner);
101 | currentMaxCorner.max(testMaxCorner);
102 | }
103 |
104 | // create inner node
105 | let flatnode0 = new BVH_FlatNode();
106 | flatnode0.idSelf = buildnodes.length;
107 | flatnode0.idPrimitive = -1; // a negative primitive id means that this is just another inner node (with pointers to children)
108 | flatnode0.idRightChild = buildnodes.length + 2;
109 | flatnode0.idParent = idParent;
110 | flatnode0.minCorner.copy(currentMinCorner);
111 | flatnode0.maxCorner.copy(currentMaxCorner);
112 | buildnodes.push(flatnode0);
113 |
114 | // if this is a right branch, fill in parent's missing link to this right child,
115 | // now that we have assigned this right child an ID
116 | if (isRightBranch)
117 | buildnodes[idParent].idRightChild = flatnode0.idSelf;
118 |
119 |
120 | // create 'left' leaf node
121 | k = workList[0];
122 | let flatnode1 = new BVH_FlatNode();
123 | flatnode1.idSelf = buildnodes.length;
124 | flatnode1.idPrimitive = k; // id of primitive (usually a triangle) that is stored inside this AABB leaf node
125 | flatnode1.idRightChild = -1; // leaf nodes do not have children
126 | flatnode1.idParent = flatnode0.idSelf;
127 | flatnode1.minCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
128 | flatnode1.maxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
129 | buildnodes.push(flatnode1);
130 |
131 | // create 'right' leaf node
132 | k = workList[1];
133 | let flatnode2 = new BVH_FlatNode();
134 | flatnode2.idSelf = buildnodes.length;
135 | flatnode2.idPrimitive = k; // id of primitive (usually a triangle) that is stored inside this AABB leaf node
136 | flatnode2.idRightChild = -1; // leaf nodes do not have children
137 | flatnode2.idParent = flatnode0.idSelf;
138 | flatnode2.minCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
139 | flatnode2.maxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
140 | buildnodes.push(flatnode2);
141 |
142 | return;
143 |
144 | } // end else if (workList.length == 2)
145 |
146 | else if (workList.length > 2)
147 | {
148 | // this is where the real work happens: we must sort an arbitrary number of primitive (usually triangles) AABBs.
149 | // to get a balanced tree, we hope for about half to be placed in left child, half to be placed in right child.
150 |
151 | // re-initialize min/max centroids
152 | minCentroid.set(Infinity, Infinity, Infinity);
153 | maxCentroid.set(-Infinity, -Infinity, -Infinity);
154 | centroidAverage.set(0, 0, 0);
155 |
156 | // construct/grow bounding box around all of the current workList's primitive(triangle) AABBs
157 | // also, calculate the average position of all the aabb's centroids
158 | for (let i = 0; i < workList.length; i++)
159 | {
160 | k = workList[i];
161 |
162 | testMinCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
163 | testMaxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
164 | currentCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
165 |
166 | currentMinCorner.min(testMinCorner);
167 | currentMaxCorner.max(testMaxCorner);
168 |
169 | minCentroid.min(currentCentroid);
170 | maxCentroid.max(currentCentroid);
171 |
172 | centroidAverage.add(currentCentroid); // sum up all aabb centroid positions
173 | }
174 | // divide the aabb centroid sum by the number of centroids to get average
175 | centroidAverage.divideScalar(workList.length);
176 |
177 | // calculate the middle point of this newly-grown bounding box (aka the 'spatial median')
178 | //spatialAverage.copy(currentMinCorner).add(currentMaxCorner).multiplyScalar(0.5);
179 |
180 | // create inner node
181 | let flatnode = new BVH_FlatNode();
182 | flatnode.idSelf = buildnodes.length; // its own id matches the number of nodes we've created so far
183 | flatnode.idPrimitive = -1; // a negative primitive id means that this is just another inner node (with pointers to children)
184 | flatnode.idRightChild = 0; // missing RightChild link will be filled in soon; don't know how deep the left branches will go while constructing top-to-bottom
185 | flatnode.idParent = idParent;
186 | flatnode.minCorner.copy(currentMinCorner);
187 | flatnode.maxCorner.copy(currentMaxCorner);
188 | buildnodes.push(flatnode);
189 |
190 | // if this is a right branch, fill in parent's missing link to this right child,
191 | // now that we have assigned this right child an ID
192 | if (isRightBranch)
193 | buildnodes[idParent].idRightChild = flatnode.idSelf;
194 |
195 |
196 | // Begin split plane determination using the Surface Area Heuristic(SAH) strategy
197 |
198 | side0 = currentMaxCorner.x - currentMinCorner.x; // length along X-axis
199 | side1 = currentMaxCorner.y - currentMinCorner.y; // length along Y-axis
200 | side2 = currentMaxCorner.z - currentMinCorner.z; // length along Z-axis
201 |
202 | minCost = workList.length * ((side0 * side1) + (side1 * side2) + (side2 * side0));
203 |
204 | // reset bestSplit and bestAxis
205 | bestSplit = null;
206 | bestAxis = null;
207 |
208 | // Try all 3 axes X, Y, Z
209 | for (let axis = 0; axis < 3; axis++)
210 | { // 0 = X, 1 = Y, 2 = Z axis
211 | // we will try dividing the triangle AABBs based on the current axis
212 |
213 | if (axis == 0)
214 | {
215 | testSplit = currentMinCorner.x;
216 | testStep = side0 / numBins;
217 | //testSplit = minCentroid.x;
218 | //testStep = (maxCentroid.x - minCentroid.x) / numBins;
219 | }
220 | else if (axis == 1)
221 | {
222 | testSplit = currentMinCorner.y;
223 | testStep = side1 / numBins;
224 | //testSplit = minCentroid.y;
225 | //testStep = (maxCentroid.y - minCentroid.y) / numBins;
226 | }
227 | else // if (axis == 2)
228 | {
229 | testSplit = currentMinCorner.z;
230 | testStep = side2 / numBins;
231 | //testSplit = minCentroid.z;
232 | //testStep = (maxCentroid.z - minCentroid.z) / numBins;
233 | }
234 |
235 | for (let partition = 1; partition < numBins; partition++)
236 | {
237 | testSplit += testStep;
238 |
239 | // Create potential left and right bounding boxes
240 | LBottomCorner.set(Infinity, Infinity, Infinity);
241 | LTopCorner.set(-Infinity, -Infinity, -Infinity);
242 | RBottomCorner.set(Infinity, Infinity, Infinity);
243 | RTopCorner.set(-Infinity, -Infinity, -Infinity);
244 |
245 | // The number of triangle AABBs in the left and right bboxes (needed to calculate SAH cost function)
246 | countLeft = 0;
247 | countRight = 0;
248 |
249 | // allocate triangle AABBs in workList based on their bbox centers
250 | // this is a fast O(N) pass, no triangle AABB sorting needed (yet)
251 | for (let i = 0; i < workList.length; i++)
252 | {
253 | k = workList[i];
254 | testMinCorner.set(aabb_array_copy[9 * k + 0], aabb_array_copy[9 * k + 1], aabb_array_copy[9 * k + 2]);
255 | testMaxCorner.set(aabb_array_copy[9 * k + 3], aabb_array_copy[9 * k + 4], aabb_array_copy[9 * k + 5]);
256 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
257 |
258 | // get bbox center
259 | if (axis == 0)
260 | { // X-axis
261 | value = testCentroid.x;
262 | }
263 | else if (axis == 1)
264 | { // Y-axis
265 | value = testCentroid.y;
266 | }
267 | else
268 | { // Z-axis
269 | value = testCentroid.z;
270 | }
271 |
272 | if (value < testSplit)
273 | {
274 | // if centroid is smaller then testSplit, put triangle box in Left bbox
275 | LBottomCorner.min(testMinCorner);
276 | LTopCorner.max(testMaxCorner);
277 | countLeft++;
278 | } else
279 | {
280 | // else put triangle box in Right bbox
281 | RBottomCorner.min(testMinCorner);
282 | RTopCorner.max(testMaxCorner);
283 | countRight++;
284 | }
285 | } // end for (let i = 0; i < workList.length; i++)
286 |
287 | // First, check for bad partitionings, i.e. bins with 0 triangle AABBs make no sense
288 | if (countLeft < 1 || countRight < 1)
289 | continue;
290 |
291 | // Now use the Surface Area Heuristic to see if this split has a better "cost"
292 |
293 | // It's a real partitioning, calculate the sides of Left and Right BBox
294 | lside0 = LTopCorner.x - LBottomCorner.x;
295 | lside1 = LTopCorner.y - LBottomCorner.y;
296 | lside2 = LTopCorner.z - LBottomCorner.z;
297 |
298 | rside0 = RTopCorner.x - RBottomCorner.x;
299 | rside1 = RTopCorner.y - RBottomCorner.y;
300 | rside2 = RTopCorner.z - RBottomCorner.z;
301 |
302 | // calculate SurfaceArea of Left and Right BBox
303 | surfaceLeft = (lside0 * lside1) + (lside1 * lside2) + (lside2 * lside0);
304 | surfaceRight = (rside0 * rside1) + (rside1 * rside2) + (rside2 * rside0);
305 |
306 | // calculate total cost by multiplying left and right bbox by number of triangle AABBs in each
307 | totalCost = (surfaceLeft * countLeft) + (surfaceRight * countRight);
308 |
309 | // keep track of cheapest split found so far
310 | if (totalCost < minCost)
311 | {
312 | minCost = totalCost;
313 | bestSplit = testSplit;
314 | bestAxis = axis;
315 | bestSplitHasBeenFound = true;
316 | }
317 | } // end for (let partition = 1; partition < numBins; partition++)
318 |
319 | } // end for (let axis = 0; axis < 3; axis++)
320 |
321 | } // end else if (workList.length > 2)
322 |
323 |
324 | // If the SAH strategy failed, now try to populate the current leftWorkLists and rightWorklists with the Object Median strategy
325 | if ( !bestSplitHasBeenFound )
326 | {
327 | //console.log("bestSplit not found, now trying Object Median strategy...");
328 | //console.log("num of AABBs remaining: " + workList.length);
329 |
330 | // determine the longest extent of the box, and start with that as splitting dimension
331 | if (side0 >= side1 && side0 >= side2)
332 | {
333 | longestAxis = 0;
334 | if (side1 >= side2)
335 | {
336 | mediumAxis = 1; shortestAxis = 2;
337 | }
338 | else
339 | {
340 | mediumAxis = 2; shortestAxis = 1;
341 | }
342 | }
343 | else if (side1 >= side0 && side1 >= side2)
344 | {
345 | longestAxis = 1;
346 | if (side0 >= side2)
347 | {
348 | mediumAxis = 0; shortestAxis = 2;
349 | }
350 | else
351 | {
352 | mediumAxis = 2; shortestAxis = 0;
353 | }
354 | }
355 | else// if (side2 >= side0 && side2 >= side1)
356 | {
357 | longestAxis = 2;
358 | if (side0 >= side1)
359 | {
360 | mediumAxis = 0; shortestAxis = 1;
361 | }
362 | else
363 | {
364 | mediumAxis = 1; shortestAxis = 0;
365 | }
366 | }
367 |
368 | // try longest axis first, then try the other two if necessary
369 | currentAxis = longestAxis; // a split along the longest axis would be optimal, so try this first
370 | // reset counters for the loop coming up
371 | leftWorkCount = 0;
372 | rightWorkCount = 0;
373 |
374 | // this loop is to count how many elements we will need for the left branch and the right branch
375 | for (let i = 0; i < workList.length; i++)
376 | {
377 | k = workList[i];
378 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
379 |
380 | // get bbox center
381 | if (currentAxis == 0)
382 | {
383 | value = testCentroid.x; // X-axis
384 | testSplit = centroidAverage.x;
385 | //testSplit = spatialAverage.x;
386 | }
387 | else if (currentAxis == 1)
388 | {
389 | value = testCentroid.y; // Y-axis
390 | testSplit = centroidAverage.y;
391 | //testSplit = spatialAverage.y;
392 | }
393 | else
394 | {
395 | value = testCentroid.z; // Z-axis
396 | testSplit = centroidAverage.z;
397 | //testSplit = spatialAverage.z;
398 | }
399 |
400 | if (value < testSplit)
401 | {
402 | leftWorkCount++;
403 | } else
404 | {
405 | rightWorkCount++;
406 | }
407 | }
408 |
409 | if (leftWorkCount > 0 && rightWorkCount > 0)
410 | {
411 | bestSplit = testSplit;
412 | bestAxis = currentAxis;
413 | bestSplitHasBeenFound = true;
414 | }
415 |
416 | if ( !bestSplitHasBeenFound ) // if longest axis failed
417 | {
418 | currentAxis = mediumAxis; // try middle-length axis next
419 | // reset counters for the loop coming up
420 | leftWorkCount = 0;
421 | rightWorkCount = 0;
422 |
423 | // this loop is to count how many elements we will need for the left branch and the right branch
424 | for (let i = 0; i < workList.length; i++)
425 | {
426 | k = workList[i];
427 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
428 |
429 | // get bbox center
430 | if (currentAxis == 0)
431 | {
432 | value = testCentroid.x; // X-axis
433 | testSplit = centroidAverage.x;
434 | //testSplit = spatialAverage.x;
435 | }
436 | else if (currentAxis == 1)
437 | {
438 | value = testCentroid.y; // Y-axis
439 | testSplit = centroidAverage.y;
440 | //testSplit = spatialAverage.y;
441 | }
442 | else
443 | {
444 | value = testCentroid.z; // Z-axis
445 | testSplit = centroidAverage.z;
446 | //testSplit = spatialAverage.z;
447 | }
448 |
449 | if (value < testSplit)
450 | {
451 | leftWorkCount++;
452 | } else
453 | {
454 | rightWorkCount++;
455 | }
456 | }
457 |
458 | if (leftWorkCount > 0 && rightWorkCount > 0)
459 | {
460 | bestSplit = testSplit;
461 | bestAxis = currentAxis;
462 | bestSplitHasBeenFound = true;
463 | }
464 | } // end if ( !bestSplitHasBeenFound ) // if longest axis failed
465 |
466 | if ( !bestSplitHasBeenFound ) // if middle-length axis failed
467 | {
468 | currentAxis = shortestAxis; // try shortest axis last
469 | // reset counters for the loop coming up
470 | leftWorkCount = 0;
471 | rightWorkCount = 0;
472 |
473 | // this loop is to count how many elements we will need for the left branch and the right branch
474 | for (let i = 0; i < workList.length; i++)
475 | {
476 | k = workList[i];
477 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
478 |
479 | // get bbox center
480 | if (currentAxis == 0)
481 | {
482 | value = testCentroid.x; // X-axis
483 | testSplit = centroidAverage.x;
484 | //testSplit = spatialAverage.x;
485 | }
486 | else if (currentAxis == 1)
487 | {
488 | value = testCentroid.y; // Y-axis
489 | testSplit = centroidAverage.y;
490 | //testSplit = spatialAverage.y;
491 | }
492 | else
493 | {
494 | value = testCentroid.z; // Z-axis
495 | testSplit = centroidAverage.z;
496 | //testSplit = spatialAverage.z;
497 | }
498 |
499 | if (value < testSplit)
500 | {
501 | leftWorkCount++;
502 | } else
503 | {
504 | rightWorkCount++;
505 | }
506 | }
507 |
508 | if (leftWorkCount > 0 && rightWorkCount > 0)
509 | {
510 | bestSplit = testSplit;
511 | bestAxis = currentAxis;
512 | bestSplitHasBeenFound = true;
513 | }
514 | } // end if ( !bestSplitHasBeenFound ) // if middle-length axis failed
515 |
516 | } // end if ( !bestSplitHasBeenFound ) // If the SAH strategy failed
517 |
518 |
519 | leftWorkCount = 0;
520 | rightWorkCount = 0;
521 |
522 | // if all strategies have failed, we must manually populate the current leftWorkLists and rightWorklists
523 | if ( !bestSplitHasBeenFound )
524 | {
525 | //console.log("bestSplit still not found, resorting to manual placement...");
526 | //console.log("num of AABBs remaining: " + workList.length);
527 |
528 | // this loop is to count how many elements we need for the left branch and the right branch
529 | for (let i = 0; i < workList.length; i++)
530 | {
531 | if (i % 2 == 0)
532 | {
533 | leftWorkCount++;
534 | } else
535 | {
536 | rightWorkCount++;
537 | }
538 | }
539 |
540 | // now that the size of each branch is known, we can initialize the left and right arrays
541 | leftWorkLists[stackptr] = new Uint32Array(leftWorkCount);
542 | rightWorkLists[stackptr] = new Uint32Array(rightWorkCount);
543 |
544 | // reset counters for the loop coming up
545 | leftWorkCount = 0;
546 | rightWorkCount = 0;
547 |
548 | for (let i = 0; i < workList.length; i++)
549 | {
550 | k = workList[i];
551 |
552 | if (i % 2 == 0)
553 | {
554 | leftWorkLists[stackptr][leftWorkCount] = k;
555 | leftWorkCount++;
556 | } else
557 | {
558 | rightWorkLists[stackptr][rightWorkCount] = k;
559 | rightWorkCount++;
560 | }
561 | }
562 |
563 | return; // return early
564 | } // end if ( !bestSplitHasBeenFound )
565 |
566 |
567 | // the following code can only be reached if (workList.length > 2) and bestSplit has been successfully found.
568 | // Other unsuccessful conditions will have been handled and will 'return' earlier
569 |
570 | // distribute the triangle AABBs in the left or right child nodes
571 | leftWorkCount = 0;
572 | rightWorkCount = 0;
573 |
574 | // this loop is to count how many elements we need for the left branch and the right branch
575 | for (let i = 0; i < workList.length; i++)
576 | {
577 | k = workList[i];
578 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
579 |
580 | // get bbox center
581 | if (bestAxis == 0) value = testCentroid.x; // X-axis
582 | else if (bestAxis == 1) value = testCentroid.y; // Y-axis
583 | else value = testCentroid.z; // Z-axis
584 |
585 | if (value < bestSplit)
586 | {
587 | leftWorkCount++;
588 | } else
589 | {
590 | rightWorkCount++;
591 | }
592 | }
593 |
594 | // now that the size of each branch is known, we can initialize the left and right arrays
595 | leftWorkLists[stackptr] = new Uint32Array(leftWorkCount);
596 | rightWorkLists[stackptr] = new Uint32Array(rightWorkCount);
597 |
598 | // reset counters for the loop coming up
599 | leftWorkCount = 0;
600 | rightWorkCount = 0;
601 |
602 | // populate the current leftWorkLists and rightWorklists
603 | for (let i = 0; i < workList.length; i++)
604 | {
605 | k = workList[i];
606 | testCentroid.set(aabb_array_copy[9 * k + 6], aabb_array_copy[9 * k + 7], aabb_array_copy[9 * k + 8]);
607 |
608 | // get bbox center
609 | if (bestAxis == 0) value = testCentroid.x; // X-axis
610 | else if (bestAxis == 1) value = testCentroid.y; // Y-axis
611 | else value = testCentroid.z; // Z-axis
612 |
613 | if (value < bestSplit)
614 | {
615 | leftWorkLists[stackptr][leftWorkCount] = k;
616 | leftWorkCount++;
617 | } else
618 | {
619 | rightWorkLists[stackptr][rightWorkCount] = k;
620 | rightWorkCount++;
621 | }
622 | }
623 |
624 | } // end function BVH_Create_Node(workList, idParent, isRightBranch)
625 |
626 |
627 |
628 |
629 | function BVH_Build_Iterative(workList, aabb_array)
630 | {
631 |
632 | currentList = workList;
633 | // save a global copy of the supplied aabb_array, so that it can be used by the various functions in this file
634 | aabb_array_copy = new Float32Array(aabb_array);
635 |
636 | // reset BVH builder arrays;
637 | buildnodes = [];
638 | leftWorkLists = [];
639 | rightWorkLists = [];
640 | parentList = [];
641 |
642 | // initialize variables
643 | stackptr = 0;
644 |
645 | // parent id of -1, meaning this is the root node, which has no parent
646 | parentList.push(-1);
647 | BVH_Create_Node(currentList, -1, false); // build root node
648 |
649 | // build the tree using the "go down left branches until done, then ascend back up right branches" approach
650 | while (stackptr > -1)
651 | {
652 | // pop the next node off of the left-side stack
653 | currentList = leftWorkLists[stackptr];
654 |
655 | if (currentList != undefined)
656 | { // left side of tree
657 |
658 | leftWorkLists[stackptr] = null; // mark as processed
659 | stackptr++;
660 |
661 | parentList.push(buildnodes.length - 1);
662 |
663 | // build the left node
664 | BVH_Create_Node(currentList, buildnodes.length - 1, false);
665 | }
666 | else
667 | {
668 | currentList = rightWorkLists[stackptr];
669 |
670 | if (currentList != undefined)
671 | {
672 | rightWorkLists[stackptr] = null; // mark as processed
673 | stackptr++;
674 |
675 | // build the right node
676 | BVH_Create_Node(currentList, parentList.pop(), true);
677 | }
678 | else
679 | {
680 | stackptr--;
681 | }
682 | }
683 |
684 | } // end while (stackptr > -1)
685 |
686 |
687 | // Copy the buildnodes array into the aabb_array
688 | for (let n = 0; n < buildnodes.length; n++)
689 | {
690 | // slot 0
691 | aabb_array[8 * n + 0] = buildnodes[n].idPrimitive; // r or x component
692 | aabb_array[8 * n + 1] = buildnodes[n].minCorner.x; // g or y component
693 | aabb_array[8 * n + 2] = buildnodes[n].minCorner.y; // b or z component
694 | aabb_array[8 * n + 3] = buildnodes[n].minCorner.z; // a or w component
695 |
696 | // slot 1
697 | aabb_array[8 * n + 4] = buildnodes[n].idRightChild; // r or x component
698 | aabb_array[8 * n + 5] = buildnodes[n].maxCorner.x; // g or y component
699 | aabb_array[8 * n + 6] = buildnodes[n].maxCorner.y; // b or z component
700 | aabb_array[8 * n + 7] = buildnodes[n].maxCorner.z; // a or w component
701 | }
702 |
703 | } // end function BVH_Build_Iterative(workList, aabb_array)
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/shaders/TheSentinel_Fragment.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | precision highp int;
3 | precision highp sampler2D;
4 | precision highp sampler2DArray;
5 |
6 | #include
7 |
8 | uniform InvMatrices_UniformsGroup {
9 | mat4 uObj3D_InvMatrices[64];
10 | };
11 |
12 | uniform TopLevelBVH_UniformsGroup {
13 | vec4 uTopLevelBVH_aabbData[256];
14 | };
15 |
16 | uniform sampler2DArray tModels_triangleDataTexture2DArray;
17 | uniform sampler2DArray tModels_aabbDataTexture2DArray;
18 | uniform sampler2D tLandscape_TriangleTexture;
19 | uniform sampler2D tLandscape_AABBTexture;
20 | uniform vec3 uSunDirection;
21 | uniform vec3 uViewRayTargetPosition;
22 | uniform float uViewRaySphereRadius;
23 | uniform float uSelectedTileIndex;
24 | uniform float uSelectedObjectIndex;
25 | uniform float uResolvingObjectIndex;
26 | uniform float uDissolveEffectStrength;
27 | uniform bool uDoingDissolveEffect;
28 | uniform bool uPlayingTeleportAnimation;
29 |
30 |
31 | #define INV_TEXTURE_WIDTH 0.00390625 // (1 / 256 texture width)
32 |
33 |
34 | //-----------------------------------------------------------------------
35 |
36 | vec3 rayOrigin, rayDirection;
37 | // recorded intersection data:
38 | vec3 hitNormal, hitEmission, hitColor;
39 | float hitT;
40 | float hitObjectID = -INFINITY;
41 | int hitTextureID;
42 | int hitType = -100;
43 |
44 |
45 | #include
46 |
47 | #include
48 |
49 | #include
50 |
51 | #include
52 |
53 | #include
54 |
55 | #include
56 |
57 | // when there are 2 stackLevels, for example stackLevels[23] and objStackLevels[23],...
58 | vec2 stackLevels[23]; // [23] is max size for my Samsung Galaxy S21, [24] crashes on compile
59 | vec2 objStackLevels[23]; // [23] is max size for my Samsung Galaxy S21, [24] crashes on compile
60 |
61 |
62 | //vec4 boxNodeData0 corresponds to .x = idTriangle, .y = aabbMin.x, .z = aabbMin.y, .w = aabbMin.z
63 | //vec4 boxNodeData1 corresponds to .x = idRightChild .y = aabbMax.x, .z = aabbMax.y, .w = aabbMax.z
64 |
65 | void GetBoxNodeData(const in float i, in sampler2D texture, inout vec4 boxNodeData0, inout vec4 boxNodeData1)
66 | {
67 | // each bounding box's data is encoded in 2 rgba(or xyzw) texture slots
68 | float ix2 = i * 2.0;
69 | // (ix2 + 0.0) corresponds to .x = idTriangle, .y = aabbMin.x, .z = aabbMin.y, .w = aabbMin.z
70 | // (ix2 + 1.0) corresponds to .x = idRightChild .y = aabbMax.x, .z = aabbMax.y, .w = aabbMax.z
71 |
72 | ivec2 uv0 = ivec2( mod(ix2 + 0.0, 256.0), (ix2 + 0.0) * INV_TEXTURE_WIDTH ); // data0
73 | ivec2 uv1 = ivec2( mod(ix2 + 1.0, 256.0), (ix2 + 1.0) * INV_TEXTURE_WIDTH ); // data1
74 |
75 | boxNodeData0 = texelFetch(texture, uv0, 0);
76 | boxNodeData1 = texelFetch(texture, uv1, 0);
77 | }
78 |
79 | void GetBoxNodeUniform(in float i, inout vec4 boxNodeData0, inout vec4 boxNodeData1)
80 | {
81 | // each bounding box's data is encoded in 2 uniform vector4(xyzw) slots
82 | float ix2 = (i * 2.0);
83 | // (ix2 + 0.0) corresponds to .x: idObject, .y: aabbMin.x, .z: aabbMin.y, .w: aabbMin.z
84 | // (ix2 + 1.0) corresponds to .x: idRightChild, .y: aabbMax.x, .z: aabbMax.y, .w: aabbMax.z
85 |
86 | boxNodeData0 = uTopLevelBVH_aabbData[int(ix2 + 0.0)];
87 | boxNodeData1 = uTopLevelBVH_aabbData[int(ix2 + 1.0)];
88 | }
89 |
90 | void GetBoxNode2DArray(in float i, in float depth, inout vec4 boxNodeData0, inout vec4 boxNodeData1)
91 | {
92 | // each bounding box's data is encoded in 2 rgba(or xyzw) texture slots
93 | float ix2 = (i * 2.0);
94 | // (ix2 + 0.0) corresponds to .x: idObject, .y: aabbMin.x, .z: aabbMin.y, .w: aabbMin.z
95 | // (ix2 + 1.0) corresponds to .x: idRightChild, .y: aabbMax.x, .z: aabbMax.y, .w: aabbMax.z
96 |
97 | ivec2 uv0 = ivec2( mod(ix2 + 0.0, 256.0), (ix2 + 0.0) * INV_TEXTURE_WIDTH ); // data0
98 | ivec2 uv1 = ivec2( mod(ix2 + 1.0, 256.0), (ix2 + 1.0) * INV_TEXTURE_WIDTH ); // data1
99 |
100 | boxNodeData0 = texelFetch(tModels_aabbDataTexture2DArray, ivec3(uv0, depth), 0);
101 | boxNodeData1 = texelFetch(tModels_aabbDataTexture2DArray, ivec3(uv1, depth), 0);
102 | }
103 |
104 |
105 | //--------------------------------------------------------------------------------------------------------------------------------------------------------
106 | void Object_BVH_Intersect( vec3 rObjOrigin, vec3 rObjDirection, mat3 invMatrix, in float depth_id, in bool objectIsSelected )
107 | //--------------------------------------------------------------------------------------------------------------------------------------------------------
108 | {
109 | vec4 currentBoxNodeData0, nodeAData0, nodeBData0, tmpNodeData0;
110 | vec4 currentBoxNodeData1, nodeAData1, nodeBData1, tmpNodeData1;
111 |
112 | vec4 vd0, vd1, vd2, vd3, vd4, vd5, vd6;
113 |
114 | vec3 inverseDir = 1.0 / rObjDirection;
115 |
116 | vec2 currentStackData, stackDataA, stackDataB, tmpStackData;
117 | ivec2 uv0, uv1, uv2, uv3, uv4, uv5, uv6;
118 |
119 | float d;
120 | float stackptr = 0.0;
121 | float id = 0.0;
122 | float tu, tv;
123 | float triangleID = 0.0;
124 | float triangleU = 0.0;
125 | float triangleV = 0.0;
126 |
127 | int skip = FALSE;
128 | int triangleLookupNeeded = FALSE;
129 |
130 |
131 | GetBoxNode2DArray(stackptr, depth_id, currentBoxNodeData0, currentBoxNodeData1);
132 | currentStackData = vec2(stackptr, BoundingBoxIntersect(currentBoxNodeData0.yzw, currentBoxNodeData1.yzw, rObjOrigin, inverseDir));
133 | objStackLevels[0] = currentStackData;
134 | skip = (currentStackData.y < hitT) ? TRUE : FALSE;
135 |
136 | while (true)
137 | {
138 | if (skip == FALSE)
139 | {
140 | // decrease pointer by 1 (0.0 is root level, 24.0 is maximum depth)
141 | if (--stackptr < 0.0) // went past the root level, terminate loop
142 | break;
143 |
144 | currentStackData = objStackLevels[int(stackptr)];
145 |
146 | if (currentStackData.y >= hitT)
147 | continue;
148 |
149 | GetBoxNode2DArray(currentStackData.x, depth_id, currentBoxNodeData0, currentBoxNodeData1);
150 | }
151 | skip = FALSE; // reset skip
152 |
153 |
154 | if (currentBoxNodeData0.x < 0.0) // < 0.0 signifies an inner node
155 | {
156 | GetBoxNode2DArray(currentStackData.x + 1.0, depth_id, nodeAData0, nodeAData1);
157 | GetBoxNode2DArray(currentBoxNodeData1.x, depth_id, nodeBData0, nodeBData1);
158 | stackDataA = vec2(currentStackData.x + 1.0, BoundingBoxIntersect(nodeAData0.yzw, nodeAData1.yzw, rObjOrigin, inverseDir));
159 | stackDataB = vec2(currentBoxNodeData1.x, BoundingBoxIntersect(nodeBData0.yzw, nodeBData1.yzw, rObjOrigin, inverseDir));
160 |
161 | // first sort the branch node data so that 'a' is the smallest
162 | if (stackDataB.y < stackDataA.y)
163 | {
164 | tmpStackData = stackDataB;
165 | stackDataB = stackDataA;
166 | stackDataA = tmpStackData;
167 |
168 | tmpNodeData0 = nodeBData0; tmpNodeData1 = nodeBData1;
169 | nodeBData0 = nodeAData0; nodeBData1 = nodeAData1;
170 | nodeAData0 = tmpNodeData0; nodeAData1 = tmpNodeData1;
171 | } // branch 'b' now has the larger rayT value of 'a' and 'b'
172 |
173 | if (stackDataB.y < hitT) // see if branch 'b' (the larger rayT) needs to be processed
174 | {
175 | currentStackData = stackDataB;
176 | currentBoxNodeData0 = nodeBData0;
177 | currentBoxNodeData1 = nodeBData1;
178 | skip = TRUE; // this will prevent the stackptr from decreasing by 1
179 | }
180 | if (stackDataA.y < hitT) // see if branch 'a' (the smaller rayT) needs to be processed
181 | {
182 | if (skip == TRUE) // if larger branch 'b' needed to be processed also,
183 | objStackLevels[int(stackptr++)] = stackDataB; // cue larger branch 'b' for future round
184 | // also, increase pointer by 1
185 |
186 | currentStackData = stackDataA;
187 | currentBoxNodeData0 = nodeAData0;
188 | currentBoxNodeData1 = nodeAData1;
189 | skip = TRUE; // this will prevent the stackptr from decreasing by 1
190 | }
191 |
192 | continue;
193 | } // end if (currentBoxNode.data0.x < 0.0) // inner node
194 |
195 |
196 | // else this is a leaf
197 |
198 | // each triangle's data is encoded in 8 rgba(or xyzw) texture slots
199 | id = 8.0 * currentBoxNodeData0.x;
200 |
201 | uv0 = ivec2( mod(id + 0.0, 256.0), (id + 0.0) * INV_TEXTURE_WIDTH );
202 | uv1 = ivec2( mod(id + 1.0, 256.0), (id + 1.0) * INV_TEXTURE_WIDTH );
203 | uv2 = ivec2( mod(id + 2.0, 256.0), (id + 2.0) * INV_TEXTURE_WIDTH );
204 |
205 | vd0 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv0, depth_id), 0);
206 | vd1 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv1, depth_id), 0);
207 | vd2 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv2, depth_id), 0);
208 |
209 | d = BVH_TriangleIntersect( vec3(vd0.xyz), vec3(vd0.w, vd1.xy), vec3(vd1.zw, vd2.x), rObjOrigin, rObjDirection, tu, tv );
210 |
211 | if (d < hitT)
212 | {
213 | hitT = d;
214 | triangleID = id;
215 | triangleU = tu;
216 | triangleV = tv;
217 | triangleLookupNeeded = TRUE;
218 | }
219 |
220 | } // end while (TRUE)
221 |
222 |
223 | if (triangleLookupNeeded == TRUE)
224 | {
225 | uv0 = ivec2( mod(triangleID + 0.0, 256.0), (triangleID + 0.0) * INV_TEXTURE_WIDTH );
226 | uv1 = ivec2( mod(triangleID + 1.0, 256.0), (triangleID + 1.0) * INV_TEXTURE_WIDTH );
227 | uv2 = ivec2( mod(triangleID + 2.0, 256.0), (triangleID + 2.0) * INV_TEXTURE_WIDTH );
228 | uv3 = ivec2( mod(triangleID + 3.0, 256.0), (triangleID + 3.0) * INV_TEXTURE_WIDTH );
229 | uv4 = ivec2( mod(triangleID + 4.0, 256.0), (triangleID + 4.0) * INV_TEXTURE_WIDTH );
230 | uv5 = ivec2( mod(triangleID + 5.0, 256.0), (triangleID + 5.0) * INV_TEXTURE_WIDTH );
231 | uv6 = ivec2( mod(triangleID + 6.0, 256.0), (triangleID + 6.0) * INV_TEXTURE_WIDTH );
232 | //uv7 = ivec2( mod(triangleID + 7.0, 256.0), (triangleID + 7.0) * INV_TEXTURE_WIDTH );
233 |
234 | vd0 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv0, depth_id), 0);
235 | vd1 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv1, depth_id), 0);
236 | vd2 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv2, depth_id), 0);
237 | vd3 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv3, depth_id), 0);
238 | vd4 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv4, depth_id), 0);
239 | vd5 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv5, depth_id), 0);
240 | vd6 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv6, depth_id), 0);
241 | //vd7 = texelFetch(tModels_triangleDataTexture2DArray, ivec3(uv7, depth_id), 0);
242 |
243 | // face normal for flat-shaded polygon look
244 | hitNormal = ( cross(vec3(vd0.w, vd1.xy) - vec3(vd0.xyz), vec3(vd1.zw, vd2.x) - vec3(vd0.xyz)) );
245 | // transfom normal back into world space
246 | hitNormal = (transpose(invMatrix) * hitNormal);
247 | // else use vertex normals
248 | //triangleW = 1.0 - triangleU - triangleV;
249 | //hitNormal = normalize(triangleW * vec3(vd4.zw, vd5.x) + triangleU * vec3(vd5.yzw) + triangleV * vec3(vd6.xyz));
250 | hitColor = (objectIsSelected && !uDoingDissolveEffect) ? vec3(0,2,1) : vec3(vd4.w, vd5.xy);
251 | hitType = depth_id == 4.0 ? COAT : DIFF;
252 |
253 | } // end if (triangleLookupNeeded == TRUE)
254 |
255 | } // end void Object_BVH_Intersect( vec3 rObjOrigin, vec3 rObjDirection, mat3 invMatrix, in float depth_id, in bool objectIsSelected, in bool doingDissolveEffect )
256 |
257 |
258 | //--------------------------------------------------------------------------------------------------
259 | void SceneIntersect( vec3 rayOrigin, vec3 rayDirection, int bounces, out float hitObjectID )
260 | //--------------------------------------------------------------------------------------------------
261 | {
262 | mat4 invMatrix;
263 |
264 | vec4 currentBoxNodeData0, nodeAData0, nodeBData0, tmpNodeData0;
265 | vec4 currentBoxNodeData1, nodeAData1, nodeBData1, tmpNodeData1;
266 |
267 | vec4 vd0, vd1, vd2, vd3, vd4, vd5, vd6, vd7;
268 |
269 | vec3 rObjOrigin, rObjDirection;
270 | vec3 inverseDir = 1.0 / rayDirection;
271 | vec3 normal;
272 | vec3 hitPos;
273 |
274 | vec2 currentStackData, stackDataA, stackDataB, tmpStackData;
275 | ivec2 uv0, uv1, uv2, uv3, uv4, uv5, uv6, uv7;
276 |
277 | float d;
278 | float stackptr = 0.0;
279 | float id = 0.0;
280 | float model_id = 0.0;
281 | float tu, tv;
282 | float triangleID = 0.0;
283 | float triangleU = 0.0;
284 | float triangleV = 0.0;
285 | float triangleW = 0.0;
286 | float posX, gridX, posZ, gridZ;
287 | float lineThickness = 0.01;
288 | float oneMinusLineThickness = 1.0 - lineThickness;
289 |
290 | int objectCount = 0;
291 |
292 | int skip = FALSE;
293 | int triangleLookupNeeded = FALSE;
294 | int isRayExiting;
295 | bool objectIsSelected = false;
296 |
297 | // reset intersection record's hitT value
298 | hitT = INFINITY;
299 |
300 |
301 |
302 | // LANDSCAPE BVH ////////////
303 |
304 | stackptr = 0.0;
305 | GetBoxNodeData(stackptr, tLandscape_AABBTexture, currentBoxNodeData0, currentBoxNodeData1);
306 | currentStackData = vec2(stackptr, BoundingBoxIntersect(currentBoxNodeData0.yzw, currentBoxNodeData1.yzw, rayOrigin, inverseDir));
307 | stackLevels[0] = currentStackData;
308 | skip = (currentStackData.y < hitT) ? TRUE : FALSE;
309 |
310 | while (true)
311 | {
312 | if (skip == FALSE)
313 | {
314 | // decrease pointer by 1 (0.0 is root level, 24.0 is maximum depth)
315 | if (--stackptr < 0.0) // went past the root level, terminate loop
316 | break;
317 |
318 | currentStackData = stackLevels[int(stackptr)];
319 |
320 | if (currentStackData.y >= hitT)
321 | continue;
322 |
323 | GetBoxNodeData(currentStackData.x, tLandscape_AABBTexture, currentBoxNodeData0, currentBoxNodeData1);
324 | }
325 | skip = FALSE; // reset skip
326 |
327 |
328 | if (currentBoxNodeData0.x < 0.0) // < 0.0 signifies an inner node
329 | {
330 | GetBoxNodeData(currentStackData.x + 1.0, tLandscape_AABBTexture, nodeAData0, nodeAData1);
331 | GetBoxNodeData(currentBoxNodeData1.x, tLandscape_AABBTexture, nodeBData0, nodeBData1);
332 | stackDataA = vec2(currentStackData.x + 1.0, BoundingBoxIntersect(nodeAData0.yzw, nodeAData1.yzw, rayOrigin, inverseDir));
333 | stackDataB = vec2(currentBoxNodeData1.x, BoundingBoxIntersect(nodeBData0.yzw, nodeBData1.yzw, rayOrigin, inverseDir));
334 |
335 | // first sort the branch node data so that 'a' is the smallest
336 | if (stackDataB.y < stackDataA.y)
337 | {
338 | tmpStackData = stackDataB;
339 | stackDataB = stackDataA;
340 | stackDataA = tmpStackData;
341 |
342 | tmpNodeData0 = nodeBData0; tmpNodeData1 = nodeBData1;
343 | nodeBData0 = nodeAData0; nodeBData1 = nodeAData1;
344 | nodeAData0 = tmpNodeData0; nodeAData1 = tmpNodeData1;
345 | } // branch 'b' now has the larger rayT value of 'a' and 'b'
346 |
347 | if (stackDataB.y < hitT) // see if branch 'b' (the larger rayT) needs to be processed
348 | {
349 | currentStackData = stackDataB;
350 | currentBoxNodeData0 = nodeBData0;
351 | currentBoxNodeData1 = nodeBData1;
352 | skip = TRUE; // this will prevent the stackptr from decreasing by 1
353 | }
354 | if (stackDataA.y < hitT) // see if branch 'a' (the smaller rayT) needs to be processed
355 | {
356 | if (skip == TRUE) // if larger branch 'b' needed to be processed also,
357 | stackLevels[int(stackptr++)] = stackDataB; // cue larger branch 'b' for future round
358 | // also, increase pointer by 1
359 |
360 | currentStackData = stackDataA;
361 | currentBoxNodeData0 = nodeAData0;
362 | currentBoxNodeData1 = nodeAData1;
363 | skip = TRUE; // this will prevent the stackptr from decreasing by 1
364 | }
365 |
366 | continue;
367 | } // end if (currentBoxNode.data0.x < 0.0) // inner node
368 |
369 |
370 | // else this is a leaf
371 |
372 | // each triangle's data is encoded in 8 rgba(or xyzw) texture slots
373 | id = 8.0 * currentBoxNodeData0.x;
374 |
375 | uv0 = ivec2( mod(id + 0.0, 256.0), (id + 0.0) * INV_TEXTURE_WIDTH );
376 | uv1 = ivec2( mod(id + 1.0, 256.0), (id + 1.0) * INV_TEXTURE_WIDTH );
377 | uv2 = ivec2( mod(id + 2.0, 256.0), (id + 2.0) * INV_TEXTURE_WIDTH );
378 |
379 | vd0 = texelFetch(tLandscape_TriangleTexture, uv0, 0);
380 | vd1 = texelFetch(tLandscape_TriangleTexture, uv1, 0);
381 | vd2 = texelFetch(tLandscape_TriangleTexture, uv2, 0);
382 |
383 | d = BVH_TriangleIntersect( vec3(vd0.xyz), vec3(vd0.w, vd1.xy), vec3(vd1.zw, vd2.x), rayOrigin, rayDirection, tu, tv );
384 |
385 | if (d < hitT)
386 | {
387 | hitT = d;
388 | triangleID = id;
389 | triangleU = tu;
390 | triangleV = tv;
391 | triangleLookupNeeded = TRUE;
392 | }
393 |
394 | } // end while (TRUE)
395 |
396 |
397 | if (triangleLookupNeeded == TRUE)
398 | {
399 | uv0 = ivec2( mod(triangleID + 0.0, 256.0), (triangleID + 0.0) * INV_TEXTURE_WIDTH );
400 | uv1 = ivec2( mod(triangleID + 1.0, 256.0), (triangleID + 1.0) * INV_TEXTURE_WIDTH );
401 | uv2 = ivec2( mod(triangleID + 2.0, 256.0), (triangleID + 2.0) * INV_TEXTURE_WIDTH );
402 | uv3 = ivec2( mod(triangleID + 3.0, 256.0), (triangleID + 3.0) * INV_TEXTURE_WIDTH );
403 | uv4 = ivec2( mod(triangleID + 4.0, 256.0), (triangleID + 4.0) * INV_TEXTURE_WIDTH );
404 | uv5 = ivec2( mod(triangleID + 5.0, 256.0), (triangleID + 5.0) * INV_TEXTURE_WIDTH );
405 | uv6 = ivec2( mod(triangleID + 6.0, 256.0), (triangleID + 6.0) * INV_TEXTURE_WIDTH );
406 | //uv7 = ivec2( mod(triangleID + 7.0, 256.0), (triangleID + 7.0) * INV_TEXTURE_WIDTH );
407 |
408 | vd0 = texelFetch(tLandscape_TriangleTexture, uv0, 0);
409 | vd1 = texelFetch(tLandscape_TriangleTexture, uv1, 0);
410 | vd2 = texelFetch(tLandscape_TriangleTexture, uv2, 0);
411 | vd3 = texelFetch(tLandscape_TriangleTexture, uv3, 0);
412 | vd4 = texelFetch(tLandscape_TriangleTexture, uv4, 0);
413 | vd5 = texelFetch(tLandscape_TriangleTexture, uv5, 0);
414 | vd6 = texelFetch(tLandscape_TriangleTexture, uv6, 0);
415 | //vd7 = texelFetch(tLandscape_TriangleTexture, uv7, 0);
416 |
417 | // face normal for flat-shaded polygon look
418 | //hitNormal = normalize( cross(vec3(vd0.w, vd1.xy) - vec3(vd0.xyz), vec3(vd1.zw, vd2.x) - vec3(vd0.xyz)) );
419 | // else use vertex normals
420 | triangleW = 1.0 - triangleU - triangleV;
421 | hitNormal = (triangleW * vec3(vd4.zw, vd5.x) + triangleU * vec3(vd5.yzw) + triangleV * vec3(vd6.xyz));
422 | hitColor = (triangleID == uSelectedTileIndex || triangleID == uSelectedTileIndex + 8.0) ? vec3(0,2,1) : vd2.yzw;
423 | hitType = DIFF;
424 | hitObjectID = float(objectCount);
425 | objectCount++;
426 |
427 | hitPos = rayOrigin + rayDirection * hitT;
428 | if (hitColor == vec3(1.0))
429 | {
430 | hitType = COAT;
431 | hitObjectID = float(objectCount);
432 | objectCount++;
433 |
434 | posX = hitPos.x * 0.1;
435 | gridX = floor(posX);
436 | posZ = hitPos.z * 0.1;
437 | gridZ = floor(posZ);
438 |
439 | hitColor = abs(hitNormal.x) > abs(hitNormal.z) ? vec3(0.4) : vec3(0.005,0.001,0.001);
440 |
441 | if (posX - gridX < lineThickness) // to the right of snap grid
442 | {
443 | hitType = SPEC;
444 | hitColor = vec3(0, 0, 1); // blue trim
445 | hitNormal.x -= 1.0;
446 | hitObjectID = float(objectCount);
447 | }
448 | if (posX - gridX > oneMinusLineThickness) // to the left of snap grid
449 | {
450 | hitType = SPEC;
451 | hitColor = vec3(0, 0, 1); // blue trim
452 | hitNormal.x += 1.0;
453 | hitObjectID = float(objectCount);
454 | }
455 | if (posZ - gridZ < lineThickness) // in front of snap grid
456 | {
457 | hitType = SPEC;
458 | hitColor = vec3(0, 0, 1); // blue trim
459 | hitNormal.z -= 1.0;
460 | hitObjectID = float(objectCount);
461 | }
462 | if (posZ - gridZ > oneMinusLineThickness) // behind snap grid
463 | {
464 | hitType = SPEC;
465 | hitColor = vec3(0, 0, 1); // blue trim
466 | hitNormal.z += 1.0;
467 | hitObjectID = float(objectCount);
468 | }
469 |
470 | ///hitNormal = normalize(hitNormal);
471 |
472 | } // end if (hitColor == vec3(1.0))
473 |
474 | } // end if (triangleLookupNeeded == TRUE)
475 |
476 |
477 | // TOP_LEVEL BVH /////////////////
478 |
479 | // reset variables
480 | stackptr = 0.0;
481 | GetBoxNodeUniform(stackptr, currentBoxNodeData0, currentBoxNodeData1);
482 | currentStackData = vec2(stackptr, BoundingBoxIntersect(currentBoxNodeData0.yzw, currentBoxNodeData1.yzw, rayOrigin, inverseDir));
483 | stackLevels[0] = currentStackData;
484 | skip = (currentStackData.y < hitT) ? TRUE : FALSE;
485 |
486 | while (true)
487 | {
488 | if (skip == FALSE)
489 | {
490 | // decrease pointer by 1 (0.0 is root level, 24.0 is maximum depth)
491 | if (--stackptr < 0.0) // went past the root level, terminate loop
492 | break;
493 |
494 | currentStackData = stackLevels[int(stackptr)];
495 |
496 | if (currentStackData.y >= hitT)
497 | continue;
498 |
499 | GetBoxNodeUniform(currentStackData.x, currentBoxNodeData0, currentBoxNodeData1);
500 | }
501 | skip = FALSE; // reset skip
502 |
503 |
504 | if (currentBoxNodeData0.x < 0.0) // < 0.0 signifies an inner node
505 | {
506 | GetBoxNodeUniform(currentStackData.x + 1.0, nodeAData0, nodeAData1);
507 | GetBoxNodeUniform(currentBoxNodeData1.x, nodeBData0, nodeBData1);
508 | stackDataA = vec2(currentStackData.x + 1.0, BoundingBoxIntersect(nodeAData0.yzw, nodeAData1.yzw, rayOrigin, inverseDir));
509 | stackDataB = vec2(currentBoxNodeData1.x, BoundingBoxIntersect(nodeBData0.yzw, nodeBData1.yzw, rayOrigin, inverseDir));
510 |
511 | // first sort the branch node data so that 'a' is the smallest
512 | if (stackDataB.y < stackDataA.y)
513 | {
514 | tmpStackData = stackDataB;
515 | stackDataB = stackDataA;
516 | stackDataA = tmpStackData;
517 |
518 | tmpNodeData0 = nodeBData0; tmpNodeData1 = nodeBData1;
519 | nodeBData0 = nodeAData0; nodeBData1 = nodeAData1;
520 | nodeAData0 = tmpNodeData0; nodeAData1 = tmpNodeData1;
521 | } // branch 'b' now has the larger rayT value of 'a' and 'b'
522 |
523 | if (stackDataB.y < hitT) // see if branch 'b' (the larger rayT) needs to be processed
524 | {
525 | currentStackData = stackDataB;
526 | currentBoxNodeData0 = nodeBData0;
527 | currentBoxNodeData1 = nodeBData1;
528 | skip = TRUE; // this will prevent the stackptr from decreasing by 1
529 | }
530 | if (stackDataA.y < hitT) // see if branch 'a' (the smaller rayT) needs to be processed
531 | {
532 | if (skip == TRUE) // if larger branch 'b' needed to be processed also,
533 | stackLevels[int(stackptr++)] = stackDataB; // cue larger branch 'b' for future round
534 | // also, increase pointer by 1
535 |
536 | currentStackData = stackDataA;
537 | currentBoxNodeData0 = nodeAData0;
538 | currentBoxNodeData1 = nodeAData1;
539 | skip = TRUE; // this will prevent the stackptr from decreasing by 1
540 | }
541 |
542 | continue;
543 | } // end if (currentBoxNodeData0.x < 0.0) // inner node
544 |
545 |
546 | // else this is a leaf
547 | objectIsSelected = uSelectedObjectIndex == currentBoxNodeData0.x || uResolvingObjectIndex == currentBoxNodeData0.x;
548 | if (currentBoxNodeData0.x != 0.0 && objectIsSelected && rng() < uDissolveEffectStrength)
549 | continue;
550 |
551 | invMatrix = uObj3D_InvMatrices[int(currentBoxNodeData0.x)];
552 | model_id = invMatrix[3][3];
553 | if (model_id == 2.0 && bounces == 0 && currentStackData.y < 0.1)
554 | continue; // don't want our view blocked by the inside of our robot's head and shoulders
555 |
556 | // once the model_id code is extracted, set this last matrix element ([15]) back to 1.0
557 | invMatrix[3][3] = 1.0;
558 | // transform ray into leaf object's object space
559 | rObjOrigin = vec3( invMatrix * vec4(rayOrigin, 1.0) );
560 | rObjDirection = vec3( invMatrix * vec4(rayDirection, 0.0) );
561 |
562 | hitObjectID = float(objectCount);
563 | Object_BVH_Intersect(rObjOrigin, rObjDirection, mat3(invMatrix), model_id, objectIsSelected);
564 |
565 | } // end while (TRUE)
566 | objectCount++;
567 |
568 | // viewing ray target metal sphere
569 | d = SphereIntersect( uViewRaySphereRadius, uViewRayTargetPosition, rayOrigin, rayDirection );
570 | if (d < hitT)
571 | {
572 | hitT = d;
573 | hitNormal = (rayOrigin + rayDirection * hitT) - uViewRayTargetPosition;
574 | hitEmission = vec3(0);
575 | hitColor = vec3(1);//vec3(1.0, 0.765557, 0.336057);
576 | hitType = SPEC;
577 | hitObjectID = float(objectCount);
578 | }
579 |
580 |
581 | } // end void SceneIntersect( vec3 rayOrigin, vec3 rayDirection, int bounces, out float hitObjectID )
582 |
583 |
584 |
585 | vec3 getSkyColor(in vec3 rayDir)
586 | {
587 | vec3 topColor = vec3(0.01, 0.2, 1.0);
588 | //topColor *= max(0.3, dot(vec3(0,1,0), uSunDirection));
589 | vec3 bottomColor = vec3(0);
590 | vec3 skyColor = mix(bottomColor, topColor, clamp(pow((rayDir.y + 1.0), 5.0), 0.0, 1.0) );
591 | float sun = max(0.0, dot(rayDir, uSunDirection));
592 | return skyColor + (pow(sun, 180.0) * vec3(0.2,0.1,0.0)) + (pow(sun, 2000.0) * vec3(1,1,0)) + (pow(sun, 10000.0) * vec3(3,2,1));
593 | }
594 |
595 | //----------------------------------------------------------------------------------------------------------------------------------------------------
596 | vec3 CalculateRadiance( out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness )
597 | //----------------------------------------------------------------------------------------------------------------------------------------------------
598 | {
599 |
600 | vec3 accumCol = vec3(0);
601 | vec3 mask = vec3(1);
602 | vec3 reflectionMask = vec3(1);
603 | vec3 reflectionRayOrigin = vec3(0);
604 | vec3 reflectionRayDirection = vec3(0);
605 | vec3 x, n, nl;
606 | vec3 up = vec3(0, 1, 0);
607 |
608 | float nc, nt, ratioIoR, Re, Tr;
609 | float randChoose;
610 | float partialAmount = 0.0;
611 | float previousObjectID;
612 |
613 | int reflectionBounces = -1;
614 | int diffuseCount = 0;
615 | int previousIntersecType = -100;
616 | hitType = -100;
617 |
618 | int bounceIsSpecular = TRUE;
619 | int sampleLight = FALSE;
620 | int willNeedReflectionRay = FALSE;
621 | int isReflectionTime = FALSE;
622 | int reflectionNeedsToBeSharp = FALSE;
623 |
624 |
625 |
626 | for (int bounces = 0; bounces < 6; bounces++)
627 | {
628 | if (isReflectionTime == TRUE)
629 | reflectionBounces++;
630 |
631 | previousIntersecType = hitType;
632 | previousObjectID = hitObjectID;
633 |
634 | SceneIntersect(rayOrigin, rayDirection, bounces, hitObjectID);
635 |
636 | // useful data
637 | n = normalize(hitNormal);
638 | nl = dot(n, rayDirection) < 0.0 ? n : -n;
639 | x = rayOrigin + rayDirection * hitT;
640 |
641 | if (bounces == 0)
642 | {
643 | objectID = hitObjectID;
644 | }
645 | if (isReflectionTime == FALSE && diffuseCount == 0 && hitObjectID != previousObjectID)
646 | {
647 | objectNormal += n;
648 | objectColor += hitColor;
649 | }
650 | if (reflectionNeedsToBeSharp == TRUE && reflectionBounces == 0)
651 | {
652 | objectNormal += n;
653 | objectColor += hitColor;
654 | }
655 |
656 |
657 | if (hitT == INFINITY)
658 | {
659 | if (bounces == 0)
660 | pixelSharpness = 1.0;
661 |
662 | if (bounceIsSpecular == TRUE || sampleLight == TRUE)
663 | accumCol += mask * getSkyColor(rayDirection);
664 |
665 | if (willNeedReflectionRay == TRUE)
666 | {
667 | mask = reflectionMask;
668 | rayOrigin = reflectionRayOrigin;
669 | rayDirection = reflectionRayDirection;
670 |
671 | willNeedReflectionRay = FALSE;
672 | bounceIsSpecular = TRUE;
673 | sampleLight = FALSE;
674 | isReflectionTime = TRUE;
675 | continue;
676 | }
677 |
678 | break;
679 | }
680 |
681 |
682 | // if we get here and sampleLight is still TRUE, shadow ray failed to find the light source
683 | // the ray hit an occluding object along its way to the light
684 | if (sampleLight == TRUE)
685 | {
686 | if (willNeedReflectionRay == TRUE)
687 | {
688 | mask = reflectionMask;
689 | rayOrigin = reflectionRayOrigin;
690 | rayDirection = reflectionRayDirection;
691 |
692 | willNeedReflectionRay = FALSE;
693 | bounceIsSpecular = TRUE;
694 | sampleLight = FALSE;
695 | isReflectionTime = TRUE;
696 | continue;
697 | }
698 |
699 | break;
700 | }
701 |
702 |
703 |
704 | if (hitType == DIFF) // Ideal DIFFUSE reflection
705 | {
706 | diffuseCount++;
707 |
708 | mask *= hitColor;
709 |
710 | if (bounceIsSpecular == TRUE)
711 | {
712 | accumCol += mask * 0.4; // ambient color
713 | }
714 |
715 |
716 | bounceIsSpecular = FALSE;
717 |
718 | rayDirection = randomDirectionInSpecularLobe(uSunDirection, 0.03);
719 | rayOrigin = x + nl * uEPS_intersect;
720 |
721 | mask *= clamp(dot(nl, rayDirection), 0.0, 1.0);
722 |
723 | sampleLight = TRUE;
724 | continue;
725 |
726 | } // end if (hitType == DIFF)
727 |
728 | if (hitType == SPEC) // Ideal SPECULAR reflection
729 | {
730 | mask *= hitColor;
731 |
732 | rayDirection = reflect(rayDirection, nl);
733 | rayOrigin = x + nl * uEPS_intersect;
734 |
735 | continue;
736 | }
737 |
738 | if (hitType == COAT) // Diffuse object underneath with ClearCoat on top
739 | {
740 | nc = 1.0; // IOR of Air
741 | nt = 1.5; // IOR of Clear Coat
742 | Re = calcFresnelReflectance(rayDirection, nl, nc, nt, ratioIoR);
743 | Tr = 1.0 - Re;
744 |
745 | if (bounces == 0)
746 | {
747 | reflectionMask = mask * Re;
748 | reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface
749 | reflectionRayOrigin = x + nl * uEPS_intersect;
750 | willNeedReflectionRay = TRUE;
751 | reflectionNeedsToBeSharp = TRUE;
752 | }
753 |
754 | diffuseCount++;
755 |
756 | mask *= Tr;
757 | mask *= hitColor;
758 |
759 | if (bounceIsSpecular == TRUE)
760 | {
761 | accumCol += mask * 0.4; // ambient color
762 | }
763 |
764 | bounceIsSpecular = FALSE;
765 |
766 | rayDirection = randomDirectionInSpecularLobe(uSunDirection, 0.03);
767 | rayOrigin = x + nl * uEPS_intersect;
768 |
769 | mask *= clamp(dot(nl, rayDirection), 0.0, 1.0);
770 |
771 | sampleLight = TRUE;
772 | continue;
773 |
774 | } //end if (hitType == COAT)
775 |
776 | } // end for (int bounces = 0; bounces < 6; bounces++)
777 |
778 | return max(vec3(0), accumCol);
779 |
780 | } // end vec3 CalculateRadiance( vec3 rayOrigin, vec3 rayDirection, out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness )
781 |
782 |
783 | // SetupScene() not used in this game
784 | /*
785 | //-----------------------------------------------------------------------
786 | void SetupScene(void)
787 | //-----------------------------------------------------------------------
788 | {
789 |
790 | }
791 | */
792 |
793 | //#include
794 |
795 | // tentFilter from Peter Shirley's 'Realistic Ray Tracing (2nd Edition)' book, pg. 60
796 | float tentFilter(float x)
797 | {
798 | return (x < 0.5) ? sqrt(2.0 * x) - 1.0 : 1.0 - sqrt(2.0 - (2.0 * x));
799 | }
800 |
801 | void main( void )
802 | {
803 | vec3 camRight = vec3( uCameraMatrix[0][0], uCameraMatrix[0][1], uCameraMatrix[0][2]);
804 | vec3 camUp = vec3( uCameraMatrix[1][0], uCameraMatrix[1][1], uCameraMatrix[1][2]);
805 | vec3 camForward = vec3(-uCameraMatrix[2][0], -uCameraMatrix[2][1], -uCameraMatrix[2][2]);
806 | // the following is not needed - three.js has a built-in uniform named cameraPosition
807 | //vec3 camPos = vec3( uCameraMatrix[3][0], uCameraMatrix[3][1], uCameraMatrix[3][2]);
808 |
809 | // calculate unique seed for rng() function
810 | seed = uvec2(uFrameCounter, uFrameCounter + 1.0) * uvec2(gl_FragCoord);
811 | // initialize rand() variables
812 | randNumber = 0.0; // the final randomly-generated number (range: 0.0 to 1.0)
813 | blueNoise = texelFetch(tBlueNoiseTexture, ivec2(mod(floor(gl_FragCoord.xy), 128.0)), 0).r;
814 |
815 | vec2 pixelOffset = vec2( tentFilter(rand()), tentFilter(rand()) );
816 | pixelOffset *= uCameraIsMoving ? 0.5 : 1.0;
817 |
818 | // we must map pixelPos into the range -1.0 to +1.0
819 | vec2 pixelPos = ((gl_FragCoord.xy + vec2(0.5) + pixelOffset) / uResolution) * 2.0 - 1.0;
820 |
821 | vec3 rayDir = normalize( pixelPos.x * camRight * uULen + pixelPos.y * camUp * uVLen + camForward );
822 |
823 | // depth of field
824 | //vec3 focalPoint = uFocusDistance * rayDir;
825 | vec3 focalPoint = (distance(cameraPosition, uViewRayTargetPosition) - 1.0) * rayDir;
826 | float randomAngle = rand() * TWO_PI; // pick random point on aperture
827 | float randomRadius = rand() * uApertureSize;
828 | vec3 randomAperturePos = ( cos(randomAngle) * camRight + sin(randomAngle) * camUp ) * sqrt(randomRadius);
829 | // point on aperture to focal point
830 | vec3 finalRayDir = normalize(focalPoint - randomAperturePos);
831 |
832 | rayOrigin = cameraPosition + randomAperturePos;
833 | rayDirection = finalRayDir;
834 |
835 | //SetupScene(); // not used in this game
836 |
837 | // 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),
838 | // 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.
839 | vec3 objectNormal, objectColor;
840 | float objectID = -INFINITY;
841 | float pixelSharpness = 0.0;
842 | //float dynamicSurface = 0.0;
843 |
844 | // perform path tracing and get resulting pixel color
845 | vec4 currentPixel = vec4( vec3(CalculateRadiance(objectNormal, objectColor, objectID, pixelSharpness)), 0.0 );
846 |
847 | // if difference between normals of neighboring pixels is less than the first edge0 threshold, the white edge line effect is considered off (0.0)
848 | float edge0 = 0.2; // edge0 is the minimum difference required between normals of neighboring pixels to start becoming a white edge line
849 | // 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)
850 | 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)
851 | float difference_Nx = fwidth(objectNormal.x);
852 | float difference_Ny = fwidth(objectNormal.y);
853 | float difference_Nz = fwidth(objectNormal.z);
854 | float normalDifference = smoothstep(edge0, edge1, difference_Nx) + smoothstep(edge0, edge1, difference_Ny) + smoothstep(edge0, edge1, difference_Nz);
855 |
856 | float objectDifference = min(fwidth(objectID), 1.0);
857 |
858 | float colorDifference = (fwidth(objectColor.r) + fwidth(objectColor.g) + fwidth(objectColor.b)) > 0.0 ? 1.0 : 0.0;
859 | // white-line debug visualization for normal difference
860 | //currentPixel.rgb += (rng() * 1.5) * vec3(normalDifference);
861 | // white-line debug visualization for object difference
862 | //currentPixel.rgb += (rng() * 1.5) * vec3(objectDifference);
863 | // white-line debug visualization for color difference
864 | //currentPixel.rgb += (rng() * 1.5) * vec3(colorDifference);
865 | // white-line debug visualization for all 3 differences
866 | //currentPixel.rgb += (rng() * 1.5) * vec3( clamp(max(normalDifference, max(objectDifference, colorDifference)), 0.0, 1.0) );
867 |
868 | vec4 previousPixel = texelFetch(tPreviousTexture, ivec2(gl_FragCoord.xy), 0);
869 |
870 |
871 | if (uCameraIsMoving)
872 | {
873 | previousPixel.rgb *= 0.6; // motion-blur trail amount (old image)
874 | currentPixel.rgb *= 0.4; // brightness of new image (noisy)
875 |
876 | previousPixel.a = 0.0;
877 | }
878 | else if (uPlayingTeleportAnimation)
879 | {
880 | previousPixel.rgb *= 0.9; // motion-blur trail amount (old image)
881 | currentPixel.rgb *= 0.1; // brightness of new image (noisy)
882 |
883 | previousPixel.a = 0.0;
884 | }
885 | else
886 | {
887 | previousPixel.rgb *= 0.8; // motion-blur trail amount (old image)
888 | currentPixel.rgb *= 0.2; // brightness of new image (noisy)
889 | }
890 |
891 | currentPixel.a = pixelSharpness;
892 |
893 | // check for all edges that are not light sources
894 | if (pixelSharpness < 1.01 && (colorDifference >= 1.0 || normalDifference >= 0.1 || objectDifference >= 1.0)) // all other edges
895 | currentPixel.a = pixelSharpness = 1.0;
896 |
897 | // makes light source edges (shape boundaries) more stable
898 | // if (previousPixel.a == 1.01)
899 | // currentPixel.a = 1.01;
900 |
901 | // makes sharp edges more stable
902 | if (previousPixel.a == 1.0)
903 | currentPixel.a = 1.0;
904 |
905 | // for dynamic scenes (to clear out old, dark, sharp pixel trails left behind from moving objects)
906 | if (previousPixel.a == 1.0 && rng() < 0.05)
907 | currentPixel.a = 0.0;
908 |
909 |
910 | pc_fragColor = vec4(previousPixel.rgb + currentPixel.rgb, currentPixel.a);
911 | }
912 |
--------------------------------------------------------------------------------
/js/BufferGeometryUtils.js:
--------------------------------------------------------------------------------
1 | import {
2 | BufferAttribute,
3 | BufferGeometry,
4 | Float32BufferAttribute,
5 | InstancedBufferAttribute,
6 | InterleavedBuffer,
7 | InterleavedBufferAttribute,
8 | TriangleFanDrawMode,
9 | TriangleStripDrawMode,
10 | TrianglesDrawMode,
11 | Vector3,
12 | } from 'three';
13 |
14 | function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
15 |
16 | if ( ! MikkTSpace || ! MikkTSpace.isReady ) {
17 |
18 | throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' );
19 |
20 | }
21 |
22 | if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) {
23 |
24 | throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' );
25 |
26 | }
27 |
28 | function getAttributeArray( attribute ) {
29 |
30 | if ( attribute.normalized || attribute.isInterleavedBufferAttribute ) {
31 |
32 | const dstArray = new Float32Array( attribute.getCount() * attribute.itemSize );
33 |
34 | for ( let i = 0, j = 0; i < attribute.getCount(); i ++ ) {
35 |
36 | dstArray[ j ++ ] = attribute.getX( i );
37 | dstArray[ j ++ ] = attribute.getY( i );
38 |
39 | if ( attribute.itemSize > 2 ) {
40 |
41 | dstArray[ j ++ ] = attribute.getZ( i );
42 |
43 | }
44 |
45 | }
46 |
47 | return dstArray;
48 |
49 | }
50 |
51 | if ( attribute.array instanceof Float32Array ) {
52 |
53 | return attribute.array;
54 |
55 | }
56 |
57 | return new Float32Array( attribute.array );
58 |
59 | }
60 |
61 | // MikkTSpace algorithm requires non-indexed input.
62 |
63 | const _geometry = geometry.index ? geometry.toNonIndexed() : geometry;
64 |
65 | // Compute vertex tangents.
66 |
67 | const tangents = MikkTSpace.generateTangents(
68 |
69 | getAttributeArray( _geometry.attributes.position ),
70 | getAttributeArray( _geometry.attributes.normal ),
71 | getAttributeArray( _geometry.attributes.uv )
72 |
73 | );
74 |
75 | // Texture coordinate convention of glTF differs from the apparent
76 | // default of the MikkTSpace library; .w component must be flipped.
77 |
78 | if ( negateSign ) {
79 |
80 | for ( let i = 3; i < tangents.length; i += 4 ) {
81 |
82 | tangents[ i ] *= - 1;
83 |
84 | }
85 |
86 | }
87 |
88 | //
89 |
90 | _geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) );
91 |
92 | if ( geometry !== _geometry ) {
93 |
94 | geometry.copy( _geometry );
95 |
96 | }
97 |
98 | return geometry;
99 |
100 | }
101 |
102 | /**
103 | * @param {Array} geometries
104 | * @param {Boolean} useGroups
105 | * @return {BufferGeometry}
106 | */
107 | function mergeGeometries( geometries, useGroups = false ) {
108 |
109 | const isIndexed = geometries[ 0 ].index !== null;
110 |
111 | const attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) );
112 | const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) );
113 |
114 | const attributes = {};
115 | const morphAttributes = {};
116 |
117 | const morphTargetsRelative = geometries[ 0 ].morphTargetsRelative;
118 |
119 | const mergedGeometry = new BufferGeometry();
120 |
121 | let offset = 0;
122 |
123 | for ( let i = 0; i < geometries.length; ++ i ) {
124 |
125 | const geometry = geometries[ i ];
126 | let attributesCount = 0;
127 |
128 | // ensure that all geometries are indexed, or none
129 |
130 | if ( isIndexed !== ( geometry.index !== null ) ) {
131 |
132 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' );
133 | return null;
134 |
135 | }
136 |
137 | // gather attributes, exit early if they're different
138 |
139 | for ( const name in geometry.attributes ) {
140 |
141 | if ( ! attributesUsed.has( name ) ) {
142 |
143 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' );
144 | return null;
145 |
146 | }
147 |
148 | if ( attributes[ name ] === undefined ) attributes[ name ] = [];
149 |
150 | attributes[ name ].push( geometry.attributes[ name ] );
151 |
152 | attributesCount ++;
153 |
154 | }
155 |
156 | // ensure geometries have the same number of attributes
157 |
158 | if ( attributesCount !== attributesUsed.size ) {
159 |
160 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' );
161 | return null;
162 |
163 | }
164 |
165 | // gather morph attributes, exit early if they're different
166 |
167 | if ( morphTargetsRelative !== geometry.morphTargetsRelative ) {
168 |
169 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' );
170 | return null;
171 |
172 | }
173 |
174 | for ( const name in geometry.morphAttributes ) {
175 |
176 | if ( ! morphAttributesUsed.has( name ) ) {
177 |
178 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' );
179 | return null;
180 |
181 | }
182 |
183 | if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = [];
184 |
185 | morphAttributes[ name ].push( geometry.morphAttributes[ name ] );
186 |
187 | }
188 |
189 | if ( useGroups ) {
190 |
191 | let count;
192 |
193 | if ( isIndexed ) {
194 |
195 | count = geometry.index.count;
196 |
197 | } else if ( geometry.attributes.position !== undefined ) {
198 |
199 | count = geometry.attributes.position.count;
200 |
201 | } else {
202 |
203 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' );
204 | return null;
205 |
206 | }
207 |
208 | mergedGeometry.addGroup( offset, count, i );
209 |
210 | offset += count;
211 |
212 | }
213 |
214 | }
215 |
216 | // merge indices
217 |
218 | if ( isIndexed ) {
219 |
220 | let indexOffset = 0;
221 | const mergedIndex = [];
222 |
223 | for ( let i = 0; i < geometries.length; ++ i ) {
224 |
225 | const index = geometries[ i ].index;
226 |
227 | for ( let j = 0; j < index.count; ++ j ) {
228 |
229 | mergedIndex.push( index.getX( j ) + indexOffset );
230 |
231 | }
232 |
233 | indexOffset += geometries[ i ].attributes.position.count;
234 |
235 | }
236 |
237 | mergedGeometry.setIndex( mergedIndex );
238 |
239 | }
240 |
241 | // merge attributes
242 |
243 | for ( const name in attributes ) {
244 |
245 | const mergedAttribute = mergeAttributes( attributes[ name ] );
246 |
247 | if ( ! mergedAttribute ) {
248 |
249 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' );
250 | return null;
251 |
252 | }
253 |
254 | mergedGeometry.setAttribute( name, mergedAttribute );
255 |
256 | }
257 |
258 | // merge morph attributes
259 |
260 | for ( const name in morphAttributes ) {
261 |
262 | const numMorphTargets = morphAttributes[ name ][ 0 ].length;
263 |
264 | if ( numMorphTargets === 0 ) break;
265 |
266 | mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
267 | mergedGeometry.morphAttributes[ name ] = [];
268 |
269 | for ( let i = 0; i < numMorphTargets; ++ i ) {
270 |
271 | const morphAttributesToMerge = [];
272 |
273 | for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) {
274 |
275 | morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] );
276 |
277 | }
278 |
279 | const mergedMorphAttribute = mergeAttributes( morphAttributesToMerge );
280 |
281 | if ( ! mergedMorphAttribute ) {
282 |
283 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' );
284 | return null;
285 |
286 | }
287 |
288 | mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute );
289 |
290 | }
291 |
292 | }
293 |
294 | return mergedGeometry;
295 |
296 | }
297 |
298 | /**
299 | * @param {Array} attributes
300 | * @return {BufferAttribute}
301 | */
302 | function mergeAttributes( attributes ) {
303 |
304 | let TypedArray;
305 | let itemSize;
306 | let normalized;
307 | let arrayLength = 0;
308 |
309 | for ( let i = 0; i < attributes.length; ++ i ) {
310 |
311 | const attribute = attributes[ i ];
312 |
313 | if ( attribute.isInterleavedBufferAttribute ) {
314 |
315 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. InterleavedBufferAttributes are not supported.' );
316 | return null;
317 |
318 | }
319 |
320 | if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
321 | if ( TypedArray !== attribute.array.constructor ) {
322 |
323 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' );
324 | return null;
325 |
326 | }
327 |
328 | if ( itemSize === undefined ) itemSize = attribute.itemSize;
329 | if ( itemSize !== attribute.itemSize ) {
330 |
331 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' );
332 | return null;
333 |
334 | }
335 |
336 | if ( normalized === undefined ) normalized = attribute.normalized;
337 | if ( normalized !== attribute.normalized ) {
338 |
339 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' );
340 | return null;
341 |
342 | }
343 |
344 | arrayLength += attribute.array.length;
345 |
346 | }
347 |
348 | const array = new TypedArray( arrayLength );
349 | let offset = 0;
350 |
351 | for ( let i = 0; i < attributes.length; ++ i ) {
352 |
353 | array.set( attributes[ i ].array, offset );
354 |
355 | offset += attributes[ i ].array.length;
356 |
357 | }
358 |
359 | return new BufferAttribute( array, itemSize, normalized );
360 |
361 | }
362 |
363 | /**
364 | * @param {BufferAttribute}
365 | * @return {BufferAttribute}
366 | */
367 | export function deepCloneAttribute( attribute ) {
368 |
369 | if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) {
370 |
371 | return deinterleaveAttribute( attribute );
372 |
373 | }
374 |
375 | if ( attribute.isInstancedBufferAttribute ) {
376 |
377 | return new InstancedBufferAttribute().copy( attribute );
378 |
379 | }
380 |
381 | return new BufferAttribute().copy( attribute );
382 |
383 | }
384 |
385 | /**
386 | * @param {Array} attributes
387 | * @return {Array}
388 | */
389 | function interleaveAttributes( attributes ) {
390 |
391 | // Interleaves the provided attributes into an InterleavedBuffer and returns
392 | // a set of InterleavedBufferAttributes for each attribute
393 | let TypedArray;
394 | let arrayLength = 0;
395 | let stride = 0;
396 |
397 | // calculate the length and type of the interleavedBuffer
398 | for ( let i = 0, l = attributes.length; i < l; ++ i ) {
399 |
400 | const attribute = attributes[ i ];
401 |
402 | if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
403 | if ( TypedArray !== attribute.array.constructor ) {
404 |
405 | console.error( 'AttributeBuffers of different types cannot be interleaved' );
406 | return null;
407 |
408 | }
409 |
410 | arrayLength += attribute.array.length;
411 | stride += attribute.itemSize;
412 |
413 | }
414 |
415 | // Create the set of buffer attributes
416 | const interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride );
417 | let offset = 0;
418 | const res = [];
419 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ];
420 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ];
421 |
422 | for ( let j = 0, l = attributes.length; j < l; j ++ ) {
423 |
424 | const attribute = attributes[ j ];
425 | const itemSize = attribute.itemSize;
426 | const count = attribute.count;
427 | const iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized );
428 | res.push( iba );
429 |
430 | offset += itemSize;
431 |
432 | // Move the data for each attribute into the new interleavedBuffer
433 | // at the appropriate offset
434 | for ( let c = 0; c < count; c ++ ) {
435 |
436 | for ( let k = 0; k < itemSize; k ++ ) {
437 |
438 | iba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) );
439 |
440 | }
441 |
442 | }
443 |
444 | }
445 |
446 | return res;
447 |
448 | }
449 |
450 | // returns a new, non-interleaved version of the provided attribute
451 | export function deinterleaveAttribute( attribute ) {
452 |
453 | const cons = attribute.data.array.constructor;
454 | const count = attribute.count;
455 | const itemSize = attribute.itemSize;
456 | const normalized = attribute.normalized;
457 |
458 | const array = new cons( count * itemSize );
459 | let newAttribute;
460 | if ( attribute.isInstancedInterleavedBufferAttribute ) {
461 |
462 | newAttribute = new InstancedBufferAttribute( array, itemSize, normalized, attribute.meshPerAttribute );
463 |
464 | } else {
465 |
466 | newAttribute = new BufferAttribute( array, itemSize, normalized );
467 |
468 | }
469 |
470 | for ( let i = 0; i < count; i ++ ) {
471 |
472 | newAttribute.setX( i, attribute.getX( i ) );
473 |
474 | if ( itemSize >= 2 ) {
475 |
476 | newAttribute.setY( i, attribute.getY( i ) );
477 |
478 | }
479 |
480 | if ( itemSize >= 3 ) {
481 |
482 | newAttribute.setZ( i, attribute.getZ( i ) );
483 |
484 | }
485 |
486 | if ( itemSize >= 4 ) {
487 |
488 | newAttribute.setW( i, attribute.getW( i ) );
489 |
490 | }
491 |
492 | }
493 |
494 | return newAttribute;
495 |
496 | }
497 |
498 | // deinterleaves all attributes on the geometry
499 | export function deinterleaveGeometry( geometry ) {
500 |
501 | const attributes = geometry.attributes;
502 | const morphTargets = geometry.morphTargets;
503 | const attrMap = new Map();
504 |
505 | for ( const key in attributes ) {
506 |
507 | const attr = attributes[ key ];
508 | if ( attr.isInterleavedBufferAttribute ) {
509 |
510 | if ( ! attrMap.has( attr ) ) {
511 |
512 | attrMap.set( attr, deinterleaveAttribute( attr ) );
513 |
514 | }
515 |
516 | attributes[ key ] = attrMap.get( attr );
517 |
518 | }
519 |
520 | }
521 |
522 | for ( const key in morphTargets ) {
523 |
524 | const attr = morphTargets[ key ];
525 | if ( attr.isInterleavedBufferAttribute ) {
526 |
527 | if ( ! attrMap.has( attr ) ) {
528 |
529 | attrMap.set( attr, deinterleaveAttribute( attr ) );
530 |
531 | }
532 |
533 | morphTargets[ key ] = attrMap.get( attr );
534 |
535 | }
536 |
537 | }
538 |
539 | }
540 |
541 | /**
542 | * @param {Array} geometry
543 | * @return {number}
544 | */
545 | function estimateBytesUsed( geometry ) {
546 |
547 | // Return the estimated memory used by this geometry in bytes
548 | // Calculate using itemSize, count, and BYTES_PER_ELEMENT to account
549 | // for InterleavedBufferAttributes.
550 | let mem = 0;
551 | for ( const name in geometry.attributes ) {
552 |
553 | const attr = geometry.getAttribute( name );
554 | mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT;
555 |
556 | }
557 |
558 | const indices = geometry.getIndex();
559 | mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0;
560 | return mem;
561 |
562 | }
563 |
564 | /**
565 | * @param {BufferGeometry} geometry
566 | * @param {number} tolerance
567 | * @return {BufferGeometry}
568 | */
569 | function mergeVertices( geometry, tolerance = 1e-4 ) {
570 |
571 | tolerance = Math.max( tolerance, Number.EPSILON );
572 |
573 | // Generate an index buffer if the geometry doesn't have one, or optimize it
574 | // if it's already available.
575 | const hashToIndex = {};
576 | const indices = geometry.getIndex();
577 | const positions = geometry.getAttribute( 'position' );
578 | const vertexCount = indices ? indices.count : positions.count;
579 |
580 | // next value for triangle indices
581 | let nextIndex = 0;
582 |
583 | // attributes and new attribute arrays
584 | const attributeNames = Object.keys( geometry.attributes );
585 | const tmpAttributes = {};
586 | const tmpMorphAttributes = {};
587 | const newIndices = [];
588 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ];
589 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ];
590 |
591 | // Initialize the arrays, allocating space conservatively. Extra
592 | // space will be trimmed in the last step.
593 | for ( let i = 0, l = attributeNames.length; i < l; i ++ ) {
594 |
595 | const name = attributeNames[ i ];
596 | const attr = geometry.attributes[ name ];
597 |
598 | tmpAttributes[ name ] = new BufferAttribute(
599 | new attr.array.constructor( attr.count * attr.itemSize ),
600 | attr.itemSize,
601 | attr.normalized
602 | );
603 |
604 | const morphAttr = geometry.morphAttributes[ name ];
605 | if ( morphAttr ) {
606 |
607 | tmpMorphAttributes[ name ] = new BufferAttribute(
608 | new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ),
609 | morphAttr.itemSize,
610 | morphAttr.normalized
611 | );
612 |
613 | }
614 |
615 | }
616 |
617 | // convert the error tolerance to an amount of decimal places to truncate to
618 | const decimalShift = Math.log10( 1 / tolerance );
619 | const shiftMultiplier = Math.pow( 10, decimalShift );
620 | for ( let i = 0; i < vertexCount; i ++ ) {
621 |
622 | const index = indices ? indices.getX( i ) : i;
623 |
624 | // Generate a hash for the vertex attributes at the current index 'i'
625 | let hash = '';
626 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {
627 |
628 | const name = attributeNames[ j ];
629 | const attribute = geometry.getAttribute( name );
630 | const itemSize = attribute.itemSize;
631 |
632 | for ( let k = 0; k < itemSize; k ++ ) {
633 |
634 | // double tilde truncates the decimal value
635 | hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * shiftMultiplier ) },`;
636 |
637 | }
638 |
639 | }
640 |
641 | // Add another reference to the vertex if it's already
642 | // used by another index
643 | if ( hash in hashToIndex ) {
644 |
645 | newIndices.push( hashToIndex[ hash ] );
646 |
647 | } else {
648 |
649 | // copy data to the new index in the temporary attributes
650 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {
651 |
652 | const name = attributeNames[ j ];
653 | const attribute = geometry.getAttribute( name );
654 | const morphAttr = geometry.morphAttributes[ name ];
655 | const itemSize = attribute.itemSize;
656 | const newarray = tmpAttributes[ name ];
657 | const newMorphArrays = tmpMorphAttributes[ name ];
658 |
659 | for ( let k = 0; k < itemSize; k ++ ) {
660 |
661 | const getterFunc = getters[ k ];
662 | const setterFunc = setters[ k ];
663 | newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) );
664 |
665 | if ( morphAttr ) {
666 |
667 | for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) {
668 |
669 | newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) );
670 |
671 | }
672 |
673 | }
674 |
675 | }
676 |
677 | }
678 |
679 | hashToIndex[ hash ] = nextIndex;
680 | newIndices.push( nextIndex );
681 | nextIndex ++;
682 |
683 | }
684 |
685 | }
686 |
687 | // generate result BufferGeometry
688 | const result = geometry.clone();
689 | for ( const name in geometry.attributes ) {
690 |
691 | const tmpAttribute = tmpAttributes[ name ];
692 |
693 | result.setAttribute( name, new BufferAttribute(
694 | tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ),
695 | tmpAttribute.itemSize,
696 | tmpAttribute.normalized,
697 | ) );
698 |
699 | if ( ! ( name in tmpMorphAttributes ) ) continue;
700 |
701 | for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) {
702 |
703 | const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ];
704 |
705 | result.morphAttributes[ name ][ j ] = new BufferAttribute(
706 | tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ),
707 | tmpMorphAttribute.itemSize,
708 | tmpMorphAttribute.normalized,
709 | );
710 |
711 | }
712 |
713 | }
714 |
715 | // indices
716 |
717 | result.setIndex( newIndices );
718 |
719 | return result;
720 |
721 | }
722 |
723 | /**
724 | * @param {BufferGeometry} geometry
725 | * @param {number} drawMode
726 | * @return {BufferGeometry}
727 | */
728 | function toTrianglesDrawMode( geometry, drawMode ) {
729 |
730 | if ( drawMode === TrianglesDrawMode ) {
731 |
732 | console.warn( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.' );
733 | return geometry;
734 |
735 | }
736 |
737 | if ( drawMode === TriangleFanDrawMode || drawMode === TriangleStripDrawMode ) {
738 |
739 | let index = geometry.getIndex();
740 |
741 | // generate index if not present
742 |
743 | if ( index === null ) {
744 |
745 | const indices = [];
746 |
747 | const position = geometry.getAttribute( 'position' );
748 |
749 | if ( position !== undefined ) {
750 |
751 | for ( let i = 0; i < position.count; i ++ ) {
752 |
753 | indices.push( i );
754 |
755 | }
756 |
757 | geometry.setIndex( indices );
758 | index = geometry.getIndex();
759 |
760 | } else {
761 |
762 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' );
763 | return geometry;
764 |
765 | }
766 |
767 | }
768 |
769 | //
770 |
771 | const numberOfTriangles = index.count - 2;
772 | const newIndices = [];
773 |
774 | if ( drawMode === TriangleFanDrawMode ) {
775 |
776 | // gl.TRIANGLE_FAN
777 |
778 | for ( let i = 1; i <= numberOfTriangles; i ++ ) {
779 |
780 | newIndices.push( index.getX( 0 ) );
781 | newIndices.push( index.getX( i ) );
782 | newIndices.push( index.getX( i + 1 ) );
783 |
784 | }
785 |
786 | } else {
787 |
788 | // gl.TRIANGLE_STRIP
789 |
790 | for ( let i = 0; i < numberOfTriangles; i ++ ) {
791 |
792 | if ( i % 2 === 0 ) {
793 |
794 | newIndices.push( index.getX( i ) );
795 | newIndices.push( index.getX( i + 1 ) );
796 | newIndices.push( index.getX( i + 2 ) );
797 |
798 | } else {
799 |
800 | newIndices.push( index.getX( i + 2 ) );
801 | newIndices.push( index.getX( i + 1 ) );
802 | newIndices.push( index.getX( i ) );
803 |
804 | }
805 |
806 | }
807 |
808 | }
809 |
810 | if ( ( newIndices.length / 3 ) !== numberOfTriangles ) {
811 |
812 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' );
813 |
814 | }
815 |
816 | // build final geometry
817 |
818 | const newGeometry = geometry.clone();
819 | newGeometry.setIndex( newIndices );
820 | newGeometry.clearGroups();
821 |
822 | return newGeometry;
823 |
824 | } else {
825 |
826 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:', drawMode );
827 | return geometry;
828 |
829 | }
830 |
831 | }
832 |
833 | /**
834 | * Calculates the morphed attributes of a morphed/skinned BufferGeometry.
835 | * Helpful for Raytracing or Decals.
836 | * @param {Mesh | Line | Points} object An instance of Mesh, Line or Points.
837 | * @return {Object} An Object with original position/normal attributes and morphed ones.
838 | */
839 | function computeMorphedAttributes( object ) {
840 |
841 | const _vA = new Vector3();
842 | const _vB = new Vector3();
843 | const _vC = new Vector3();
844 |
845 | const _tempA = new Vector3();
846 | const _tempB = new Vector3();
847 | const _tempC = new Vector3();
848 |
849 | const _morphA = new Vector3();
850 | const _morphB = new Vector3();
851 | const _morphC = new Vector3();
852 |
853 | function _calculateMorphedAttributeData(
854 | object,
855 | attribute,
856 | morphAttribute,
857 | morphTargetsRelative,
858 | a,
859 | b,
860 | c,
861 | modifiedAttributeArray
862 | ) {
863 |
864 | _vA.fromBufferAttribute( attribute, a );
865 | _vB.fromBufferAttribute( attribute, b );
866 | _vC.fromBufferAttribute( attribute, c );
867 |
868 | const morphInfluences = object.morphTargetInfluences;
869 |
870 | if ( morphAttribute && morphInfluences ) {
871 |
872 | _morphA.set( 0, 0, 0 );
873 | _morphB.set( 0, 0, 0 );
874 | _morphC.set( 0, 0, 0 );
875 |
876 | for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {
877 |
878 | const influence = morphInfluences[ i ];
879 | const morph = morphAttribute[ i ];
880 |
881 | if ( influence === 0 ) continue;
882 |
883 | _tempA.fromBufferAttribute( morph, a );
884 | _tempB.fromBufferAttribute( morph, b );
885 | _tempC.fromBufferAttribute( morph, c );
886 |
887 | if ( morphTargetsRelative ) {
888 |
889 | _morphA.addScaledVector( _tempA, influence );
890 | _morphB.addScaledVector( _tempB, influence );
891 | _morphC.addScaledVector( _tempC, influence );
892 |
893 | } else {
894 |
895 | _morphA.addScaledVector( _tempA.sub( _vA ), influence );
896 | _morphB.addScaledVector( _tempB.sub( _vB ), influence );
897 | _morphC.addScaledVector( _tempC.sub( _vC ), influence );
898 |
899 | }
900 |
901 | }
902 |
903 | _vA.add( _morphA );
904 | _vB.add( _morphB );
905 | _vC.add( _morphC );
906 |
907 | }
908 |
909 | if ( object.isSkinnedMesh ) {
910 |
911 | object.applyBoneTransform( a, _vA );
912 | object.applyBoneTransform( b, _vB );
913 | object.applyBoneTransform( c, _vC );
914 |
915 | }
916 |
917 | modifiedAttributeArray[ a * 3 + 0 ] = _vA.x;
918 | modifiedAttributeArray[ a * 3 + 1 ] = _vA.y;
919 | modifiedAttributeArray[ a * 3 + 2 ] = _vA.z;
920 | modifiedAttributeArray[ b * 3 + 0 ] = _vB.x;
921 | modifiedAttributeArray[ b * 3 + 1 ] = _vB.y;
922 | modifiedAttributeArray[ b * 3 + 2 ] = _vB.z;
923 | modifiedAttributeArray[ c * 3 + 0 ] = _vC.x;
924 | modifiedAttributeArray[ c * 3 + 1 ] = _vC.y;
925 | modifiedAttributeArray[ c * 3 + 2 ] = _vC.z;
926 |
927 | }
928 |
929 | const geometry = object.geometry;
930 | const material = object.material;
931 |
932 | let a, b, c;
933 | const index = geometry.index;
934 | const positionAttribute = geometry.attributes.position;
935 | const morphPosition = geometry.morphAttributes.position;
936 | const morphTargetsRelative = geometry.morphTargetsRelative;
937 | const normalAttribute = geometry.attributes.normal;
938 | const morphNormal = geometry.morphAttributes.position;
939 |
940 | const groups = geometry.groups;
941 | const drawRange = geometry.drawRange;
942 | let i, j, il, jl;
943 | let group;
944 | let start, end;
945 |
946 | const modifiedPosition = new Float32Array( positionAttribute.count * positionAttribute.itemSize );
947 | const modifiedNormal = new Float32Array( normalAttribute.count * normalAttribute.itemSize );
948 |
949 | if ( index !== null ) {
950 |
951 | // indexed buffer geometry
952 |
953 | if ( Array.isArray( material ) ) {
954 |
955 | for ( i = 0, il = groups.length; i < il; i ++ ) {
956 |
957 | group = groups[ i ];
958 |
959 | start = Math.max( group.start, drawRange.start );
960 | end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );
961 |
962 | for ( j = start, jl = end; j < jl; j += 3 ) {
963 |
964 | a = index.getX( j );
965 | b = index.getX( j + 1 );
966 | c = index.getX( j + 2 );
967 |
968 | _calculateMorphedAttributeData(
969 | object,
970 | positionAttribute,
971 | morphPosition,
972 | morphTargetsRelative,
973 | a, b, c,
974 | modifiedPosition
975 | );
976 |
977 | _calculateMorphedAttributeData(
978 | object,
979 | normalAttribute,
980 | morphNormal,
981 | morphTargetsRelative,
982 | a, b, c,
983 | modifiedNormal
984 | );
985 |
986 | }
987 |
988 | }
989 |
990 | } else {
991 |
992 | start = Math.max( 0, drawRange.start );
993 | end = Math.min( index.count, ( drawRange.start + drawRange.count ) );
994 |
995 | for ( i = start, il = end; i < il; i += 3 ) {
996 |
997 | a = index.getX( i );
998 | b = index.getX( i + 1 );
999 | c = index.getX( i + 2 );
1000 |
1001 | _calculateMorphedAttributeData(
1002 | object,
1003 | positionAttribute,
1004 | morphPosition,
1005 | morphTargetsRelative,
1006 | a, b, c,
1007 | modifiedPosition
1008 | );
1009 |
1010 | _calculateMorphedAttributeData(
1011 | object,
1012 | normalAttribute,
1013 | morphNormal,
1014 | morphTargetsRelative,
1015 | a, b, c,
1016 | modifiedNormal
1017 | );
1018 |
1019 | }
1020 |
1021 | }
1022 |
1023 | } else {
1024 |
1025 | // non-indexed buffer geometry
1026 |
1027 | if ( Array.isArray( material ) ) {
1028 |
1029 | for ( i = 0, il = groups.length; i < il; i ++ ) {
1030 |
1031 | group = groups[ i ];
1032 |
1033 | start = Math.max( group.start, drawRange.start );
1034 | end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );
1035 |
1036 | for ( j = start, jl = end; j < jl; j += 3 ) {
1037 |
1038 | a = j;
1039 | b = j + 1;
1040 | c = j + 2;
1041 |
1042 | _calculateMorphedAttributeData(
1043 | object,
1044 | positionAttribute,
1045 | morphPosition,
1046 | morphTargetsRelative,
1047 | a, b, c,
1048 | modifiedPosition
1049 | );
1050 |
1051 | _calculateMorphedAttributeData(
1052 | object,
1053 | normalAttribute,
1054 | morphNormal,
1055 | morphTargetsRelative,
1056 | a, b, c,
1057 | modifiedNormal
1058 | );
1059 |
1060 | }
1061 |
1062 | }
1063 |
1064 | } else {
1065 |
1066 | start = Math.max( 0, drawRange.start );
1067 | end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );
1068 |
1069 | for ( i = start, il = end; i < il; i += 3 ) {
1070 |
1071 | a = i;
1072 | b = i + 1;
1073 | c = i + 2;
1074 |
1075 | _calculateMorphedAttributeData(
1076 | object,
1077 | positionAttribute,
1078 | morphPosition,
1079 | morphTargetsRelative,
1080 | a, b, c,
1081 | modifiedPosition
1082 | );
1083 |
1084 | _calculateMorphedAttributeData(
1085 | object,
1086 | normalAttribute,
1087 | morphNormal,
1088 | morphTargetsRelative,
1089 | a, b, c,
1090 | modifiedNormal
1091 | );
1092 |
1093 | }
1094 |
1095 | }
1096 |
1097 | }
1098 |
1099 | const morphedPositionAttribute = new Float32BufferAttribute( modifiedPosition, 3 );
1100 | const morphedNormalAttribute = new Float32BufferAttribute( modifiedNormal, 3 );
1101 |
1102 | return {
1103 |
1104 | positionAttribute: positionAttribute,
1105 | normalAttribute: normalAttribute,
1106 | morphedPositionAttribute: morphedPositionAttribute,
1107 | morphedNormalAttribute: morphedNormalAttribute
1108 |
1109 | };
1110 |
1111 | }
1112 |
1113 | function mergeGroups( geometry ) {
1114 |
1115 | if ( geometry.groups.length === 0 ) {
1116 |
1117 | console.warn( 'THREE.BufferGeometryUtils.mergeGroups(): No groups are defined. Nothing to merge.' );
1118 | return geometry;
1119 |
1120 | }
1121 |
1122 | let groups = geometry.groups;
1123 |
1124 | // sort groups by material index
1125 |
1126 | groups = groups.sort( ( a, b ) => {
1127 |
1128 | if ( a.materialIndex !== b.materialIndex ) return a.materialIndex - b.materialIndex;
1129 |
1130 | return a.start - b.start;
1131 |
1132 | } );
1133 |
1134 | // create index for non-indexed geometries
1135 |
1136 | if ( geometry.getIndex() === null ) {
1137 |
1138 | const positionAttribute = geometry.getAttribute( 'position' );
1139 | const indices = [];
1140 |
1141 | for ( let i = 0; i < positionAttribute.count; i += 3 ) {
1142 |
1143 | indices.push( i, i + 1, i + 2 );
1144 |
1145 | }
1146 |
1147 | geometry.setIndex( indices );
1148 |
1149 | }
1150 |
1151 | // sort index
1152 |
1153 | const index = geometry.getIndex();
1154 |
1155 | const newIndices = [];
1156 |
1157 | for ( let i = 0; i < groups.length; i ++ ) {
1158 |
1159 | const group = groups[ i ];
1160 |
1161 | const groupStart = group.start;
1162 | const groupLength = groupStart + group.count;
1163 |
1164 | for ( let j = groupStart; j < groupLength; j ++ ) {
1165 |
1166 | newIndices.push( index.getX( j ) );
1167 |
1168 | }
1169 |
1170 | }
1171 |
1172 | geometry.dispose(); // Required to force buffer recreation
1173 | geometry.setIndex( newIndices );
1174 |
1175 | // update groups indices
1176 |
1177 | let start = 0;
1178 |
1179 | for ( let i = 0; i < groups.length; i ++ ) {
1180 |
1181 | const group = groups[ i ];
1182 |
1183 | group.start = start;
1184 | start += group.count;
1185 |
1186 | }
1187 |
1188 | // merge groups
1189 |
1190 | let currentGroup = groups[ 0 ];
1191 |
1192 | geometry.groups = [ currentGroup ];
1193 |
1194 | for ( let i = 1; i < groups.length; i ++ ) {
1195 |
1196 | const group = groups[ i ];
1197 |
1198 | if ( currentGroup.materialIndex === group.materialIndex ) {
1199 |
1200 | currentGroup.count += group.count;
1201 |
1202 | } else {
1203 |
1204 | currentGroup = group;
1205 | geometry.groups.push( currentGroup );
1206 |
1207 | }
1208 |
1209 | }
1210 |
1211 | return geometry;
1212 |
1213 | }
1214 |
1215 |
1216 | // Creates a new, non-indexed geometry with smooth normals everywhere except faces that meet at
1217 | // an angle greater than the crease angle.
1218 | function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) {
1219 |
1220 | const creaseDot = Math.cos( creaseAngle );
1221 | const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
1222 |
1223 | // reusable vertors
1224 | const verts = [ new Vector3(), new Vector3(), new Vector3() ];
1225 | const tempVec1 = new Vector3();
1226 | const tempVec2 = new Vector3();
1227 | const tempNorm = new Vector3();
1228 | const tempNorm2 = new Vector3();
1229 |
1230 | // hashes a vector
1231 | function hashVertex( v ) {
1232 |
1233 | const x = ~ ~ ( v.x * hashMultiplier );
1234 | const y = ~ ~ ( v.y * hashMultiplier );
1235 | const z = ~ ~ ( v.z * hashMultiplier );
1236 | return `${x},${y},${z}`;
1237 |
1238 | }
1239 |
1240 | const resultGeometry = geometry.toNonIndexed();
1241 | const posAttr = resultGeometry.attributes.position;
1242 | const vertexMap = {};
1243 |
1244 | // find all the normals shared by commonly located vertices
1245 | for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {
1246 |
1247 | const i3 = 3 * i;
1248 | const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );
1249 | const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );
1250 | const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );
1251 |
1252 | tempVec1.subVectors( c, b );
1253 | tempVec2.subVectors( a, b );
1254 |
1255 | // add the normal to the map for all vertices
1256 | const normal = new Vector3().crossVectors( tempVec1, tempVec2 ).normalize();
1257 | for ( let n = 0; n < 3; n ++ ) {
1258 |
1259 | const vert = verts[ n ];
1260 | const hash = hashVertex( vert );
1261 | if ( ! ( hash in vertexMap ) ) {
1262 |
1263 | vertexMap[ hash ] = [];
1264 |
1265 | }
1266 |
1267 | vertexMap[ hash ].push( normal );
1268 |
1269 | }
1270 |
1271 | }
1272 |
1273 | // average normals from all vertices that share a common location if they are within the
1274 | // provided crease threshold
1275 | const normalArray = new Float32Array( posAttr.count * 3 );
1276 | const normAttr = new BufferAttribute( normalArray, 3, false );
1277 | for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {
1278 |
1279 | // get the face normal for this vertex
1280 | const i3 = 3 * i;
1281 | const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );
1282 | const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );
1283 | const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );
1284 |
1285 | tempVec1.subVectors( c, b );
1286 | tempVec2.subVectors( a, b );
1287 |
1288 | tempNorm.crossVectors( tempVec1, tempVec2 ).normalize();
1289 |
1290 | // average all normals that meet the threshold and set the normal value
1291 | for ( let n = 0; n < 3; n ++ ) {
1292 |
1293 | const vert = verts[ n ];
1294 | const hash = hashVertex( vert );
1295 | const otherNormals = vertexMap[ hash ];
1296 | tempNorm2.set( 0, 0, 0 );
1297 |
1298 | for ( let k = 0, lk = otherNormals.length; k < lk; k ++ ) {
1299 |
1300 | const otherNorm = otherNormals[ k ];
1301 | if ( tempNorm.dot( otherNorm ) > creaseDot ) {
1302 |
1303 | tempNorm2.add( otherNorm );
1304 |
1305 | }
1306 |
1307 | }
1308 |
1309 | tempNorm2.normalize();
1310 | normAttr.setXYZ( i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z );
1311 |
1312 | }
1313 |
1314 | }
1315 |
1316 | resultGeometry.setAttribute( 'normal', normAttr );
1317 | return resultGeometry;
1318 |
1319 | }
1320 |
1321 | function mergeBufferGeometries( geometries, useGroups = false ) {
1322 |
1323 | console.warn( 'THREE.BufferGeometryUtils: mergeBufferGeometries() has been renamed to mergeGeometries().' ); // @deprecated, r151
1324 | return mergeGeometries( geometries, useGroups );
1325 |
1326 | }
1327 |
1328 | function mergeBufferAttributes( attributes ) {
1329 |
1330 | console.warn( 'THREE.BufferGeometryUtils: mergeBufferAttributes() has been renamed to mergeAttributes().' ); // @deprecated, r151
1331 | return mergeAttributes( attributes );
1332 |
1333 | }
1334 |
1335 | export {
1336 | computeMikkTSpaceTangents,
1337 | mergeGeometries,
1338 | mergeBufferGeometries,
1339 | mergeAttributes,
1340 | mergeBufferAttributes,
1341 | interleaveAttributes,
1342 | estimateBytesUsed,
1343 | mergeVertices,
1344 | toTrianglesDrawMode,
1345 | computeMorphedAttributes,
1346 | mergeGroups,
1347 | toCreasedNormals
1348 | };
1349 |
--------------------------------------------------------------------------------