├── textures └── BlueNoise_R_128.png ├── models ├── TheSentinel_tree_GeoffCrammond.glb ├── TheSentinel_boulder_GeoffCrammond.glb ├── TheSentinel_meanie_GeoffCrammond.glb ├── TheSentinel_robot_GeoffCrammond.glb ├── TheSentinel_sentry_GeoffCrammond.glb ├── TheSentinel_pedestal_GeoffCrammond.glb └── TheSentinel_sentinel_GeoffCrammond.glb ├── shaders ├── common_PathTracing_Vertex.glsl ├── ScreenCopy_Fragment.glsl ├── ScreenOutput_Fragment.glsl └── TheSentinel_Fragment.glsl ├── TheSentinel_2nd_Look.html ├── css └── default.css ├── js ├── stats.module.js ├── BVH_Acc_Structure_Iterative_Fast_Builder.js ├── SimplexNoise.js ├── BVH_Acc_Structure_Iterative_SAH_Builder.js ├── lil-gui.module.min.js ├── MobileJoystickControls.js └── BufferGeometryUtils.js ├── README.md └── LICENSE /textures/BlueNoise_R_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/textures/BlueNoise_R_128.png -------------------------------------------------------------------------------- /models/TheSentinel_tree_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_tree_GeoffCrammond.glb -------------------------------------------------------------------------------- /models/TheSentinel_boulder_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_boulder_GeoffCrammond.glb -------------------------------------------------------------------------------- /models/TheSentinel_meanie_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_meanie_GeoffCrammond.glb -------------------------------------------------------------------------------- /models/TheSentinel_robot_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_robot_GeoffCrammond.glb -------------------------------------------------------------------------------- /models/TheSentinel_sentry_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_sentry_GeoffCrammond.glb -------------------------------------------------------------------------------- /models/TheSentinel_pedestal_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_pedestal_GeoffCrammond.glb -------------------------------------------------------------------------------- /models/TheSentinel_sentinel_GeoffCrammond.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erichlof/TheSentinel-2nd-Look/HEAD/models/TheSentinel_sentinel_GeoffCrammond.glb -------------------------------------------------------------------------------- /shaders/common_PathTracing_Vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | 4 | void main() 5 | { 6 | gl_Position = vec4( position, 1.0 ); 7 | } -------------------------------------------------------------------------------- /shaders/ScreenCopy_Fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | precision highp sampler2D; 4 | 5 | uniform sampler2D tPathTracedImageTexture; 6 | 7 | void main() 8 | { 9 | pc_fragColor = texelFetch(tPathTracedImageTexture, ivec2(gl_FragCoord.xy), 0); 10 | } -------------------------------------------------------------------------------- /TheSentinel_2nd_Look.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Sentinel: 2nd Look (a fully path traced remake) - W.I.P. 6 | 7 | 8 | 9 | 10 | 18 | 19 | 32 | 33 | 34 | 35 | 36 |
37 |
The Sentinel: 2nd Look (a fully path traced remake) - W.I.P.
38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /css/default.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | font-family: Monospace; 5 | background-color: #000; 6 | color: #000; 7 | margin: 0px; 8 | overflow: hidden; 9 | touch-action: none; 10 | cursor: default; 11 | user-select: none; 12 | -webkit-user-select: none; 13 | } 14 | 15 | #info { 16 | position: fixed; 17 | top: 5px; 18 | width: 100%; 19 | text-align: center; 20 | color: #ffffff; 21 | cursor: default; 22 | user-select: none; 23 | } 24 | 25 | #cameraInfo { 26 | position: fixed; 27 | left: 3%; 28 | bottom: 2%; 29 | font-family: Arial; 30 | color: #ffffff; 31 | cursor: default; 32 | user-select: none; 33 | } 34 | 35 | #instructions { 36 | position: fixed; 37 | left: 3%; 38 | bottom: 1%; 39 | font-family: Arial; 40 | color: #ffffff; 41 | cursor: default; 42 | user-select: none; 43 | } 44 | 45 | .toggleButton { 46 | position:fixed; 47 | background-color: gray; 48 | border-color: darkgray; 49 | color: white; 50 | top: 5px; 51 | right: 5px; 52 | padding: 2px 5px; 53 | text-align: center; 54 | text-decoration: none; 55 | font-size: 14px; 56 | margin: 4px 2px; 57 | cursor: pointer; 58 | user-select: none; 59 | z-index: 11; 60 | } 61 | #timePauseButton { 62 | top: 50px; 63 | } 64 | #gravityButton { 65 | top: 40px; 66 | } 67 | 68 | /* begin Web Audio user-interaction requirement for MacOS & iOS */ 69 | #overlay { 70 | position: absolute; 71 | font-size: 16px; 72 | z-index: 2; 73 | top: 0; 74 | left: 0; 75 | width: 100%; 76 | height: 100%; 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | flex-direction: column; 81 | background: rgba(0,0,0,0.7); 82 | } 83 | 84 | #overlay button { 85 | background: transparent; 86 | border: 0; 87 | border: 1px solid rgb(255, 255, 255); 88 | border-radius: 4px; 89 | color: #ffffff; 90 | padding: 12px 18px; 91 | text-transform: uppercase; 92 | cursor: pointer; 93 | } 94 | /* end Web Audio user-interaction requirement for MacOS & iOS */ 95 | -------------------------------------------------------------------------------- /js/stats.module.js: -------------------------------------------------------------------------------- 1 | var Stats = function () { 2 | 3 | var mode = 0; 4 | 5 | var container = document.createElement( 'div' ); 6 | container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000'; 7 | container.addEventListener( 'click', function ( event ) { 8 | 9 | event.preventDefault(); 10 | showPanel( ++ mode % container.children.length ); 11 | 12 | }, false ); 13 | 14 | // 15 | 16 | function addPanel( panel ) { 17 | 18 | container.appendChild( panel.dom ); 19 | return panel; 20 | 21 | } 22 | 23 | function showPanel( id ) { 24 | 25 | for ( var i = 0; i < container.children.length; i ++ ) { 26 | 27 | container.children[ i ].style.display = i === id ? 'block' : 'none'; 28 | 29 | } 30 | 31 | mode = id; 32 | 33 | } 34 | 35 | // 36 | 37 | var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0; 38 | 39 | var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) ); 40 | var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) ); 41 | 42 | if ( self.performance && self.performance.memory ) { 43 | 44 | var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) ); 45 | 46 | } 47 | 48 | showPanel( 0 ); 49 | 50 | return { 51 | 52 | REVISION: 16, 53 | 54 | dom: container, 55 | 56 | addPanel: addPanel, 57 | showPanel: showPanel, 58 | 59 | begin: function () { 60 | 61 | beginTime = ( performance || Date ).now(); 62 | 63 | }, 64 | 65 | end: function () { 66 | 67 | frames ++; 68 | 69 | var time = ( performance || Date ).now(); 70 | 71 | msPanel.update( time - beginTime, 200 ); 72 | 73 | if ( time >= prevTime + 1000 ) { 74 | 75 | fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 ); 76 | 77 | prevTime = time; 78 | frames = 0; 79 | 80 | if ( memPanel ) { 81 | 82 | var memory = performance.memory; 83 | memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 ); 84 | 85 | } 86 | 87 | } 88 | 89 | return time; 90 | 91 | }, 92 | 93 | update: function () { 94 | 95 | beginTime = this.end(); 96 | 97 | }, 98 | 99 | // Backwards Compatibility 100 | 101 | domElement: container, 102 | setMode: showPanel 103 | 104 | }; 105 | 106 | }; 107 | 108 | Stats.Panel = function ( name, fg, bg ) { 109 | 110 | var min = Infinity, max = 0, round = Math.round; 111 | var PR = round( window.devicePixelRatio || 1 ); 112 | 113 | var WIDTH = 80 * PR, HEIGHT = 48 * PR, 114 | TEXT_X = 3 * PR, TEXT_Y = 2 * PR, 115 | GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR, 116 | GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR; 117 | 118 | var canvas = document.createElement( 'canvas' ); 119 | canvas.width = WIDTH; 120 | canvas.height = HEIGHT; 121 | canvas.style.cssText = 'width:80px;height:48px'; 122 | 123 | var context = canvas.getContext( '2d' ); 124 | context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif'; 125 | context.textBaseline = 'top'; 126 | 127 | context.fillStyle = bg; 128 | context.fillRect( 0, 0, WIDTH, HEIGHT ); 129 | 130 | context.fillStyle = fg; 131 | context.fillText( name, TEXT_X, TEXT_Y ); 132 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 133 | 134 | context.fillStyle = bg; 135 | context.globalAlpha = 0.9; 136 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 137 | 138 | return { 139 | 140 | dom: canvas, 141 | 142 | update: function ( value, maxValue ) { 143 | 144 | min = Math.min( min, value ); 145 | max = Math.max( max, value ); 146 | 147 | context.fillStyle = bg; 148 | context.globalAlpha = 1; 149 | context.fillRect( 0, 0, WIDTH, GRAPH_Y ); 150 | context.fillStyle = fg; 151 | context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y ); 152 | 153 | context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT ); 154 | 155 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT ); 156 | 157 | context.fillStyle = bg; 158 | context.globalAlpha = 0.9; 159 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) ); 160 | 161 | } 162 | 163 | }; 164 | 165 | }; 166 | 167 | export default Stats; 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TheSentinel-2nd-Look (W.I.P.) 2 | A fully path traced remake of Geoff Crammond's iconic 1986 game, The Sentinel - runs in your Desktop/Mobile browser!
3 | Click to Play --> https://erichlof.github.io/TheSentinel-2nd-Look/TheSentinel_2nd_Look.html 4 |

5 | 6 |

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