three.js RayTracing Renderer - The 6 Platonic Solids
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/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 | #demoInfo {
26 | position: fixed;
27 | left: 3%;
28 | bottom: 8%;
29 | font-family: Arial;
30 | font-size: 14px;
31 | color: #ffffff;
32 | cursor: default;
33 | user-select: none;
34 | }
35 |
36 | #cameraInfo {
37 | position: fixed;
38 | left: 3%;
39 | bottom: 2%;
40 | font-family: Arial;
41 | color: #ffffff;
42 | cursor: default;
43 | user-select: none;
44 | }
45 |
46 | .toggleButton {
47 | position:fixed;
48 | background-color: gray;
49 | border-color: darkgray;
50 | color: white;
51 | top: 5px;
52 | right: 5px;
53 | padding: 2px 5px;
54 | text-align: center;
55 | text-decoration: none;
56 | font-size: 14px;
57 | margin: 4px 2px;
58 | cursor: pointer;
59 | user-select: none;
60 | z-index: 11;
61 | }
62 |
63 | #timePauseButton {
64 | top: 50px;
65 | }
66 |
67 |
68 | /* the rest of this file is for various games projects */
69 |
70 | #instructions {
71 | position: fixed;
72 | left: 3%;
73 | bottom: 1%;
74 | font-family: Arial;
75 | color: #ffffff;
76 | cursor: default;
77 | user-select: none;
78 | }
79 |
80 | #gravityButton {
81 | top: 40px;
82 | }
83 |
84 | /* begin Web Audio user-interaction requirement for MacOS & iOS */
85 | #overlay {
86 | position: absolute;
87 | font-size: 16px;
88 | z-index: 2;
89 | top: 0;
90 | left: 0;
91 | width: 100%;
92 | height: 100%;
93 | display: flex;
94 | align-items: center;
95 | justify-content: center;
96 | flex-direction: column;
97 | background: rgba(0,0,0,0.7);
98 | }
99 |
100 | #overlay button {
101 | background: transparent;
102 | border: 0;
103 | border: 1px solid rgb(255, 255, 255);
104 | border-radius: 4px;
105 | color: #ffffff;
106 | padding: 12px 18px;
107 | text-transform: uppercase;
108 | cursor: pointer;
109 | }
110 | /* end Web Audio user-interaction requirement for MacOS & iOS */
--------------------------------------------------------------------------------
/js/Whitted_RayTracing.js:
--------------------------------------------------------------------------------
1 | // scene/demo-specific variables go here
2 | let textureImagesTotalCount = 1;
3 | let numOfTextureImagesLoaded = 0;
4 | let tileNormalMapTexture;
5 |
6 |
7 | // called automatically from within initTHREEjs() function (located in InitCommon.js file)
8 | function initSceneData()
9 | {
10 | demoFragmentShaderFileName = 'WhittedRayTracing_Fragment.glsl';
11 |
12 | // scene/demo-specific three.js objects setup goes here
13 | sceneIsDynamic = true;
14 |
15 | cameraFlightSpeed = 60;
16 |
17 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution)
18 | pixelRatio = mouseControl ? 1.0 : 1.0; // mobile devices can also handle full resolution for this raytracing (not full pathtracing) demo
19 |
20 | // needs 0.1 precision instead of 0.01 to avoid artifacts on yellow sphere
21 | EPS_intersect = 0.1;
22 |
23 | // set camera's field of view
24 | worldCamera.fov = 55;
25 | focusDistance = 119.0;
26 | apertureChangeSpeed = 5;
27 |
28 | // position and orient camera
29 | cameraControlsObject.position.set(-10, 79, 195);
30 | // look slightly downward
31 | cameraControlsPitchObject.rotation.x = -0.05;
32 |
33 |
34 | // scene/demo-specific uniforms go here
35 | rayTracingUniforms.uTileNormalMapTexture = { value: tileNormalMapTexture };
36 |
37 | } // end function initSceneData()
38 |
39 |
40 |
41 | // called automatically from within the animate() function (located in InitCommon.js file)
42 | function updateVariablesAndUniforms()
43 | {
44 | // INFO
45 | cameraInfoElement.innerHTML = "FOV: " + worldCamera.fov + " / Aperture: " + apertureSize.toFixed(2) +
46 | " / FocusDistance: " + focusDistance.toFixed(2) + " " + "Samples: " + sampleCounter;
47 |
48 | } // end function updateUniforms()
49 |
50 |
51 |
52 | // load a resource
53 | tileNormalMapTexture = textureLoader.load(
54 | // resource URL
55 | 'textures/tileNormalMap.png',
56 | //'textures/uvgrid0.png',
57 |
58 | // onLoad callback
59 | function (texture)
60 | {
61 | texture.wrapS = THREE.RepeatWrapping;
62 | texture.wrapT = THREE.RepeatWrapping;
63 | texture.flipY = false;
64 | texture.minFilter = THREE.NearestFilter;
65 | texture.magFilter = THREE.NearestFilter;
66 | texture.generateMipmaps = false;
67 |
68 | numOfTextureImagesLoaded++;
69 | // if all textures have been loaded, we can init
70 | if (numOfTextureImagesLoaded == textureImagesTotalCount)
71 | init(); // init app and start animating
72 | }
73 | );
74 |
--------------------------------------------------------------------------------
/js/RoyHall_TheGallery.js:
--------------------------------------------------------------------------------
1 | // scene/demo-specific variables go here
2 | let cube, platform;
3 |
4 |
5 | // called automatically from within initTHREEjs() function (located in InitCommon.js file)
6 | function initSceneData()
7 | {
8 | demoFragmentShaderFileName = 'RoyHallTheGallery_Fragment.glsl';
9 |
10 | // scene/demo-specific three.js objects setup goes here
11 | sceneIsDynamic = false;
12 |
13 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution)
14 | pixelRatio = mouseControl ? 1.0 : 0.75; // less demanding on battery-powered mobile devices
15 |
16 | //EPS_intersect = 0.01;
17 |
18 |
19 | // set camera's field of view and other options
20 | worldCamera.fov = 37;
21 | focusDistance = 36.0;
22 | //apertureChangeSpeed = 1; // 1 is default
23 | //focusChangeSpeed = 1; // 1 is default
24 |
25 | cameraFlightSpeed = 15;
26 |
27 | // position and orient camera
28 | cameraControlsObject.position.set(10.6, 11.1, 28.9);
29 | // look left or right
30 | cameraControlsYawObject.rotation.y = 0.2;
31 | // look up or down
32 | cameraControlsPitchObject.rotation.x = 0;
33 |
34 |
35 | // CUBE sculpture in middle of room
36 | cube = new RayTracingShape("box");
37 | cube.transform.scale.set(3.2, 3.2, 3.2);
38 | cube.transform.position.set(-2, 6.75, -8);
39 | cube.transform.rotation.set(Math.PI * 0.73, Math.PI * 0.74, 0);
40 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
41 | cube.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
42 |
43 | // platform box underneath the cube sculpture
44 | platform = new RayTracingShape("box");
45 | platform.transform.scale.set(2.75, 1.25, 2.75);
46 | platform.transform.position.set(-2, 0, -8);
47 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
48 | platform.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
49 |
50 |
51 |
52 | // In addition to the default GUI on all demos, add any special GUI elements that this particular demo requires
53 |
54 |
55 | // scene/demo-specific uniforms go here
56 | rayTracingUniforms.uCubeInvMatrix = { value: new THREE.Matrix4() };
57 | rayTracingUniforms.uPlatformInvMatrix = { value: new THREE.Matrix4() };
58 |
59 | // go ahead and calculate/set the inverse matrices, because this scene is static - these objects don't move
60 | rayTracingUniforms.uCubeInvMatrix.value.copy(cube.transform.matrixWorld).invert();
61 | rayTracingUniforms.uPlatformInvMatrix.value.copy(platform.transform.matrixWorld).invert();
62 |
63 | } // end function initSceneData()
64 |
65 |
66 |
67 | // called automatically from within the animate() function (located in InitCommon.js file)
68 | function updateVariablesAndUniforms()
69 | {
70 |
71 | // INFO
72 | cameraInfoElement.innerHTML = "FOV: " + worldCamera.fov + " / Aperture: " + apertureSize.toFixed(2) +
73 | " / FocusDistance: " + focusDistance.toFixed(2) + " " + "Samples: " + sampleCounter;
74 |
75 | } // end function updateVariablesAndUniforms()
76 |
77 |
78 | init(); // init app and start animating
79 |
--------------------------------------------------------------------------------
/js/Instance_Mapping.js:
--------------------------------------------------------------------------------
1 | // scene/demo-specific variables go here
2 | let UVGridTexture;
3 | let imageTexturesTotalCount = 1;
4 | let numOfImageTexturesLoaded = 0;
5 |
6 | let material_TypeObject;
7 | let material_TypeController;
8 | let changeMaterialType = false;
9 | let matType = 0;
10 |
11 |
12 | // called automatically from within initTHREEjs() function (located in InitCommon.js file)
13 | function initSceneData()
14 | {
15 | demoFragmentShaderFileName = 'InstanceMapping_Fragment.glsl';
16 |
17 | // scene/demo-specific three.js objects setup goes here
18 | sceneIsDynamic = false;
19 |
20 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution)
21 | pixelRatio = mouseControl ? 1.0 : 0.75; // less demanding on battery-powered mobile devices
22 |
23 | EPS_intersect = 0.01;
24 |
25 |
26 | // set camera's field of view and other options
27 | worldCamera.fov = 60;
28 | focusDistance = 23.0;
29 | //apertureChangeSpeed = 1; // 1 is default
30 | //focusChangeSpeed = 1; // 1 is default
31 |
32 | // position and orient camera
33 | cameraControlsObject.position.set(1, 6, 16);
34 | // look left or right
35 | ///cameraControlsYawObject.rotation.y = 0.3;
36 | // look up or down
37 | cameraControlsPitchObject.rotation.x = 0.005;
38 |
39 | cameraFlightSpeed = 10;
40 |
41 | // In addition to the default GUI on all demos, add any special GUI elements that this particular demo requires
42 |
43 | material_TypeObject = {
44 | LeftSpheres_Material: 'Metal'
45 | };
46 |
47 | function handleMaterialTypeChange()
48 | {
49 | changeMaterialType = true;
50 | }
51 |
52 | material_TypeController = gui.add(material_TypeObject, 'LeftSpheres_Material', ['Diffuse',
53 | 'Metal', 'ClearCoat Diffuse', 'Transparent Refractive']).onChange(handleMaterialTypeChange);
54 |
55 | // scene/demo-specific uniforms go here
56 | rayTracingUniforms.uUVGridTexture = { value: UVGridTexture };
57 | rayTracingUniforms.uMaterialType = { value: 2 };
58 |
59 | } // end function initSceneData()
60 |
61 |
62 |
63 | // called automatically from within the animate() function (located in InitCommon.js file)
64 | function updateVariablesAndUniforms()
65 | {
66 | if (changeMaterialType)
67 | {
68 | matType = material_TypeController.getValue();
69 |
70 | if (matType == 'Diffuse')
71 | {
72 | rayTracingUniforms.uMaterialType.value = 1;
73 | }
74 | else if (matType == 'Metal')
75 | {
76 | rayTracingUniforms.uMaterialType.value = 2;
77 | }
78 | else if (matType == 'ClearCoat Diffuse')
79 | {
80 | rayTracingUniforms.uMaterialType.value = 3;
81 | }
82 | else if (matType == 'Transparent Refractive')
83 | {
84 | rayTracingUniforms.uMaterialType.value = 4;
85 | }
86 |
87 | cameraIsMoving = true;
88 | changeMaterialType = false;
89 | }
90 |
91 |
92 | // INFO
93 | cameraInfoElement.innerHTML = "FOV: " + worldCamera.fov + " / Aperture: " + apertureSize.toFixed(2) +
94 | " / FocusDistance: " + focusDistance.toFixed(2) + " " + "Samples: " + sampleCounter;
95 |
96 | } // end function updateVariablesAndUniforms()
97 |
98 |
99 | UVGridTexture = textureLoader.load(
100 | // resource URL
101 | 'textures/uvgrid0.png',
102 |
103 | // onLoad callback
104 | function (texture)
105 | {
106 | texture.wrapS = THREE.RepeatWrapping;
107 | texture.wrapT = THREE.RepeatWrapping;
108 | texture.flipY = false;
109 | texture.minFilter = THREE.NearestFilter;
110 | texture.magFilter = THREE.NearestFilter;
111 | texture.generateMipmaps = false;
112 |
113 | numOfImageTexturesLoaded++;
114 | // if all textures have been loaded, we can init
115 | if (numOfImageTexturesLoaded == imageTexturesTotalCount)
116 | init(); // init app and start animating
117 | }
118 | );
119 |
--------------------------------------------------------------------------------
/js/Classic_Geometry_Showcase.js:
--------------------------------------------------------------------------------
1 | // scene/demo-specific variables go here
2 | let UVGridTexture;
3 | let imageTexturesTotalCount = 1;
4 | let numOfImageTexturesLoaded = 0;
5 | let material_RoughnessObject, material_RoughnessController;
6 | let needChangeMaterialRoughness = false;
7 | let material_MetalnessObject, material_MetalnessController;
8 | let needChangeMaterialMetalness = false;
9 |
10 | // called automatically from within initTHREEjs() function (located in InitCommon.js file)
11 | function initSceneData()
12 | {
13 | demoFragmentShaderFileName = 'ClassicGeometryShowcase_Fragment.glsl';
14 |
15 | // scene/demo-specific three.js objects setup goes here
16 | sceneIsDynamic = false;
17 |
18 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution)
19 | pixelRatio = mouseControl ? 1.0 : 0.75; // less demanding on battery-powered mobile devices
20 |
21 | //EPS_intersect = 0.01;
22 |
23 |
24 | // set camera's field of view and other options
25 | worldCamera.fov = 60;
26 | focusDistance = 23.0;
27 | //apertureChangeSpeed = 1; // 1 is default
28 | //focusChangeSpeed = 1; // 1 is default
29 |
30 | // position and orient camera
31 | cameraControlsObject.position.set(3, 5, 25);
32 | // look left or right
33 | cameraControlsYawObject.rotation.y = 0.3;
34 | // look up or down
35 | cameraControlsPitchObject.rotation.x = 0.005;
36 |
37 | cameraFlightSpeed = 30;
38 |
39 | // In addition to the default GUI on all demos, add any special GUI elements that this particular demo requires
40 | material_RoughnessObject = { Material_Roughness: 0.0 };
41 | material_MetalnessObject = { GoldSphere_Metalness: 1.0 };
42 |
43 | function handleMaterialRoughnessChange()
44 | {
45 | needChangeMaterialRoughness = true;
46 | }
47 | function handleMaterialMetalnessChange()
48 | {
49 | needChangeMaterialMetalness = true;
50 | }
51 |
52 | material_RoughnessController = gui.add(material_RoughnessObject, 'Material_Roughness', 0.0, 1.0, 0.01).onChange(handleMaterialRoughnessChange);
53 | material_MetalnessController = gui.add(material_MetalnessObject, 'GoldSphere_Metalness', 0.0, 1.0, 0.01).onChange(handleMaterialMetalnessChange);
54 |
55 | // jumpstart some of the gui controller-change handlers so that the pathtracing fragment shader uniforms are correct and up-to-date
56 | handleMaterialRoughnessChange();
57 | handleMaterialMetalnessChange();
58 |
59 |
60 | // scene/demo-specific uniforms go here
61 | rayTracingUniforms.uUVGridTexture = { value: UVGridTexture };
62 | rayTracingUniforms.uRoughness = { value: 0.0 };
63 | rayTracingUniforms.uMetalness = { value: 1.0 };
64 |
65 | } // end function initSceneData()
66 |
67 |
68 |
69 | // called automatically from within the animate() function (located in InitCommon.js file)
70 | function updateVariablesAndUniforms()
71 | {
72 |
73 | if (needChangeMaterialRoughness)
74 | {
75 | rayTracingUniforms.uRoughness.value = material_RoughnessController.getValue();
76 | cameraIsMoving = true;
77 | needChangeMaterialRoughness = false;
78 | }
79 |
80 | if (needChangeMaterialMetalness)
81 | {
82 | rayTracingUniforms.uMetalness.value = material_MetalnessController.getValue();
83 | cameraIsMoving = true;
84 | needChangeMaterialMetalness = false;
85 | }
86 |
87 | // INFO
88 | cameraInfoElement.innerHTML = "FOV: " + worldCamera.fov + " / Aperture: " + apertureSize.toFixed(2) +
89 | " / FocusDistance: " + focusDistance.toFixed(2) + " " + "Samples: " + sampleCounter;
90 |
91 | } // end function updateVariablesAndUniforms()
92 |
93 |
94 | UVGridTexture = textureLoader.load(
95 | // resource URL
96 | 'textures/uvgrid0.png',
97 |
98 | // onLoad callback
99 | function (texture)
100 | {
101 | texture.wrapS = THREE.RepeatWrapping;
102 | texture.wrapT = THREE.RepeatWrapping;
103 | texture.flipY = false;
104 | texture.minFilter = THREE.NearestFilter;
105 | texture.magFilter = THREE.NearestFilter;
106 | texture.generateMipmaps = false;
107 |
108 | numOfImageTexturesLoaded++;
109 | // if all textures have been loaded, we can init
110 | if (numOfImageTexturesLoaded == imageTexturesTotalCount)
111 | init(); // init app and start animating
112 | }
113 | );
114 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/js/Ray_Tracer_Challenge.js:
--------------------------------------------------------------------------------
1 | // scene/demo-specific variables go here
2 | let boxes = [];
3 | let boxesInvMatrices = [];
4 | let glassSphere;
5 | let glassSphereInvMatrix = new THREE.Matrix4();
6 | let groupTransform = new THREE.Object3D();
7 | let phaseOffsets = [];
8 | let lightPosition = new THREE.Vector3();
9 |
10 | let animation_TypeObject;
11 | let animation_TypeController;
12 | let changeAnimationType = false;
13 | let animationType;
14 |
15 | let noAnimation = true;
16 | let runAnimation1 = false;
17 | let runAnimation2 = false;
18 | let runAnimation3 = false;
19 | let runAnimation4 = false;
20 | let runAnimation5 = false;
21 |
22 |
23 | // called automatically from within initTHREEjs() function (located in InitCommon.js file)
24 | function initSceneData()
25 | {
26 | demoFragmentShaderFileName = 'RayTracerChallenge_Fragment.glsl';
27 |
28 | // scene/demo-specific three.js objects setup goes here
29 |
30 | // this demo opens up with a static scene, but as soon as the user selects an animation from the menu,
31 | // the scene will be changed to dynamic (sceneIsDynamic = true)
32 | sceneIsDynamic = false;
33 |
34 | // Since this demo has a pure white background, turn tone mapping Off - otherwise, background will appear light gray when run through the tone mapper.
35 | // If the tone mapper was on, the background color would have to be set to a really high brightness in order to reach the levels of a 'pure white' color.
36 | useToneMapping = false;
37 |
38 | // change webpage's info text to black so it shows up against the white background
39 | infoElement.style.color = 'rgb(0,0,0)';
40 | cameraInfoElement.style.color = 'rgb(0,0,0)';
41 |
42 | // mobile devices must use darker buttons so that they show up against the white background
43 | mobileUseDarkButtons = true;
44 |
45 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution)
46 | pixelRatio = mouseControl ? 1.0 : 0.75; // less demanding on battery-powered mobile devices
47 |
48 | //EPS_intersect = 0.01;
49 |
50 |
51 | // set camera's field of view and other options
52 | worldCamera.fov = 50;
53 | focusDistance = 12.0;
54 | //apertureChangeSpeed = 1; // 1 is default
55 | //focusChangeSpeed = 1; // 1 is default
56 |
57 | // position and orient camera
58 | cameraControlsObject.position.set(-6, 3.5, 15);
59 | // look left or right
60 | cameraControlsYawObject.rotation.y = -0.6;
61 | // look up or down
62 | cameraControlsPitchObject.rotation.x = -0.25;
63 |
64 |
65 | cameraFlightSpeed = 15;
66 |
67 | // glass sphere
68 | glassSphere = new RayTracingShape("sphere");
69 |
70 | glassSphere.material.color.set(0.4, 1.0, 0.6); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
71 | glassSphere.material.opacity = 0.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
72 | glassSphere.material.ior = 1.5; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
73 | glassSphere.material.clearcoat = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
74 | glassSphere.material.metalness = 0.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
75 | glassSphere.material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
76 |
77 | glassSphere.uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
78 |
79 | glassSphere.transform.scale.set(1.75, 1.75, 1.75);
80 | glassSphere.transform.position.set(0, 1.75, 4.7);
81 |
82 | groupTransform.add(glassSphere.transform)
83 |
84 |
85 |
86 | // ClearCoat boxes
87 | for (let i = 0; i < 17; i++)
88 | {
89 | boxes[i] = new RayTracingShape("box");
90 |
91 | boxes[i].material.color.set(1.0, 1.0, 1.0); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
92 | boxes[i].material.opacity = 1.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
93 | boxes[i].material.ior = 1.4; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
94 | boxes[i].material.clearcoat = 1.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
95 | boxes[i].material.metalness = 0.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
96 | boxes[i].material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
97 |
98 | boxes[i].uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
99 | boxesInvMatrices[i] = new THREE.Matrix4();
100 |
101 | groupTransform.add(boxes[i].transform);
102 | }
103 |
104 |
105 | setupDefaultScene();
106 |
107 |
108 | // In addition to the default GUI on all demos, add any special GUI elements that this particular demo requires
109 |
110 | animation_TypeObject = {
111 | Play_Animation: 'None'
112 | };
113 |
114 | function handleAnimationTypeChange()
115 | {
116 | changeAnimationType = true;
117 | }
118 |
119 | animation_TypeController = gui.add(animation_TypeObject, 'Play_Animation', ['None', 'Animation #1',
120 | 'Animation #2', 'Animation #3', 'Animation #4', 'Animation #5']).onChange(handleAnimationTypeChange);
121 |
122 |
123 |
124 | // scene/demo-specific uniforms go here
125 | rayTracingUniforms.uBoxesInvMatrices = { value: boxesInvMatrices };
126 | rayTracingUniforms.uGlassSphereInvMatrix = { value: glassSphereInvMatrix };
127 | rayTracingUniforms.uLightPosition = { value: lightPosition };
128 |
129 | } // end function initSceneData()
130 |
131 |
132 | function setupDefaultScene()
133 | {
134 | boxes[0].transform.rotation.set(0, 0, 0);
135 | boxes[0].transform.scale.set(1.5, 1.5, 1.5);
136 | boxes[0].transform.position.set(4, 1.75, 4.7);
137 |
138 | boxes[1].transform.rotation.set(0, 0, 0);
139 | boxes[1].transform.scale.set(1.75, 1.75, 1.75);
140 | boxes[1].transform.position.set(8.5, 3.25, 5.2);
141 |
142 | boxes[2].transform.rotation.set(0, 0, 0);
143 | boxes[2].transform.scale.set(1.75, 1.75, 1.75);
144 | boxes[2].transform.position.set(0, 1.75, 0.7);
145 |
146 | boxes[3].transform.rotation.set(0, 0, 0);
147 | boxes[3].transform.scale.set(1, 1, 1);
148 | boxes[3].transform.position.set(4, 1.75, 0.7);
149 |
150 | boxes[4].transform.rotation.set(0, 0, 0);
151 | boxes[4].transform.scale.set(1.5, 1.5, 1.5);
152 | boxes[4].transform.position.set(7.5, 2.25, 0.7);
153 |
154 | boxes[5].transform.rotation.set(0, 0, 0);
155 | boxes[5].transform.scale.set(1.5, 1.5, 1.5);
156 | boxes[5].transform.position.set(-0.25, 2, -3.3);
157 |
158 | boxes[6].transform.rotation.set(0, 0, 0);
159 | boxes[6].transform.scale.set(1.75, 1.75, 1.75);
160 | boxes[6].transform.position.set(4, 2.75, -2.8);
161 |
162 | boxes[7].transform.rotation.set(0, 0, 0);
163 | boxes[7].transform.scale.set(1.5, 1.5, 1.5);
164 | boxes[7].transform.position.set(10, 3.75, -2.8);
165 |
166 | boxes[8].transform.rotation.set(0, 0, 0);
167 | boxes[8].transform.scale.set(1, 1, 1);
168 | boxes[8].transform.position.set(8, 3.75, -7.3);
169 |
170 | boxes[9].transform.rotation.set(0, 0, 0);
171 | boxes[9].transform.scale.set(1, 1, 1);
172 | boxes[9].transform.position.set(20, 2.75, -4.3);
173 |
174 | boxes[10].transform.rotation.set(0, 0, 0);
175 | boxes[10].transform.scale.set(1.75, 1.75, 1.75);
176 | boxes[10].transform.position.set(-0.5, -3.25, 4.45);
177 |
178 | boxes[11].transform.rotation.set(0, 0, 0);
179 | boxes[11].transform.scale.set(1.75, 1.75, 1.75);
180 | boxes[11].transform.position.set(4, -2.25, 4.7);
181 |
182 | boxes[12].transform.rotation.set(0, 0, 0);
183 | boxes[12].transform.scale.set(1.75, 1.75, 1.75);
184 | boxes[12].transform.position.set(8.5, -2.25, 4.7);
185 |
186 | boxes[13].transform.rotation.set(0, 0, 0);
187 | boxes[13].transform.scale.set(1.75, 1.75, 1.75);
188 | boxes[13].transform.position.set(0, -2.25, 0.7);
189 |
190 | boxes[14].transform.rotation.set(0, 0, 0);
191 | boxes[14].transform.scale.set(1.75, 1.75, 1.75);
192 | boxes[14].transform.position.set(-0.5, -2.75, -3.3);
193 |
194 | boxes[15].transform.rotation.set(0, 0, 0);
195 | boxes[15].transform.scale.set(1.75, 1.75, 1.75);
196 | boxes[15].transform.position.set(0, -6.25, 0.7);
197 |
198 | boxes[16].transform.rotation.set(0, 0, 0);
199 | boxes[16].transform.scale.set(1.75, 1.75, 1.75);
200 | boxes[16].transform.position.set(-0.5, -6.75, -3.3);
201 |
202 |
203 | groupTransform.rotation.set(0, 0, 0);
204 | groupTransform.rotateX(0.4);
205 | groupTransform.updateMatrixWorld(true);
206 |
207 | glassSphere.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
208 | glassSphereInvMatrix.copy(glassSphere.transform.matrixWorld).invert();
209 |
210 | for (let i = 0; i < 17; i++)
211 | {
212 | boxes[i].transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
213 | boxesInvMatrices[i].copy(boxes[i].transform.matrixWorld).invert();
214 | }
215 |
216 | for (let i = 0; i < 17; i++)
217 | {
218 | phaseOffsets[i] = Math.random() * TWO_PI;
219 | }
220 |
221 | lightPosition.set(50, 100, 50);
222 | lightPosition.applyMatrix4(groupTransform.matrixWorld);
223 |
224 | } // end function setupDefaultScene()
225 |
226 |
227 | // called automatically from within the animate() function (located in InitCommon.js file)
228 | function updateVariablesAndUniforms()
229 | {
230 |
231 | if (changeAnimationType)
232 | {
233 | animationType = animation_TypeController.getValue();
234 |
235 | if (animationType == 'None')
236 | {
237 | noAnimation = true;
238 | runAnimation1 = false;
239 | runAnimation2 = false;
240 | runAnimation3 = false;
241 | runAnimation4 = false;
242 | runAnimation5 = false;
243 | sceneIsDynamic = false;
244 | }
245 | else if (animationType == 'Animation #1')
246 | {
247 | noAnimation = false;
248 | runAnimation1 = true;
249 | runAnimation2 = false;
250 | runAnimation3 = false;
251 | runAnimation4 = false;
252 | runAnimation5 = false;
253 | sceneIsDynamic = true;
254 | }
255 | else if (animationType == 'Animation #2')
256 | {
257 | noAnimation = false;
258 | runAnimation1 = false;
259 | runAnimation2 = true;
260 | runAnimation3 = false;
261 | runAnimation4 = false;
262 | runAnimation5 = false;
263 | sceneIsDynamic = true;
264 | }
265 | else if (animationType == 'Animation #3')
266 | {
267 | noAnimation = false;
268 | runAnimation1 = false;
269 | runAnimation2 = false;
270 | runAnimation3 = true;
271 | runAnimation4 = false;
272 | runAnimation5 = false;
273 | sceneIsDynamic = true;
274 | }
275 | else if (animationType == 'Animation #4')
276 | {
277 | noAnimation = false;
278 | runAnimation1 = false;
279 | runAnimation2 = false;
280 | runAnimation3 = false;
281 | runAnimation4 = true;
282 | runAnimation5 = false;
283 | sceneIsDynamic = true;
284 | }
285 | else if (animationType == 'Animation #5')
286 | {
287 | noAnimation = false;
288 | runAnimation1 = false;
289 | runAnimation2 = false;
290 | runAnimation3 = false;
291 | runAnimation4 = false;
292 | runAnimation5 = true;
293 | sceneIsDynamic = true;
294 | }
295 |
296 | cameraIsMoving = true;
297 | changeAnimationType = false;
298 |
299 | setupDefaultScene();
300 |
301 | } // end if (changeAnimationType)
302 |
303 |
304 | if (runAnimation1)
305 | {
306 | groupTransform.rotateX(0.5 * frameTime);
307 | groupTransform.updateMatrixWorld(true);
308 |
309 | glassSphere.transform.updateMatrixWorld(true);
310 | glassSphereInvMatrix.copy(glassSphere.transform.matrixWorld).invert();
311 | for (let i = 0; i < 17; i++)
312 | {
313 | boxes[i].transform.updateMatrixWorld(true);
314 | boxesInvMatrices[i].copy(boxes[i].transform.matrixWorld).invert();
315 | }
316 | }
317 |
318 | if (runAnimation2)
319 | {
320 | groupTransform.rotation.set(0, 0, 0);
321 | groupTransform.rotateY(0.5 * frameTime);
322 | groupTransform.updateMatrixWorld(true);
323 |
324 | lightPosition.applyMatrix4(groupTransform.matrixWorld);
325 | }
326 |
327 | if (runAnimation3)
328 | {
329 | for (let i = 0; i < 17; i++)
330 | {
331 | boxes[i].transform.position.y += Math.sin(((elapsedTime * 0.5) + phaseOffsets[i]) % TWO_PI) * 0.02;
332 | boxes[i].transform.updateMatrixWorld(true);
333 | boxesInvMatrices[i].copy(boxes[i].transform.matrixWorld).invert();
334 | }
335 | }
336 |
337 | if (runAnimation4)
338 | {
339 | for (let i = 0; i < 17; i++)
340 | {
341 | boxes[i].transform.scale.x += Math.sin(((elapsedTime * 0.5) + phaseOffsets[i]) % TWO_PI) * 0.005;
342 | boxes[i].transform.scale.y += Math.sin(((elapsedTime * 0.5) + phaseOffsets[i]) % TWO_PI) * 0.005;
343 | boxes[i].transform.scale.z += Math.sin(((elapsedTime * 0.5) + phaseOffsets[i]) % TWO_PI) * 0.005;
344 | boxes[i].transform.updateMatrixWorld(true);
345 | boxesInvMatrices[i].copy(boxes[i].transform.matrixWorld).invert();
346 | }
347 | }
348 |
349 | if (runAnimation5)
350 | {
351 | for (let i = 0; i < 17; i++)
352 | {
353 | boxes[i].transform.rotation.x += (0.5 + (phaseOffsets[i] / TWO_PI)) * frameTime;
354 | boxes[i].transform.rotation.y += (0.5 + (phaseOffsets[i] / TWO_PI)) * frameTime;
355 | boxes[i].transform.updateMatrixWorld(true);
356 | boxesInvMatrices[i].copy(boxes[i].transform.matrixWorld).invert();
357 | }
358 | }
359 |
360 | // INFO
361 | cameraInfoElement.innerHTML = "FOV: " + worldCamera.fov + " / Aperture: " + apertureSize.toFixed(2) +
362 | " / FocusDistance: " + focusDistance.toFixed(2) + " " + "Samples: " + sampleCounter;
363 |
364 | } // end function updateVariablesAndUniforms()
365 |
366 |
367 | init(); // init app and start animating
368 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # THREE.js-RayTracing-Renderer
2 | Real-time Classic Ray Tracing on all devices with a browser, with real reflections, refractions, and depth of field, all on top of the Three.js WebGL framework.
3 |
4 |
Quick Controls Notes
5 |
6 | * *Desktop*: Mouse click anywhere to capture mouse, then the usual Mouse-move and WASD/QZ keys control 1st person camera. Mousewheel to zoom in and out. O and P keys toggle Orthographic and Perspective camera modes. Left/Right arrow keys control camera's aperture size (depth of field blur effect), while Up/Down arrow keys control the Focal point distance from the camera. ESC key to exit and return the mouse pointer.
7 |
8 | * *Mobile*: Swipe to rotate 1st person camera. The 4 Large Arrow buttons control camera movement. Horizontal pinch to zoom in and out. Vertical Pinch controls camera's aperture size (depth of field blur effect). The 2 smaller Up/Down Arrow buttons control the Focal point distance from the camera. Orthographic camera mode can be turned on/off through the GUI checkbox.
9 |
10 |
LIVE DEMOS
11 |
12 | * [Classic Geometry Showcase Demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/Classic_Geometry_Showcase.html) features some classic ray tracing shapes (spheres, planes, etc) while showing the main capabilities of the RayTracing Renderer: real reflections, refractions, pixel-perfect shadows, shadow caustics, 4 different material types (Phong, Metal, ClearCoat, and Transparent) - all running at 60fps, even on your cell phone!
13 |
14 | * [Quadric Shapes Demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/Quadric_Shapes.html) contains a directional light source (like the Sun) and demonstrates every shape from the Quadric Shapes family (spheres, cylinders, cones, paraboloids, hyperboloids, etc). All of the objects are instantiated with easy Three.js library-type calls. Each shape has its own transform (scaling, position, rotation, shearing) as well as its own material and uv coordinates. All of this info is fed into the GPU via uniforms. Lastly, the Ray Tracing fragment shader (which runs on the GPU) takes in all of this data, then raycasts each quadric shape, handles all of the reflections, refractions, and direct lighting (and shadows) from the directional light source, and finally delivers the beautiful ray traced images to your screen with blazing-fast speed!
15 |
16 | * [Instance Mapping Demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/Instance_Mapping.html) You may have heard of bump mapping before, but this new technique I've developed goes a step beyond that and actually creates new instanced geometry along the surface of the parent object. For example, we can start out with a large sphere parent object, and then as a ray intersects that parent sphere, the UV coordinates of the intersection point are recorded. Then a smaller shape (a tiny sphere, for example) spawns out of the surface at that exact UV coordinate. By scaling/rounding the floating-point UV coordinates up and down, we can have more or less tiny child shapes pop up all over the parent's surface. Now, just like normal maps and bump mapping, the illusion is broken as you glide the camera along the silhouette edge of the parent shape (it is just a smooth parent sphere after all!) - but, since we are using the power of ray tracing with its realistic lighting, every single tiny shape that pops up behaves like a real raytraced object - complete with reflections, refractions, and shadows! This is something you just can't get from normal maps and traditional bump mapping. The best part is that since the camera rays are spawning all of these tiny shapes in parallel on the GPU, we don't have to pay the cost for having thousands or millions of more shapes appear along the surface - maybe just a couple of more adjacent tiny shapes to help make shadows on the sides of the shape 'bumps'. I wasn't sure what to call this unique raytracing method, so for now I'm naming this technique 'Instance Mapping'. I think this name succinctly describes what is happening along the parent shape's surface. Enjoy!
17 |
18 |
19 | 
20 |
21 | * [Dynamic Scene Demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/Ray_Tracer_Challenge.html) This scene is from the cover of the fairly recent book on ray tracing called 'The Ray Tracer Challenge' by Jamis Buck. At the end of the book is a scene description for re-creating the scene featured on the book's cover. Not only did I enjoy going through the various challenges in Buck's book including rendering this final scene, I cranked the challenge up a notch and added real-time animations to his cool scene! From the GUI menu in the top-right corner, you can select one of 5 different animations that will bring Buck's scene to life. The nice thing about Ray Tracing vs. Path Tracing is that with Ray Tracing, there's hardly any noise to contend with. Even at 1 sample per pixel per frame, you get back a solid, stable image (which is definitely *not* the case for Path Tracing!). This strength makes Ray Tracing a great candidate for animated/dynamic scenes and real-time games in the browser. I may have an idea or two for some simple browser games that could be rendered with this Ray Tracing technology - stay tuned! ;-)
22 |
23 |
24 |
25 |
Classic Scenes / Ray Tracing History
26 |
27 | 
28 |
29 | The above image is the cover of the classic book, An Introduction To Ray Tracing (of which I own a copy!). The iconic cover image inspired me to try and recreate this classic scene inside my Three.js RayTracing Renderer. Not only did I want to try and recapture the look and feel of this image, I wanted to be able to move the camera and fly around the scene in real time (30 to 60fps) on any device - even a cell phone! I am happy to present my homage to this classic scene and the great book which it comes from:
30 |
31 | * [The 6 Platonic Solids demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/The_6_Platonic_Solids.html) This demo features two techniques that I have recently developed: Convex Polyhedra raycasting and Instance Mapping. The 5 Platonic solids (minus the 'Teapotahedron', lol) featured in the demo are efficiently rendered with a plane-cutting technique first developed by Eric Haines (one of the authors of this book!) back in 1991. This algorithm allows you to raycast a convex faceted shape consisting of 4 to 20 planes. The 'Teapotaheadron' is rendered with the traditional triangular model and BVH acceleration system. The other technique, Instance Mapping, is used on the 6 marble pillars. This algorithm for efficiently raycasting multiple small instances located on and around a large parent shape produces the dozens of smaller cylinder columns around each large pillar. Without these two techniques, we would have no hope of running this complex classic scene at real time frame rates, let alone on mobile devices! Enjoy! ;-)
32 |
33 |
34 |
35 |
36 | 
37 |
38 | While working at Bell Labs in the late 1970's and writing his now-famous paper [An Improved Illumination Model for Shaded Display](http://artis.imag.fr/Members/David.Roger/whitted.pdf), J. Turner Whitted developed a brand new technique of recursive ray tracing (a.k.a. 'Whitted-style' ray tracing). Following the laws of optics such as The Law of Reflection, Snell's Refraction Law, and Lambert's Cosine Law (for diffuse lighting), Whitted was able to create a handful of iconic images containing checkered planes and spheres that featured photo-realistic reflections, refractions, and pixel-perfect shadows. The level of realism he was able to achieve with his new recursive raytracing technique shocked the entire CG community - no one had ever seen this level of realism in computer-generated images before! Whitted quickly became famous in the computer world after he included these ray-traced images in his paper (which would later be published in June 1980). Then for an upcoming SIGGRAPH conference submission, Whitted decided to create an animated sequence of individual ray-traced images. Thus the first ever ray-traced animation was born!
39 |
40 | [Vintage 1979 Video: 'The Compleat Angler' by J. Turner Whitted](https://youtu.be/0KrCh5qD9Ho)
41 |
42 | Although this movie appears as a smooth animation, it took around 45 minutes to render each individual frame back in 1979! Fast-forward to today and using WebGL 2.0 and the parallel processing power of GPUs, here is the same iconic scene rendered at 60 times a second in your browser! :
43 | * [Whitted Recursive RayTracing demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/Whitted_RayTracing.html)
44 |
45 | My GitHub repo here closely follows the 'Whitted-style' ray tracing method that made Turner Whitted famous back in 1980. I use his recursive ray tracing technique to handle realistic reflections, refractions, and sharp shadows, just as Whitted did decades ago. The only place where my technique diverges from, or builds upon, Whitted's technique is the use of the Hall Shading model (named after Roy Hall, another highly influental CG researcher in the 1980's). Hall's model builds upon Whitted's technique by incorporating the Fresnel equations to correctly display the amount of reflected light vs. the amount of transmitted (or refracted) light on Transparent and ClearCoat surfaces. Also Hall's model adds shadow caustics from transparent objects - not true path-traced caustics, but visually interesting in their own way.
46 |
47 |
48 |
49 | 
50 |
51 | The year is 1983, and CG graphics researcher Roy Hall has just developed his own model, the Hall Shading Model, that builds on and improves Whitted's recursive raytracing technique. Hall created the above image, which he named *The Gallery*, for the SIGGRAPH 1983 art show. His image showcases his improved shading model, including soft diffuse fall-off from the spotlights on the walls and carpet, color-tinted reflections from Metals (which improves on Whitted's technique), and a soft spotlight highlight on the large mirror (upper right side of mirror) that goes back further and further in the reflections of the mirror itself - a cool effect that simulates a fine layer of dust on the mirror's surface. I was so intrigued by Hall's classic image that I decided to try and duplicate it. I'm happy to report that I not only recreated Hall's beautiful scene, I was able to bring it into real time - 60 FPS in the browser, even on your cell phone!
52 |
53 | * [Roy Hall 'The Gallery' demo](https://erichlof.github.io/THREE.js-RayTracing-Renderer/RoyHall_TheGallery.html)
54 |
55 | Hall later rendered this scene from a different camera angle and used it for the cover of his book, Illumination and Color in Computer Generated Imagery:
56 |
57 | 
58 |
59 | This iconic image from Roy Hall represents the pinnacle of traditional recursive Ray Tracing. The recursiveness is showcased with the infinity of mirror reflections in The Gallery. Hall's improved and refined model shines in this classic scene with its soft falloff from multiple spotlights and realistic metallic reflections. In the very next year, 1984, Robert Cook would introduce his new Ray Distribution method, which randomizes reflection rays to simulate rough metallic/shiny surfaces. Although this would bring added realism to rough surfaces, with randomness comes pesky noise that must be painfully endured while viewing, or dealt with - either by taking and averaging many more expensive samples (which Cook suggests), or using modern-day techniques of denoising. And just 2 years after that, in 1986, Jim Kajiya would shock the CG world with his Rendering Equation, which makes all surfaces (not just rough metallic) use randomness in the ray reflection directions, producing the ultimate in photo-realistic images. But with that increased randomness, comes even more pesky noise (maximum noise, actually) that must be painfully endured while viewing, or dealt with in a clever or expensive way.
60 |
61 |
TODO
62 |
63 | * For simple scenes without gltf models, instead of scene description hard-coded in the ray tracing shaders, let the scene be defined using familiar Three.js mesh creation commands. The ultimate goal is to be able to create and load any arbritrary scene that uses the standard, simple Three.js library calls for scene construction.
64 | * Dynamic Scene description/BVH rigged model animation streamed real-time to the GPU ray tracer (1/21/21 made progress in this area by working on my new game The Sentinel: 2nd Look. Featues a dynamic top-level BVH that can change and update itself every animation frame)
65 |
66 |
67 |
ABOUT
68 |
69 | * This project is a cross between a labor of love (I love creating all types of ray tracers!) and an educational resource for showing how Classic (1980's and early 90's) Ray Tracing was achieved. Inspiration for this ray tracer comes from the seminal work of Arthur Appel (late 1960's), Turner Whitted (late 1970's and 80's), the MAGI group (who did the Ray Tracing for the movie TRON in the early 80's), and Robert Cook (mid 1980's). Although this type of rendering is not as complete and photo-realistic as path tracing (especially on diffuse color bleeding and physically-accurate caustics), I think you'll agree that this Classic style of Ray Tracing has its own cool, nostalgic, unique look that you just can't get from traditional rasterization!
70 |
71 | More examples, features, and content to come!
72 |
73 |
--------------------------------------------------------------------------------
/shaders/WhittedRayTracing_Fragment.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | precision highp int;
3 | precision highp sampler2D;
4 |
5 | #include
6 |
7 | uniform sampler2D uTileNormalMapTexture;
8 |
9 | #define N_SPHERES 3
10 | #define N_RECTANGLES 1
11 |
12 |
13 | //-----------------------------------------------------------------------
14 |
15 | vec3 rayOrigin, rayDirection;
16 | // recorded intersection data:
17 | vec3 intersectionNormal;
18 | vec2 intersectionUV;
19 | int intersectionTextureID;
20 | int intersectionShapeIsClosed;
21 |
22 |
23 | struct Material { int type; int isCheckered; vec3 color; vec3 color2; float metalness; float roughness; float IoR; int textureID; };
24 | struct Rectangle { vec3 position; vec3 normal; vec3 vectorU; vec3 vectorV; float radiusU; float radiusV; vec2 uvScale; Material material; };
25 | struct Sphere { float radius; vec3 position; vec2 uvScale; Material material; };
26 |
27 | Material intersectionMaterial;
28 | Sphere spheres[N_SPHERES];
29 | Rectangle rectangles[N_RECTANGLES];
30 |
31 |
32 | #include
33 |
34 | #include
35 |
36 | #include
37 |
38 |
39 | vec3 perturbNormal(vec3 normal, vec2 bumpScale, vec2 uv)
40 | {
41 | // note: incoming vec3 normal is assumed to be normalized
42 | vec3 S = normalize(cross(vec3(0, 0.9938837346736189, 0.11043152607484655), normal));
43 | vec3 T = cross(normal, S);
44 | vec3 N = normal;
45 | // invert S, T when the UV direction is backwards (from mirrored faces),
46 | // otherwise it will do the normal mapping backwards.
47 | // vec3 NfromST = cross( S, T );
48 | // if( dot( NfromST, N ) < 0.0 )
49 | // {
50 | // S *= -1.0;
51 | // T *= -1.0;
52 | // }
53 | mat3 tsn = mat3( S, T, N );
54 |
55 | vec3 mapN = texture(uTileNormalMapTexture, uv).xyz * 2.0 - 1.0;
56 | //mapN = normalize(mapN);
57 | mapN.xy *= bumpScale;
58 |
59 | return normalize( tsn * mapN );
60 | }
61 |
62 |
63 |
64 | //-----------------------------------------------------------------------
65 | float SceneIntersect()
66 | //-----------------------------------------------------------------------
67 | {
68 | vec3 intersectionPoint, normal;
69 | float d;
70 | float t = INFINITY;
71 | float u, v;
72 |
73 |
74 | // Ground checkered Rectangle
75 | d = RectangleIntersect( rectangles[0].position, rectangles[0].normal, rectangles[0].vectorU, rectangles[0].vectorV,
76 | rectangles[0].radiusU, rectangles[0].radiusV, rayOrigin, rayDirection, u, v );
77 | if (d < t)
78 | {
79 | t = d;
80 | intersectionNormal = rectangles[0].normal;
81 | intersectionMaterial = rectangles[0].material;
82 | intersectionUV = vec2(u, v) * rectangles[0].uvScale;
83 | intersectionShapeIsClosed = FALSE;
84 | }
85 |
86 | d = SphereIntersect( spheres[0].radius, spheres[0].position, rayOrigin, rayDirection );
87 | if (d < t)
88 | {
89 | t = d;
90 | intersectionPoint = rayOrigin + (t * rayDirection);
91 | intersectionNormal = intersectionPoint - spheres[0].position;
92 | intersectionMaterial = spheres[0].material;
93 | //intersectionUV = calcSphereUV(intersectionPoint, spheres[0].radius, spheres[0].position) * spheres[0].uvScale;
94 | intersectionShapeIsClosed = TRUE;
95 | }
96 |
97 | d = SphereIntersect( spheres[1].radius, spheres[1].position, rayOrigin, rayDirection );
98 | if (d < t)
99 | {
100 | t = d;
101 | intersectionPoint = rayOrigin + (t * rayDirection);
102 | intersectionNormal = intersectionPoint - spheres[1].position;
103 | intersectionMaterial = spheres[1].material;
104 | //intersectionUV = calcSphereUV(intersectionPoint, spheres[1].radius, spheres[1].position) * spheres[1].uvScale;
105 | intersectionShapeIsClosed = TRUE;
106 | }
107 |
108 | d = SphereIntersect( spheres[2].radius, spheres[2].position, rayOrigin, rayDirection );
109 | if (d < t)
110 | {
111 | t = d;
112 | intersectionPoint = rayOrigin + (t * rayDirection);
113 | intersectionNormal = intersectionPoint - spheres[2].position;
114 | intersectionMaterial = spheres[2].material;
115 | intersectionUV = calcSphereUV(intersectionPoint, spheres[2].radius, spheres[2].position) * spheres[2].uvScale;
116 | intersectionShapeIsClosed = TRUE;
117 | }
118 |
119 |
120 | return t;
121 |
122 | } // end float SceneIntersect()
123 |
124 |
125 |
126 | //-----------------------------------------------------------------------
127 | vec3 RayTrace()
128 | //-----------------------------------------------------------------------
129 | {
130 | vec3 accumulatedColor = vec3(0);
131 | vec3 rayColorMask = vec3(1);
132 | vec3 reflectionRayColorMask = vec3(1);
133 | vec3 reflectionRayOrigin = vec3(0);
134 | vec3 reflectionRayDirection = vec3(0);
135 | vec3 reflectionRayColorMask2 = vec3(1);
136 | vec3 reflectionRayOrigin2 = vec3(0);
137 | vec3 reflectionRayDirection2 = vec3(0);
138 | vec3 reflectionRayColorMask3 = vec3(1);
139 | vec3 reflectionRayOrigin3 = vec3(0);
140 | vec3 reflectionRayDirection3 = vec3(0);
141 | vec3 skyColor = vec3(0.01, 0.15, 0.7);
142 | vec3 sunlightColor = vec3(1);
143 | vec3 ambientColor = vec3(0);
144 | vec3 diffuseColor = vec3(0);
145 | vec3 specularColor = vec3(0);
146 | vec3 tdir;
147 | vec3 directionToLight = normalize(vec3(-0.2, 1.0, 0.7));
148 | vec3 geometryNormal, shadingNormal;
149 | vec3 intersectionPoint;
150 | vec3 halfwayVector;
151 |
152 | vec2 sphereUV;
153 |
154 | float t;
155 | float ni, nt, ratioIoR, Re, Tr;
156 | float ambientIntensity = 0.2;
157 | float diffuseIntensity;
158 | float specularIntensity;
159 |
160 | int previousIntersectionMaterialType;
161 | int bounceIsSpecular = FALSE;
162 | int sampleLight = FALSE;
163 | int willNeedReflectionRay = FALSE;
164 | int willNeedReflectionRay2 = FALSE;
165 | int willNeedReflectionRay3 = FALSE;
166 | int reflectionIsFromMetal = FALSE;
167 |
168 | intersectionMaterial.type = -100;
169 |
170 |
171 | for (int bounces = 0; bounces < 12; bounces++)
172 | {
173 | previousIntersectionMaterialType = intersectionMaterial.type;
174 |
175 | t = SceneIntersect();
176 |
177 | if (t == INFINITY)
178 | {
179 | if (bounces == 0)
180 | {
181 | accumulatedColor += rayColorMask * skyColor;
182 | break;
183 | }
184 | else if (sampleLight == TRUE)
185 | {
186 | accumulatedColor += diffuseColor + specularColor;
187 | }
188 | else if (bounceIsSpecular == TRUE && reflectionIsFromMetal == FALSE)
189 | {
190 | accumulatedColor += rayColorMask * skyColor;
191 | }
192 |
193 |
194 | if (willNeedReflectionRay == TRUE)
195 | {
196 | rayColorMask = reflectionRayColorMask;
197 | rayOrigin = reflectionRayOrigin;
198 | rayDirection = reflectionRayDirection;
199 | intersectionMaterial.type = -100;
200 | willNeedReflectionRay = FALSE;
201 | sampleLight = FALSE;
202 | bounceIsSpecular = TRUE;
203 | continue;
204 | }
205 |
206 | if (willNeedReflectionRay2 == TRUE)
207 | {
208 | rayColorMask = reflectionRayColorMask2;
209 | rayOrigin = reflectionRayOrigin2;
210 | rayDirection = reflectionRayDirection2;
211 | intersectionMaterial.type = -100;
212 | willNeedReflectionRay2 = FALSE;
213 | sampleLight = FALSE;
214 | bounceIsSpecular = TRUE;
215 | continue;
216 | }
217 |
218 | if (willNeedReflectionRay3 == TRUE)
219 | {
220 | rayColorMask = reflectionRayColorMask3;
221 | rayOrigin = reflectionRayOrigin3;
222 | rayDirection = reflectionRayDirection3;
223 | intersectionMaterial.type = -100;
224 | willNeedReflectionRay3 = FALSE;
225 | sampleLight = FALSE;
226 | bounceIsSpecular = TRUE;
227 | continue;
228 | }
229 |
230 | break;
231 | }
232 |
233 |
234 | // if we get here and sampleLight is still TRUE, shadow ray failed to find the light source
235 | // the ray hit an occluding object along its way to the light
236 | if (sampleLight == TRUE)
237 | {
238 | if (bounces == 1 && intersectionMaterial.type == TRANSPARENT && previousIntersectionMaterialType == DIFFUSE)
239 | {
240 | accumulatedColor *= 3.5; // make shadow underneath glass sphere a little lighter
241 | break;
242 | }
243 |
244 | if (willNeedReflectionRay == TRUE)
245 | {
246 | rayColorMask = reflectionRayColorMask;
247 | rayOrigin = reflectionRayOrigin;
248 | rayDirection = reflectionRayDirection;
249 | intersectionMaterial.type = -100;
250 | willNeedReflectionRay = FALSE;
251 | sampleLight = FALSE;
252 | bounceIsSpecular = TRUE;
253 | continue;
254 | }
255 |
256 | if (willNeedReflectionRay2 == TRUE)
257 | {
258 | rayColorMask = reflectionRayColorMask2;
259 | rayOrigin = reflectionRayOrigin2;
260 | rayDirection = reflectionRayDirection2;
261 | intersectionMaterial.type = -100;
262 | willNeedReflectionRay2 = FALSE;
263 | sampleLight = FALSE;
264 | bounceIsSpecular = TRUE;
265 | continue;
266 | }
267 |
268 | if (willNeedReflectionRay3 == TRUE)
269 | {
270 | rayColorMask = reflectionRayColorMask3;
271 | rayOrigin = reflectionRayOrigin3;
272 | rayDirection = reflectionRayDirection3;
273 | intersectionMaterial.type = -100;
274 | willNeedReflectionRay3 = FALSE;
275 | sampleLight = FALSE;
276 | bounceIsSpecular = TRUE;
277 | continue;
278 | }
279 |
280 | break;
281 | }
282 |
283 |
284 | // useful data
285 | geometryNormal = normalize(intersectionNormal); // geometry normals are the unaltered normals from the intersected shape definition / or from the triangle mesh data
286 | shadingNormal = dot(geometryNormal, rayDirection) < 0.0 ? geometryNormal : -geometryNormal; // if geometry normal is pointing in the same manner as ray, must flip the shading normal (negate it)
287 | intersectionPoint = rayOrigin + (t * rayDirection); // use the ray equation to find intersection point (P = O + tD)
288 | halfwayVector = normalize(-rayDirection + directionToLight); // this is Blinn's modification to Phong's model
289 | //diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
290 |
291 |
292 | if (intersectionMaterial.isCheckered == TRUE)
293 | {
294 | intersectionMaterial.color = mod(floor(intersectionUV.x) + floor(intersectionUV.y), 2.0) == 0.0 ? intersectionMaterial.color : intersectionMaterial.color2;
295 | }
296 |
297 |
298 | if (intersectionMaterial.type == DIFFUSE)
299 | {
300 | bounceIsSpecular = FALSE;
301 |
302 | ambientColor = doAmbientLighting(rayColorMask, intersectionMaterial.color, ambientIntensity);
303 | accumulatedColor += ambientColor;
304 |
305 | diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
306 | diffuseColor = doDiffuseDirectLighting(rayColorMask, intersectionMaterial.color, sunlightColor, diffuseIntensity);
307 |
308 | specularColor = vec3(0);
309 |
310 | rayDirection = directionToLight; // shadow ray
311 | rayOrigin = intersectionPoint + shadingNormal * uEPS_intersect;
312 | sampleLight = TRUE;
313 | continue;
314 | }
315 |
316 | if (intersectionMaterial.type == CLEARCOAT)
317 | {
318 | bounceIsSpecular = FALSE;
319 |
320 | shadingNormal = perturbNormal(shadingNormal, vec2(0.5, 0.5), intersectionUV);
321 |
322 | ambientColor = doAmbientLighting(rayColorMask, intersectionMaterial.color, ambientIntensity);
323 | accumulatedColor += ambientColor * 0.6;
324 |
325 | diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
326 | diffuseColor = doDiffuseDirectLighting(rayColorMask, intersectionMaterial.color, sunlightColor, diffuseIntensity);
327 |
328 | specularColor = doBlinnPhongSpecularLighting(rayColorMask, shadingNormal, halfwayVector, sunlightColor * 2.0, 0.6, diffuseIntensity);
329 |
330 | if (bounces == 0)
331 | {
332 | reflectionRayColorMask = rayColorMask * 0.15;
333 | reflectionRayDirection = reflect(rayDirection, shadingNormal); // reflect ray from surface
334 | reflectionRayOrigin = intersectionPoint + shadingNormal * uEPS_intersect;
335 | willNeedReflectionRay = TRUE;
336 | reflectionIsFromMetal = TRUE;
337 | }
338 |
339 | rayDirection = directionToLight; // shadow ray
340 | rayOrigin = intersectionPoint + shadingNormal * uEPS_intersect;
341 | sampleLight = TRUE;
342 | continue;
343 | }
344 |
345 | if (intersectionMaterial.type == TRANSPARENT)
346 | {
347 | ni = 1.0; // IOR of Air
348 | nt = intersectionMaterial.IoR; // IOR of this classic demo's Glass
349 | //Re = calcFresnelReflectance(rayDirection, geometryNormal, ni, nt, ratioIoR);
350 | ratioIoR = ni / nt;
351 |
352 | if (bounces == 0)
353 | {
354 | reflectionRayColorMask = rayColorMask * 0.05;// * Re;
355 | reflectionRayDirection = reflect(rayDirection, shadingNormal); // reflect ray from surface
356 | reflectionRayOrigin = intersectionPoint + shadingNormal * uEPS_intersect;
357 | willNeedReflectionRay = TRUE;
358 | }
359 |
360 | if (bounces == 1 && previousIntersectionMaterialType == TRANSPARENT)
361 | {
362 | reflectionRayColorMask2 = rayColorMask * 0.05;// * Re;
363 | reflectionRayDirection2 = reflect(rayDirection, shadingNormal); // reflect ray from surface
364 | reflectionRayOrigin2 = intersectionPoint + shadingNormal * uEPS_intersect;
365 | willNeedReflectionRay2 = TRUE;
366 | }
367 |
368 | if (bounces == 2 && previousIntersectionMaterialType == TRANSPARENT)
369 | {
370 | reflectionRayColorMask3 = rayColorMask * 0.05;// * Re;
371 | reflectionRayDirection3 = reflect(rayDirection, shadingNormal); // reflect ray from surface
372 | reflectionRayOrigin3 = intersectionPoint + shadingNormal * uEPS_intersect;
373 | willNeedReflectionRay3 = TRUE;
374 | }
375 |
376 | ambientColor = vec3(0);
377 | diffuseColor = vec3(0);
378 |
379 | diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
380 | specularColor = doBlinnPhongSpecularLighting(rayColorMask, shadingNormal, halfwayVector, sunlightColor * 1.5, 0.5, diffuseIntensity);
381 | if (bounces == 0)
382 | accumulatedColor += specularColor;
383 | else accumulatedColor += specularColor * 0.2;
384 | specularColor = vec3(0);
385 |
386 | // transmit ray through surface
387 | //rayColorMask *= intersectionMaterial.color;
388 | //rayColorMask *= Tr;
389 | rayColorMask *= 0.95;
390 |
391 | tdir = refract(rayDirection, shadingNormal, ratioIoR);
392 | rayDirection = tdir;
393 | rayOrigin = intersectionPoint - shadingNormal * uEPS_intersect;
394 | bounceIsSpecular = TRUE;
395 |
396 | continue;
397 |
398 | } // end if (intersectionMaterial.type == TRANSPARENT)
399 |
400 | } // end for (int bounces = 0; bounces < 12; bounces++)
401 |
402 |
403 | return max(vec3(0), accumulatedColor);
404 |
405 | } // end vec3 RayTrace()
406 |
407 |
408 | //-----------------------------------------------------------------------
409 | void SetupScene(void)
410 | //-----------------------------------------------------------------------
411 | {
412 | Material yellowRedCheckerMaterial = Material(DIFFUSE, TRUE, vec3(1,1,0) * 0.8, vec3(1,0,0) * 0.8, 0.0, 0.0, 0.0, -1);
413 | Material yellowClearCoatMaterial = Material(CLEARCOAT, FALSE, vec3(1.0, 0.85, 0.0), vec3(0), 0.0, 0.0, 1.4, -1);
414 | Material glassMaterial0 = Material(TRANSPARENT, FALSE, vec3(1), vec3(0), 0.0, 0.0, 1.01, -1);
415 | Material glassMaterial1 = Material(TRANSPARENT, FALSE, vec3(1), vec3(0), 0.0, 0.0, 1.04, -1);
416 |
417 | vec3 glassSpherePos = vec3(-10, 78, 70);
418 | vec3 yellowSpherePos = glassSpherePos + vec3(0,-19, 5);
419 | //vec3 yellowSpherePos = glassSpherePos + vec3(50,-25, 70);
420 | float orbitRadius = 70.0;
421 | float testTime = 2.0;
422 | spheres[0] = Sphere( 28.0, glassSpherePos, vec2(1, 1), glassMaterial0);//glass sphere, radius=28.0
423 | spheres[1] = Sphere( 26.5, glassSpherePos, vec2(1, 1), glassMaterial1);//glass sphere, radius=26.5
424 | spheres[2] = Sphere( 27.0, yellowSpherePos + vec3(-cos(mod(uTime * 1.1, TWO_PI)) * orbitRadius, 0, sin(mod(uTime * 1.1, TWO_PI)) * orbitRadius),
425 | vec2(2, 2), yellowClearCoatMaterial);//yellow reflective sphere
426 |
427 | rectangles[0] = Rectangle(vec3(100, 0, -100), vec3(0,1,0), vec3(1,0,0), vec3(0,0,-1), 200.0, 400.0, vec2(16, 32), yellowRedCheckerMaterial); // Checkerboard Ground plane
428 | }
429 |
430 |
431 | #include
432 |
--------------------------------------------------------------------------------
/shaders/RoyHallTheGallery_Fragment.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | precision highp int;
3 | precision highp sampler2D;
4 |
5 | #include
6 |
7 | uniform mat4 uCubeInvMatrix;
8 | uniform mat4 uPlatformInvMatrix;
9 |
10 | #define N_BOX_INTERIORS 1
11 | #define N_SPHERES 12
12 | #define N_SPOTLIGHT_SPHERES 6
13 |
14 | vec3 rayOrigin, rayDirection;
15 | // recorded intersection data:
16 | vec3 intersectionNormal;
17 | vec2 intersectionUV;
18 | int intersectionTextureID;
19 | int intersectionShapeIsClosed;
20 |
21 | struct Material { int type; int isCheckered; vec3 color; vec3 color2; float metalness; float roughness; float IoR; int textureID; };
22 | struct BoxInterior { vec3 minCorner; vec3 maxCorner; vec2 uvScale; Material material; };
23 | struct Sphere { float radius; vec3 position; vec2 uvScale; Material material; };
24 | struct SpotLightSphere { float radius; vec3 position; vec3 direction; vec3 color; };
25 |
26 | Material intersectionMaterial;
27 | BoxInterior boxInteriors[N_BOX_INTERIORS];
28 | Sphere spheres[N_SPHERES];
29 | SpotLightSphere spotlightSpheres[N_SPOTLIGHT_SPHERES];
30 |
31 |
32 | #include
33 |
34 | #include // for intersecting Point lights
35 |
36 | #include
37 |
38 | #include
39 |
40 |
41 | //---------------------------------------------------------------------------------------
42 | float SceneIntersect( int isShadowRay )
43 | //---------------------------------------------------------------------------------------
44 | {
45 |
46 | vec3 rObjOrigin, rObjDirection;
47 | vec3 intersectionPoint, normal;
48 | float d;
49 | float t = INFINITY;
50 | float u, v;
51 | int isRayExiting = FALSE;
52 |
53 |
54 | // Room (Box Interior)
55 | d = BoxInteriorIntersect(boxInteriors[0].minCorner, boxInteriors[0].maxCorner, rayOrigin, rayDirection, normal);
56 | if (d < t)
57 | {
58 | t = d;
59 | intersectionNormal = normal;
60 | intersectionMaterial = boxInteriors[0].material;
61 | if (intersectionNormal == vec3(0,1,0))
62 | intersectionMaterial.color = vec3(0.001, 0.001, 0.1);
63 | else if (intersectionNormal == vec3(0,-1,0))
64 | intersectionMaterial.color *= vec3(1,1.5,2);
65 | else if (intersectionNormal == vec3(0,0,1) || intersectionNormal == vec3(0,0,-1))
66 | {
67 | intersectionMaterial.type = PERFECT_MIRROR;
68 | intersectionMaterial.color = vec3(0.8);
69 | }
70 | intersectionShapeIsClosed = FALSE;
71 | }
72 |
73 |
74 | d = SphereIntersect( spheres[0].radius, spheres[0].position, rayOrigin, rayDirection );
75 | if (d < t)
76 | {
77 | t = d;
78 | intersectionPoint = rayOrigin + (t * rayDirection);
79 | intersectionNormal = intersectionPoint - spheres[0].position;
80 | intersectionMaterial = spheres[0].material;
81 | intersectionShapeIsClosed = TRUE;
82 | }
83 | d = SphereIntersect( spheres[1].radius, spheres[1].position, rayOrigin, rayDirection );
84 | if (d < t)
85 | {
86 | t = d;
87 | intersectionPoint = rayOrigin + (t * rayDirection);
88 | intersectionNormal = intersectionPoint - spheres[1].position;
89 | intersectionMaterial = spheres[1].material;
90 | intersectionShapeIsClosed = TRUE;
91 | }
92 | d = SphereIntersect( spheres[2].radius, spheres[2].position, rayOrigin, rayDirection );
93 | if (d < t)
94 | {
95 | t = d;
96 | intersectionPoint = rayOrigin + (t * rayDirection);
97 | intersectionNormal = intersectionPoint - spheres[2].position;
98 | intersectionMaterial = spheres[2].material;
99 | intersectionShapeIsClosed = TRUE;
100 | }
101 | d = SphereIntersect( spheres[3].radius, spheres[3].position, rayOrigin, rayDirection );
102 | if (d < t)
103 | {
104 | t = d;
105 | intersectionPoint = rayOrigin + (t * rayDirection);
106 | intersectionNormal = intersectionPoint - spheres[3].position;
107 | intersectionMaterial = spheres[3].material;
108 | intersectionShapeIsClosed = TRUE;
109 | }
110 | d = SphereIntersect( spheres[4].radius, spheres[4].position, rayOrigin, rayDirection );
111 | if (d < t)
112 | {
113 | t = d;
114 | intersectionPoint = rayOrigin + (t * rayDirection);
115 | intersectionNormal = intersectionPoint - spheres[4].position;
116 | intersectionMaterial = spheres[4].material;
117 | intersectionShapeIsClosed = TRUE;
118 | }
119 | d = SphereIntersect( spheres[5].radius, spheres[5].position, rayOrigin, rayDirection );
120 | if (d < t)
121 | {
122 | t = d;
123 | intersectionPoint = rayOrigin + (t * rayDirection);
124 | intersectionNormal = intersectionPoint - spheres[5].position;
125 | intersectionMaterial = spheres[5].material;
126 | intersectionShapeIsClosed = TRUE;
127 | }
128 | d = SphereIntersect( spheres[6].radius, spheres[6].position, rayOrigin, rayDirection );
129 | if (d < t)
130 | {
131 | t = d;
132 | intersectionPoint = rayOrigin + (t * rayDirection);
133 | intersectionNormal = intersectionPoint - spheres[6].position;
134 | intersectionMaterial = spheres[6].material;
135 | intersectionShapeIsClosed = TRUE;
136 | }
137 | d = SphereIntersect( spheres[7].radius, spheres[7].position, rayOrigin, rayDirection );
138 | if (d < t)
139 | {
140 | t = d;
141 | intersectionPoint = rayOrigin + (t * rayDirection);
142 | intersectionNormal = intersectionPoint - spheres[7].position;
143 | intersectionMaterial = spheres[7].material;
144 | intersectionShapeIsClosed = TRUE;
145 | }
146 | d = SphereIntersect( spheres[8].radius, spheres[8].position, rayOrigin, rayDirection );
147 | if (d < t)
148 | {
149 | t = d;
150 | intersectionPoint = rayOrigin + (t * rayDirection);
151 | intersectionNormal = intersectionPoint - spheres[8].position;
152 | intersectionMaterial = spheres[8].material;
153 | intersectionShapeIsClosed = TRUE;
154 | }
155 | d = SphereIntersect( spheres[9].radius, spheres[9].position, rayOrigin, rayDirection );
156 | if (d < t)
157 | {
158 | t = d;
159 | intersectionPoint = rayOrigin + (t * rayDirection);
160 | intersectionNormal = intersectionPoint - spheres[9].position;
161 | intersectionMaterial = spheres[9].material;
162 | intersectionShapeIsClosed = TRUE;
163 | }
164 | d = SphereIntersect( spheres[10].radius, spheres[10].position, rayOrigin, rayDirection );
165 | if (d < t)
166 | {
167 | t = d;
168 | intersectionPoint = rayOrigin + (t * rayDirection);
169 | intersectionNormal = intersectionPoint - spheres[10].position;
170 | intersectionMaterial = spheres[10].material;
171 | intersectionShapeIsClosed = TRUE;
172 | }
173 | d = SphereIntersect( spheres[11].radius, spheres[11].position, rayOrigin, rayDirection );
174 | if (d < t)
175 | {
176 | t = d;
177 | intersectionPoint = rayOrigin + (t * rayDirection);
178 | intersectionNormal = intersectionPoint - spheres[11].position;
179 | intersectionMaterial = spheres[11].material;
180 | intersectionShapeIsClosed = TRUE;
181 | }
182 |
183 | // SPOTLIGHT SPHERES
184 | d = SphereIntersect( spotlightSpheres[0].radius, spotlightSpheres[0].position, rayOrigin, rayDirection );
185 | if (d < t)
186 | {
187 | t = d;
188 | intersectionMaterial.type = SPOT_LIGHT;
189 | intersectionMaterial.color = spotlightSpheres[0].color;
190 | }
191 | d = SphereIntersect( spotlightSpheres[1].radius, spotlightSpheres[1].position, rayOrigin, rayDirection );
192 | if (d < t)
193 | {
194 | t = d;
195 | intersectionMaterial.type = SPOT_LIGHT;
196 | intersectionMaterial.color = spotlightSpheres[1].color;
197 | }
198 | d = SphereIntersect( spotlightSpheres[2].radius, spotlightSpheres[2].position, rayOrigin, rayDirection );
199 | if (d < t)
200 | {
201 | t = d;
202 | intersectionMaterial.type = SPOT_LIGHT;
203 | intersectionMaterial.color = spotlightSpheres[2].color;
204 | }
205 | d = SphereIntersect( spotlightSpheres[3].radius, spotlightSpheres[3].position, rayOrigin, rayDirection );
206 | if (d < t)
207 | {
208 | t = d;
209 | intersectionMaterial.type = SPOT_LIGHT;
210 | intersectionMaterial.color = spotlightSpheres[3].color;
211 | }
212 | d = SphereIntersect( spotlightSpheres[4].radius, spotlightSpheres[4].position, rayOrigin, rayDirection );
213 | if (d < t)
214 | {
215 | t = d;
216 | intersectionMaterial.type = SPOT_LIGHT;
217 | intersectionMaterial.color = spotlightSpheres[4].color;
218 | }
219 | d = SphereIntersect( spotlightSpheres[5].radius, spotlightSpheres[5].position, rayOrigin, rayDirection );
220 | if (d < t)
221 | {
222 | t = d;
223 | intersectionMaterial.type = SPOT_LIGHT;
224 | intersectionMaterial.color = spotlightSpheres[5].color;
225 | }
226 |
227 | // cube sculpture in center of gallery
228 | rObjOrigin = vec3( uCubeInvMatrix * vec4(rayOrigin, 1.0) );
229 | rObjDirection = vec3( uCubeInvMatrix * vec4(rayDirection, 0.0) );
230 | d = BoxIntersect(vec3(-1), vec3(1), rObjOrigin, rObjDirection, normal, isRayExiting);
231 | if (d < t)
232 | {
233 | t = d;
234 | intersectionNormal = transpose(mat3(uCubeInvMatrix)) * normal;
235 | intersectionMaterial.type = METAL;
236 | intersectionMaterial.color = vec3(1.0, 0.8, 0.5);
237 | //intersectionMaterial.metalness = 1.0;
238 | intersectionMaterial.roughness = 0.5;
239 | intersectionShapeIsClosed = TRUE;
240 | }
241 |
242 | // platform box underneath the cube sculpture
243 | rObjOrigin = vec3( uPlatformInvMatrix * vec4(rayOrigin, 1.0) );
244 | rObjDirection = vec3( uPlatformInvMatrix * vec4(rayDirection, 0.0) );
245 | d = BoxIntersect(vec3(-1), vec3(1), rObjOrigin, rObjDirection, normal, isRayExiting);
246 | if (d < t)
247 | {
248 | t = d;
249 | intersectionNormal = transpose(mat3(uPlatformInvMatrix)) * normal;
250 | intersectionMaterial.type = METAL;
251 | intersectionMaterial.color = vec3(1.0, 1.0, 1.0);
252 | //intersectionMaterial.metalness = 1.0;
253 | intersectionMaterial.roughness = 1.0;
254 | intersectionShapeIsClosed = TRUE;
255 | }
256 |
257 | return t;
258 | } // end float SceneIntersect( )
259 |
260 |
261 |
262 | //-------------------------------------------------------------------------------------------
263 | vec3 RayTrace()
264 | //-------------------------------------------------------------------------------------------
265 | {
266 | //int lightChoice = int(rng() * 2.0);
267 |
268 | vec3 accumulatedColor = vec3(0);
269 | vec3 rayColorMask = vec3(1);
270 | vec3 reflectionRayOrigin, reflectionRayDirection;
271 | vec3 reflectionRayColorMask;
272 | vec3 geometryNormal, shadingNormal;
273 | vec3 halfwayVector;
274 | vec3 intersectionPoint;
275 | vec3 directionToLight;
276 | vec3 ambientContribution = vec3(0);
277 | vec3 diffuseContribution = vec3(0);
278 | vec3 specularContribution = vec3(0);
279 | vec3 textureColor;
280 | vec3 lightPosition;
281 | vec3 lightColor;
282 | vec3 spotlightAimDirection;
283 | vec3 diffuseIntersectionColor;
284 |
285 | float t = INFINITY;
286 | float initial_t = INFINITY;
287 | float ambientIntensity = 0.02;
288 | float diffuseIntensity;
289 | float fogStart;
290 | float reflectance, transmittance;
291 | //float ni, nt;
292 | float IoR_ratio;
293 | float transparentThickness;
294 | float previousMaterialIoR = 1.0;
295 | float currentMaterialIoR = 1.0;
296 | //float lightDistanceAttenuation;
297 | float diffuseIntersectionRoughness;
298 |
299 | int willNeedShadowRays = FALSE;
300 | int isShadowRay = FALSE;
301 | int willNeedReflectionRay = FALSE;
302 | int sceneUsesDirectionalLight = FALSE;
303 | int previousIntersectionMaterialType;
304 | intersectionMaterial.type = -100;
305 |
306 |
307 | // since this room has opposing mirrors that give 'infinite' reflections, we must use many more bounces than usual
308 | for (int bounces = 0; bounces < 20; bounces++)
309 | {
310 |
311 | previousIntersectionMaterialType = intersectionMaterial.type;
312 |
313 | t = SceneIntersect( isShadowRay );
314 |
315 | // shouldn't happen in this closed-room demo, but just in case...
316 | if (t == INFINITY) // ray has missed all objects and hit the background
317 | {
318 | break;
319 | }
320 |
321 |
322 | if (intersectionMaterial.type == SPOT_LIGHT)
323 | {
324 | if (bounces == 0 || previousIntersectionMaterialType == PERFECT_MIRROR ||
325 | previousIntersectionMaterialType == METAL)
326 | accumulatedColor += rayColorMask * clamp(intersectionMaterial.color, 0.0, 2.0);
327 |
328 | break;
329 | }
330 |
331 |
332 | // useful data
333 | geometryNormal = normalize(intersectionNormal);
334 | shadingNormal = dot(geometryNormal, rayDirection) < 0.0 ? geometryNormal : -geometryNormal;
335 | intersectionPoint = rayOrigin + (t * rayDirection);
336 |
337 |
338 | if (intersectionMaterial.type == PERFECT_MIRROR)
339 | {
340 | // tint ray color with metal mirror color
341 | rayColorMask *= intersectionMaterial.color;
342 |
343 | lightPosition = spotlightSpheres[3].position;
344 | lightColor = spotlightSpheres[3].color;
345 | directionToLight = normalize(lightPosition - intersectionPoint);
346 | halfwayVector = normalize(-rayDirection + directionToLight);
347 | diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
348 | // add highlight in mirror (due to fine dust particles on mirror's surface)
349 | specularContribution = doBlinnPhongSpecularLighting(rayColorMask, shadingNormal, halfwayVector, lightColor, 0.2, diffuseIntensity);
350 | accumulatedColor += (0.2 * specularContribution);
351 |
352 | rayOrigin = intersectionPoint + (uEPS_intersect * shadingNormal);
353 | rayDirection = reflect(rayDirection, shadingNormal);
354 | continue;
355 | } // end if (intersectionMaterial.type == PERFECT_MIRROR)
356 |
357 |
358 | if (intersectionMaterial.type == DIFFUSE)
359 | {
360 | ambientContribution = doAmbientLighting(rayColorMask, intersectionMaterial.color, ambientIntensity);
361 | if (intersectionMaterial.color == vec3(1.0, 1.6, 2.0)) // spotlight casing
362 | ambientContribution *= 0.3;
363 | accumulatedColor += ambientContribution;
364 |
365 |
366 | // record current intersectionMaterial data for use later with shadow rays
367 | diffuseIntersectionColor = intersectionMaterial.color;
368 | diffuseIntersectionRoughness = intersectionMaterial.roughness;
369 |
370 | willNeedShadowRays = TRUE;
371 | break;
372 | } // if (intersectionMaterial.type == DIFFUSE)
373 |
374 |
375 | if (intersectionMaterial.type == METAL)
376 | {
377 | // ambientContribution = doAmbientLighting(rayColorMask, intersectionMaterial.color, ambientIntensity);
378 | // ambientContribution *= (1.0 - intersectionMaterial.metalness);
379 | // accumulatedColor += ambientContribution;
380 |
381 | // tint ray color with metal color
382 | rayColorMask *= intersectionMaterial.color;
383 |
384 | for (int nLight = 0; nLight < N_SPOTLIGHT_SPHERES; nLight++)
385 | {
386 | lightPosition = spotlightSpheres[nLight].position;
387 | directionToLight = normalize(lightPosition - intersectionPoint);
388 | if (dot(directionToLight, -spotlightSpheres[nLight].direction) < 0.8)
389 | continue;
390 | halfwayVector = normalize(-rayDirection + directionToLight);
391 | diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
392 |
393 | specularContribution = doBlinnPhongSpecularLighting(rayColorMask, shadingNormal, halfwayVector, 4.0 * spotlightSpheres[nLight].color, intersectionMaterial.roughness, diffuseIntensity);
394 | accumulatedColor += specularContribution;
395 | } // end for (int nLight = 0; nLight < 6; nLight++)
396 |
397 | // this is a reflection bounce, so simply reflect the ray and continue ray tracing
398 | rayOrigin = intersectionPoint + (uEPS_intersect * shadingNormal); // nudge the reflection rayOrigin out from the surface to avoid self-intersection
399 | rayDirection = reflect(rayDirection, shadingNormal);
400 | continue;
401 | } // end if (intersectionMaterial.type == METAL)
402 |
403 | } // end for (int bounces = 0; bounces < 20; bounces++)
404 |
405 |
406 | if (willNeedShadowRays == FALSE)
407 | return max(vec3(0), accumulatedColor);
408 |
409 |
410 | // now handle shadow rays from diffuse surfaces. We must send out 1 shadow ray for every light source,
411 | // so in this scene lit by 6 spotlights, we technically need 6 shadow rays for every diffuse surface
412 |
413 | // first we must nudge shadow ray origin out from the surface (along the surface normal) just a little bit, in order to avoid self-intersection
414 | rayOrigin = intersectionPoint + (uEPS_intersect * shadingNormal);
415 |
416 | for (int nLight = 0; nLight < N_SPOTLIGHT_SPHERES; nLight++)
417 | {
418 | directionToLight = normalize(spotlightSpheres[nLight].position - intersectionPoint);
419 | float LdotS = dot(directionToLight, -spotlightSpheres[nLight].direction);
420 | if (LdotS < 0.55)
421 | continue;
422 | lightColor = spotlightSpheres[nLight].color;
423 | halfwayVector = normalize(-rayDirection + directionToLight);
424 | diffuseIntensity = max(0.0, dot(shadingNormal, directionToLight));
425 |
426 | diffuseContribution = doDiffuseDirectLighting(rayColorMask, diffuseIntersectionColor, lightColor, diffuseIntensity);
427 | //diffuseContribution *= lightDistanceAttenuation;
428 | diffuseContribution *= pow(max(0.0, LdotS), 15.0);
429 |
430 | specularContribution = doBlinnPhongSpecularLighting(rayColorMask, shadingNormal, halfwayVector, lightColor, diffuseIntersectionRoughness, diffuseIntensity);
431 |
432 | isShadowRay = TRUE;
433 | rayDirection = directionToLight;
434 |
435 | t = SceneIntersect( isShadowRay );
436 |
437 | if (intersectionMaterial.type == SPOT_LIGHT)
438 | {
439 | accumulatedColor += diffuseContribution;
440 | accumulatedColor += specularContribution;
441 | }
442 | } // end for (int nLight = 0; nLight < 6; nLight++)
443 |
444 |
445 | return max(vec3(0), accumulatedColor);
446 |
447 | } // end vec3 RayTrace()
448 |
449 |
450 | //-----------------------------------------------------------------------
451 | void SetupScene(void)
452 | //-----------------------------------------------------------------------
453 | {
454 | // rgb values for common metals
455 | // Gold: (1.000, 0.766, 0.336) / Aluminum: (0.913, 0.921, 0.925) / Copper: (0.955, 0.637, 0.538) / Silver: (0.972, 0.960, 0.915)
456 |
457 | //struct Material { int type; int isCheckered; vec3 color; vec3 color2; float metalness; float roughness; float IoR; int textureID; };
458 | Material wallMaterial = Material(DIFFUSE, FALSE, vec3(0.3, 0.6, 1.0), vec3(0), 0.0, 1.0, 1.4, -1);
459 | Material darkGoldMetalMaterial = Material(METAL, FALSE, vec3(1.0, 0.5, 0.3) * 0.1, vec3(0), 1.0, 0.5, 1.4, -1);
460 | Material medGoldMetalMaterial = Material(METAL, FALSE, vec3(1.0, 0.4, 0.2) * 0.6, vec3(0), 1.0, 0.5, 1.4, -1);
461 | Material lightMetalMaterial = Material(METAL, FALSE, vec3(1.0, 0.7, 0.5), vec3(0), 1.0, 0.5, 1.4, -1);
462 |
463 | Material redMaterial = Material(DIFFUSE, FALSE, vec3(0.2, 0.01, 0.01), vec3(0), 0.0, 1.0, 1.4, -1);
464 | Material greenMaterial = Material(DIFFUSE, FALSE, vec3(0.01, 0.1, 0.01), vec3(0), 0.0, 1.0, 1.4, -1);
465 | Material blueMaterial = Material(DIFFUSE, FALSE, vec3(0.01, 0.01, 0.2), vec3(0), 0.0, 1.0, 1.4, -1);
466 | Material lightCasingMaterial = Material(DIFFUSE, FALSE, vec3(1.0, 1.6, 2.0), vec3(0), 0.0, 1.0, 1.4, -1);
467 |
468 | boxInteriors[0] = BoxInterior(vec3(-25,0,-52), vec3(20,24,40),vec2(1, 1), wallMaterial);
469 |
470 | float spotlightPower = 10.0;
471 | spotlightSpheres[0] = SpotLightSphere(0.6, vec3(-4, 23, -38), normalize(vec3(-1,-0.7,0.3)), vec3(1.0, 1.0, 1.0) * spotlightPower);
472 | spheres[0] = Sphere(0.8, spotlightSpheres[0].position + (0.3 * -spotlightSpheres[0].direction), vec2(1, 1), lightCasingMaterial);
473 | spotlightSpheres[1] = SpotLightSphere(0.6, vec3(-6, 23, -25), normalize(vec3(-1,-0.7,-0.2)), vec3(1.0, 1.0, 1.0) * spotlightPower);
474 | spheres[1] = Sphere(0.8, spotlightSpheres[1].position + (0.3 * -spotlightSpheres[1].direction), vec2(1, 1), lightCasingMaterial);
475 | spotlightSpheres[2] = SpotLightSphere(0.6, vec3(2, 23, -45), normalize(vec3(1,-1.4,0)), vec3(1.0, 1.0, 1.0) * spotlightPower);
476 | spheres[2] = Sphere(0.8, spotlightSpheres[2].position + (0.3 * -spotlightSpheres[2].direction), vec2(1, 1), lightCasingMaterial);
477 | spotlightSpheres[3] = SpotLightSphere(0.6, vec3(7, 23, -40), normalize(vec3(0.5,-1,-1)), vec3(1.0, 1.0, 1.0) * spotlightPower);
478 | spheres[3] = Sphere(0.8, spotlightSpheres[3].position + (0.3 * -spotlightSpheres[3].direction), vec2(1, 1), lightCasingMaterial);
479 | spotlightSpheres[4] = SpotLightSphere(0.6, vec3(-14, 23, -8), normalize(vec3(0.8,-1,0)), vec3(1.0, 1.0, 1.0) * spotlightPower);
480 | spheres[4] = Sphere(0.8, spotlightSpheres[4].position + (0.3 * -spotlightSpheres[4].direction), vec2(1, 1), lightCasingMaterial);
481 | spotlightSpheres[5] = SpotLightSphere(0.6, vec3(8, 22, -9), normalize(vec3(-0.8,-1,0)), vec3(1.0, 1.0, 1.0) * spotlightPower);
482 | spheres[5] = Sphere(0.8, spotlightSpheres[5].position + (0.3 * -spotlightSpheres[5].direction), vec2(1, 1), lightCasingMaterial);
483 |
484 | spheres[6] = Sphere(4.0, vec3(15, 4, -45), vec2(1, 1), medGoldMetalMaterial);
485 | spheres[7] = Sphere(4.0, vec3(-25.5, 12, -34.5),vec2(1, 1), darkGoldMetalMaterial);
486 | spheres[8] = Sphere(3.3, vec3(-25, 15, -28), vec2(1, 1), lightMetalMaterial);
487 | spheres[9] = Sphere(1.7, vec3(-22.5, 18, -24), vec2(1, 1), redMaterial);
488 | spheres[10] = Sphere(1.4, vec3(-22, 11, -25.5), vec2(1, 1), greenMaterial);
489 | spheres[11] = Sphere(1.7, vec3(-25.5, 6.5, -22.5), vec2(1, 1), blueMaterial);
490 | }
491 |
492 |
493 | #include
--------------------------------------------------------------------------------
/js/The_6_Platonic_Solids.js:
--------------------------------------------------------------------------------
1 | // scene/demo-specific variables go here
2 | let marbleTexture;
3 | let imageTexturesTotalCount = 1;
4 | let numOfImageTexturesLoaded = 0;
5 |
6 | let material_TypeObject;
7 | let material_TypeController;
8 | let changeMaterialType = false;
9 | let matType = 0;
10 |
11 | let tetrahedron, cube, octahedron, dodecahedron, icosahedron;
12 |
13 | // triangular model
14 | let vp0 = new THREE.Vector3(); // vertex positions data
15 | let vp1 = new THREE.Vector3();
16 | let vp2 = new THREE.Vector3();
17 | let vn0 = new THREE.Vector3(); // vertex normals data
18 | let vn1 = new THREE.Vector3();
19 | let vn2 = new THREE.Vector3();
20 | let vt0 = new THREE.Vector2(); // vertex texture-coordinates(UV) data
21 | let vt1 = new THREE.Vector2();
22 | let vt2 = new THREE.Vector2();
23 | let modelMesh;
24 | let modelPositionOffset = new THREE.Vector3();
25 | let modelScale = 1.0;
26 | let meshList = [];
27 | let geoList = [];
28 | let triangleDataTexture;
29 | let aabbDataTexture;
30 | let totalGeometryCount = 0;
31 | let total_number_of_triangles = 0;
32 | let totalWork;
33 | let triangle_array = new Float32Array(2048 * 2048 * 4);
34 | // 2048 = width of texture, 2048 = height of texture, 4 = r,g,b, and a components
35 | let aabb_array = new Float32Array(2048 * 2048 * 4);
36 | // 2048 = width of texture, 2048 = height of texture, 4 = r,g,b, and a components
37 | let triangleMaterialMarkers = [];
38 | let pathTracingMaterialList = [];
39 |
40 |
41 |
42 | function load_GLTF_Model()
43 | {
44 |
45 | let gltfLoader = new GLTFLoader();
46 |
47 | gltfLoader.load("models/UtahTeapot.glb", function (meshGroup)
48 | { // Triangles: 992
49 |
50 | if (meshGroup.scene)
51 | meshGroup = meshGroup.scene;
52 |
53 | meshGroup.traverse(function (child)
54 | {
55 | if (child.isMesh)
56 | {
57 | triangleMaterialMarkers.push(child.geometry.attributes.position.array.length / 9);
58 | meshList.push(child);
59 | }
60 | });
61 |
62 | modelMesh = meshList[0].clone();
63 |
64 | for (let i = 0; i < meshList.length; i++)
65 | {
66 | geoList.push(meshList[i].geometry);
67 | }
68 |
69 | modelMesh.geometry = mergeGeometries(geoList);
70 |
71 | if (modelMesh.geometry.index)
72 | modelMesh.geometry = modelMesh.geometry.toNonIndexed();
73 |
74 | modelMesh.geometry.center();
75 |
76 | // now that the model has loaded, we can init
77 | init();
78 |
79 | }); // end gltfLoader.load()
80 |
81 | } // end function load_GLTF_Model()
82 |
83 |
84 | // called automatically from within initTHREEjs() function (located in InitCommon.js file)
85 | function initSceneData()
86 | {
87 | demoFragmentShaderFileName = 'The6PlatonicSolids_Fragment.glsl';
88 |
89 | // scene/demo-specific three.js objects setup goes here
90 | sceneIsDynamic = false;
91 |
92 | // pixelRatio is resolution - range: 0.5(half resolution) to 1.0(full resolution)
93 | pixelRatio = mouseControl ? 1.0 : 0.75;
94 |
95 | EPS_intersect = 0.01;
96 |
97 | // in order to match the color palette of this classic scene, the tone mapper is not needed
98 | useToneMapping = false;
99 |
100 | // set camera's field of view and other options
101 | worldCamera.fov = 30;
102 | focusDistance = 34.0;
103 | //apertureChangeSpeed = 1; // 1 is default
104 | //focusChangeSpeed = 1; // 1 is default
105 |
106 | // position and orient camera
107 | cameraControlsObject.position.set(10, 18.6, 30.5);
108 | // look left or right
109 | cameraControlsYawObject.rotation.y = 0.2756;
110 | // look up or down
111 | cameraControlsPitchObject.rotation.x = -0.375;
112 |
113 | cameraFlightSpeed = 10;
114 |
115 |
116 | tetrahedron = new RayTracingShape("convex polyhedron");
117 |
118 | tetrahedron.material.color.set(1.0, 1.0, 1.0); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
119 | tetrahedron.material.opacity = 1.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
120 | tetrahedron.material.ior = 1.5; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
121 | tetrahedron.material.clearcoat = 1.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
122 | tetrahedron.material.metalness = 1.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
123 | tetrahedron.material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
124 |
125 | tetrahedron.uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
126 |
127 | tetrahedron.transform.scale.set(1.31, 1.31, 1.31);
128 | tetrahedron.transform.position.set(0.2, 7.9, 0.33);
129 | tetrahedron.transform.rotation.set(-0.65, 0.3, 1.0); // this rotates the rectangle back from upright (default) to flat along the ground
130 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
131 | tetrahedron.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
132 |
133 |
134 | cube = new RayTracingShape("box");
135 |
136 | cube.material.color.set(1.0, 1.0, 1.0); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
137 | cube.material.opacity = 1.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
138 | cube.material.ior = 1.5; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
139 | cube.material.clearcoat = 1.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
140 | cube.material.metalness = 1.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
141 | cube.material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
142 |
143 | cube.uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
144 |
145 | cube.transform.scale.set(1, 1, 1);
146 | cube.transform.position.set(-5.5, 8.75, 0);
147 | cube.transform.rotation.set(Math.PI * 0.5, Math.PI * -0.3, Math.PI * 0.75);
148 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
149 | cube.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
150 |
151 |
152 | octahedron = new RayTracingShape("convex polyhedron");
153 |
154 | octahedron.material.color.set(1.0, 1.0, 1.0); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
155 | octahedron.material.opacity = 1.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
156 | octahedron.material.ior = 1.5; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
157 | octahedron.material.clearcoat = 1.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
158 | octahedron.material.metalness = 1.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
159 | octahedron.material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
160 |
161 | octahedron.uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
162 |
163 | octahedron.transform.scale.set(1.8, 1.8, 1.8);
164 | octahedron.transform.position.set(5.5, 8.87, 0);
165 | octahedron.transform.rotation.set(0, Math.PI * 0.25, 0);
166 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
167 | octahedron.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
168 |
169 |
170 | dodecahedron = new RayTracingShape("convex polyhedron");
171 |
172 | dodecahedron.material.color.set(1.0, 1.0, 1.0); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
173 | dodecahedron.material.opacity = 1.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
174 | dodecahedron.material.ior = 1.5; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
175 | dodecahedron.material.clearcoat = 1.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
176 | dodecahedron.material.metalness = 1.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
177 | dodecahedron.material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
178 |
179 | dodecahedron.uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
180 |
181 | dodecahedron.transform.scale.set(1.7, 1.7, 1.7);
182 | dodecahedron.transform.position.set(-5.5, 11.3, -6);
183 | dodecahedron.transform.rotation.set(Math.PI * 0.5, 0, 0);
184 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
185 | dodecahedron.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
186 |
187 |
188 | icosahedron = new RayTracingShape("convex polyhedron");
189 |
190 | icosahedron.material.color.set(1.0, 1.0, 1.0); // (r,g,b) range: 0.0 to 1.0 / default is rgb(1,1,1) white
191 | icosahedron.material.opacity = 1.0; // range: 0.0 to 1.0 / default is 1.0 (fully opaque)
192 | icosahedron.material.ior = 1.5; // range: 1.0(air) to 2.33(diamond) / default is 1.5(glass) / other useful ior is 1.33(water)
193 | icosahedron.material.clearcoat = 1.0; // range: 0.0 to 1.0 / default is 0.0 (no clearcoat)
194 | icosahedron.material.metalness = 1.0; // range: either 0.0 or 1.0 / default is 0.0 (not metal)
195 | icosahedron.material.roughness = 0.0; // range: 0.0 to 1.0 / default is 0.0 (no roughness, perfectly smooth)
196 |
197 | icosahedron.uvScale.set(1, 1); // if checkered or using a texture, how many times should the uv's repeat in the X axis / Y axis?
198 |
199 | icosahedron.transform.scale.set(1.8, 1.8, 1.8);
200 | icosahedron.transform.position.set(5.5, 11.25, -6);
201 | icosahedron.transform.rotation.set(Math.PI * 0.5, 0, 0);
202 | // after specifying any desired transforms (scale, position, rotation), we must call updateMatrixWorld() to actually fill in the shape's matrix with these new values
203 | icosahedron.transform.updateMatrixWorld(true); // 'true' forces a matrix update now, rather than waiting for Three.js' 'renderer.render()' call which happens last
204 |
205 |
206 | // scale and place Utah teapot glTF model
207 | modelMesh.scale.set(0.18, 0.18, 0.18);
208 | modelMesh.rotation.set(0, -Math.PI * 0.7, 0);
209 | modelMesh.position.set(0, 10.95, -6);
210 | modelMesh.updateMatrixWorld(true); // 'true' forces immediate matrix update
211 |
212 |
213 |
214 | // In addition to the default GUI on all demos, add any special GUI elements that this particular demo requires
215 |
216 | material_TypeObject = {
217 | Solids_Metal_Type: 'Aluminum'
218 | };
219 |
220 | function handleMaterialTypeChange()
221 | {
222 | changeMaterialType = true;
223 | }
224 |
225 | material_TypeController = gui.add(material_TypeObject, 'Solids_Metal_Type', ['Aluminum',
226 | 'Gold', 'Copper', 'Silver']).onChange(handleMaterialTypeChange);
227 |
228 | handleMaterialTypeChange();
229 |
230 |
231 | // triangle mesh
232 | total_number_of_triangles = modelMesh.geometry.attributes.position.array.length / 9;
233 | console.log("Triangle count:" + (total_number_of_triangles));
234 |
235 | totalWork = new Uint32Array(total_number_of_triangles);
236 |
237 | let triangle_b_box_min = new THREE.Vector3();
238 | let triangle_b_box_max = new THREE.Vector3();
239 | let triangle_b_box_centroid = new THREE.Vector3();
240 |
241 |
242 | let vpa = new Float32Array(modelMesh.geometry.attributes.position.array);
243 | let vna = new Float32Array(modelMesh.geometry.attributes.normal.array);
244 | let vta = null;
245 | let modelHasUVs = false;
246 | if (modelMesh.geometry.attributes.uv !== undefined)
247 | {
248 | vta = new Float32Array(modelMesh.geometry.attributes.uv.array);
249 | modelHasUVs = true;
250 | }
251 |
252 | let materialNumber = 0;
253 | let ix32, ix9;
254 |
255 | for (let i = 0; i < total_number_of_triangles; i++)
256 | {
257 | ix32 = i * 32;
258 | ix9 = i * 9;
259 |
260 | triangle_b_box_min.set(Infinity, Infinity, Infinity);
261 | triangle_b_box_max.set(-Infinity, -Infinity, -Infinity);
262 |
263 | for (let j = 0; j < pathTracingMaterialList.length; j++)
264 | {
265 | if (i < triangleMaterialMarkers[j])
266 | {
267 | materialNumber = j;
268 | break;
269 | }
270 | }
271 |
272 | // record vertex texture coordinates (UVs)
273 | if (modelHasUVs)
274 | {
275 | vt0.set(vta[6 * i + 0], vta[6 * i + 1]);
276 | vt1.set(vta[6 * i + 2], vta[6 * i + 3]);
277 | vt2.set(vta[6 * i + 4], vta[6 * i + 5]);
278 | }
279 | else
280 | {
281 | vt0.set(-1, -1);
282 | vt1.set(-1, -1);
283 | vt2.set(-1, -1);
284 | }
285 |
286 | // record vertex normals
287 | vn0.set(vna[ix9 + 0], vna[ix9 + 1], vna[ix9 + 2]).normalize();
288 | vn1.set(vna[ix9 + 3], vna[ix9 + 4], vna[ix9 + 5]).normalize();
289 | vn2.set(vna[ix9 + 6], vna[ix9 + 7], vna[ix9 + 8]).normalize();
290 |
291 | // record vertex positions
292 | vp0.set(vpa[ix9 + 0], vpa[ix9 + 1], vpa[ix9 + 2]);
293 | vp1.set(vpa[ix9 + 3], vpa[ix9 + 4], vpa[ix9 + 5]);
294 | vp2.set(vpa[ix9 + 6], vpa[ix9 + 7], vpa[ix9 + 8]);
295 |
296 | vp0.multiplyScalar(modelScale);
297 | vp1.multiplyScalar(modelScale);
298 | vp2.multiplyScalar(modelScale);
299 |
300 | vp0.add(modelPositionOffset);
301 | vp1.add(modelPositionOffset);
302 | vp2.add(modelPositionOffset);
303 |
304 | //slot 0
305 | triangle_array[ix32 + 0] = vp0.x; // r or x
306 | triangle_array[ix32 + 1] = vp0.y; // g or y
307 | triangle_array[ix32 + 2] = vp0.z; // b or z
308 | triangle_array[ix32 + 3] = vp1.x; // a or w
309 |
310 | //slot 1
311 | triangle_array[ix32 + 4] = vp1.y; // r or x
312 | triangle_array[ix32 + 5] = vp1.z; // g or y
313 | triangle_array[ix32 + 6] = vp2.x; // b or z
314 | triangle_array[ix32 + 7] = vp2.y; // a or w
315 |
316 | //slot 2
317 | triangle_array[ix32 + 8] = vp2.z; // r or x
318 | triangle_array[ix32 + 9] = vn0.x; // g or y
319 | triangle_array[ix32 + 10] = vn0.y; // b or z
320 | triangle_array[ix32 + 11] = vn0.z; // a or w
321 |
322 | //slot 3
323 | triangle_array[ix32 + 12] = vn1.x; // r or x
324 | triangle_array[ix32 + 13] = vn1.y; // g or y
325 | triangle_array[ix32 + 14] = vn1.z; // b or z
326 | triangle_array[ix32 + 15] = vn2.x; // a or w
327 |
328 | //slot 4
329 | triangle_array[ix32 + 16] = vn2.y; // r or x
330 | triangle_array[ix32 + 17] = vn2.z; // g or y
331 | triangle_array[ix32 + 18] = vt0.x; // b or z
332 | triangle_array[ix32 + 19] = vt0.y; // a or w
333 |
334 | //slot 5
335 | triangle_array[ix32 + 20] = vt1.x; // r or x
336 | triangle_array[ix32 + 21] = vt1.y; // g or y
337 | triangle_array[ix32 + 22] = vt2.x; // b or z
338 | triangle_array[ix32 + 23] = vt2.y; // a or w
339 |
340 | /*
341 | // the remaining slots are used for PBR material properties
342 |
343 | //slot 6
344 | triangle_array[ix32 + 24] = pathTracingMaterialList[materialNumber].type; // r or x
345 | triangle_array[ix32 + 25] = pathTracingMaterialList[materialNumber].color.r; // g or y
346 | triangle_array[ix32 + 26] = pathTracingMaterialList[materialNumber].color.g; // b or z
347 | triangle_array[ix32 + 27] = pathTracingMaterialList[materialNumber].color.b; // a or w
348 |
349 | //slot 7
350 | triangle_array[ix32 + 28] = pathTracingMaterialList[materialNumber].albedoTextureID; // r or x
351 | triangle_array[ix32 + 29] = 0.0; // g or y
352 | triangle_array[ix32 + 30] = 0; // b or z
353 | triangle_array[ix32 + 31] = 0; // a or w
354 | */
355 |
356 | triangle_b_box_min.copy(triangle_b_box_min.min(vp0));
357 | triangle_b_box_max.copy(triangle_b_box_max.max(vp0));
358 | triangle_b_box_min.copy(triangle_b_box_min.min(vp1));
359 | triangle_b_box_max.copy(triangle_b_box_max.max(vp1));
360 | triangle_b_box_min.copy(triangle_b_box_min.min(vp2));
361 | triangle_b_box_max.copy(triangle_b_box_max.max(vp2));
362 |
363 | triangle_b_box_centroid.copy(triangle_b_box_min).add(triangle_b_box_max).multiplyScalar(0.5);
364 |
365 | aabb_array[ix9 + 0] = triangle_b_box_min.x;
366 | aabb_array[ix9 + 1] = triangle_b_box_min.y;
367 | aabb_array[ix9 + 2] = triangle_b_box_min.z;
368 | aabb_array[ix9 + 3] = triangle_b_box_max.x;
369 | aabb_array[ix9 + 4] = triangle_b_box_max.y;
370 | aabb_array[ix9 + 5] = triangle_b_box_max.z;
371 | aabb_array[ix9 + 6] = triangle_b_box_centroid.x;
372 | aabb_array[ix9 + 7] = triangle_b_box_centroid.y;
373 | aabb_array[ix9 + 8] = triangle_b_box_centroid.z;
374 |
375 | totalWork[i] = i;
376 | }
377 |
378 |
379 |
380 | console.time("BvhGeneration");
381 | console.log("BvhGeneration...");
382 |
383 | // Build the BVH acceleration structure, which places a bounding box ('root' of the tree) around all of the
384 | // triangles of the entire mesh, then subdivides each box into 2 smaller boxes. It continues until it reaches 1 triangle,
385 | // which it then designates as a 'leaf'
386 | BVH_Build_Iterative(totalWork, aabb_array);
387 | //console.log(buildnodes);
388 |
389 | console.timeEnd("BvhGeneration");
390 |
391 |
392 | triangleDataTexture = new THREE.DataTexture(
393 | triangle_array,
394 | 2048,
395 | 2048,
396 | THREE.RGBAFormat,
397 | THREE.FloatType,
398 | THREE.Texture.DEFAULT_MAPPING,
399 | THREE.ClampToEdgeWrapping,
400 | THREE.ClampToEdgeWrapping,
401 | THREE.NearestFilter,
402 | THREE.NearestFilter,
403 | 1,
404 | THREE.NoColorSpace
405 | );
406 |
407 | triangleDataTexture.flipY = false;
408 | triangleDataTexture.generateMipmaps = false;
409 | triangleDataTexture.needsUpdate = true;
410 |
411 |
412 | aabbDataTexture = new THREE.DataTexture(
413 | aabb_array,
414 | 2048,
415 | 2048,
416 | THREE.RGBAFormat,
417 | THREE.FloatType,
418 | THREE.Texture.DEFAULT_MAPPING,
419 | THREE.ClampToEdgeWrapping,
420 | THREE.ClampToEdgeWrapping,
421 | THREE.NearestFilter,
422 | THREE.NearestFilter,
423 | 1,
424 | THREE.NoColorSpace
425 | );
426 |
427 | aabbDataTexture.flipY = false;
428 | aabbDataTexture.generateMipmaps = false;
429 | aabbDataTexture.needsUpdate = true;
430 |
431 |
432 | // scene/demo-specific uniforms go here
433 | rayTracingUniforms.uTriangleTexture = { value: triangleDataTexture };
434 | rayTracingUniforms.uAABBTexture = { value: aabbDataTexture };
435 | rayTracingUniforms.uMarbleTexture = { value: marbleTexture };
436 | rayTracingUniforms.uMaterialColor = { value: new THREE.Color() };
437 | rayTracingUniforms.uTetrahedronInvMatrix = { value: new THREE.Matrix4() };
438 | rayTracingUniforms.uCubeInvMatrix = { value: new THREE.Matrix4() };
439 | rayTracingUniforms.uOctahedronInvMatrix = { value: new THREE.Matrix4() };
440 | rayTracingUniforms.uDodecahedronInvMatrix = { value: new THREE.Matrix4() };
441 | rayTracingUniforms.uIcosahedronInvMatrix = { value: new THREE.Matrix4() };
442 | rayTracingUniforms.uTeapotInvMatrix = { value: new THREE.Matrix4() };
443 |
444 | rayTracingUniforms.uTetrahedronInvMatrix.value.copy(tetrahedron.transform.matrixWorld).invert();
445 | rayTracingUniforms.uCubeInvMatrix.value.copy(cube.transform.matrixWorld).invert();
446 | rayTracingUniforms.uOctahedronInvMatrix.value.copy(octahedron.transform.matrixWorld).invert();
447 | rayTracingUniforms.uDodecahedronInvMatrix.value.copy(dodecahedron.transform.matrixWorld).invert();
448 | rayTracingUniforms.uIcosahedronInvMatrix.value.copy(icosahedron.transform.matrixWorld).invert();
449 | rayTracingUniforms.uTeapotInvMatrix.value.copy(modelMesh.matrixWorld).invert();
450 |
451 | } // end function initSceneData()
452 |
453 |
454 |
455 | // called automatically from within the animate() function (located in InitCommon.js file)
456 | function updateVariablesAndUniforms()
457 | {
458 | if (changeMaterialType)
459 | {
460 | matType = material_TypeController.getValue();
461 |
462 | // rgb values for common metals
463 | // Gold: (1.000, 0.766, 0.336) / Aluminum: (0.913, 0.921, 0.925) / Copper: (0.955, 0.637, 0.538) / Silver: (0.972, 0.960, 0.915)
464 | if (matType == 'Aluminum')
465 | { // more of a 'blue-steel' in an attempt to match the original raytraced source image of this demo
466 | rayTracingUniforms.uMaterialColor.value.setRGB(0.9, 0.95, 1.0);
467 | }
468 | else if (matType == 'Gold')
469 | {
470 | rayTracingUniforms.uMaterialColor.value.setRGB(1.000, 0.766, 0.336);
471 | }
472 | else if (matType == 'Copper')
473 | {
474 | rayTracingUniforms.uMaterialColor.value.setRGB(0.955, 0.637, 0.538);
475 | }
476 | else if (matType == 'Silver')
477 | {
478 | rayTracingUniforms.uMaterialColor.value.setRGB(0.972, 0.960, 0.915);
479 | }
480 |
481 | cameraIsMoving = true;
482 | changeMaterialType = false;
483 | }
484 |
485 |
486 | // INFO
487 | cameraInfoElement.innerHTML = "FOV: " + worldCamera.fov + " / Aperture: " + apertureSize.toFixed(2) +
488 | " / FocusDistance: " + focusDistance.toFixed(2) + " " + "Samples: " + sampleCounter;
489 |
490 | } // end function updateVariablesAndUniforms()
491 |
492 |
493 | marbleTexture = textureLoader.load(
494 | // resource URL
495 | 'textures/marble0.png',
496 |
497 | // onLoad callback
498 | function (texture)
499 | {
500 | texture.wrapS = THREE.RepeatWrapping;
501 | texture.wrapT = THREE.RepeatWrapping;
502 | texture.flipY = false;
503 | texture.minFilter = THREE.NearestFilter;
504 | texture.magFilter = THREE.NearestFilter;
505 | texture.generateMipmaps = false;
506 |
507 | numOfImageTexturesLoaded++;
508 | // if all textures have been loaded, we can load the model
509 | if (numOfImageTexturesLoaded == imageTexturesTotalCount)
510 | load_GLTF_Model(); // load model, init app, and start animating
511 | }
512 | );
513 |
--------------------------------------------------------------------------------
/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)
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)
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 |
--------------------------------------------------------------------------------