├── .gitignore
├── LICENSE.md
├── README.md
├── colormaps
├── cool-warm-paraview.png
├── matplotlib-plasma.png
├── matplotlib-virdis.png
├── rainbow.png
├── samsel-linear-green.png
└── samsel-linear-ygb-1211g.png
├── index.html
└── js
├── FileSaver.js
├── gl-matrix-min.js
├── shader-srcs.js
├── volume-raycaster.js
└── webgl-util.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.raw
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Will Usher
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebGL Volume Raycaster
2 |
3 | A scientific visualization style volume raycaster written
4 | using WebGL2 and Javascript. The renderer uses an arcball camera which supports
5 | mouse or touch input, and dynamically adjusts the sampling rate
6 | to maintain a smooth framerate, even on mobile devices. The volumes
7 | are downloaded via XMLHttpRequest from Dropbox when selected.
8 | [Try it out online!](https://www.willusher.io/webgl-volume-raycaster/)
9 | I've also written a [blog post](https://www.willusher.io/webgl/2019/01/13/volume-rendering-with-webgl)
10 | about how this renderer is implemented.
11 |
12 | Uses [webgl-util](https://github.com/Twinklebear/webgl-util) for some WebGL utilities and [glMatrix](http://glmatrix.net/) for matrix/vector operations.
13 |
14 | Check out my [WebGPU Volume Raycaster](https://github.com/Twinklebear/webgpu-volume-raycaster/) and [Path Tracer](https://github.com/Twinklebear/webgpu-volume-pathtracer) for WebGPU-based examples.
15 |
16 | ## Images
17 |
18 | 
19 |
20 |
--------------------------------------------------------------------------------
/colormaps/cool-warm-paraview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Twinklebear/webgl-volume-raycaster/c1859bef4e186c92c8b38c9826c5415dccda4d6f/colormaps/cool-warm-paraview.png
--------------------------------------------------------------------------------
/colormaps/matplotlib-plasma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Twinklebear/webgl-volume-raycaster/c1859bef4e186c92c8b38c9826c5415dccda4d6f/colormaps/matplotlib-plasma.png
--------------------------------------------------------------------------------
/colormaps/matplotlib-virdis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Twinklebear/webgl-volume-raycaster/c1859bef4e186c92c8b38c9826c5415dccda4d6f/colormaps/matplotlib-virdis.png
--------------------------------------------------------------------------------
/colormaps/rainbow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Twinklebear/webgl-volume-raycaster/c1859bef4e186c92c8b38c9826c5415dccda4d6f/colormaps/rainbow.png
--------------------------------------------------------------------------------
/colormaps/samsel-linear-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Twinklebear/webgl-volume-raycaster/c1859bef4e186c92c8b38c9826c5415dccda4d6f/colormaps/samsel-linear-green.png
--------------------------------------------------------------------------------
/colormaps/samsel-linear-ygb-1211g.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Twinklebear/webgl-volume-raycaster/c1859bef4e186c92c8b38c9826c5415dccda4d6f/colormaps/samsel-linear-ygb-1211g.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Desktop: Left-click + drag to rotate, scroll to zoom,
30 | right-click + drag to pan.
31 |
32 | Touch: One finger drag to rotate, pinch to zoom, two finger drag to pan.
33 |
34 |
35 |
36 |
38 |
39 |
Description
40 | This is a WebGL implementation of a standard volume raycaster
41 | which uses a single render-pass to raycast the volume. The sampling rate is dynamically
42 | adjusted to maintain an interactive framerate across different devices (desktops, laptops, phones).
43 |
44 | Get the code on GitHub!
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/js/FileSaver.js:
--------------------------------------------------------------------------------
1 | /*
2 | * FileSaver.js
3 | * A saveAs() FileSaver implementation.
4 | *
5 | * By Eli Grey, http://eligrey.com
6 | *
7 | * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
8 | * source : http://purl.eligrey.com/github/FileSaver.js
9 | */
10 |
11 |
12 | // The one and only way of getting global scope in all environments
13 | // https://stackoverflow.com/q/3277182/1008999
14 | var _global = typeof window === 'object' && window.window === window
15 | ? window : typeof self === 'object' && self.self === self
16 | ? self : typeof global === 'object' && global.global === global
17 | ? global
18 | : this
19 |
20 | function bom (blob, opts) {
21 | if (typeof opts === 'undefined') opts = { autoBom: false }
22 | else if (typeof opts !== 'object') {
23 | console.warn('Depricated: Expected third argument to be a object')
24 | opts = { autoBom: !opts }
25 | }
26 |
27 | // prepend BOM for UTF-8 XML and text/* types (including HTML)
28 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
29 | if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
30 | return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
31 | }
32 | return blob
33 | }
34 |
35 | function download (url, name, opts) {
36 | var xhr = new XMLHttpRequest()
37 | xhr.open('GET', url)
38 | xhr.responseType = 'blob'
39 | xhr.onload = function () {
40 | saveAs(xhr.response, name, opts)
41 | }
42 | xhr.onerror = function () {
43 | console.error('could not download file')
44 | }
45 | xhr.send()
46 | }
47 |
48 | function corsEnabled (url) {
49 | var xhr = new XMLHttpRequest()
50 | // use sync to avoid popup blocker
51 | xhr.open('HEAD', url, false)
52 | xhr.send()
53 | return xhr.status >= 200 && xhr.status <= 299
54 | }
55 |
56 | // `a.click()` doesn't work for all browsers (#465)
57 | function click(node) {
58 | try {
59 | node.dispatchEvent(new MouseEvent('click'))
60 | } catch (e) {
61 | var evt = document.createEvent('MouseEvents')
62 | evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
63 | 20, false, false, false, false, 0, null)
64 | node.dispatchEvent(evt)
65 | }
66 | }
67 |
68 | var saveAs = _global.saveAs ||
69 | // probably in some web worker
70 | (typeof window !== 'object' || window !== _global)
71 | ? function saveAs () { /* noop */ }
72 |
73 | // Use download attribute first if possible (#193 Lumia mobile)
74 | : 'download' in HTMLAnchorElement.prototype
75 | ? function saveAs (blob, name, opts) {
76 | var URL = _global.URL || _global.webkitURL
77 | var a = document.createElement('a')
78 | name = name || blob.name || 'download'
79 |
80 | a.download = name
81 | a.rel = 'noopener' // tabnabbing
82 |
83 | // TODO: detect chrome extensions & packaged apps
84 | // a.target = '_blank'
85 |
86 | if (typeof blob === 'string') {
87 | // Support regular links
88 | a.href = blob
89 | if (a.origin !== location.origin) {
90 | corsEnabled(a.href)
91 | ? download(blob, name, opts)
92 | : click(a, a.target = '_blank')
93 | } else {
94 | click(a)
95 | }
96 | } else {
97 | // Support blobs
98 | a.href = URL.createObjectURL(blob)
99 | setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
100 | setTimeout(function () { click(a) }, 0)
101 | }
102 | }
103 |
104 | // Use msSaveOrOpenBlob as a second approach
105 | : 'msSaveOrOpenBlob' in navigator
106 | ? function saveAs (blob, name, opts) {
107 | name = name || blob.name || 'download'
108 |
109 | if (typeof blob === 'string') {
110 | if (corsEnabled(blob)) {
111 | download(blob, name, opts)
112 | } else {
113 | var a = document.createElement('a')
114 | a.href = blob
115 | a.target = '_blank'
116 | setTimeout(function () { click(a) })
117 | }
118 | } else {
119 | navigator.msSaveOrOpenBlob(bom(blob, opts), name)
120 | }
121 | }
122 |
123 | // Fallback to using FileReader and a popup
124 | : function saveAs (blob, name, opts, popup) {
125 | // Open a popup immediately do go around popup blocker
126 | // Mostly only avalible on user interaction and the fileReader is async so...
127 | popup = popup || open('', '_blank')
128 | if (popup) {
129 | popup.document.title =
130 | popup.document.body.innerText = 'downloading...'
131 | }
132 |
133 | if (typeof blob === 'string') return download(blob, name, opts)
134 |
135 | var force = blob.type === 'application/octet-stream'
136 | var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
137 | var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
138 |
139 | if ((isChromeIOS || (force && isSafari)) && typeof FileReader === 'object') {
140 | // Safari doesn't allow downloading of blob urls
141 | var reader = new FileReader()
142 | reader.onloadend = function () {
143 | var url = reader.result
144 | url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
145 | if (popup) popup.location.href = url
146 | else location = url
147 | popup = null // reverse-tabnabbing #460
148 | }
149 | reader.readAsDataURL(blob)
150 | } else {
151 | var URL = _global.URL || _global.webkitURL
152 | var url = URL.createObjectURL(blob)
153 | if (popup) popup.location = url
154 | else location.href = url
155 | popup = null // reverse-tabnabbing #460
156 | setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
157 | }
158 | }
159 |
160 | _global.saveAs = saveAs.saveAs = saveAs
161 |
162 | if (typeof module !== 'undefined') {
163 | module.exports = saveAs;
164 | }
165 |
166 |
--------------------------------------------------------------------------------
/js/gl-matrix-min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview gl-matrix - High performance matrix and vector operations
3 | * @author Brandon Jones
4 | * @author Colin MacKenzie IV
5 | * @version 2.4.0
6 | */
7 |
8 | /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of this software and associated documentation files (the "Software"), to deal
12 | in the Software without restriction, including without limitation the rights
13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | copies of the Software, and to permit persons to whom the Software is
15 | furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in
18 | all copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | THE SOFTWARE. */
27 |
28 | !function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var r=n();for(var a in r)("object"==typeof exports?exports:t)[a]=r[a]}}("undefined"!=typeof self?self:this,function(){return function(t){function n(a){if(r[a])return r[a].exports;var e=r[a]={i:a,l:!1,exports:{}};return t[a].call(e.exports,e,e.exports,n),e.l=!0,e.exports}var r={};return n.m=t,n.c=r,n.d=function(t,r,a){n.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:a})},n.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(r,"a",r),r},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=4)}([function(t,n,r){"use strict";function a(t){n.ARRAY_TYPE=i=t}function e(t){return t*s}function u(t,n){return Math.abs(t-n)<=o*Math.max(1,Math.abs(t),Math.abs(n))}Object.defineProperty(n,"__esModule",{value:!0}),n.setMatrixArrayType=a,n.toRadian=e,n.equals=u;var o=n.EPSILON=1e-6,i=n.ARRAY_TYPE="undefined"!=typeof Float32Array?Float32Array:Array,s=(n.RANDOM=Math.random,Math.PI/180)},function(t,n,r){"use strict";function a(){var t=new g.ARRAY_TYPE(9);return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function e(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[4],t[4]=n[5],t[5]=n[6],t[6]=n[8],t[7]=n[9],t[8]=n[10],t}function u(t){var n=new g.ARRAY_TYPE(9);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n[6]=t[6],n[7]=t[7],n[8]=t[8],n}function o(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t}function i(t,n,r,a,e,u,o,i,s){var c=new g.ARRAY_TYPE(9);return c[0]=t,c[1]=n,c[2]=r,c[3]=a,c[4]=e,c[5]=u,c[6]=o,c[7]=i,c[8]=s,c}function s(t,n,r,a,e,u,o,i,s,c){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t[4]=u,t[5]=o,t[6]=i,t[7]=s,t[8]=c,t}function c(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function f(t,n){if(t===n){var r=n[1],a=n[2],e=n[5];t[1]=n[3],t[2]=n[6],t[3]=r,t[5]=n[7],t[6]=a,t[7]=e}else t[0]=n[0],t[1]=n[3],t[2]=n[6],t[3]=n[1],t[4]=n[4],t[5]=n[7],t[6]=n[2],t[7]=n[5],t[8]=n[8];return t}function M(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=f*o-i*c,h=-f*u+i*s,l=c*u-o*s,v=r*M+a*h+e*l;return v?(v=1/v,t[0]=M*v,t[1]=(-f*a+e*c)*v,t[2]=(i*a-e*o)*v,t[3]=h*v,t[4]=(f*r-e*s)*v,t[5]=(-i*r+e*u)*v,t[6]=l*v,t[7]=(-c*r+a*s)*v,t[8]=(o*r-a*u)*v,t):null}function h(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8];return t[0]=o*f-i*c,t[1]=e*c-a*f,t[2]=a*i-e*o,t[3]=i*s-u*f,t[4]=r*f-e*s,t[5]=e*u-r*i,t[6]=u*c-o*s,t[7]=a*s-r*c,t[8]=r*o-a*u,t}function l(t){var n=t[0],r=t[1],a=t[2],e=t[3],u=t[4],o=t[5],i=t[6],s=t[7],c=t[8];return n*(c*u-o*s)+r*(-c*e+o*i)+a*(s*e-u*i)}function v(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=r[0],l=r[1],v=r[2],d=r[3],b=r[4],m=r[5],p=r[6],P=r[7],E=r[8];return t[0]=h*a+l*o+v*c,t[1]=h*e+l*i+v*f,t[2]=h*u+l*s+v*M,t[3]=d*a+b*o+m*c,t[4]=d*e+b*i+m*f,t[5]=d*u+b*s+m*M,t[6]=p*a+P*o+E*c,t[7]=p*e+P*i+E*f,t[8]=p*u+P*s+E*M,t}function d(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=r[0],l=r[1];return t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=i,t[5]=s,t[6]=h*a+l*o+c,t[7]=h*e+l*i+f,t[8]=h*u+l*s+M,t}function b(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=Math.sin(r),l=Math.cos(r);return t[0]=l*a+h*o,t[1]=l*e+h*i,t[2]=l*u+h*s,t[3]=l*o-h*a,t[4]=l*i-h*e,t[5]=l*s-h*u,t[6]=c,t[7]=f,t[8]=M,t}function m(t,n,r){var a=r[0],e=r[1];return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=e*n[3],t[4]=e*n[4],t[5]=e*n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t}function p(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=n[0],t[7]=n[1],t[8]=1,t}function P(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=0,t[3]=-r,t[4]=a,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function E(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=0,t[4]=n[1],t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function O(t,n){return t[0]=n[0],t[1]=n[1],t[2]=0,t[3]=n[2],t[4]=n[3],t[5]=0,t[6]=n[4],t[7]=n[5],t[8]=1,t}function x(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,s=e+e,c=r*o,f=a*o,M=a*i,h=e*o,l=e*i,v=e*s,d=u*o,b=u*i,m=u*s;return t[0]=1-M-v,t[3]=f-m,t[6]=h+b,t[1]=f+m,t[4]=1-c-v,t[7]=l-d,t[2]=h-b,t[5]=l+d,t[8]=1-c-M,t}function A(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=n[9],h=n[10],l=n[11],v=n[12],d=n[13],b=n[14],m=n[15],p=r*i-a*o,P=r*s-e*o,E=r*c-u*o,O=a*s-e*i,x=a*c-u*i,A=e*c-u*s,q=f*d-M*v,y=f*b-h*v,w=f*m-l*v,R=M*b-h*d,L=M*m-l*d,S=h*m-l*b,_=p*S-P*L+E*R+O*w-x*y+A*q;return _?(_=1/_,t[0]=(i*S-s*L+c*R)*_,t[1]=(s*w-o*S-c*y)*_,t[2]=(o*L-i*w+c*q)*_,t[3]=(e*L-a*S-u*R)*_,t[4]=(r*S-e*w+u*y)*_,t[5]=(a*w-r*L-u*q)*_,t[6]=(d*A-b*x+m*O)*_,t[7]=(b*E-v*A-m*P)*_,t[8]=(v*x-d*E+m*p)*_,t):null}function q(t,n,r){return t[0]=2/n,t[1]=0,t[2]=0,t[3]=0,t[4]=-2/r,t[5]=0,t[6]=-1,t[7]=1,t[8]=1,t}function y(t){return"mat3("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+")"}function w(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2))}function R(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t[6]=n[6]+r[6],t[7]=n[7]+r[7],t[8]=n[8]+r[8],t}function L(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t[6]=n[6]-r[6],t[7]=n[7]-r[7],t[8]=n[8]-r[8],t}function S(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=n[7]*r,t[8]=n[8]*r,t}function _(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t[6]=n[6]+r[6]*a,t[7]=n[7]+r[7]*a,t[8]=n[8]+r[8]*a,t}function I(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]&&t[8]===n[8]}function N(t,n){var r=t[0],a=t[1],e=t[2],u=t[3],o=t[4],i=t[5],s=t[6],c=t[7],f=t[8],M=n[0],h=n[1],l=n[2],v=n[3],d=n[4],b=n[5],m=n[6],p=n[7],P=n[8];return Math.abs(r-M)<=g.EPSILON*Math.max(1,Math.abs(r),Math.abs(M))&&Math.abs(a-h)<=g.EPSILON*Math.max(1,Math.abs(a),Math.abs(h))&&Math.abs(e-l)<=g.EPSILON*Math.max(1,Math.abs(e),Math.abs(l))&&Math.abs(u-v)<=g.EPSILON*Math.max(1,Math.abs(u),Math.abs(v))&&Math.abs(o-d)<=g.EPSILON*Math.max(1,Math.abs(o),Math.abs(d))&&Math.abs(i-b)<=g.EPSILON*Math.max(1,Math.abs(i),Math.abs(b))&&Math.abs(s-m)<=g.EPSILON*Math.max(1,Math.abs(s),Math.abs(m))&&Math.abs(c-p)<=g.EPSILON*Math.max(1,Math.abs(c),Math.abs(p))&&Math.abs(f-P)<=g.EPSILON*Math.max(1,Math.abs(f),Math.abs(P))}Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=a,n.fromMat4=e,n.clone=u,n.copy=o,n.fromValues=i,n.set=s,n.identity=c,n.transpose=f,n.invert=M,n.adjoint=h,n.determinant=l,n.multiply=v,n.translate=d,n.rotate=b,n.scale=m,n.fromTranslation=p,n.fromRotation=P,n.fromScaling=E,n.fromMat2d=O,n.fromQuat=x,n.normalFromMat4=A,n.projection=q,n.str=y,n.frob=w,n.add=R,n.subtract=L,n.multiplyScalar=S,n.multiplyScalarAndAdd=_,n.exactEquals=I,n.equals=N;var Y=r(0),g=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(Y);n.mul=v,n.sub=L},function(t,n,r){"use strict";function a(){var t=new Z.ARRAY_TYPE(3);return t[0]=0,t[1]=0,t[2]=0,t}function e(t){var n=new Z.ARRAY_TYPE(3);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n}function u(t){var n=t[0],r=t[1],a=t[2];return Math.sqrt(n*n+r*r+a*a)}function o(t,n,r){var a=new Z.ARRAY_TYPE(3);return a[0]=t,a[1]=n,a[2]=r,a}function i(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t}function s(t,n,r,a){return t[0]=n,t[1]=r,t[2]=a,t}function c(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t}function f(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t}function M(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t[2]=n[2]*r[2],t}function h(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t[2]=n[2]/r[2],t}function l(t,n){return t[0]=Math.ceil(n[0]),t[1]=Math.ceil(n[1]),t[2]=Math.ceil(n[2]),t}function v(t,n){return t[0]=Math.floor(n[0]),t[1]=Math.floor(n[1]),t[2]=Math.floor(n[2]),t}function d(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t[2]=Math.min(n[2],r[2]),t}function b(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t[2]=Math.max(n[2],r[2]),t}function m(t,n){return t[0]=Math.round(n[0]),t[1]=Math.round(n[1]),t[2]=Math.round(n[2]),t}function p(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t}function P(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t}function E(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return Math.sqrt(r*r+a*a+e*e)}function O(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return r*r+a*a+e*e}function x(t){var n=t[0],r=t[1],a=t[2];return n*n+r*r+a*a}function A(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t}function q(t,n){return t[0]=1/n[0],t[1]=1/n[1],t[2]=1/n[2],t}function y(t,n){var r=n[0],a=n[1],e=n[2],u=r*r+a*a+e*e;return u>0&&(u=1/Math.sqrt(u),t[0]=n[0]*u,t[1]=n[1]*u,t[2]=n[2]*u),t}function w(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function R(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2];return t[0]=e*s-u*i,t[1]=u*o-a*s,t[2]=a*i-e*o,t}function L(t,n,r,a){var e=n[0],u=n[1],o=n[2];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t}function S(t,n,r,a,e,u){var o=u*u,i=o*(2*u-3)+1,s=o*(u-2)+u,c=o*(u-1),f=o*(3-2*u);return t[0]=n[0]*i+r[0]*s+a[0]*c+e[0]*f,t[1]=n[1]*i+r[1]*s+a[1]*c+e[1]*f,t[2]=n[2]*i+r[2]*s+a[2]*c+e[2]*f,t}function _(t,n,r,a,e,u){var o=1-u,i=o*o,s=u*u,c=i*o,f=3*u*i,M=3*s*o,h=s*u;return t[0]=n[0]*c+r[0]*f+a[0]*M+e[0]*h,t[1]=n[1]*c+r[1]*f+a[1]*M+e[1]*h,t[2]=n[2]*c+r[2]*f+a[2]*M+e[2]*h,t}function I(t,n){n=n||1;var r=2*Z.RANDOM()*Math.PI,a=2*Z.RANDOM()-1,e=Math.sqrt(1-a*a)*n;return t[0]=Math.cos(r)*e,t[1]=Math.sin(r)*e,t[2]=a*n,t}function N(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[3]*a+r[7]*e+r[11]*u+r[15];return o=o||1,t[0]=(r[0]*a+r[4]*e+r[8]*u+r[12])/o,t[1]=(r[1]*a+r[5]*e+r[9]*u+r[13])/o,t[2]=(r[2]*a+r[6]*e+r[10]*u+r[14])/o,t}function Y(t,n,r){var a=n[0],e=n[1],u=n[2];return t[0]=a*r[0]+e*r[3]+u*r[6],t[1]=a*r[1]+e*r[4]+u*r[7],t[2]=a*r[2]+e*r[5]+u*r[8],t}function g(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2],c=r[3],f=c*a+i*u-s*e,M=c*e+s*a-o*u,h=c*u+o*e-i*a,l=-o*a-i*e-s*u;return t[0]=f*c+l*-o+M*-s-h*-i,t[1]=M*c+l*-i+h*-o-f*-s,t[2]=h*c+l*-s+f*-i-M*-o,t}function T(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[0],u[1]=e[1]*Math.cos(a)-e[2]*Math.sin(a),u[2]=e[1]*Math.sin(a)+e[2]*Math.cos(a),t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t}function j(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[2]*Math.sin(a)+e[0]*Math.cos(a),u[1]=e[1],u[2]=e[2]*Math.cos(a)-e[0]*Math.sin(a),t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t}function D(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[0]*Math.cos(a)-e[1]*Math.sin(a),u[1]=e[0]*Math.sin(a)+e[1]*Math.cos(a),u[2]=e[2],t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t}function V(t,n){var r=o(t[0],t[1],t[2]),a=o(n[0],n[1],n[2]);y(r,r),y(a,a);var e=w(r,a);return e>1?0:e<-1?Math.PI:Math.acos(e)}function z(t){return"vec3("+t[0]+", "+t[1]+", "+t[2]+")"}function F(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]}function Q(t,n){var r=t[0],a=t[1],e=t[2],u=n[0],o=n[1],i=n[2];return Math.abs(r-u)<=Z.EPSILON*Math.max(1,Math.abs(r),Math.abs(u))&&Math.abs(a-o)<=Z.EPSILON*Math.max(1,Math.abs(a),Math.abs(o))&&Math.abs(e-i)<=Z.EPSILON*Math.max(1,Math.abs(e),Math.abs(i))}Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.len=n.sqrDist=n.dist=n.div=n.mul=n.sub=void 0,n.create=a,n.clone=e,n.length=u,n.fromValues=o,n.copy=i,n.set=s,n.add=c,n.subtract=f,n.multiply=M,n.divide=h,n.ceil=l,n.floor=v,n.min=d,n.max=b,n.round=m,n.scale=p,n.scaleAndAdd=P,n.distance=E,n.squaredDistance=O,n.squaredLength=x,n.negate=A,n.inverse=q,n.normalize=y,n.dot=w,n.cross=R,n.lerp=L,n.hermite=S,n.bezier=_,n.random=I,n.transformMat4=N,n.transformMat3=Y,n.transformQuat=g,n.rotateX=T,n.rotateY=j,n.rotateZ=D,n.angle=V,n.str=z,n.exactEquals=F,n.equals=Q;var X=r(0),Z=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(X);n.sub=f,n.mul=M,n.div=h,n.dist=E,n.sqrDist=O,n.len=u,n.sqrLen=x,n.forEach=function(){var t=a();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=3),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i0&&(o=1/Math.sqrt(o),t[0]=r*o,t[1]=a*o,t[2]=e*o,t[3]=u*o),t}function w(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]+t[3]*n[3]}function R(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t[3]=i+a*(r[3]-i),t}function L(t,n){return n=n||1,t[0]=T.RANDOM(),t[1]=T.RANDOM(),t[2]=T.RANDOM(),t[3]=T.RANDOM(),y(t,t),m(t,t,n),t}function S(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3];return t[0]=r[0]*a+r[4]*e+r[8]*u+r[12]*o,t[1]=r[1]*a+r[5]*e+r[9]*u+r[13]*o,t[2]=r[2]*a+r[6]*e+r[10]*u+r[14]*o,t[3]=r[3]*a+r[7]*e+r[11]*u+r[15]*o,t}function _(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2],c=r[3],f=c*a+i*u-s*e,M=c*e+s*a-o*u,h=c*u+o*e-i*a,l=-o*a-i*e-s*u;return t[0]=f*c+l*-o+M*-s-h*-i,t[1]=M*c+l*-i+h*-o-f*-s,t[2]=h*c+l*-s+f*-i-M*-o,t[3]=n[3],t}function I(t){return"vec4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"}function N(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]}function Y(t,n){var r=t[0],a=t[1],e=t[2],u=t[3],o=n[0],i=n[1],s=n[2],c=n[3];return Math.abs(r-o)<=T.EPSILON*Math.max(1,Math.abs(r),Math.abs(o))&&Math.abs(a-i)<=T.EPSILON*Math.max(1,Math.abs(a),Math.abs(i))&&Math.abs(e-s)<=T.EPSILON*Math.max(1,Math.abs(e),Math.abs(s))&&Math.abs(u-c)<=T.EPSILON*Math.max(1,Math.abs(u),Math.abs(c))}Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.len=n.sqrDist=n.dist=n.div=n.mul=n.sub=void 0,n.create=a,n.clone=e,n.fromValues=u,n.copy=o,n.set=i,n.add=s,n.subtract=c,n.multiply=f,n.divide=M,n.ceil=h,n.floor=l,n.min=v,n.max=d,n.round=b,n.scale=m,n.scaleAndAdd=p,n.distance=P,n.squaredDistance=E,n.length=O,n.squaredLength=x,n.negate=A,n.inverse=q,n.normalize=y,n.dot=w,n.lerp=R,n.random=L,n.transformMat4=S,n.transformQuat=_,n.str=I,n.exactEquals=N,n.equals=Y;var g=r(0),T=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(g);n.sub=c,n.mul=f,n.div=M,n.dist=P,n.sqrDist=E,n.len=O,n.sqrLen=x,n.forEach=function(){var t=a();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=4),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i0?(a=2*Math.sqrt(r+1),t[3]=.25*a,t[0]=(n[6]-n[9])/a,t[1]=(n[8]-n[2])/a,t[2]=(n[1]-n[4])/a):n[0]>n[5]&n[0]>n[10]?(a=2*Math.sqrt(1+n[0]-n[5]-n[10]),t[3]=(n[6]-n[9])/a,t[0]=.25*a,t[1]=(n[1]+n[4])/a,t[2]=(n[8]+n[2])/a):n[5]>n[10]?(a=2*Math.sqrt(1+n[5]-n[0]-n[10]),t[3]=(n[8]-n[2])/a,t[0]=(n[1]+n[4])/a,t[1]=.25*a,t[2]=(n[6]+n[9])/a):(a=2*Math.sqrt(1+n[10]-n[0]-n[5]),t[3]=(n[1]-n[4])/a,t[0]=(n[8]+n[2])/a,t[1]=(n[6]+n[9])/a,t[2]=.25*a),t}function _(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3],s=e+e,c=u+u,f=o+o,M=e*s,h=e*c,l=e*f,v=u*c,d=u*f,b=o*f,m=i*s,p=i*c,P=i*f,E=a[0],O=a[1],x=a[2];return t[0]=(1-(v+b))*E,t[1]=(h+P)*E,t[2]=(l-p)*E,t[3]=0,t[4]=(h-P)*O,t[5]=(1-(M+b))*O,t[6]=(d+m)*O,t[7]=0,t[8]=(l+p)*x,t[9]=(d-m)*x,t[10]=(1-(M+v))*x,t[11]=0,t[12]=r[0],t[13]=r[1],t[14]=r[2],t[15]=1,t}function I(t,n,r,a,e){var u=n[0],o=n[1],i=n[2],s=n[3],c=u+u,f=o+o,M=i+i,h=u*c,l=u*f,v=u*M,d=o*f,b=o*M,m=i*M,p=s*c,P=s*f,E=s*M,O=a[0],x=a[1],A=a[2],q=e[0],y=e[1],w=e[2];return t[0]=(1-(d+m))*O,t[1]=(l+E)*O,t[2]=(v-P)*O,t[3]=0,t[4]=(l-E)*x,t[5]=(1-(h+m))*x,t[6]=(b+p)*x,t[7]=0,t[8]=(v+P)*A,t[9]=(b-p)*A,t[10]=(1-(h+d))*A,t[11]=0,t[12]=r[0]+q-(t[0]*q+t[4]*y+t[8]*w),t[13]=r[1]+y-(t[1]*q+t[5]*y+t[9]*w),t[14]=r[2]+w-(t[2]*q+t[6]*y+t[10]*w),t[15]=1,t}function N(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,s=e+e,c=r*o,f=a*o,M=a*i,h=e*o,l=e*i,v=e*s,d=u*o,b=u*i,m=u*s;return t[0]=1-M-v,t[1]=f+m,t[2]=h-b,t[3]=0,t[4]=f-m,t[5]=1-c-v,t[6]=l+d,t[7]=0,t[8]=h+b,t[9]=l-d,t[10]=1-c-M,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t}function Y(t,n,r,a,e,u,o){var i=1/(r-n),s=1/(e-a),c=1/(u-o);return t[0]=2*u*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=2*u*s,t[6]=0,t[7]=0,t[8]=(r+n)*i,t[9]=(e+a)*s,t[10]=(o+u)*c,t[11]=-1,t[12]=0,t[13]=0,t[14]=o*u*2*c,t[15]=0,t}function g(t,n,r,a,e){var u=1/Math.tan(n/2),o=1/(a-e);return t[0]=u/r,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=u,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=(e+a)*o,t[11]=-1,t[12]=0,t[13]=0,t[14]=2*e*a*o,t[15]=0,t}function T(t,n,r,a){var e=Math.tan(n.upDegrees*Math.PI/180),u=Math.tan(n.downDegrees*Math.PI/180),o=Math.tan(n.leftDegrees*Math.PI/180),i=Math.tan(n.rightDegrees*Math.PI/180),s=2/(o+i),c=2/(e+u);return t[0]=s,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c,t[6]=0,t[7]=0,t[8]=-(o-i)*s*.5,t[9]=(e-u)*c*.5,t[10]=a/(r-a),t[11]=-1,t[12]=0,t[13]=0,t[14]=a*r/(r-a),t[15]=0,t}function j(t,n,r,a,e,u,o){var i=1/(n-r),s=1/(a-e),c=1/(u-o);return t[0]=-2*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=-2*s,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=2*c,t[11]=0,t[12]=(n+r)*i,t[13]=(e+a)*s,t[14]=(o+u)*c,t[15]=1,t}function D(t,n,r,a){var e=void 0,u=void 0,o=void 0,i=void 0,s=void 0,c=void 0,f=void 0,M=void 0,h=void 0,l=void 0,v=n[0],d=n[1],b=n[2],m=a[0],p=a[1],P=a[2],E=r[0],O=r[1],x=r[2];return Math.abs(v-E)0&&(l=1/Math.sqrt(l),f*=l,M*=l,h*=l);var v=s*h-c*M,d=c*f-i*h,b=i*M-s*f;return t[0]=v,t[1]=d,t[2]=b,t[3]=0,t[4]=M*b-h*d,t[5]=h*v-f*b,t[6]=f*d-M*v,t[7]=0,t[8]=f,t[9]=M,t[10]=h,t[11]=0,t[12]=e,t[13]=u,t[14]=o,t[15]=1,t}function z(t){return"mat4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+", "+t[9]+", "+t[10]+", "+t[11]+", "+t[12]+", "+t[13]+", "+t[14]+", "+t[15]+")"}function F(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2)+Math.pow(t[9],2)+Math.pow(t[10],2)+Math.pow(t[11],2)+Math.pow(t[12],2)+Math.pow(t[13],2)+Math.pow(t[14],2)+Math.pow(t[15],2))}function Q(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t[6]=n[6]+r[6],t[7]=n[7]+r[7],t[8]=n[8]+r[8],t[9]=n[9]+r[9],t[10]=n[10]+r[10],t[11]=n[11]+r[11],t[12]=n[12]+r[12],t[13]=n[13]+r[13],t[14]=n[14]+r[14],t[15]=n[15]+r[15],t}function X(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t[6]=n[6]-r[6],t[7]=n[7]-r[7],t[8]=n[8]-r[8],t[9]=n[9]-r[9],t[10]=n[10]-r[10],t[11]=n[11]-r[11],t[12]=n[12]-r[12],t[13]=n[13]-r[13],t[14]=n[14]-r[14],t[15]=n[15]-r[15],t}function Z(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=n[7]*r,t[8]=n[8]*r,t[9]=n[9]*r,t[10]=n[10]*r,t[11]=n[11]*r,t[12]=n[12]*r,t[13]=n[13]*r,t[14]=n[14]*r,t[15]=n[15]*r,t}function k(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t[6]=n[6]+r[6]*a,t[7]=n[7]+r[7]*a,t[8]=n[8]+r[8]*a,t[9]=n[9]+r[9]*a,t[10]=n[10]+r[10]*a,t[11]=n[11]+r[11]*a,t[12]=n[12]+r[12]*a,t[13]=n[13]+r[13]*a,t[14]=n[14]+r[14]*a,t[15]=n[15]+r[15]*a,t}function U(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]&&t[8]===n[8]&&t[9]===n[9]&&t[10]===n[10]&&t[11]===n[11]&&t[12]===n[12]&&t[13]===n[13]&&t[14]===n[14]&&t[15]===n[15]}function W(t,n){var r=t[0],a=t[1],e=t[2],u=t[3],o=t[4],i=t[5],s=t[6],c=t[7],f=t[8],M=t[9],h=t[10],l=t[11],v=t[12],d=t[13],b=t[14],m=t[15],p=n[0],P=n[1],E=n[2],O=n[3],x=n[4],A=n[5],q=n[6],y=n[7],w=n[8],R=n[9],L=n[10],S=n[11],_=n[12],I=n[13],N=n[14],Y=n[15];return Math.abs(r-p)<=C.EPSILON*Math.max(1,Math.abs(r),Math.abs(p))&&Math.abs(a-P)<=C.EPSILON*Math.max(1,Math.abs(a),Math.abs(P))&&Math.abs(e-E)<=C.EPSILON*Math.max(1,Math.abs(e),Math.abs(E))&&Math.abs(u-O)<=C.EPSILON*Math.max(1,Math.abs(u),Math.abs(O))&&Math.abs(o-x)<=C.EPSILON*Math.max(1,Math.abs(o),Math.abs(x))&&Math.abs(i-A)<=C.EPSILON*Math.max(1,Math.abs(i),Math.abs(A))&&Math.abs(s-q)<=C.EPSILON*Math.max(1,Math.abs(s),Math.abs(q))&&Math.abs(c-y)<=C.EPSILON*Math.max(1,Math.abs(c),Math.abs(y))&&Math.abs(f-w)<=C.EPSILON*Math.max(1,Math.abs(f),Math.abs(w))&&Math.abs(M-R)<=C.EPSILON*Math.max(1,Math.abs(M),Math.abs(R))&&Math.abs(h-L)<=C.EPSILON*Math.max(1,Math.abs(h),Math.abs(L))&&Math.abs(l-S)<=C.EPSILON*Math.max(1,Math.abs(l),Math.abs(S))&&Math.abs(v-_)<=C.EPSILON*Math.max(1,Math.abs(v),Math.abs(_))&&Math.abs(d-I)<=C.EPSILON*Math.max(1,Math.abs(d),Math.abs(I))&&Math.abs(b-N)<=C.EPSILON*Math.max(1,Math.abs(b),Math.abs(N))&&Math.abs(m-Y)<=C.EPSILON*Math.max(1,Math.abs(m),Math.abs(Y))}Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=a,n.clone=e,n.copy=u,n.fromValues=o,n.set=i,n.identity=s,n.transpose=c,n.invert=f,n.adjoint=M,n.determinant=h,n.multiply=l,n.translate=v,n.scale=d,n.rotate=b,n.rotateX=m,n.rotateY=p,n.rotateZ=P,n.fromTranslation=E,n.fromScaling=O,n.fromRotation=x,n.fromXRotation=A,n.fromYRotation=q,n.fromZRotation=y,n.fromRotationTranslation=w,n.getTranslation=R,n.getScaling=L,n.getRotation=S,n.fromRotationTranslationScale=_,n.fromRotationTranslationScaleOrigin=I,n.fromQuat=N,n.frustum=Y,n.perspective=g,n.perspectiveFromFieldOfView=T,n.ortho=j,n.lookAt=D,n.targetTo=V,n.str=z,n.frob=F,n.add=Q,n.subtract=X,n.multiplyScalar=Z,n.multiplyScalarAndAdd=k,n.exactEquals=U,n.equals=W;var B=r(0),C=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(B);n.mul=l,n.sub=X},function(t,n,r){"use strict";function a(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}function e(){var t=new E.ARRAY_TYPE(4);return t[0]=0,t[1]=0,t[2]=0,t[3]=1,t}function u(t){return t[0]=0,t[1]=0,t[2]=0,t[3]=1,t}function o(t,n,r){r*=.5;var a=Math.sin(r);return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=Math.cos(r),t}function i(t,n){var r=2*Math.acos(n[3]),a=Math.sin(r/2);return 0!=a?(t[0]=n[0]/a,t[1]=n[1]/a,t[2]=n[2]/a):(t[0]=1,t[1]=0,t[2]=0),r}function s(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],s=r[1],c=r[2],f=r[3];return t[0]=a*f+o*i+e*c-u*s,t[1]=e*f+o*s+u*i-a*c,t[2]=u*f+o*c+a*s-e*i,t[3]=o*f-a*i-e*s-u*c,t}function c(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+o*i,t[1]=e*s+u*i,t[2]=u*s-e*i,t[3]=o*s-a*i,t}function f(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s-u*i,t[1]=e*s+o*i,t[2]=u*s+a*i,t[3]=o*s-e*i,t}function M(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+e*i,t[1]=e*s-a*i,t[2]=u*s+o*i,t[3]=o*s-u*i,t}function h(t,n){var r=n[0],a=n[1],e=n[2];return t[0]=r,t[1]=a,t[2]=e,t[3]=Math.sqrt(Math.abs(1-r*r-a*a-e*e)),t}function l(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3],s=r[0],c=r[1],f=r[2],M=r[3],h=void 0,l=void 0,v=void 0,d=void 0,b=void 0;return l=e*s+u*c+o*f+i*M,l<0&&(l=-l,s=-s,c=-c,f=-f,M=-M),1-l>1e-6?(h=Math.acos(l),v=Math.sin(h),d=Math.sin((1-a)*h)/v,b=Math.sin(a*h)/v):(d=1-a,b=a),t[0]=d*e+b*s,t[1]=d*u+b*c,t[2]=d*o+b*f,t[3]=d*i+b*M,t}function v(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*r+a*a+e*e+u*u,i=o?1/o:0;return t[0]=-r*i,t[1]=-a*i,t[2]=-e*i,t[3]=u*i,t}function d(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t[3]=n[3],t}function b(t,n){var r=n[0]+n[4]+n[8],a=void 0;if(r>0)a=Math.sqrt(r+1),t[3]=.5*a,a=.5/a,t[0]=(n[5]-n[7])*a,t[1]=(n[6]-n[2])*a,t[2]=(n[1]-n[3])*a;else{var e=0;n[4]>n[0]&&(e=1),n[8]>n[3*e+e]&&(e=2);var u=(e+1)%3,o=(e+2)%3;a=Math.sqrt(n[3*e+e]-n[3*u+u]-n[3*o+o]+1),t[e]=.5*a,a=.5/a,t[3]=(n[3*u+o]-n[3*o+u])*a,t[u]=(n[3*u+e]+n[3*e+u])*a,t[o]=(n[3*o+e]+n[3*e+o])*a}return t}function m(t,n,r,a){var e=.5*Math.PI/180;n*=e,r*=e,a*=e;var u=Math.sin(n),o=Math.cos(n),i=Math.sin(r),s=Math.cos(r),c=Math.sin(a),f=Math.cos(a);return t[0]=u*s*f-o*i*c,t[1]=o*i*f+u*s*c,t[2]=o*s*c-u*i*f,t[3]=o*s*f+u*i*c,t}function p(t){return"quat("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"}Object.defineProperty(n,"__esModule",{value:!0}),n.setAxes=n.sqlerp=n.rotationTo=n.equals=n.exactEquals=n.normalize=n.sqrLen=n.squaredLength=n.len=n.length=n.lerp=n.dot=n.scale=n.mul=n.add=n.set=n.copy=n.fromValues=n.clone=void 0,n.create=e,n.identity=u,n.setAxisAngle=o,n.getAxisAngle=i,n.multiply=s,n.rotateX=c,n.rotateY=f,n.rotateZ=M,n.calculateW=h,n.slerp=l,n.invert=v,n.conjugate=d,n.fromMat3=b,n.fromEuler=m,n.str=p;var P=r(0),E=a(P),O=r(1),x=a(O),A=r(2),q=a(A),y=r(3),w=a(y),R=(n.clone=w.clone,n.fromValues=w.fromValues,n.copy=w.copy,n.set=w.set,n.add=w.add,n.mul=s,n.scale=w.scale,n.dot=w.dot,n.lerp=w.lerp,n.length=w.length),L=(n.len=R,n.squaredLength=w.squaredLength),S=(n.sqrLen=L,n.normalize=w.normalize);n.exactEquals=w.exactEquals,n.equals=w.equals,n.rotationTo=function(){var t=q.create(),n=q.fromValues(1,0,0),r=q.fromValues(0,1,0);return function(a,e,u){var i=q.dot(e,u);return i<-.999999?(q.cross(t,n,e),q.len(t)<1e-6&&q.cross(t,r,e),q.normalize(t,t),o(a,t,Math.PI),a):i>.999999?(a[0]=0,a[1]=0,a[2]=0,a[3]=1,a):(q.cross(t,e,u),a[0]=t[0],a[1]=t[1],a[2]=t[2],a[3]=1+i,S(a,a))}}(),n.sqlerp=function(){var t=e(),n=e();return function(r,a,e,u,o,i){return l(t,a,o,i),l(n,e,u,i),l(r,t,n,2*i*(1-i)),r}}(),n.setAxes=function(){var t=x.create();return function(n,r,a,e){return t[0]=a[0],t[3]=a[1],t[6]=a[2],t[1]=e[0],t[4]=e[1],t[7]=e[2],t[2]=-r[0],t[5]=-r[1],t[8]=-r[2],S(n,b(n,t))}}()},function(t,n,r){"use strict";function a(){var t=new V.ARRAY_TYPE(2);return t[0]=0,t[1]=0,t}function e(t){var n=new V.ARRAY_TYPE(2);return n[0]=t[0],n[1]=t[1],n}function u(t,n){var r=new V.ARRAY_TYPE(2);return r[0]=t,r[1]=n,r}function o(t,n){return t[0]=n[0],t[1]=n[1],t}function i(t,n,r){return t[0]=n,t[1]=r,t}function s(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t}function c(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t}function f(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t}function M(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t}function h(t,n){return t[0]=Math.ceil(n[0]),t[1]=Math.ceil(n[1]),t}function l(t,n){return t[0]=Math.floor(n[0]),t[1]=Math.floor(n[1]),t}function v(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t}function d(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t}function b(t,n){return t[0]=Math.round(n[0]),t[1]=Math.round(n[1]),t}function m(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t}function p(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t}function P(t,n){var r=n[0]-t[0],a=n[1]-t[1];return Math.sqrt(r*r+a*a)}function E(t,n){var r=n[0]-t[0],a=n[1]-t[1];return r*r+a*a}function O(t){var n=t[0],r=t[1];return Math.sqrt(n*n+r*r)}function x(t){var n=t[0],r=t[1];return n*n+r*r}function A(t,n){return t[0]=-n[0],t[1]=-n[1],t}function q(t,n){return t[0]=1/n[0],t[1]=1/n[1],t}function y(t,n){var r=n[0],a=n[1],e=r*r+a*a;return e>0&&(e=1/Math.sqrt(e),t[0]=n[0]*e,t[1]=n[1]*e),t}function w(t,n){return t[0]*n[0]+t[1]*n[1]}function R(t,n,r){var a=n[0]*r[1]-n[1]*r[0];return t[0]=t[1]=0,t[2]=a,t}function L(t,n,r,a){var e=n[0],u=n[1];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t}function S(t,n){n=n||1;var r=2*V.RANDOM()*Math.PI;return t[0]=Math.cos(r)*n,t[1]=Math.sin(r)*n,t}function _(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e,t[1]=r[1]*a+r[3]*e,t}function I(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e+r[4],t[1]=r[1]*a+r[3]*e+r[5],t}function N(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[3]*e+r[6],t[1]=r[1]*a+r[4]*e+r[7],t}function Y(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[4]*e+r[12],t[1]=r[1]*a+r[5]*e+r[13],t}function g(t){return"vec2("+t[0]+", "+t[1]+")"}function T(t,n){return t[0]===n[0]&&t[1]===n[1]}function j(t,n){var r=t[0],a=t[1],e=n[0],u=n[1];return Math.abs(r-e)<=V.EPSILON*Math.max(1,Math.abs(r),Math.abs(e))&&Math.abs(a-u)<=V.EPSILON*Math.max(1,Math.abs(a),Math.abs(u))}Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.sqrDist=n.dist=n.div=n.mul=n.sub=n.len=void 0,n.create=a,n.clone=e,n.fromValues=u,n.copy=o,n.set=i,n.add=s,n.subtract=c,n.multiply=f,n.divide=M,n.ceil=h,n.floor=l,n.min=v,n.max=d,n.round=b,n.scale=m,n.scaleAndAdd=p,n.distance=P,n.squaredDistance=E,n.length=O,n.squaredLength=x,n.negate=A,n.inverse=q,n.normalize=y,n.dot=w,n.cross=R,n.lerp=L,n.random=S,n.transformMat2=_,n.transformMat2d=I,n.transformMat3=N,n.transformMat4=Y,n.str=g,n.exactEquals=T,n.equals=j;var D=r(0),V=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(D);n.len=O,n.sub=c,n.mul=f,n.div=M,n.dist=P,n.sqrDist=E,n.sqrLen=x,n.forEach=function(){var t=a();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=2),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i> 16);
53 | seed *= 9;
54 | seed = seed ^ (seed >> 4);
55 | seed *= 0x27d4eb2d;
56 | seed = seed ^ (seed >> 15);
57 | return float(seed % 2147483647) / float(2147483647);
58 | }
59 |
60 | float linear_to_srgb(float x) {
61 | if (x <= 0.0031308f) {
62 | return 12.92f * x;
63 | }
64 | return 1.055f * pow(x, 1.f / 2.4f) - 0.055f;
65 | }
66 |
67 | void main(void) {
68 | vec3 ray_dir = normalize(vray_dir);
69 | vec2 t_hit = intersect_box(transformed_eye, ray_dir);
70 | if (t_hit.x > t_hit.y) {
71 | discard;
72 | }
73 | t_hit.x = max(t_hit.x, 0.0);
74 | vec3 dt_vec = 1.0 / (vec3(volume_dims) * abs(ray_dir));
75 | float dt = dt_scale * min(dt_vec.x, min(dt_vec.y, dt_vec.z));
76 | float offset = wang_hash(int(gl_FragCoord.x + 640.0 * gl_FragCoord.y));
77 | vec3 p = transformed_eye + (t_hit.x + offset * dt) * ray_dir;
78 | for (float t = t_hit.x; t < t_hit.y; t += dt) {
79 | float val = texture(volume, p).r;
80 | vec4 val_color = vec4(texture(colormap, vec2(val, 0.5)).rgb, val);
81 | // Opacity correction
82 | val_color.a = 1.0 - pow(1.0 - val_color.a, dt_scale);
83 | color.rgb += (1.0 - color.a) * val_color.a * val_color.rgb;
84 | color.a += (1.0 - color.a) * val_color.a;
85 | if (color.a >= 0.95) {
86 | break;
87 | }
88 | p += ray_dir * dt;
89 | }
90 | color.r = linear_to_srgb(color.r);
91 | color.g = linear_to_srgb(color.g);
92 | color.b = linear_to_srgb(color.b);
93 | }`;
94 |
95 |
--------------------------------------------------------------------------------
/js/volume-raycaster.js:
--------------------------------------------------------------------------------
1 | var cubeStrip = [
2 | 1, 1, 0,
3 | 0, 1, 0,
4 | 1, 1, 1,
5 | 0, 1, 1,
6 | 0, 0, 1,
7 | 0, 1, 0,
8 | 0, 0, 0,
9 | 1, 1, 0,
10 | 1, 0, 0,
11 | 1, 1, 1,
12 | 1, 0, 1,
13 | 0, 0, 1,
14 | 1, 0, 0,
15 | 0, 0, 0
16 | ];
17 |
18 | var takeScreenShot = false;
19 | var canvas = null;
20 |
21 | var gl = null;
22 | var shader = null;
23 | var volumeTexture = null;
24 | var colormapTex = null;
25 | var fileRegex = /.*\/(\w+)_(\d+)x(\d+)x(\d+)_(\w+)\.*/;
26 | var proj = null;
27 | var camera = null;
28 | var projView = null;
29 | var tabFocused = true;
30 | var newVolumeUpload = true;
31 | var targetFrameTime = 32;
32 | var samplingRate = 1.0;
33 | var WIDTH = 640;
34 | var HEIGHT = 480;
35 |
36 | const defaultEye = vec3.set(vec3.create(), 0.5, 0.5, 1.5);
37 | const center = vec3.set(vec3.create(), 0.5, 0.5, 0.5);
38 | const up = vec3.set(vec3.create(), 0.0, 1.0, 0.0);
39 |
40 | var volumes = {
41 | "Fuel": "7d87jcsh0qodk78/fuel_64x64x64_uint8.raw",
42 | "Neghip": "zgocya7h33nltu9/neghip_64x64x64_uint8.raw",
43 | "Hydrogen Atom": "jwbav8s3wmmxd5x/hydrogen_atom_128x128x128_uint8.raw",
44 | "Boston Teapot": "w4y88hlf2nbduiv/boston_teapot_256x256x178_uint8.raw",
45 | "Engine": "ld2sqwwd3vaq4zf/engine_256x256x128_uint8.raw",
46 | "Bonsai": "rdnhdxmxtfxe0sa/bonsai_256x256x256_uint8.raw",
47 | "Foot": "ic0mik3qv4vqacm/foot_256x256x256_uint8.raw",
48 | "Skull": "5rfjobn0lvb7tmo/skull_256x256x256_uint8.raw",
49 | "Aneurysm": "3ykigaiym8uiwbp/aneurism_256x256x256_uint8.raw",
50 | };
51 |
52 | var colormaps = {
53 | "Cool Warm": "colormaps/cool-warm-paraview.png",
54 | "Matplotlib Plasma": "colormaps/matplotlib-plasma.png",
55 | "Matplotlib Virdis": "colormaps/matplotlib-virdis.png",
56 | "Rainbow": "colormaps/rainbow.png",
57 | "Samsel Linear Green": "colormaps/samsel-linear-green.png",
58 | "Samsel Linear YGB 1211G": "colormaps/samsel-linear-ygb-1211g.png",
59 | };
60 |
61 | var loadVolume = function(file, onload) {
62 | var m = file.match(fileRegex);
63 | var volDims = [parseInt(m[2]), parseInt(m[3]), parseInt(m[4])];
64 |
65 | var url = "https://www.dl.dropboxusercontent.com/s/" + file + "?dl=1";
66 | var req = new XMLHttpRequest();
67 | var loadingProgressText = document.getElementById("loadingText");
68 | var loadingProgressBar = document.getElementById("loadingProgressBar");
69 |
70 | loadingProgressText.innerHTML = "Loading Volume";
71 | loadingProgressBar.setAttribute("style", "width: 0%");
72 |
73 | req.open("GET", url, true);
74 | req.responseType = "arraybuffer";
75 | req.onprogress = function(evt) {
76 | var vol_size = volDims[0] * volDims[1] * volDims[2];
77 | var percent = evt.loaded / vol_size * 100;
78 | loadingProgressBar.setAttribute("style", "width: " + percent.toFixed(2) + "%");
79 | };
80 | req.onerror = function(evt) {
81 | loadingProgressText.innerHTML = "Error Loading Volume";
82 | loadingProgressBar.setAttribute("style", "width: 0%");
83 | };
84 | req.onload = function(evt) {
85 | loadingProgressText.innerHTML = "Loaded Volume";
86 | loadingProgressBar.setAttribute("style", "width: 100%");
87 | var dataBuffer = req.response;
88 | if (dataBuffer) {
89 | dataBuffer = new Uint8Array(dataBuffer);
90 | onload(file, dataBuffer);
91 | } else {
92 | alert("Unable to load buffer properly from volume?");
93 | console.log("no buffer?");
94 | }
95 | };
96 | req.send();
97 | }
98 |
99 | var selectVolume = function() {
100 | var selection = document.getElementById("volumeList").value;
101 | history.replaceState(history.state, "#" + selection, "#" + selection);
102 |
103 | loadVolume(volumes[selection], function(file, dataBuffer) {
104 | var m = file.match(fileRegex);
105 | var volDims = [parseInt(m[2]), parseInt(m[3]), parseInt(m[4])];
106 |
107 | var tex = gl.createTexture();
108 | gl.activeTexture(gl.TEXTURE0);
109 | gl.bindTexture(gl.TEXTURE_3D, tex);
110 | gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R8, volDims[0], volDims[1], volDims[2]);
111 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
112 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
113 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
114 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
115 | gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0,
116 | volDims[0], volDims[1], volDims[2],
117 | gl.RED, gl.UNSIGNED_BYTE, dataBuffer);
118 |
119 | var longestAxis = Math.max(volDims[0], Math.max(volDims[1], volDims[2]));
120 | var volScale = [volDims[0] / longestAxis, volDims[1] / longestAxis,
121 | volDims[2] / longestAxis];
122 |
123 | gl.uniform3iv(shader.uniforms["volume_dims"], volDims);
124 | gl.uniform3fv(shader.uniforms["volume_scale"], volScale);
125 |
126 | newVolumeUpload = true;
127 | if (!volumeTexture) {
128 | volumeTexture = tex;
129 | setInterval(function() {
130 | // Save them some battery if they're not viewing the tab
131 | if (document.hidden) {
132 | return;
133 | }
134 | var startTime = performance.now();
135 | gl.clearColor(1.0, 1.0, 1.0, 1.0);
136 | gl.clear(gl.COLOR_BUFFER_BIT);
137 |
138 | // Reset the sampling rate and camera for new volumes
139 | if (newVolumeUpload) {
140 | camera = new ArcballCamera(defaultEye, center, up, 2, [WIDTH, HEIGHT]);
141 | samplingRate = 1.0;
142 | gl.uniform1f(shader.uniforms["dt_scale"], samplingRate);
143 | }
144 | projView = mat4.mul(projView, proj, camera.camera);
145 | gl.uniformMatrix4fv(shader.uniforms["proj_view"], false, projView);
146 |
147 | var eye = [camera.invCamera[12], camera.invCamera[13], camera.invCamera[14]];
148 | gl.uniform3fv(shader.uniforms["eye_pos"], eye);
149 |
150 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, cubeStrip.length / 3);
151 | // Wait for rendering to actually finish
152 | gl.finish();
153 | var endTime = performance.now();
154 | var renderTime = endTime - startTime;
155 | var targetSamplingRate = renderTime / targetFrameTime;
156 |
157 | if (takeScreenShot) {
158 | takeScreenShot = false;
159 | canvas.toBlob(function(b) { saveAs(b, "screen.png"); }, "image/png");
160 | }
161 |
162 | // If we're dropping frames, decrease the sampling rate
163 | if (!newVolumeUpload && targetSamplingRate > samplingRate) {
164 | samplingRate = 0.8 * samplingRate + 0.2 * targetSamplingRate;
165 | gl.uniform1f(shader.uniforms["dt_scale"], samplingRate);
166 | }
167 |
168 | newVolumeUpload = false;
169 | startTime = endTime;
170 | }, targetFrameTime);
171 | } else {
172 | gl.deleteTexture(volumeTexture);
173 | volumeTexture = tex;
174 | }
175 | });
176 | }
177 |
178 | var selectColormap = function() {
179 | var selection = document.getElementById("colormapList").value;
180 | var colormapImage = new Image();
181 | colormapImage.onload = function() {
182 | gl.activeTexture(gl.TEXTURE1);
183 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 180, 1,
184 | gl.RGBA, gl.UNSIGNED_BYTE, colormapImage);
185 | };
186 | colormapImage.src = colormaps[selection];
187 | }
188 |
189 | window.onload = function(){
190 | fillVolumeSelector();
191 | fillcolormapSelector();
192 |
193 | canvas = document.getElementById("glcanvas");
194 | gl = canvas.getContext("webgl2");
195 | if (!gl) {
196 | alert("Unable to initialize WebGL2. Your browser may not support it");
197 | return;
198 | }
199 | WIDTH = canvas.getAttribute("width");
200 | HEIGHT = canvas.getAttribute("height");
201 |
202 | proj = mat4.perspective(mat4.create(), 60 * Math.PI / 180.0,
203 | WIDTH / HEIGHT, 0.1, 100);
204 |
205 | camera = new ArcballCamera(defaultEye, center, up, 2, [WIDTH, HEIGHT]);
206 | projView = mat4.create();
207 |
208 | // Register mouse and touch listeners
209 | var controller = new Controller();
210 | controller.mousemove = function(prev, cur, evt) {
211 | if (evt.buttons == 1) {
212 | camera.rotate(prev, cur);
213 |
214 | } else if (evt.buttons == 2) {
215 | camera.pan([cur[0] - prev[0], prev[1] - cur[1]]);
216 | }
217 | };
218 | controller.wheel = function(amt) { camera.zoom(amt); };
219 | controller.pinch = controller.wheel;
220 | controller.twoFingerDrag = function(drag) { camera.pan(drag); };
221 |
222 | document.addEventListener("keydown", function(evt) {
223 | if (evt.key == "p") {
224 | takeScreenShot = true;
225 | }
226 | });
227 |
228 | controller.registerForCanvas(canvas);
229 |
230 | // Setup VAO and VBO to render the cube to run the raymarching shader
231 | var vao = gl.createVertexArray();
232 | gl.bindVertexArray(vao);
233 |
234 | var vbo = gl.createBuffer();
235 | gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
236 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cubeStrip), gl.STATIC_DRAW);
237 |
238 | gl.enableVertexAttribArray(0);
239 | gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
240 |
241 | shader = new Shader(gl, vertShader, fragShader);
242 | shader.use(gl);
243 |
244 | gl.uniform1i(shader.uniforms["volume"], 0);
245 | gl.uniform1i(shader.uniforms["colormap"], 1);
246 | gl.uniform1f(shader.uniforms["dt_scale"], 1.0);
247 |
248 | // Setup required OpenGL state for drawing the back faces and
249 | // composting with the background color
250 | gl.enable(gl.CULL_FACE);
251 | gl.cullFace(gl.FRONT);
252 | gl.enable(gl.BLEND);
253 | gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
254 |
255 | // See if we were linked to a datset
256 | if (window.location.hash) {
257 | var linkedDataset = decodeURI(window.location.hash.substr(1));
258 | if (linkedDataset in volumes) {
259 | document.getElementById("volumeList").value = linkedDataset;
260 | }
261 | }
262 |
263 | // Load the default colormap and upload it, after which we
264 | // load the default volume.
265 | var colormapImage = new Image();
266 | colormapImage.onload = function() {
267 | var colormap = gl.createTexture();
268 | gl.activeTexture(gl.TEXTURE1);
269 | gl.bindTexture(gl.TEXTURE_2D, colormap);
270 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.SRGB8_ALPHA8, 180, 1);
271 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
272 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
273 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
274 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 180, 1,
275 | gl.RGBA, gl.UNSIGNED_BYTE, colormapImage);
276 |
277 | selectVolume();
278 | };
279 | colormapImage.src = "colormaps/cool-warm-paraview.png";
280 | }
281 |
282 | var fillVolumeSelector = function() {
283 | var selector = document.getElementById("volumeList");
284 | for (v in volumes) {
285 | var opt = document.createElement("option");
286 | opt.value = v;
287 | opt.innerHTML = v;
288 | selector.appendChild(opt);
289 | }
290 | }
291 |
292 | var fillcolormapSelector = function() {
293 | var selector = document.getElementById("colormapList");
294 | for (p in colormaps) {
295 | var opt = document.createElement("option");
296 | opt.value = p;
297 | opt.innerHTML = p;
298 | selector.appendChild(opt);
299 | }
300 | }
301 |
302 |
--------------------------------------------------------------------------------
/js/webgl-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | // Compute the view frustum in world space from the provided
3 | // column major projection * view matrix
4 | var Frustum = function(projView) {
5 | var rows = [vec4.create(), vec4.create(), vec4.create(), vec4.create()];
6 | for (var i = 0; i < rows.length; ++i) {
7 | rows[i] = vec4.set(rows[i], projView[i], projView[4 + i],
8 | projView[8 + i], projView[12 + i]);
9 | }
10 |
11 | this.planes = [
12 | // -x plane
13 | vec4.add(vec4.create(), rows[3], rows[0]),
14 | // +x plane
15 | vec4.sub(vec4.create(), rows[3], rows[0]),
16 | // -y plane
17 | vec4.add(vec4.create(), rows[3], rows[1]),
18 | // +y plane
19 | vec4.sub(vec4.create(), rows[3], rows[1]),
20 | // -z plane
21 | vec4.add(vec4.create(), rows[3], rows[2]),
22 | // +z plane
23 | vec4.sub(vec4.create(), rows[3], rows[2])
24 | ];
25 |
26 | // Normalize the planes
27 | for (var i = 0; i < this.planes.length; ++i) {
28 | var s = 1.0 / Math.sqrt(this.planes[i][0] * this.planes[i][0] +
29 | this.planes[i][1] * this.planes[i][1] + this.planes[i][2] * this.planes[i][2]);
30 | this.planes[i][0] *= s;
31 | this.planes[i][1] *= s;
32 | this.planes[i][2] *= s;
33 | this.planes[i][3] *= s;
34 | }
35 |
36 | // Compute the frustum points as well
37 | var invProjView = mat4.invert(mat4.create(), projView);
38 | this.points = [
39 | // x_l, y_l, z_l
40 | vec4.set(vec4.create(), -1, -1, -1, 1),
41 | // x_h, y_l, z_l
42 | vec4.set(vec4.create(), 1, -1, -1, 1),
43 | // x_l, y_h, z_l
44 | vec4.set(vec4.create(), -1, 1, -1, 1),
45 | // x_h, y_h, z_l
46 | vec4.set(vec4.create(), 1, 1, -1, 1),
47 | // x_l, y_l, z_h
48 | vec4.set(vec4.create(), -1, -1, 1, 1),
49 | // x_h, y_l, z_h
50 | vec4.set(vec4.create(), 1, -1, 1, 1),
51 | // x_l, y_h, z_h
52 | vec4.set(vec4.create(), -1, 1, 1, 1),
53 | // x_h, y_h, z_h
54 | vec4.set(vec4.create(), 1, 1, 1, 1)
55 | ];
56 | for (var i = 0; i < 8; ++i) {
57 | this.points[i] = vec4.transformMat4(this.points[i], this.points[i], invProjView);
58 | this.points[i][0] /= this.points[i][3];
59 | this.points[i][1] /= this.points[i][3];
60 | this.points[i][2] /= this.points[i][3];
61 | this.points[i][3] = 1.0;
62 | }
63 | }
64 |
65 | // Check if the box is contained in the Frustum
66 | // The box should be [x_lower, y_lower, z_lower, x_upper, y_upper, z_upper]
67 | // This is done using Inigo Quilez's approach to help with large
68 | // bounds: https://www.iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm
69 | Frustum.prototype.containsBox = function(box) {
70 | // Test the box against each plane
71 | var vec = vec4.create();
72 | var out = 0;
73 | for (var i = 0; i < this.planes.length; ++i) {
74 | out = 0;
75 | // x_l, y_l, z_l
76 | vec4.set(vec, box[0], box[1], box[2], 1.0);
77 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
78 | // x_h, y_l, z_l
79 | vec4.set(vec, box[3], box[1], box[2], 1.0);
80 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
81 | // x_l, y_h, z_l
82 | vec4.set(vec, box[0], box[4], box[2], 1.0);
83 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
84 | // x_h, y_h, z_l
85 | vec4.set(vec, box[3], box[4], box[2], 1.0);
86 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
87 | // x_l, y_l, z_h
88 | vec4.set(vec, box[0], box[1], box[5], 1.0);
89 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
90 | // x_h, y_l, z_h
91 | vec4.set(vec, box[3], box[1], box[5], 1.0);
92 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
93 | // x_l, y_h, z_h
94 | vec4.set(vec, box[0], box[4], box[5], 1.0);
95 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
96 | // x_h, y_h, z_h
97 | vec4.set(vec, box[3], box[4], box[5], 1.0);
98 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0;
99 |
100 | if (out == 8) {
101 | return false;
102 | }
103 | }
104 |
105 | // Test the frustum against the box
106 | out = 0;
107 | for (var i = 0; i < 8; ++i) {
108 | out += this.points[i][0] > box[3] ? 1 : 0;
109 | }
110 | if (out == 8) {
111 | return false;
112 | }
113 |
114 | out = 0;
115 | for (var i = 0; i < 8; ++i) {
116 | out += this.points[i][0] < box[0] ? 1 : 0;
117 | }
118 | if (out == 8) {
119 | return false;
120 | }
121 |
122 | out = 0;
123 | for (var i = 0; i < 8; ++i) {
124 | out += this.points[i][1] > box[4] ? 1 : 0;
125 | }
126 | if (out == 8) {
127 | return false;
128 | }
129 |
130 | out = 0;
131 | for (var i = 0; i < 8; ++i) {
132 | out += this.points[i][1] < box[1] ? 1 : 0;
133 | }
134 | if (out == 8) {
135 | return false;
136 | }
137 |
138 | out = 0;
139 | for (var i = 0; i < 8; ++i) {
140 | out += this.points[i][2] > box[5] ? 1 : 0;
141 | }
142 | if (out == 8) {
143 | return false;
144 | }
145 |
146 | out = 0;
147 | for (var i = 0; i < 8; ++i) {
148 | out += this.points[i][2] < box[2] ? 1 : 0;
149 | }
150 | if (out == 8) {
151 | return false;
152 | }
153 | return true;
154 | }
155 |
156 |
157 | var Shader = function(gl, vertexSrc, fragmentSrc) {
158 | var self = this;
159 | this.program = compileShader(gl, vertexSrc, fragmentSrc);
160 |
161 | var regexUniform = /uniform[^;]+[ ](\w+);/g
162 | var matchUniformName = /uniform[^;]+[ ](\w+);/
163 |
164 | this.uniforms = {};
165 |
166 | var vertexUnifs = vertexSrc.match(regexUniform);
167 | var fragUnifs = fragmentSrc.match(regexUniform);
168 |
169 | if (vertexUnifs) {
170 | vertexUnifs.forEach(function(unif) {
171 | var m = unif.match(matchUniformName);
172 | self.uniforms[m[1]] = -1;
173 | });
174 | }
175 | if (fragUnifs) {
176 | fragUnifs.forEach(function(unif) {
177 | var m = unif.match(matchUniformName);
178 | self.uniforms[m[1]] = -1;
179 | });
180 | }
181 |
182 | for (var unif in this.uniforms) {
183 | this.uniforms[unif] = gl.getUniformLocation(this.program, unif);
184 | }
185 | }
186 |
187 | Shader.prototype.use = function(gl) {
188 | gl.useProgram(this.program);
189 | }
190 |
191 | // Compile and link the shaders vert and frag. vert and frag should contain
192 | // the shader source code for the vertex and fragment shaders respectively
193 | // Returns the compiled and linked program, or null if compilation or linking failed
194 | var compileShader = function(gl, vert, frag){
195 | var vs = gl.createShader(gl.VERTEX_SHADER);
196 | gl.shaderSource(vs, vert);
197 | gl.compileShader(vs);
198 | if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)){
199 | alert("Vertex shader failed to compile, see console for log");
200 | console.log(gl.getShaderInfoLog(vs));
201 | return null;
202 | }
203 |
204 | var fs = gl.createShader(gl.FRAGMENT_SHADER);
205 | gl.shaderSource(fs, frag);
206 | gl.compileShader(fs);
207 | if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)){
208 | alert("Fragment shader failed to compile, see console for log");
209 | console.log(gl.getShaderInfoLog(fs));
210 | return null;
211 | }
212 |
213 | var program = gl.createProgram();
214 | gl.attachShader(program, vs);
215 | gl.attachShader(program, fs);
216 | gl.linkProgram(program);
217 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)){
218 | alert("Shader failed to link, see console for log");
219 | console.log(gl.getProgramInfoLog(program));
220 | return null;
221 | }
222 | return program;
223 | }
224 |
225 | var getGLExtension = function(ext) {
226 | if (!gl.getExtension(ext)) {
227 | alert("Missing " + ext + " WebGL extension");
228 | return false;
229 | }
230 | return true;
231 | }
232 |
233 | /* The arcball camera will be placed at the position 'eye', rotating
234 | * around the point 'center', with the up vector 'up'. 'screenDims'
235 | * should be the dimensions of the canvas or region taking mouse input
236 | * so the mouse positions can be normalized into [-1, 1] from the pixel
237 | * coordinates.
238 | */
239 | var ArcballCamera = function(eye, center, up, zoomSpeed, screenDims) {
240 | var veye = vec3.set(vec3.create(), eye[0], eye[1], eye[2]);
241 | var vcenter = vec3.set(vec3.create(), center[0], center[1], center[2]);
242 | var vup = vec3.set(vec3.create(), up[0], up[1], up[2]);
243 | vec3.normalize(vup, vup);
244 |
245 | var zAxis = vec3.sub(vec3.create(), vcenter, veye);
246 | var viewDist = vec3.len(zAxis);
247 | vec3.normalize(zAxis, zAxis);
248 |
249 | var xAxis = vec3.cross(vec3.create(), zAxis, vup);
250 | vec3.normalize(xAxis, xAxis);
251 |
252 | var yAxis = vec3.cross(vec3.create(), xAxis, zAxis);
253 | vec3.normalize(yAxis, yAxis);
254 |
255 | vec3.cross(xAxis, zAxis, yAxis);
256 | vec3.normalize(xAxis, xAxis);
257 |
258 | this.zoomSpeed = zoomSpeed;
259 | this.invScreen = [1.0 / screenDims[0], 1.0 / screenDims[1]];
260 |
261 | this.centerTranslation = mat4.fromTranslation(mat4.create(), center);
262 | mat4.invert(this.centerTranslation, this.centerTranslation);
263 |
264 | var vt = vec3.set(vec3.create(), 0, 0, -1.0 * viewDist);
265 | this.translation = mat4.fromTranslation(mat4.create(), vt);
266 |
267 | var rotMat = mat3.fromValues(xAxis[0], xAxis[1], xAxis[2],
268 | yAxis[0], yAxis[1], yAxis[2],
269 | -zAxis[0], -zAxis[1], -zAxis[2]);
270 | mat3.transpose(rotMat, rotMat);
271 | this.rotation = quat.fromMat3(quat.create(), rotMat);
272 | quat.normalize(this.rotation, this.rotation);
273 |
274 | this.camera = mat4.create();
275 | this.invCamera = mat4.create();
276 | this.updateCameraMatrix();
277 | }
278 |
279 | ArcballCamera.prototype.rotate = function(prevMouse, curMouse) {
280 | var mPrev = vec2.set(vec2.create(),
281 | clamp(prevMouse[0] * 2.0 * this.invScreen[0] - 1.0, -1.0, 1.0),
282 | clamp(1.0 - prevMouse[1] * 2.0 * this.invScreen[1], -1.0, 1.0));
283 |
284 | var mCur = vec2.set(vec2.create(),
285 | clamp(curMouse[0] * 2.0 * this.invScreen[0] - 1.0, -1.0, 1.0),
286 | clamp(1.0 - curMouse[1] * 2.0 * this.invScreen[1], -1.0, 1.0));
287 |
288 | var mPrevBall = screenToArcball(mPrev);
289 | var mCurBall = screenToArcball(mCur);
290 | // rotation = curBall * prevBall * rotation
291 | this.rotation = quat.mul(this.rotation, mPrevBall, this.rotation);
292 | this.rotation = quat.mul(this.rotation, mCurBall, this.rotation);
293 |
294 | this.updateCameraMatrix();
295 | }
296 |
297 | ArcballCamera.prototype.zoom = function(amount) {
298 | var vt = vec3.set(vec3.create(), 0.0, 0.0, amount * this.invScreen[1] * this.zoomSpeed);
299 | var t = mat4.fromTranslation(mat4.create(), vt);
300 | this.translation = mat4.mul(this.translation, t, this.translation);
301 | if (this.translation[14] >= -0.2) {
302 | this.translation[14] = -0.2;
303 | }
304 | this.updateCameraMatrix();
305 | }
306 |
307 | ArcballCamera.prototype.pan = function(mouseDelta) {
308 | var delta = vec4.set(vec4.create(), mouseDelta[0] * this.invScreen[0] * Math.abs(this.translation[14]),
309 | mouseDelta[1] * this.invScreen[1] * Math.abs(this.translation[14]), 0, 0);
310 | var worldDelta = vec4.transformMat4(vec4.create(), delta, this.invCamera);
311 | var translation = mat4.fromTranslation(mat4.create(), worldDelta);
312 | this.centerTranslation = mat4.mul(this.centerTranslation, translation, this.centerTranslation);
313 | this.updateCameraMatrix();
314 | }
315 |
316 | ArcballCamera.prototype.updateCameraMatrix = function() {
317 | // camera = translation * rotation * centerTranslation
318 | var rotMat = mat4.fromQuat(mat4.create(), this.rotation);
319 | this.camera = mat4.mul(this.camera, rotMat, this.centerTranslation);
320 | this.camera = mat4.mul(this.camera, this.translation, this.camera);
321 | this.invCamera = mat4.invert(this.invCamera, this.camera);
322 | }
323 |
324 | ArcballCamera.prototype.eyePos = function() {
325 | return [camera.invCamera[12], camera.invCamera[13], camera.invCamera[14]];
326 | }
327 |
328 | ArcballCamera.prototype.eyeDir = function() {
329 | var dir = vec4.set(vec4.create(), 0.0, 0.0, -1.0, 0.0);
330 | dir = vec4.transformMat4(dir, dir, this.invCamera);
331 | dir = vec4.normalize(dir, dir);
332 | return [dir[0], dir[1], dir[2]];
333 | }
334 |
335 | ArcballCamera.prototype.upDir = function() {
336 | var dir = vec4.set(vec4.create(), 0.0, 1.0, 0.0, 0.0);
337 | dir = vec4.transformMat4(dir, dir, this.invCamera);
338 | dir = vec4.normalize(dir, dir);
339 | return [dir[0], dir[1], dir[2]];
340 | }
341 |
342 | var screenToArcball = function(p) {
343 | var dist = vec2.dot(p, p);
344 | if (dist <= 1.0) {
345 | return quat.set(quat.create(), p[0], p[1], Math.sqrt(1.0 - dist), 0);
346 | } else {
347 | var unitP = vec2.normalize(vec2.create(), p);
348 | // cgmath is w, x, y, z
349 | // glmatrix is x, y, z, w
350 | return quat.set(quat.create(), unitP[0], unitP[1], 0, 0);
351 | }
352 | }
353 | var clamp = function(a, min, max) {
354 | return a < min ? min : a > max ? max : a;
355 | }
356 |
357 | var pointDist = function(a, b) {
358 | var v = [b[0] - a[0], b[1] - a[1]];
359 | return Math.sqrt(Math.pow(v[0], 2.0) + Math.pow(v[1], 2.0));
360 | }
361 |
362 | var Buffer = function(capacity, dtype) {
363 | this.len = 0;
364 | this.capacity = capacity;
365 | if (dtype == "uint8") {
366 | this.buffer = new Uint8Array(capacity);
367 | } else if (dtype == "int8") {
368 | this.buffer = new Int8Array(capacity);
369 | } else if (dtype == "uint16") {
370 | this.buffer = new Uint16Array(capacity);
371 | } else if (dtype == "int16") {
372 | this.buffer = new Int16Array(capacity);
373 | } else if (dtype == "uint32") {
374 | this.buffer = new Uint32Array(capacity);
375 | } else if (dtype == "int32") {
376 | this.buffer = new Int32Array(capacity);
377 | } else if (dtype == "float32") {
378 | this.buffer = new Float32Array(capacity);
379 | } else if (dtype == "float64") {
380 | this.buffer = new Float64Array(capacity);
381 | } else {
382 | console.log("ERROR: unsupported type " + dtype);
383 | }
384 | }
385 |
386 | Buffer.prototype.append = function(buf) {
387 | if (this.len + buf.length >= this.capacity) {
388 | var newCap = Math.floor(this.capacity * 1.5);
389 | var tmp = new (this.buffer.constructor)(newCap);
390 | tmp.set(this.buffer);
391 |
392 | this.capacity = newCap;
393 | this.buffer = tmp;
394 | }
395 | this.buffer.set(buf, this.len);
396 | this.len += buf.length;
397 | }
398 |
399 | Buffer.prototype.clear = function() {
400 | this.len = 0;
401 | }
402 |
403 | Buffer.prototype.stride = function() {
404 | return this.buffer.BYTES_PER_ELEMENT;
405 | }
406 |
407 | Buffer.prototype.view = function(offset, length) {
408 | return new (this.buffer.constructor)(this.buffer.buffer, offset, length);
409 | }
410 |
411 | // Various utilities that don't really fit anywhere else
412 |
413 | // Parse the hex string to RGB values in [0, 255]
414 | var hexToRGB = function(hex) {
415 | var val = parseInt(hex.substr(1), 16);
416 | var r = (val >> 16) & 255;
417 | var g = (val >> 8) & 255;
418 | var b = val & 255;
419 | return [r, g, b];
420 | }
421 |
422 | // Parse the hex string to RGB values in [0, 1]
423 | var hexToRGBf = function(hex) {
424 | var c = hexToRGB(hex);
425 | return [c[0] / 255.0, c[1] / 255.0, c[2] / 255.0];
426 | }
427 |
428 | /* The controller can register callbacks for various events on a canvas:
429 | *
430 | * mousemove: function(prevMouse, curMouse, evt)
431 | * receives both regular mouse events, and single-finger drags (sent as a left-click),
432 | *
433 | * press: function(curMouse, evt)
434 | * receives mouse click and touch start events
435 | *
436 | * wheel: function(amount)
437 | * mouse wheel scrolling
438 | *
439 | * pinch: function(amount)
440 | * two finger pinch, receives the distance change between the fingers
441 | *
442 | * twoFingerDrag: function(dragVector)
443 | * two finger drag, receives the drag movement amount
444 | */
445 | var Controller = function() {
446 | this.mousemove = null;
447 | this.press = null;
448 | this.wheel = null;
449 | this.twoFingerDrag = null;
450 | this.pinch = null;
451 | }
452 |
453 | Controller.prototype.registerForCanvas = function(canvas) {
454 | var prevMouse = null;
455 | var mouseState = [false, false];
456 | var self = this;
457 | canvas.addEventListener("mousemove", function(evt) {
458 | evt.preventDefault();
459 | var rect = canvas.getBoundingClientRect();
460 | var curMouse = [evt.clientX - rect.left, evt.clientY - rect.top];
461 | if (!prevMouse) {
462 | prevMouse = [evt.clientX - rect.left, evt.clientY - rect.top];
463 | } else if (self.mousemove) {
464 | self.mousemove(prevMouse, curMouse, evt);
465 | }
466 | prevMouse = curMouse;
467 | });
468 |
469 | canvas.addEventListener("mousedown", function(evt) {
470 | evt.preventDefault();
471 | var rect = canvas.getBoundingClientRect();
472 | var curMouse = [evt.clientX - rect.left, evt.clientY - rect.top];
473 | if (self.press) {
474 | self.press(curMouse, evt);
475 | }
476 | });
477 |
478 | canvas.addEventListener("wheel", function(evt) {
479 | evt.preventDefault();
480 | if (self.wheel) {
481 | self.wheel(-evt.deltaY);
482 | }
483 | });
484 |
485 | canvas.oncontextmenu = function (evt) {
486 | evt.preventDefault();
487 | };
488 |
489 | var touches = {};
490 | canvas.addEventListener("touchstart", function(evt) {
491 | var rect = canvas.getBoundingClientRect();
492 | evt.preventDefault();
493 | for (var i = 0; i < evt.changedTouches.length; ++i) {
494 | var t = evt.changedTouches[i];
495 | touches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top];
496 | if (evt.changedTouches.length == 1 && self.press) {
497 | self.press(touches[t.identifier], evt);
498 | }
499 | }
500 | });
501 |
502 | canvas.addEventListener("touchmove", function(evt) {
503 | evt.preventDefault();
504 | var rect = canvas.getBoundingClientRect();
505 | var numTouches = Object.keys(touches).length;
506 | // Single finger to rotate the camera
507 | if (numTouches == 1) {
508 | if (self.mousemove) {
509 | var t = evt.changedTouches[0];
510 | var prevTouch = touches[t.identifier];
511 | var curTouch = [t.clientX - rect.left, t.clientY - rect.top];
512 | evt.buttons = 1;
513 | self.mousemove(prevTouch, curTouch, evt);
514 | }
515 | } else {
516 | var curTouches = {};
517 | for (var i = 0; i < evt.changedTouches.length; ++i) {
518 | var t = evt.changedTouches[i];
519 | curTouches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top];
520 | }
521 |
522 | // If some touches didn't change make sure we have them in
523 | // our curTouches list to compute the pinch distance
524 | // Also get the old touch points to compute the distance here
525 | var oldTouches = [];
526 | for (t in touches) {
527 | if (!(t in curTouches)) {
528 | curTouches[t] = touches[t];
529 | }
530 | oldTouches.push(touches[t]);
531 | }
532 |
533 | var newTouches = [];
534 | for (t in curTouches) {
535 | newTouches.push(curTouches[t]);
536 | }
537 |
538 | // Determine if the user is pinching or panning
539 | var motionVectors = [
540 | vec2.set(vec2.create(), newTouches[0][0] - oldTouches[0][0],
541 | newTouches[0][1] - oldTouches[0][1]),
542 | vec2.set(vec2.create(), newTouches[1][0] - oldTouches[1][0],
543 | newTouches[1][1] - oldTouches[1][1])
544 | ];
545 | var motionDirs = [vec2.create(), vec2.create()];
546 | vec2.normalize(motionDirs[0], motionVectors[0]);
547 | vec2.normalize(motionDirs[1], motionVectors[1]);
548 |
549 | var pinchAxis = vec2.set(vec2.create(), oldTouches[1][0] - oldTouches[0][0],
550 | oldTouches[1][1] - oldTouches[0][1]);
551 | vec2.normalize(pinchAxis, pinchAxis);
552 |
553 | var panAxis = vec2.lerp(vec2.create(), motionVectors[0], motionVectors[1], 0.5);
554 | vec2.normalize(panAxis, panAxis);
555 |
556 | var pinchMotion = [
557 | vec2.dot(pinchAxis, motionDirs[0]),
558 | vec2.dot(pinchAxis, motionDirs[1])
559 | ];
560 | var panMotion = [
561 | vec2.dot(panAxis, motionDirs[0]),
562 | vec2.dot(panAxis, motionDirs[1])
563 | ];
564 |
565 | // If we're primarily moving along the pinching axis and in the opposite direction with
566 | // the fingers, then the user is zooming.
567 | // Otherwise, if the fingers are moving along the same direction they're panning
568 | if (self.pinch && Math.abs(pinchMotion[0]) > 0.5 && Math.abs(pinchMotion[1]) > 0.5
569 | && Math.sign(pinchMotion[0]) != Math.sign(pinchMotion[1]))
570 | {
571 | // Pinch distance change for zooming
572 | var oldDist = pointDist(oldTouches[0], oldTouches[1]);
573 | var newDist = pointDist(newTouches[0], newTouches[1]);
574 | self.pinch(newDist - oldDist);
575 | } else if (self.twoFingerDrag && Math.abs(panMotion[0]) > 0.5 && Math.abs(panMotion[1]) > 0.5
576 | && Math.sign(panMotion[0]) == Math.sign(panMotion[1]))
577 | {
578 | // Pan by the average motion of the two fingers
579 | var panAmount = vec2.lerp(vec2.create(), motionVectors[0], motionVectors[1], 0.5);
580 | panAmount[1] = -panAmount[1];
581 | self.twoFingerDrag(panAmount);
582 | }
583 | }
584 |
585 | // Update the existing list of touches with the current positions
586 | for (var i = 0; i < evt.changedTouches.length; ++i) {
587 | var t = evt.changedTouches[i];
588 | touches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top];
589 | }
590 | });
591 |
592 | var touchEnd = function(evt) {
593 | evt.preventDefault();
594 | for (var i = 0; i < evt.changedTouches.length; ++i) {
595 | var t = evt.changedTouches[i];
596 | delete touches[t.identifier];
597 | }
598 | }
599 | canvas.addEventListener("touchcancel", touchEnd);
600 | canvas.addEventListener("touchend", touchEnd);
601 | }
602 |
603 |
--------------------------------------------------------------------------------