├── .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 | ![volume renderings](https://i.imgur.com/YqdyKCj.png) 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 | 4 | 5 | 6 | 7 | 10 | 11 | WebGL Volume Raycaster 12 | 13 | 14 |
15 |
16 |
17 |

WebGL Volume Raycaster

18 | 19 |
20 |
21 |
22 | Volume: 23 |
24 |
25 | Colormap: 26 |
27 |
28 |

Controls

29 |

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