<\/div>";
1513 | }
1514 | $('Hmodel').innerHTML = html;
1515 |
1516 | //Load alpha strip
1517 | html='';
1518 | for(i=0; i<=this.size; i++) {
1519 | opac=1.0-i/this.size;
1520 | html += "
<\/div>";
1521 | }
1522 | $('Omodel').innerHTML = html;
1523 | }
1524 |
1525 | //Inherits from MoveWindow
1526 | ColourPicker.prototype = new MoveWindow;
1527 | ColourPicker.prototype.constructor = MoveWindow;
1528 |
1529 | ColourPicker.prototype.pick = function(colour, x, y) {
1530 | //Show the picker, with selected colour
1531 | this.update(colour.HSVA());
1532 | if (this.element.style.display == 'block') return;
1533 | MoveWindow.prototype.open.call(this, x, y);
1534 | }
1535 |
1536 | ColourPicker.prototype.select = function(element, x, y) {
1537 | if (!x || !y) {
1538 | var offset = findElementPos(element); //Requires: mouse.js
1539 | x = x ? x : offset[0]+32;
1540 | y = y ? y : offset[1]+32;
1541 | }
1542 | var colour = new Colour(element.style.backgroundColor);
1543 | //Show the picker, with selected colour
1544 | this.update(colour.HSVA());
1545 | if (this.element.style.display == 'block') return;
1546 | MoveWindow.prototype.open.call(this, x, y);
1547 | this.target = element;
1548 | }
1549 |
1550 | //Mouse event handling
1551 | ColourPicker.prototype.click = function(e, mouse) {
1552 | if (mouse.target.id == "pickCLOSE") {
1553 | if (this.abortfn) this.abortfn();
1554 | toggle('picker');
1555 | } else if (mouse.target.id == "pickOK") {
1556 | if (this.savefn)
1557 | this.savefn(this.picked);
1558 |
1559 | //Set element background
1560 | if (this.target) {
1561 | var colour = new Colour(this.picked);
1562 | this.target.style.backgroundColor = colour.html();
1563 | }
1564 |
1565 | toggle('picker');
1566 | } else if (mouse.target.id == 'SV')
1567 | this.setSV(mouse);
1568 | else if (mouse.target.id == 'Hslide' || mouse.target.className == 'hue')
1569 | this.setHue(mouse);
1570 | else if (mouse.target.id == 'Oslide' || mouse.target.className == 'opacity')
1571 | this.setOpacity(mouse);
1572 | }
1573 |
1574 | ColourPicker.prototype.move = function(e, mouse) {
1575 | //Process left drag
1576 | if (mouse.isdown && mouse.button == 0) {
1577 | if (mouse.target.id == 'picker' || mouse.target.id == 'pickCUR' || mouse.target.id == 'pickRGB') {
1578 | //Call base class function
1579 | MoveWindow.prototype.move.call(this, e, mouse);
1580 | } else if (mouse.target) {
1581 | //Drag on H/O slider acts as click
1582 | this.click(e, mouse);
1583 | }
1584 | }
1585 | }
1586 |
1587 | ColourPicker.prototype.wheel = function(e, mouse) {
1588 | this.incHue(-e.spin);
1589 | }
1590 |
1591 | ColourPicker.prototype.setSV = function(mouse) {
1592 | var X = mouse.clientx - parseInt($('SV').offsetLeft),
1593 | Y = mouse.clienty - parseInt($('SV').offsetTop);
1594 | //Saturation & brightness adjust
1595 | this.picked.S = scale(X, this.size, 0, this.max['S']);
1596 | this.picked.V = this.max['V'] - scale(Y, this.size, 0, this.max['V']);
1597 | this.update(this.picked);
1598 | }
1599 |
1600 | ColourPicker.prototype.setHue = function(mouse) {
1601 | var X = mouse.clientx - parseInt($('H').offsetLeft),
1602 | Y = mouse.clienty - parseInt($('H').offsetTop);
1603 | //Hue adjust
1604 | this.picked.H = scale(Y, this.size, 0, this.max['H']);
1605 | this.update(this.picked);
1606 | }
1607 |
1608 | ColourPicker.prototype.incHue = function(inc) {
1609 | //Hue adjust incrementally
1610 | this.picked.H += inc;
1611 | this.picked.H = clamp(this.picked.H, 0, this.max['H']);
1612 | this.update(this.picked);
1613 | }
1614 |
1615 | ColourPicker.prototype.setOpacity = function(mouse) {
1616 | var X = mouse.clientx - parseInt($('O').offsetLeft),
1617 | Y = mouse.clienty - parseInt($('O').offsetTop);
1618 | //Alpha adjust
1619 | this.picked.A = 1.0 - clamp(Y / this.size, 0, 1);
1620 | this.update(this.picked);
1621 | }
1622 |
1623 | ColourPicker.prototype.updateString = function(str) {
1624 | if (!str) str = prompt('Edit colour:', this.colour.html());
1625 | if (!str) return;
1626 | this.colour = new Colour(str);
1627 | this.update(this.colour.HSV());
1628 | }
1629 |
1630 | ColourPicker.prototype.update = function(HSV) {
1631 | this.picked = HSV;
1632 | this.colour = new Colour(HSV),
1633 | rgba = this.colour.rgbaObj(),
1634 | rgbaStr = this.colour.html(),
1635 | bgcol = new Colour({H:HSV.H, S:100, V:100, A:255});
1636 |
1637 | $('pickRGB').innerHTML=this.colour.printString();
1638 | $S('pickCUR').background=rgbaStr;
1639 | $S('pickCUR').backgroundColour=rgbaStr;
1640 | $S('SV').backgroundColor=bgcol.htmlHex();
1641 |
1642 | //Hue adjust
1643 | $S('Hslide').top = this.size * (HSV.H/360.0) - this.oh + 'px';
1644 | //SV adjust
1645 | $S('SVslide').top = Math.round(this.size - this.size*(HSV.V/100.0) - this.sv) + 'px';
1646 | $S('SVslide').left = Math.round(this.size*(HSV.S/100.0) - this.sv) + 'px';
1647 | //Alpha adjust
1648 | $S('Oslide').top = this.size * (1.0-HSV.A) - this.oh - 1 + 'px';
1649 | };
1650 |
1651 |
1652 |
1653 | /**
1654 | * @constructor
1655 | */
1656 | function GradientEditor(canvas, callback, premultiply, nopicker, scrollable) {
1657 | this.canvas = canvas;
1658 | this.callback = callback;
1659 | this.premultiply = premultiply;
1660 | this.changed = true;
1661 | this.inserting = false;
1662 | this.editing = null;
1663 | this.element = null;
1664 | this.spin = 0;
1665 | this.scrollable = scrollable;
1666 | var self = this;
1667 | function saveColour(val) {self.save(val);}
1668 | function abortColour() {self.cancel();}
1669 | if (!nopicker)
1670 | this.picker = new ColourPicker(this.save.bind(this), this.cancel.bind(this));
1671 |
1672 | //Create default palette object (enable premultiply if required)
1673 | this.palette = new Palette(null, premultiply);
1674 | //Event handling for palette
1675 | this.canvas.mouse = new Mouse(this.canvas, this);
1676 | this.canvas.oncontextmenu="return false;";
1677 | this.canvas.oncontextmenu = function() { return false; }
1678 |
1679 | //this.update();
1680 | }
1681 |
1682 | //Palette management
1683 | GradientEditor.prototype.read = function(source) {
1684 | //Read a new palette from source data
1685 | this.palette = new Palette(source, this.premultiply);
1686 | this.reset();
1687 | this.update(true);
1688 | }
1689 |
1690 | GradientEditor.prototype.update = function(nocallback) {
1691 | //Redraw and flag change
1692 | this.changed = true;
1693 | this.palette.draw(this.canvas, true);
1694 | //Trigger callback if any
1695 | if (!nocallback && this.callback) this.callback(this);
1696 | }
1697 |
1698 | //Draw gradient to passed canvas if data has changed
1699 | //If no changes, return false
1700 | GradientEditor.prototype.get = function(canvas, cache) {
1701 | if (cache && !this.changed) return false;
1702 | this.changed = false;
1703 | //Update passed canvas
1704 | this.palette.draw(canvas, false);
1705 | return true;
1706 | }
1707 |
1708 | GradientEditor.prototype.insert = function(position, x, y) {
1709 | //Flag unsaved new colour
1710 | this.inserting = true;
1711 | var col = new Colour();
1712 | this.editing = this.palette.newColour(position, col)
1713 | this.update();
1714 | //Edit new colour
1715 | this.picker.pick(col, x, y);
1716 | }
1717 |
1718 | GradientEditor.prototype.editBackground = function(element) {
1719 | this.editing = -1;
1720 | var offset = findElementPos(element); //From mouse.js
1721 | this.element = element;
1722 | this.picker.pick(this.palette.background, offset[0]+32, offset[1]+32);
1723 | }
1724 |
1725 | GradientEditor.prototype.edit = function(val, x, y) {
1726 | if (typeof(val) == 'number') {
1727 | this.editing = val;
1728 | this.picker.pick(this.palette.colours[val].colour, x, y);
1729 | } else if (typeof(val) == 'object') {
1730 | //Edit element
1731 | this.cancel(); //Abort any current edit first
1732 | this.element = val;
1733 | var col = new Colour(val.style.backgroundColor)
1734 | var offset = findElementPos(val); //From mouse.js
1735 | this.picker.pick(col, offset[0]+32, offset[1]+32);
1736 | }
1737 | this.update();
1738 | }
1739 |
1740 | GradientEditor.prototype.save = function(val) {
1741 | if (this.editing != null) {
1742 | if (this.editing >= 0)
1743 | //Update colour with selected
1744 | this.palette.colours[this.editing].colour.setHSV(val);
1745 | else
1746 | //Update background colour with selected
1747 | this.palette.background.setHSV(val);
1748 | }
1749 | if (this.element) {
1750 | var col = new Colour(0);
1751 | col.setHSV(val);
1752 | this.element.style.backgroundColor = col.html();
1753 | if (this.element.onchange) this.element.onchange(); //Call change function
1754 | }
1755 | this.reset();
1756 | this.update();
1757 | }
1758 |
1759 | GradientEditor.prototype.cancel = function() {
1760 | //If aborting a new colour add, delete it
1761 | if (this.editing >= 0 && this.inserting)
1762 | this.palette.remove(this.editing);
1763 | this.reset();
1764 | this.update();
1765 | }
1766 |
1767 | GradientEditor.prototype.reset = function() {
1768 | //Reset editing data
1769 | this.inserting = false;
1770 | this.editing = null;
1771 | this.element = null;
1772 | }
1773 |
1774 | //Mouse event handling
1775 | GradientEditor.prototype.click = function(event, mouse) {
1776 | //this.changed = true;
1777 | if (event.ctrlKey) {
1778 | //Flip
1779 | for (var i = 0; i < this.palette.colours.length; i++)
1780 | this.palette.colours[i].position = 1.0 - this.palette.colours[i].position;
1781 | this.update();
1782 | return false;
1783 | }
1784 |
1785 | //Use non-scrolling position
1786 | if (!this.scrollable) mouse.x = mouse.clientx;
1787 |
1788 | if (mouse.slider != null)
1789 | {
1790 | //Slider moved, update texture
1791 | mouse.slider = null;
1792 | this.palette.sort(); //Fix any out of order colours
1793 | this.update();
1794 | return false;
1795 | }
1796 | var pal = this.canvas;
1797 | if (pal.getContext){
1798 | this.cancel(); //Abort any current edit first
1799 | var context = pal.getContext('2d');
1800 | var ypos = findElementPos(pal)[1]+30;
1801 |
1802 | //Get selected colour
1803 | //In range of a colour pos +/- 0.5*slider width?
1804 | var i = this.palette.inRange(mouse.x, this.palette.slider.width, pal.width);
1805 | if (i >= 0) {
1806 | if (event.button == 0) {
1807 | //Edit colour on left click
1808 | this.edit(i, event.clientX-128, ypos);
1809 | } else if (event.button == 2) {
1810 | //Delete on right click
1811 | this.palette.remove(i);
1812 | this.update();
1813 | }
1814 | } else {
1815 | //Clicked elsewhere, add new colour
1816 | this.insert(mouse.x / pal.width, event.clientX-128, ypos);
1817 | }
1818 | }
1819 | return false;
1820 | }
1821 |
1822 | GradientEditor.prototype.down = function(event, mouse) {
1823 | return false;
1824 | }
1825 |
1826 | GradientEditor.prototype.move = function(event, mouse) {
1827 | if (!mouse.isdown) return true;
1828 |
1829 | //Use non-scrolling position
1830 | if (!this.scrollable) mouse.x = mouse.clientx;
1831 |
1832 | if (mouse.slider == null) {
1833 | //Colour slider dragged on?
1834 | var i = this.palette.inDragRange(mouse.x, this.palette.slider.width, this.canvas.width);
1835 | if (i>0) mouse.slider = i;
1836 | }
1837 |
1838 | if (mouse.slider == null)
1839 | mouse.isdown = false; //Abort action if not on slider
1840 | else {
1841 | if (mouse.x < 1) mouse.x = 1;
1842 | if (mouse.x > this.canvas.width-1) mouse.x = this.canvas.width-1;
1843 | //Move to adjusted position and redraw
1844 | this.palette.colours[mouse.slider].position = mouse.x / this.canvas.width;
1845 | this.update(true);
1846 | }
1847 | }
1848 |
1849 | GradientEditor.prototype.wheel = function(event, mouse) {
1850 | if (this.timer)
1851 | clearTimeout(this.timer);
1852 | else
1853 | this.canvas.style.cursor = "wait";
1854 | this.spin += 0.01 * event.spin;
1855 | //this.cycle(0.01 * event.spin);
1856 | var this_ = this;
1857 | this.timer = setTimeout(function() {this_.cycle(this_.spin); this_.spin = 0;}, 150);
1858 | }
1859 |
1860 | GradientEditor.prototype.leave = function(event, mouse) {
1861 | }
1862 |
1863 | GradientEditor.prototype.cycle = function(inc) {
1864 | this.canvas.style.cursor = "default";
1865 | this.timer = null;
1866 | //Shift all colours cyclically
1867 | for (var i = 1; i < this.palette.colours.length-1; i++)
1868 | {
1869 | var x = this.palette.colours[i].position;
1870 | x += inc;
1871 | if (x <= 0) x += 1.0;
1872 | if (x >= 1.0) x -= 1.0;
1873 | this.palette.colours[i].position = x;
1874 | }
1875 | this.palette.sort(); //Fix any out of order colours
1876 | this.update();
1877 | }
1878 |
1879 |
1880 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
WebGL Volume Viewer
7 |
8 |
9 |
13 |
14 |
18 |
19 |
23 |
24 |
28 |
29 |
33 |
34 |
38 |
39 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
101 |
102 |
123 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /** @preserve
2 | * ShareVol
3 | * Lightweight WebGL volume viewer/slicer
4 | *
5 | * Copyright (c) 2014, Monash University. All rights reserved.
6 | * Author: Owen Kaluza - owen.kaluza ( at ) monash.edu
7 | *
8 | * Licensed under the GNU Lesser General Public License
9 | * https://www.gnu.org/licenses/lgpl.html
10 | *
11 | */
12 | //TODO: colourmaps per slicer/volume not shared (global shared list of selectable maps?)
13 | var volume;
14 | var slicer;
15 | var colours;
16 | //Windows...
17 | var info, colourmaps;
18 | var state = {};
19 | var reset;
20 | var filename;
21 | var mobile;
22 |
23 | function initPage() {
24 | window.onresize = autoResize;
25 |
26 | //Create tool windows
27 | info = new Popup("info");
28 | info.show();
29 | colourmaps = new Popup("colourmap", 400, 200);
30 |
31 | try {
32 | if (!window.WebGLRenderingContext)
33 | throw "No browser WebGL support";
34 | var canvas = document.createElement('canvas');
35 | var ctx = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
36 | if (!ctx)
37 | throw "No WebGL context available";
38 | canvas = ctx = null;
39 | } catch (e) {
40 | $('status').innerHTML = "Sorry, ShareVol requires a
WebGL capable browser!";
41 | return;
42 | }
43 |
44 | //Yes it's user agent sniffing, but we need to attempt to detect mobile devices so we don't over-stress their gpu...
45 | mobile = (screen.width <= 760 || /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent));
46 |
47 | //Colour editing and palette management
48 | colours = new GradientEditor($('palette'), updateColourmap);
49 |
50 | //Load json data?
51 | var json = getSearchVariable("data");
52 | //Attempt to load default.json
53 | if (!json) json = "default.json";
54 |
55 | $('status').innerHTML = "Loading params...";
56 | ajaxReadFile(decodeURI(json), loadData, true);
57 | }
58 |
59 | function loadStoredData(key) {
60 | if (localStorage[key]) {
61 | try {
62 | var parsed = JSON.parse(localStorage[key]);
63 | state = parsed;
64 | } catch (e) {
65 | //if erroneous data in local storage, delete
66 | //console.log("parse error: " + e.message);
67 | alert("parse error: " + e.message);
68 | localStorage[key] = null;
69 | }
70 | }
71 | }
72 |
73 | function loadData(src, fn) {
74 | var parsed = JSON.parse(src);
75 | if (parsed.volume) {
76 | //Old data format
77 | state = {}
78 | state.properties = {};
79 | state.colourmaps = [{}];
80 | object = {};
81 | view = {};
82 | state.views = [view];
83 | state.objects = [object];
84 | //Copy fields to their new locations
85 | //Objects
86 | object.name = "volume";
87 | object.samples = parsed.volume.properties.samples;
88 | object.isovalue = parsed.volume.properties.isovalue;
89 | object.isowalls = parsed.volume.properties.drawWalls;
90 | object.isoalpha = parsed.volume.properties.isoalpha;
91 | object.isosmooth = parsed.volume.properties.isosmooth;
92 | object.colour = parsed.volume.properties.isocolour;
93 | object.density = parsed.volume.properties.density;
94 | object.power = parsed.volume.properties.power;
95 | if (parsed.volume.properties.usecolourmap) object.colourmap = 0;
96 | object.tricubicfilter = parsed.volume.properties.tricubicFilter;
97 | object.zmin = parsed.volume.properties.Zmin;
98 | object.zmax = parsed.volume.properties.Zmax;
99 | object.ymin = parsed.volume.properties.Ymin;
100 | object.ymax = parsed.volume.properties.Ymax;
101 | object.xmin = parsed.volume.properties.Xmin;
102 | object.xmax = parsed.volume.properties.Xmax;
103 | object.brightness = parsed.volume.properties.brightness;
104 | object.contrast = parsed.volume.properties.contrast;
105 | //The volume data sub-object
106 | object.volume = {};
107 | object.volume.url = parsed.url;
108 | object.volume.res = parsed.res;
109 | object.volume.scale = parsed.scale;
110 | //The slicer properties
111 | object.slices = parsed.slicer;
112 | //Properties - global rendering properties
113 | state.properties.nogui = parsed.nogui;
114 | //Views - single only in old data
115 | view.axes = parsed.volume.properties.axes;
116 | view.border = parsed.volume.properties.border;
117 | view.translate = parsed.volume.translate;
118 | view.rotate = parsed.volume.rotate;
119 | view.focus = parsed.volume.focus;
120 |
121 | //Colourmap
122 | colours.read(parsed.volume.colourmap);
123 | colours.update();
124 | state.colourmaps = [colours.palette.get()];
125 | delete state.colourmaps[0].background;
126 | state.properties.background = colours.palette.background.html();
127 | } else {
128 | //New format - LavaVu compatible
129 | state = parsed;
130 | }
131 |
132 | reset = state; //Store orig for reset
133 | //Storage reset?
134 | if (getSearchVariable("reset")) {localStorage.removeItem(fn); console.log("Storage cleared");}
135 | /* LOCALSTORAGE DISABLED
136 | //Load any stored presets for this file
137 | filename = fn;
138 | loadStoredData(fn);
139 | */
140 |
141 | //Setup default props from original data...
142 | //state.objects = reset.objects;
143 | if (!state.objects[0].volume.res) state.objects[0].volume.res = [256, 256, 256];
144 | if (!state.objects[0].volume.scale) state.objects[0].volume.scale = [1.0, 1.0, 1.0];
145 |
146 | //Load the image
147 | loadTexture();
148 | }
149 |
150 | function saveData() {
151 | try {
152 | localStorage[filename] = getData();
153 | } catch(e) {
154 | //data wasn’t successfully saved due to quota exceed so throw an error
155 | console.log('LocalStorage Error: Quota exceeded? ' + e);
156 | }
157 | }
158 |
159 | function getData(compact, matrix) {
160 | if (volume) {
161 | var vdat = volume.get(matrix);
162 | var object = state.objects[0];
163 | object.saturation = vdat.properties.saturation;
164 | object.brightness = vdat.properties.brightness;
165 | object.contrast = vdat.properties.contrast;
166 | object.zmin = vdat.properties.zmin;
167 | object.zmax = vdat.properties.zmax;
168 | object.ymin = vdat.properties.ymin;
169 | object.ymax = vdat.properties.ymax;
170 | object.xmin = vdat.properties.xmin;
171 | object.xmax = vdat.properties.xmax;
172 | //object.volume.res = parsed.res;
173 | //object.volume.scale = parsed.scale;
174 | object.samples = vdat.properties.samples;
175 | object.isovalue = vdat.properties.isovalue;
176 | object.isowalls = vdat.properties.isowalls
177 | object.isoalpha = vdat.properties.isoalpha;
178 | object.isosmooth = vdat.properties.isosmooth;
179 | object.colour = vdat.properties.colour;
180 | object.density = vdat.properties.density;
181 | object.power = vdat.properties.power;
182 | object.minclip = parsed.volume.properties.minclip;;
183 | object.maxclip = parsed.volume.properties.maxclip;;
184 | object.tricubicfilter = vdat.properties.tricubicFilter;
185 | if (vdat.properties.usecolourmap)
186 | object.colourmap = 0;
187 | else
188 | delete object.colourmap;
189 |
190 | //Views - single only in old data
191 | state.views[0].axes = vdat.properties.axes;
192 | state.views[0].border = vdat.properties.border;
193 | state.views[0].translate = vdat.translate;
194 | state.views[0].rotate = vdat.rotate;
195 |
196 | if (slicer)
197 | state.objects[0].slices = slicer.get();
198 |
199 | //Colourmap
200 | state.colourmaps = [colours.palette.get()];
201 | delete state.colourmaps[0].background;
202 | state.properties.background = colours.palette.background.html();
203 | }
204 |
205 | //Return compact json string
206 | console.log(JSON.stringify(state, null, 2));
207 | if (compact) return JSON.stringify(state);
208 | //Otherwise return indented json string
209 | return JSON.stringify(state, null, 2);
210 | }
211 |
212 | function exportData() {
213 | window.open('data:text/json;base64,' + window.btoa(getData()));
214 | }
215 |
216 | function resetFromData(src) {
217 | //Restore data from saved props
218 | if (src.objects[0].volume && volume) {
219 | volume.load(src.objects[0]);
220 | volume.draw();
221 | }
222 |
223 | if (src.objects[0].slices && slicer) {
224 | slicer.load(src.objects[0].slices);
225 | slicer.draw();
226 | }
227 | }
228 |
229 | function loadTexture() {
230 | $('status').innerHTML = "Loading image data... ";
231 | var image;
232 |
233 | loadImage(state.objects[0].volume.url, function () {
234 | image = new Image();
235 |
236 | var headers = request.getAllResponseHeaders();
237 | var match = headers.match( /^Content-Type\:\s*(.*?)$/mi );
238 | var mimeType = match[1] || 'image/png';
239 | var blob = new Blob([request.response], {type: mimeType} );
240 | image.src = window.URL.createObjectURL(blob);
241 | var imageElement = document.createElement("img");
242 |
243 | image.onload = function () {
244 | console.log("Loaded image: " + image.width + " x " + image.height);
245 | imageLoaded(image);
246 | }
247 | }
248 | );
249 | }
250 |
251 | function imageLoaded(image) {
252 | //Create the slicer
253 | if (state.objects[0].slices) {
254 | if (mobile) state.objects[0].slices.show = false; //Start hidden on small screen
255 | slicer = new Slicer(state.objects[0], image, "linear");
256 | }
257 |
258 | //Create the volume viewer
259 | if (state.objects[0].volume) {
260 | interactive = true;
261 | if (mobile || state.properties.interactive == false) interactive = false;
262 | volume = new Volume(state.objects[0], image, interactive);
263 | volume.slicer = slicer; //For axis position
264 | }
265 |
266 | //Volume draw on mouseup to apply changes from other controls (including slicer)
267 | document.addEventListener("mouseup", function(ev) {if (volume) volume.delayedRender(250, true);}, false);
268 | document.addEventListener("wheel", function(ev) {if (volume) volume.delayedRender(250, true);}, false);
269 |
270 | //Update colours (and draw objects)
271 | colours.read(state.colourmaps[0].colours);
272 | //Copy the global background colour
273 | colours.palette.background = new Colour(state.properties.background);
274 | colours.update();
275 |
276 | info.hide(); //Status
277 |
278 | /*/Draw speed test
279 | frames = 0;
280 | testtime = new Date().getTime();
281 | info.show();
282 | volume.draw(false, true);*/
283 |
284 | if (!state.properties.nogui) {
285 | var gui = new dat.GUI();
286 | if (state.properties.server)
287 | gui.add({"Update" : function() {ajaxPost(state.properties.server + "/update", "data=" + encodeURIComponent(getData(true, true)));}}, 'Update');
288 | /* LOCALSTORAGE DISABLED
289 | gui.add({"Reset" : function() {resetFromData(reset);}}, 'Reset');*/
290 | gui.add({"Restore" : function() {resetFromData(state);}}, 'Restore');
291 | gui.add({"Export" : function() {exportData();}}, 'Export');
292 | //gui.add({"loadFile" : function() {document.getElementById('fileupload').click();}}, 'loadFile'). name('Load Image file');
293 | gui.add({"ColourMaps" : function() {window.colourmaps.toggle();}}, 'ColourMaps');
294 |
295 | var f = gui.addFolder('Views');
296 | var ir2 = 1.0 / Math.sqrt(2.0);
297 | f.add({"XY" : function() {volume.rotate = quat4.create([0, 0, 0, 1]);}}, 'XY');
298 | f.add({"YX" : function() {volume.rotate = quat4.create([0, 1, 0, 0]);}}, 'YX');
299 | f.add({"XZ" : function() {volume.rotate = quat4.create([ir2, 0, 0, -ir2]);}}, 'XZ');
300 | f.add({"ZX" : function() {volume.rotate = quat4.create([ir2, 0, 0, ir2]);}}, 'ZX');
301 | f.add({"YZ" : function() {volume.rotate = quat4.create([0, -ir2, 0, -ir2]);}}, 'YZ');
302 | f.add({"ZY" : function() {volume.rotate = quat4.create([0, -ir2, 0, ir2]);}}, 'ZY');
303 |
304 | if (volume) volume.addGUI(gui);
305 | if (slicer) slicer.addGUI(gui);
306 | }
307 |
308 | //Save props on exit
309 | window.onbeforeunload = saveData;
310 | }
311 |
312 | /////////////////////////////////////////////////////////////////////////
313 | function autoResize() {
314 | if (volume) {
315 | volume.width = 0; //volume.canvas.width = window.innerWidth;
316 | volume.height = 0; //volume.canvas.height = window.innerHeight;
317 | volume.draw();
318 | }
319 | }
320 |
321 | function updateColourmap() {
322 | if (!colours) return;
323 | var gradient = $('gradient');
324 | colours.palette.draw(gradient, false);
325 |
326 | if (volume && volume.webgl) {
327 | volume.webgl.updateTexture(volume.webgl.gradientTexture, gradient, volume.gl.TEXTURE1); //Use 2nd texture unit
328 | volume.applyBackground(colours.palette.background.html());
329 | volume.draw();
330 | }
331 |
332 | if (slicer) {
333 | slicer.updateColourmap();
334 | slicer.draw();
335 | }
336 | }
337 |
338 | var request, progressBar;
339 |
340 | function loadImage(imageURI, callback)
341 | {
342 | request = new XMLHttpRequest();
343 | request.onloadstart = showProgressBar;
344 | request.onprogress = updateProgressBar;
345 | request.onload = callback;
346 | request.onloadend = hideProgressBar;
347 | request.open("GET", imageURI, true);
348 | request.responseType = 'arraybuffer';
349 | request.send(null);
350 | }
351 |
352 | function showProgressBar()
353 | {
354 | progressBar = document.createElement("progress");
355 | progressBar.value = 0;
356 | progressBar.max = 100;
357 | progressBar.removeAttribute("value");
358 | document.getElementById('status').appendChild(progressBar);
359 | }
360 |
361 | function updateProgressBar(e)
362 | {
363 | if (e.lengthComputable)
364 | progressBar.value = e.loaded / e.total * 100;
365 | else
366 | progressBar.removeAttribute("value");
367 | }
368 |
369 | function hideProgressBar()
370 | {
371 | document.getElementById('status').removeChild(progressBar);
372 | }
373 |
374 | /**
375 | * @constructor
376 | */
377 | function Popup(id, x, y) {
378 | this.el = $(id);
379 | this.style = $S(id);
380 | if (x && y) {
381 | this.style.left = x + 'px';
382 | this.style.top = y + 'px';
383 | } else {
384 | this.style.left = ((window.innerWidth - this.el.offsetWidth) * 0.5) + 'px';
385 | this.style.top = ((window.innerHeight - this.el.offsetHeight) * 0.5) + 'px';
386 | }
387 | this.drag = false;
388 | }
389 |
390 | Popup.prototype.toggle = function() {
391 | if (this.style.visibility == 'visible')
392 | this.hide();
393 | else
394 | this.show();
395 | }
396 |
397 | Popup.prototype.show = function() {
398 | this.style.visibility = 'visible';
399 | }
400 |
401 | Popup.prototype.hide = function() {
402 | this.style.visibility = 'hidden';
403 | }
404 |
405 |
--------------------------------------------------------------------------------
/src/shaders/lineShaderWEBGL.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | varying vec4 vColour;
3 |
4 | void main(void)
5 | {
6 | gl_FragColor = vColour;
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/shaders/lineShaderWEBGL.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | attribute vec3 aVertexPosition;
4 | attribute vec4 aVertexColour;
5 |
6 | uniform mat4 uMVMatrix;
7 | uniform mat4 uPMatrix;
8 |
9 | uniform vec4 uColour;
10 | uniform float uAlpha;
11 |
12 | varying vec4 vColour;
13 |
14 | void main(void)
15 | {
16 | vec4 mvPosition = uMVMatrix * vec4(aVertexPosition, 1.0);
17 | gl_Position = uPMatrix * mvPosition;
18 | vec4 colour = aVertexColour;
19 | float alpha = 1.0;
20 | if (uColour.a > 0.01) colour = uColour;
21 | if (uAlpha > 0.01) alpha = uAlpha;
22 | vColour = vec4(colour.rgb, colour.a * alpha);
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/shaders/textureShaderWEBGL.frag:
--------------------------------------------------------------------------------
1 | //Texture fragment shader
2 | precision mediump float;
3 | #define rgba vec4
4 |
5 | //Palette lookup mu = [0,1]
6 | #define gradient(mu) texture2D(palette, vec2(mu, 0.0))
7 |
8 | //Uniform data
9 | uniform sampler2D palette;
10 | uniform sampler2D texture;
11 |
12 | uniform int colourmap;
13 | uniform float bright;
14 | uniform float cont;
15 | uniform float power;
16 |
17 | uniform int axis;
18 | uniform vec3 slice;
19 | uniform ivec3 res;
20 | uniform vec2 dim;
21 |
22 | uniform ivec2 select;
23 |
24 | //Current coordinate
25 | varying vec2 vCoord;
26 |
27 | void main()
28 | {
29 | bool invert = false;
30 | vec2 coord;
31 | float z;
32 |
33 | if (int(gl_FragCoord.x) == select.x) invert = true;
34 | if (int(gl_FragCoord.y) == select.y) invert = true;
35 |
36 | if (axis==0)
37 | {
38 | //x-axis slice
39 | //slice offset coords from vCoord.x, inside coords from (slice,vCoord.y)
40 | z = vCoord.x * float(res.z);
41 | coord = vec2(clamp(slice.x, 0.0, 0.999), vCoord.y);
42 | }
43 | else if (axis==1)
44 | {
45 | //y-axis slice
46 | //slice offset coords from vCoord.y, inside coords from (vCoord.x,slice)
47 | z = vCoord.y * float(res.z);
48 | coord = vec2(vCoord.x, clamp(slice.y, 0.0, 0.999));
49 | }
50 | else if (axis==2)
51 | {
52 | //z-axis slice
53 | //slice offset coords from slice.z, inside coords unchanged (vCoord.xy)
54 | z = slice.z * float(res.z);
55 | coord = vCoord;
56 | }
57 |
58 | //Get offsets to selected slice
59 | float xy = z/dim.x;
60 | int row = int(xy);
61 | //mod() function doesn't work properly on safari, use fract() instead
62 | //int col = int(fract(xy) * dim.x);
63 | int col = int(fract(xy) * dim.x);
64 | coord += vec2(float(col), float(row));
65 | //Rescale to texture coords [0,1]
66 | coord /= dim;
67 |
68 | //Get texture value at coord and calculate final colour
69 | vec4 tex = texture2D(texture, coord);
70 | float lum = tex.r; //0.3 * tex.r + 0.59 * tex.g + 0.11 * tex.b;
71 | lum = pow(lum, power);
72 | vec4 pixelColor;
73 | if (colourmap == 1)
74 | {
75 | pixelColor = gradient(lum);
76 | }
77 | else
78 | {
79 | pixelColor = vec4(lum, lum, lum, 1.0);
80 | }
81 | pixelColor.rgb = ((pixelColor.rgb - 0.5) * max(cont, 0.0)) + 0.5;
82 | pixelColor.rgb += bright;
83 | if (invert)
84 | {
85 | pixelColor.rgb = vec3(1.0) - pixelColor.rgb;
86 | pixelColor.a = 1.0;
87 | }
88 | gl_FragColor = pixelColor;
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/src/shaders/textureShaderWEBGL.vert:
--------------------------------------------------------------------------------
1 | //A simple vertex shader for 2d image processing
2 | //Pass the vertex coords to fragment shader in vCoord
3 | precision highp float;
4 | attribute vec3 aVertexPosition;
5 | uniform mat4 uMVMatrix;
6 | varying vec2 vCoord;
7 | void main(void) {
8 | gl_Position = vec4(aVertexPosition, 1.0);
9 | //Apply translation, rotation & scaling matrix to vertices to get coords
10 | vec4 coords = uMVMatrix * vec4(aVertexPosition.xy, 0.0, 1.0);
11 | vCoord = coords.xy;
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/shaders/volumeShaderWEBGL.frag:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014, Monash University. All rights reserved.
3 | * Author: Owen Kaluza - owen.kaluza ( at ) monash.edu
4 | *
5 | * Licensed under the GNU Lesser General Public License
6 | * https://www.gnu.org/licenses/lgpl.html
7 | */
8 | precision highp float;
9 |
10 | //Defined dynamically before compile...
11 | //const vec2 slices = vec2(16.0,16.0);
12 | //const int maxSamples = 256;
13 |
14 | uniform sampler2D uVolume;
15 | uniform sampler2D uTransferFunction;
16 |
17 | uniform vec3 uBBMin;
18 | uniform vec3 uBBMax;
19 | uniform vec3 uResolution;
20 |
21 | uniform bool uEnableColour;
22 |
23 | uniform float uBrightness;
24 | uniform float uContrast;
25 | uniform float uSaturation;
26 | uniform float uPower;
27 |
28 | uniform mat4 uPMatrix;
29 | uniform mat4 uInvPMatrix;
30 | uniform mat4 uMVMatrix;
31 | uniform mat4 uNMatrix;
32 | uniform vec4 uViewport;
33 | uniform int uSamples;
34 | uniform float uDensityFactor;
35 | uniform float uIsoValue;
36 | uniform vec4 uIsoColour;
37 | uniform float uIsoSmooth;
38 | uniform int uIsoWalls;
39 | uniform int uFilter;
40 | uniform vec2 uRange;
41 | uniform vec2 uDenMinMax;
42 |
43 | //#define tex3D(pos) interpolate_tricubic_fast(pos)
44 | //#define tex3D(pos) texture3Dfrom2D(pos).x
45 |
46 | vec2 islices = vec2(1.0 / slices.x, 1.0 / slices.y);
47 |
48 | vec4 texture3Dfrom2D(vec3 pos)
49 | {
50 | //Get z slice index and position between two slices
51 | float Z = pos.z * slices.x * slices.y;
52 | int slice = int(Z); //Index of first slice
53 |
54 | //X & Y coords of sample scaled to slice size
55 | vec2 sampleOffset = pos.xy * islices;
56 | //Offsets in 2D texture of given slice indices
57 | //(add offsets to scaled position within slice to get sample positions)
58 | float A = float(slice) * islices.x;
59 | float B = float(slice+1) * islices.x;
60 | vec2 z1offset = vec2(fract(A), floor(A) / slices.y) + sampleOffset;
61 | vec2 z2offset = vec2(fract(B), floor(B) / slices.y) + sampleOffset;
62 |
63 | //Interpolate the final value by position between slices [0,1]
64 | return mix(texture2D(uVolume, z1offset), texture2D(uVolume, z2offset), fract(Z));
65 | }
66 |
67 | float interpolate_tricubic_fast(vec3 coord);
68 |
69 | float tex3D(vec3 pos)
70 | {
71 | if (uFilter > 0)
72 | return interpolate_tricubic_fast(pos);
73 | return texture3Dfrom2D(pos).x;
74 | }
75 |
76 | // It seems WebGL has no transpose
77 | mat4 transpose(in mat4 m)
78 | {
79 | return mat4(
80 | vec4(m[0].x, m[1].x, m[2].x, m[3].x),
81 | vec4(m[0].y, m[1].y, m[2].y, m[3].y),
82 | vec4(m[0].z, m[1].z, m[2].z, m[3].z),
83 | vec4(m[0].w, m[1].w, m[2].w, m[3].w)
84 | );
85 | }
86 |
87 | //Light moves with camera
88 | const vec3 lightPos = vec3(0.5, 0.5, 5.0);
89 | const float ambient = 0.2;
90 | const float diffuse = 0.8;
91 | const vec3 diffColour = vec3(1.0, 1.0, 1.0); //Colour of diffuse light
92 | const vec3 ambColour = vec3(0.2, 0.2, 0.2); //Colour of ambient light
93 |
94 | void lighting(in vec3 pos, in vec3 normal, inout vec3 colour)
95 | {
96 | vec4 vertPos = uMVMatrix * vec4(pos, 1.0);
97 | vec3 lightDir = normalize(lightPos - vertPos.xyz);
98 | vec3 lightWeighting = ambColour + diffColour * diffuse * clamp(abs(dot(normal, lightDir)), 0.1, 1.0);
99 |
100 | colour *= lightWeighting;
101 | }
102 |
103 | vec3 isoNormal(in vec3 pos, in vec3 shift, in float density)
104 | {
105 | vec3 shiftpos = vec3(pos.x + shift.x, pos.y + shift.y, pos.z + shift.z);
106 | vec3 shiftx = vec3(shiftpos.x, pos.y, pos.z);
107 | vec3 shifty = vec3(pos.x, shiftpos.y, pos.z);
108 | vec3 shiftz = vec3(pos.x, pos.y, shiftpos.z);
109 |
110 | //Detect bounding box hit (walls)
111 | if (uIsoWalls > 0)
112 | {
113 | if (pos.x <= uBBMin.x) return vec3(-1.0, 0.0, 0.0);
114 | if (pos.x >= uBBMax.x) return vec3(1.0, 0.0, 0.0);
115 | if (pos.y <= uBBMin.y) return vec3(0.0, -1.0, 0.0);
116 | if (pos.y >= uBBMax.y) return vec3(0.0, 1.0, 0.0);
117 | if (pos.z <= uBBMin.z) return vec3(0.0, 0.0, -1.0);
118 | if (pos.z >= uBBMax.z) return vec3(0.0, 0.0, 1.0);
119 | }
120 |
121 | //Calculate normal
122 | return vec3(density) - vec3(tex3D(shiftx), tex3D(shifty), tex3D(shiftz));
123 | }
124 |
125 | vec2 rayIntersectBox(vec3 rayDirection, vec3 rayOrigin)
126 | {
127 | //Intersect ray with bounding box
128 | vec3 rayInvDirection = 1.0 / rayDirection;
129 | vec3 bbMinDiff = (uBBMin - rayOrigin) * rayInvDirection;
130 | vec3 bbMaxDiff = (uBBMax - rayOrigin) * rayInvDirection;
131 | vec3 imax = max(bbMaxDiff, bbMinDiff);
132 | vec3 imin = min(bbMaxDiff, bbMinDiff);
133 | float back = min(imax.x, min(imax.y, imax.z));
134 | float front = max(max(imin.x, 0.0), max(imin.y, imin.z));
135 | return vec2(back, front);
136 | }
137 |
138 | void main()
139 | {
140 | //Compute eye space coord from window space to get the ray direction
141 | mat4 invMVMatrix = transpose(uMVMatrix);
142 | //ObjectSpace *[MV] = EyeSpace *[P] = Clip /w = Normalised device coords ->VP-> Window
143 | //Window ->[VP^]-> NDC ->[/w]-> Clip ->[P^]-> EyeSpace ->[MV^]-> ObjectSpace
144 | vec4 ndcPos;
145 | ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * uViewport.xy)) / (uViewport.zw) - 1.0;
146 | ndcPos.z = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far) /
147 | (gl_DepthRange.far - gl_DepthRange.near);
148 | ndcPos.w = 1.0;
149 | vec4 clipPos = ndcPos / gl_FragCoord.w;
150 | //vec4 eyeSpacePos = uInvPMatrix * clipPos;
151 | vec3 rayDirection = normalize((invMVMatrix * uInvPMatrix * clipPos).xyz);
152 |
153 | //Ray origin from the camera position
154 | vec4 camPos = -vec4(uMVMatrix[3]); //4th column of modelview
155 | vec3 rayOrigin = (invMVMatrix * camPos).xyz;
156 |
157 | //Calc step
158 | float stepSize = 1.732 / float(uSamples); //diagonal of [0,1] normalised coord cube = sqrt(3)
159 |
160 | //Intersect ray with bounding box
161 | vec2 intersection = rayIntersectBox(rayDirection, rayOrigin);
162 | //Subtract small increment to avoid errors on front boundary
163 | intersection.y -= 0.000001;
164 | //Discard points outside the box (no intersection)
165 | if (intersection.x <= intersection.y) discard;
166 |
167 | vec3 rayStart = rayOrigin + rayDirection * intersection.y;
168 | vec3 rayStop = rayOrigin + rayDirection * intersection.x;
169 |
170 | vec3 step = normalize(rayStop-rayStart) * stepSize;
171 | vec3 pos = rayStart;
172 |
173 | float T = 1.0;
174 | vec3 colour = vec3(0.0);
175 | bool inside = false;
176 | vec3 shift = uIsoSmooth / uResolution;
177 | //Number of samples to take along this ray before we pass out back of volume...
178 | float travel = distance(rayStop, rayStart) / stepSize;
179 | int samples = int(ceil(travel));
180 | float range = uRange.y - uRange.x;
181 | if (range <= 0.0) range = 1.0;
182 | //Scale isoValue
183 | float isoValue = uRange.x + uIsoValue * range;
184 |
185 | //Raymarch, front to back
186 | for (int i=0; i < maxSamples; ++i)
187 | {
188 | //Render samples until we pass out back of cube or fully opaque
189 | #ifndef IE11
190 | if (i == samples || T < 0.01) break;
191 | #else
192 | //This is slower but allows IE 11 to render, break on non-uniform condition causes it to fail
193 | if (i == uSamples) break;
194 | if (all(greaterThanEqual(pos, uBBMin)) && all(lessThanEqual(pos, uBBMax)))
195 | #endif
196 | {
197 | //Get density
198 | float density = tex3D(pos);
199 |
200 | #define ISOSURFACE
201 | #ifdef ISOSURFACE
202 | //Passed through isosurface?
203 | if (isoValue > uRange.x && ((!inside && density >= isoValue) || (inside && density < isoValue)))
204 | {
205 | inside = !inside;
206 | //Find closer to exact position by iteration
207 | //http://sizecoding.blogspot.com.au/2008/08/isosurfaces-in-glsl.html
208 | float exact;
209 | float a = intersection.y + (float(i)*stepSize);
210 | float b = a - stepSize;
211 | for (int j = 0; j < 5; j++)
212 | {
213 | exact = (b + a) * 0.5;
214 | pos = rayDirection * exact + rayOrigin;
215 | density = tex3D(pos);
216 | if (density - isoValue < 0.0)
217 | b = exact;
218 | else
219 | a = exact;
220 | }
221 |
222 | //Skip edges unless flagged to draw
223 | if (uIsoWalls > 0 || all(greaterThanEqual(pos, uBBMin)) && all(lessThanEqual(pos, uBBMax)))
224 | {
225 | vec4 value = vec4(uIsoColour.rgb, 1.0);
226 |
227 | //normal = normalize(normal);
228 | //if (length(normal) < 1.0) normal = vec3(0.0, 1.0, 0.0);
229 | vec3 normal = normalize(mat3(uNMatrix) * isoNormal(pos, shift, density));
230 |
231 | vec3 light = value.rgb;
232 | lighting(pos, normal, light);
233 | //Front-to-back blend equation
234 | colour += T * uIsoColour.a * light;
235 | T *= (1.0 - uIsoColour.a);
236 | }
237 | }
238 | #endif
239 |
240 | if (uDensityFactor > 0.0)
241 | {
242 | //Normalise the density over provided range
243 | density = (density - uRange.x) / range;
244 | density = clamp(density, 0.0, 1.0);
245 | if (density < uDenMinMax[0] || density > uDenMinMax[1])
246 | {
247 | //Skip to next sample...
248 | pos += step;
249 | continue;
250 | }
251 |
252 | density = pow(density, uPower); //Apply power
253 |
254 | vec4 value;
255 | if (uEnableColour)
256 | value = texture2D(uTransferFunction, vec2(density, 0.5));
257 | else
258 | value = vec4(density);
259 |
260 | value *= uDensityFactor * stepSize;
261 |
262 | //Color
263 | colour += T * value.rgb;
264 | //Alpha
265 | T *= 1.0 - value.a;
266 | }
267 | }
268 |
269 | //Next sample...
270 | pos += step;
271 | }
272 |
273 | //Apply brightness, saturation & contrast
274 | colour += uBrightness;
275 | const vec3 LumCoeff = vec3(0.2125, 0.7154, 0.0721);
276 | vec3 AvgLumin = vec3(0.5, 0.5, 0.5);
277 | vec3 intensity = vec3(dot(colour, LumCoeff));
278 | colour = mix(intensity, colour, uSaturation);
279 | colour = mix(AvgLumin, colour, uContrast);
280 |
281 | if (T > 0.95) discard;
282 | gl_FragColor = vec4(colour, 1.0 - T);
283 |
284 | #ifdef WRITE_DEPTH
285 | /* Write the depth !Not supported in WebGL without extension */
286 | vec4 clip_space_pos = uPMatrix * uMVMatrix * vec4(rayStart, 1.0);
287 | float ndc_depth = clip_space_pos.z / clip_space_pos.w;
288 | float depth = (((gl_DepthRange.far - gl_DepthRange.near) * ndc_depth) +
289 | gl_DepthRange.near + gl_DepthRange.far) / 2.0;
290 | gl_FragDepth = depth;
291 | #endif
292 | }
293 |
294 | float interpolate_tricubic_fast(vec3 coord)
295 | {
296 | /* License applicable to this function:
297 | Copyright (c) 2008-2013, Danny Ruijters. All rights reserved.
298 |
299 | Redistribution and use in source and binary forms, with or without
300 | modification, are permitted provided that the following conditions are met:
301 | * Redistributions of source code must retain the above copyright
302 | notice, this list of conditions and the following disclaimer.
303 | * Redistributions in binary form must reproduce the above copyright
304 | notice, this list of conditions and the following disclaimer in the
305 | documentation and/or other materials provided with the distribution.
306 | * Neither the name of the copyright holders nor the names of its
307 | contributors may be used to endorse or promote products derived from
308 | this software without specific prior written permission.
309 |
310 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
311 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
312 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
313 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
314 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
315 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
316 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
317 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
318 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
319 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
320 | POSSIBILITY OF SUCH DAMAGE.
321 |
322 | The views and conclusions contained in the software and documentation are
323 | those of the authors and should not be interpreted as representing official
324 | policies, either expressed or implied.
325 |
326 | When using this code in a scientific project, please cite one or all of the
327 | following papers:
328 | * Daniel Ruijters and Philippe Thévenaz,
329 | GPU Prefilter for Accurate Cubic B-Spline Interpolation,
330 | The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012.
331 | * Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens,
332 | Efficient GPU-Based Texture Interpolation using Uniform B-Splines,
333 | Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008.
334 | */
335 | // shift the coordinate from [0,1] to [-0.5, nrOfVoxels-0.5]
336 | vec3 nrOfVoxels = uResolution; //textureSize3D(tex, 0));
337 | vec3 coord_grid = coord * nrOfVoxels - 0.5;
338 | vec3 index = floor(coord_grid);
339 | vec3 fraction = coord_grid - index;
340 | vec3 one_frac = 1.0 - fraction;
341 |
342 | vec3 w0 = 1.0/6.0 * one_frac*one_frac*one_frac;
343 | vec3 w1 = 2.0/3.0 - 0.5 * fraction*fraction*(2.0-fraction);
344 | vec3 w2 = 2.0/3.0 - 0.5 * one_frac*one_frac*(2.0-one_frac);
345 | vec3 w3 = 1.0/6.0 * fraction*fraction*fraction;
346 |
347 | vec3 g0 = w0 + w1;
348 | vec3 g1 = w2 + w3;
349 | vec3 mult = 1.0 / nrOfVoxels;
350 | vec3 h0 = mult * ((w1 / g0) - 0.5 + index); //h0 = w1/g0 - 1, move from [-0.5, nrOfVoxels-0.5] to [0,1]
351 | vec3 h1 = mult * ((w3 / g1) + 1.5 + index); //h1 = w3/g1 + 1, move from [-0.5, nrOfVoxels-0.5] to [0,1]
352 |
353 | // fetch the eight linear interpolations
354 | // weighting and fetching is interleaved for performance and stability reasons
355 | float tex000 = texture3Dfrom2D(h0).r;
356 | float tex100 = texture3Dfrom2D(vec3(h1.x, h0.y, h0.z)).r;
357 | tex000 = mix(tex100, tex000, g0.x); //weigh along the x-direction
358 | float tex010 = texture3Dfrom2D(vec3(h0.x, h1.y, h0.z)).r;
359 | float tex110 = texture3Dfrom2D(vec3(h1.x, h1.y, h0.z)).r;
360 | tex010 = mix(tex110, tex010, g0.x); //weigh along the x-direction
361 | tex000 = mix(tex010, tex000, g0.y); //weigh along the y-direction
362 | float tex001 = texture3Dfrom2D(vec3(h0.x, h0.y, h1.z)).r;
363 | float tex101 = texture3Dfrom2D(vec3(h1.x, h0.y, h1.z)).r;
364 | tex001 = mix(tex101, tex001, g0.x); //weigh along the x-direction
365 | float tex011 = texture3Dfrom2D(vec3(h0.x, h1.y, h1.z)).r;
366 | float tex111 = texture3Dfrom2D(h1).r;
367 | tex011 = mix(tex111, tex011, g0.x); //weigh along the x-direction
368 | tex001 = mix(tex011, tex001, g0.y); //weigh along the y-direction
369 |
370 | return mix(tex001, tex000, g0.z); //weigh along the z-direction
371 | }
372 |
373 |
--------------------------------------------------------------------------------
/src/shaders/volumeShaderWEBGL.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | attribute vec3 aVertexPosition;
3 | void main(void)
4 | {
5 | gl_Position = vec4(aVertexPosition, 1.0);
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/src/slicer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * ShareVol
3 | * Lightweight WebGL volume viewer/slicer
4 | *
5 | * Copyright (c) 2014, Monash University. All rights reserved.
6 | * Author: Owen Kaluza - owen.kaluza ( at ) monash.edu
7 | *
8 | * Licensed under the GNU Lesser General Public License
9 | * https://www.gnu.org/licenses/lgpl.html
10 | *
11 | */
12 |
13 | function Slicer(props, image, filter, parentEl) {
14 | this.image = image;
15 | this.res = props.volume.res;
16 | this.dims = [props.volume.res[0] * props.volume.scale[0],
17 | props.volume.res[1] * props.volume.scale[1],
18 | props.volume.res[2] * props.volume.scale[2]];
19 | this.slices = [0.5, 0.5, 0.5];
20 |
21 | // Set properties
22 | this.properties = {};
23 | this.properties.show = true;
24 | this.properties.X = Math.round(this.res[0] / 2);
25 | this.properties.Y = Math.round(this.res[1] / 2);
26 | this.properties.Z = Math.round(this.res[2] / 2);
27 | this.properties.brightness = 0.0;
28 | this.properties.contrast = 1.0;
29 | this.properties.power = 1.0;
30 | this.properties.usecolourmap = false;
31 | this.properties.layout = "xyz";
32 | this.flipY = false;
33 | this.properties.zoom = 1.0;
34 |
35 | this.container = document.createElement("div");
36 | this.container.style.cssText = "position: absolute; bottom: 10px; left: 10px; margin: 0px; padding: 0px; pointer-events: none;";
37 | if (!parentEl) parentEl = document.body;
38 | parentEl.appendChild(this.container);
39 |
40 | //Load from local storage or previously loaded file
41 | if (props.slices) this.load(props.slices);
42 |
43 | this.canvas = document.createElement("canvas");
44 | this.canvas.style.cssText = "position: absolute; bottom: 0px; margin: 0px; padding: 0px; border: none; background: rgba(0,0,0,0); pointer-events: none;";
45 |
46 | this.doLayout();
47 |
48 | this.canvas.mouse = new Mouse(this.canvas, this);
49 |
50 | this.webgl = new WebGL(this.canvas);
51 | this.gl = this.webgl.gl;
52 |
53 | this.filter = this.gl.NEAREST; //Nearest-neighbour (default)
54 | if (filter == "linear") this.filter = this.gl.LINEAR;
55 |
56 | //Use the default buffers
57 | this.webgl.init2dBuffers(this.gl.TEXTURE2);
58 |
59 | //Compile the shaders
60 | this.program = new WebGLProgram(this.gl, 'texture-vs', 'texture-fs');
61 | if (this.program.errors) OK.debug(this.program.errors);
62 | this.program.setup(["aVertexPosition"], ["palette", "texture", "colourmap", "cont", "bright", "power", "slice", "dim", "res", "axis", "select"]);
63 |
64 | this.gl.clearColor(0, 0, 0, 0);
65 | this.gl.enable(this.gl.BLEND);
66 | this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
67 | this.gl.enable(this.gl.SCISSOR_TEST);
68 |
69 | //Load the textures
70 | this.loadImage(this.image);
71 |
72 | //Hidden?
73 | if (!this.properties.show) this.toggle();
74 | }
75 |
76 | Slicer.prototype.toggle = function() {
77 | if (this.container.style.visibility == 'hidden')
78 | this.container.style.visibility = 'visible';
79 | else
80 | this.container.style.visibility = 'hidden';
81 | }
82 |
83 | Slicer.prototype.addGUI = function(gui) {
84 | this.gui = gui;
85 | var that = this;
86 | //Add folder
87 | var f1 = this.gui.addFolder('Slices');
88 | f1.add(this.properties, 'show').onFinishChange(function(l) {that.toggle();});
89 | //["hide/show"] = function() {};
90 | f1.add(this.properties, 'layout').onFinishChange(function(l) {that.doLayout(); that.draw();});
91 | //f1.add(this.properties, 'X', 0, this.res[0], 1).listen();
92 | //f1.add(this.properties, 'Y', 0, this.res[1], 1).listen();
93 | //f1.add(this.properties, 'Z', 0, this.res[2], 1).listen();
94 | f1.add(this.properties, 'zoom', 0.01, 4.0, 0.1).onFinishChange(function(l) {that.doLayout(); that.draw();});
95 |
96 | f1.add(this.properties, 'brightness', -1.0, 1.0, 0.01);
97 | f1.add(this.properties, 'contrast', 0.0, 3.0, 0.01);
98 | f1.add(this.properties, 'power', 0.01, 5.0, 0.01);
99 | f1.add(this.properties, 'usecolourmap');
100 | f1.open();
101 |
102 | var changefn = function(value) {that.draw();};
103 | for (var i in f1.__controllers)
104 | f1.__controllers[i].onChange(changefn);
105 | }
106 |
107 | Slicer.prototype.get = function() {
108 | var data = {};
109 | //data.colourmap = colours.palette.toString();
110 | data.properties = this.properties;
111 | return data;
112 | }
113 |
114 | Slicer.prototype.load = function(src) {
115 | //colours.read(data.colourmap);
116 | //colours.update();
117 | for (var key in src.properties)
118 | this.properties[key] = src.properties[key]
119 | }
120 |
121 | Slicer.prototype.setX = function(val) {this.properties.X = val * this.res[0]; this.draw();}
122 |
123 |
124 | Slicer.prototype.setY = function(val) {this.properties.Y = val * this.res[1]; this.draw();}
125 | Slicer.prototype.setZ = function(val) {this.properties.Z = val * this.res[2]; this.draw();}
126 |
127 | Slicer.prototype.doLayout = function() {
128 | this.viewers = [];
129 |
130 | var x = 0;
131 | var y = 0;
132 | var xmax = 0;
133 | var ymax = 0;
134 | var rotate = 0;
135 | var alignTop = true;
136 |
137 | removeChildren(this.container);
138 |
139 | var that = this;
140 | var buffer = "";
141 | var rowHeight = 0, rowWidth = 0;
142 | var addViewer = function(idx) {
143 | var mag = 1.0;
144 | if (buffer) mag = parseFloat(buffer);
145 | var v = new SliceView(that, x, y, idx, rotate, mag);
146 | that.viewers.push(v);
147 | that.container.appendChild(v.div);
148 |
149 | // x += v.viewport.width + 5; //Offset by previous width
150 | // var h = v.viewport.height + 5;
151 | // if (h > rowHeight) rowHeight = h;
152 | // if (x > xmax) xmax = x;
153 |
154 | y += v.viewport.height + 5; //Offset by previous height
155 | var w = v.viewport.width + 5;
156 | if (w > rowWidth) rowWidth = w;
157 | if (y > ymax) ymax = y;
158 | }
159 |
160 | //Process based on layout
161 | this.flipY = false;
162 | for (var i=0; i
0) this.properties.samples -= this.properties.samples % 32;
217 | f.add(this.properties, 'samples', 32, 1024, 32);
218 | f.add(this.properties, 'density', 0.0, 50.0, 1.0);
219 | f.add(this.properties, 'brightness', -1.0, 1.0, 0.05);
220 | f.add(this.properties, 'contrast', 0.0, 2.0, 0.05);
221 | f.add(this.properties, 'saturation', 0.0, 2.0, 0.05);
222 | f.add(this.properties, 'power', 0.01, 5.0, 0.05);
223 | f.add(this.properties, 'minclip', 0.0, 1.0, 0.0);
224 | f.add(this.properties, 'maxclip', 0.0, 1.0, 1.0);
225 | f.add(this.properties, 'axes');
226 | f.add(this.properties, 'border');
227 | f.add(this.properties, 'tricubicFilter');
228 | f.open();
229 | //this.gui.__folders.f.controllers[1].updateDisplay(); //Update samples display
230 |
231 | //Clip planes folder
232 | var f0 = this.gui.addFolder('Clip planes');
233 | f0.add(this.properties, 'xmin', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setX(l);});
234 | f0.add(this.properties, 'xmax', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setX(l);});
235 | f0.add(this.properties, 'ymin', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setY(l);});
236 | f0.add(this.properties, 'ymax', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setY(l);});
237 | f0.add(this.properties, 'zmin', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setZ(l);});
238 | f0.add(this.properties, 'zmax', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setZ(l);});
239 | //f0.open();
240 |
241 | //Isosurfaces folder
242 | var f1 = this.gui.addFolder('Isosurface');
243 | f1.add(this.properties, 'isovalue', 0.0, 1.0, 0.01);
244 | f1.add(this.properties, 'isowalls');
245 | f1.add(this.properties, 'isoalpha', 0.0, 1.0, 0.01);
246 | f1.add(this.properties, 'isosmooth', 0.1, 3.0, 0.1);
247 | f1.addColor(this.properties, 'colour');
248 | //f1.open();
249 |
250 | // Iterate over all controllers and set change function
251 | var that = this;
252 | var changefn = function(value) {that.delayedRender(250);}; //Use delayed high quality render for faster interaction
253 | for (var i in f.__controllers)
254 | f.__controllers[i].onChange(changefn);
255 | for (var i in f0.__controllers)
256 | f0.__controllers[i].onChange(changefn);
257 | for (var i in f1.__controllers)
258 | f1.__controllers[i].onChange(changefn);
259 | }
260 |
261 | Volume.prototype.load = function(src) {
262 | for (var key in src)
263 | this.properties[key] = src[key]
264 |
265 | if (src.colourmap != undefined) this.properties.usecolourmap = true;
266 | this.properties.axes = state.views[0].axes;
267 | this.properties.border = state.views[0].border;
268 | this.properties.tricubicFilter = src.tricubicfilter;
269 |
270 | if (state.views[0].translate) this.translate = state.views[0].translate;
271 | //Initial rotation (Euler angles or quaternion accepted)
272 | if (state.views[0].rotate) {
273 | if (state.views[0].rotate.length == 3) {
274 | this.rotateZ(-state.views[0].rotate[2]);
275 | this.rotateY(-state.views[0].rotate[1]);
276 | this.rotateX(-state.views[0].rotate[0]);
277 | } else if (state.views[0].rotate[3] != 0)
278 | this.rotate = quat4.create(state.views[0].rotate);
279 | }
280 | }
281 |
282 | Volume.prototype.get = function(matrix) {
283 | var data = {};
284 | if (matrix) {
285 | //Include the modelview matrix array
286 | data.modelview = this.webgl.modelView.toArray();
287 | } else {
288 | //Translate + rotation quaternion
289 | data.translate = this.translate;
290 | data.rotate = [this.rotate[0], this.rotate[1], this.rotate[2], this.rotate[3]];
291 | }
292 | data.properties = this.properties;
293 | return data;
294 | }
295 |
296 | var frames = 0;
297 | var testtime;
298 |
299 | Volume.prototype.draw = function(lowquality, testmode) {
300 | if (!this.properties || !this.webgl) return; //Getting called before vars defined, TODO:fix
301 | //this.time = new Date().getTime();
302 | if (this.width == 0 || this.height == 0) {
303 | //Get size from window
304 | this.width = window.innerWidth;
305 | this.height = window.innerHeight;
306 | }
307 |
308 | if (this.width != this.canvas.width || this.height != this.canvas.height) {
309 | //Get size from element
310 | this.canvas.width = this.width;
311 | this.canvas.height = this.height;
312 | this.canvas.setAttribute("width", this.width);
313 | this.canvas.setAttribute("height", this.height);
314 | if (this.gl) {
315 | this.gl.viewportWidth = this.width;
316 | this.gl.viewportHeight = this.height;
317 | this.webgl.viewport = new Viewport(0, 0, this.width, this.height);
318 | }
319 | }
320 | //Reset to auto-size...
321 | //this.width = this.height = 0;
322 | //console.log(this.width + "," + this.height);
323 |
324 | this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
325 | this.gl.viewport(this.webgl.viewport.x, this.webgl.viewport.y, this.webgl.viewport.width, this.webgl.viewport.height);
326 |
327 | //box/axes draw fully opaque behind volume
328 | if (this.properties.border) this.drawBox(1.0);
329 | if (this.properties.axes) this.drawAxis(1.0);
330 |
331 | //Volume render (skip while interacting if lowpower device flag is set)
332 | if (!(lowquality && !this.properties.interactive)) {
333 | //Setup volume camera
334 | this.webgl.modelView.push();
335 | this.rayCamera();
336 |
337 | this.webgl.use(this.program);
338 | //this.webgl.modelView.scale(this.scaling); //Apply scaling
339 | this.gl.disableVertexAttribArray(this.program.attributes["aVertexColour"]);
340 |
341 | this.gl.activeTexture(this.gl.TEXTURE0);
342 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.webgl.textures[0]);
343 |
344 | this.gl.activeTexture(this.gl.TEXTURE1);
345 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.webgl.gradientTexture);
346 |
347 | //Only render full quality when not interacting
348 | //this.gl.uniform1i(this.program.uniforms["uSamples"], this.samples);
349 | this.gl.uniform1i(this.program.uniforms["uSamples"], lowquality ? this.properties.samples * 0.5 : this.properties.samples);
350 | this.gl.uniform1i(this.program.uniforms["uVolume"], 0);
351 | this.gl.uniform1i(this.program.uniforms["uTransferFunction"], 1);
352 | this.gl.uniform1i(this.program.uniforms["uEnableColour"], this.properties.usecolourmap);
353 | this.gl.uniform1i(this.program.uniforms["uFilter"], lowquality ? false : this.properties.tricubicFilter);
354 | this.gl.uniform4fv(this.program.uniforms["uViewport"], new Float32Array([0, 0, this.gl.viewportWidth, this.gl.viewportHeight]));
355 |
356 | var bbmin = [this.properties.xmin, this.properties.ymin, this.properties.zmin];
357 | var bbmax = [this.properties.xmax, this.properties.ymax, this.properties.zmax];
358 | this.gl.uniform3fv(this.program.uniforms["uBBMin"], new Float32Array(bbmin));
359 | this.gl.uniform3fv(this.program.uniforms["uBBMax"], new Float32Array(bbmax));
360 | this.gl.uniform3fv(this.program.uniforms["uResolution"], new Float32Array(this.resolution));
361 |
362 | this.gl.uniform1f(this.program.uniforms["uDensityFactor"], this.properties.density);
363 | // brightness and contrast
364 | this.gl.uniform1f(this.program.uniforms["uSaturation"], this.properties.saturation);
365 | this.gl.uniform1f(this.program.uniforms["uBrightness"], this.properties.brightness);
366 | this.gl.uniform1f(this.program.uniforms["uContrast"], this.properties.contrast);
367 | this.gl.uniform1f(this.program.uniforms["uPower"], this.properties.power);
368 |
369 | this.gl.uniform1f(this.program.uniforms["uIsoValue"], this.properties.isovalue);
370 | var colour = new Colour(this.properties.colour);
371 | colour.alpha = this.properties.isoalpha;
372 | this.gl.uniform4fv(this.program.uniforms["uIsoColour"], colour.rgbaGL());
373 | this.gl.uniform1f(this.program.uniforms["uIsoSmooth"], this.properties.isosmooth);
374 | this.gl.uniform1i(this.program.uniforms["uIsoWalls"], this.properties.isowalls);
375 |
376 | //Data value range (default only for now)
377 | this.gl.uniform2fv(this.program.uniforms["uRange"], new Float32Array([0.0, 1.0]));
378 | //Density clip range
379 | this.gl.uniform2fv(this.program.uniforms["uDenMinMax"], new Float32Array([this.properties.minclip, this.properties.maxclip]));
380 |
381 | //Draw two triangles
382 | this.webgl.initDraw2d(); //This sends the matrices, uNMatrix may not be correct here though
383 | this.gl.uniformMatrix4fv(this.program.uniforms["uInvPMatrix"], false, this.invPMatrix);
384 | //this.gl.enableVertexAttribArray(this.program.attributes["aVertexPosition"]);
385 | //this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.webgl.vertexPositionBuffer);
386 | //this.gl.vertexAttribPointer(this.program.attributes["aVertexPosition"], this.webgl.vertexPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0);
387 | //this.webgl.setMatrices();
388 |
389 | this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, this.webgl.vertexPositionBuffer.numItems);
390 |
391 | this.webgl.modelView.pop();
392 | } else {
393 | //Always draw axis even if turned off to show interaction
394 | if (!this.properties.axes) this.drawAxis(1.0);
395 | //Bounding box
396 | this.drawBox(1.0);
397 | }
398 |
399 | //this.timeAction("Render", this.time);
400 |
401 | //Draw box/axes again as overlay (near transparent)
402 | this.gl.clear(this.gl.DEPTH_BUFFER_BIT);
403 | if (this.properties.axes) this.drawAxis(0.2);
404 | if (this.properties.border) this.drawBox(0.2);
405 |
406 | //Running speed test?
407 | if (testmode) {
408 | frames++;
409 | $('status').innerHTML = "Speed test: frame " + frames;
410 | if (frames == 5) {
411 | var elapsed = new Date().getTime() - testtime;
412 | console.log("5 frames in " + (elapsed / 1000) + " seconds");
413 | //Reduce quality for slower device
414 | if (elapsed > 1000) {
415 | this.properties.samples = Math.floor(this.properties.samples * 1000 / elapsed);
416 | if (this.properties.samples < 32) this.properties.samples = 32;
417 | $('status').innerHTML = "5 frames in " + (elapsed / 1000) + " seconds, Reduced quality to " + this.properties.samples;
418 | //Hide info window in 2 sec
419 | setTimeout(function() {info.hide()}, 2000);
420 | } else {
421 | info.hide();
422 | }
423 | } else {
424 | this.draw(true, true);
425 | }
426 | }
427 | }
428 |
429 | Volume.prototype.camera = function() {
430 | //Apply translation to origin, any rotation and scaling
431 | this.webgl.modelView.identity()
432 | this.webgl.modelView.translate(this.translate)
433 | // Adjust centre of rotation, default is same as focal point so this does nothing...
434 | adjust = [-(this.focus[0] - this.centre[0]), -(this.focus[1] - this.centre[1]), -(this.focus[2] - this.centre[2])];
435 | this.webgl.modelView.translate(adjust);
436 |
437 | // rotate model
438 | var rotmat = quat4.toMat4(this.rotate);
439 | this.webgl.modelView.mult(rotmat);
440 | //this.webgl.modelView.mult(this.rotate);
441 |
442 | // Adjust back for rotation centre
443 | adjust = [this.focus[0] - this.centre[0], this.focus[1] - this.centre[1], this.focus[2] - this.centre[2]];
444 | this.webgl.modelView.translate(adjust);
445 |
446 | // Translate back by centre of model to align eye with model centre
447 | this.webgl.modelView.translate([-this.focus[0], -this.focus[1], -this.focus[2] * this.orientation]);
448 |
449 | //Perspective matrix
450 | this.webgl.setPerspective(this.fov, this.gl.viewportWidth / this.gl.viewportHeight, 0.1, 100.0);
451 | }
452 |
453 | Volume.prototype.rayCamera = function() {
454 | //Apply translation to origin, any rotation and scaling
455 | this.webgl.modelView.identity()
456 | this.webgl.modelView.translate(this.translate)
457 |
458 | // rotate model
459 | var rotmat = quat4.toMat4(this.rotate);
460 | this.webgl.modelView.mult(rotmat);
461 |
462 | //For a volume cube other than [0,0,0] - [1,1,1], need to translate/scale here...
463 | this.webgl.modelView.translate([-this.scaling[0]*0.5, -this.scaling[1]*0.5, -this.scaling[2]*0.5]); //Translate to origin
464 | //Inverse of scaling
465 | this.webgl.modelView.scale([this.iscale[0], this.iscale[1], this.iscale[2]]);
466 |
467 | //Perspective matrix
468 | this.webgl.setPerspective(this.fov, this.gl.viewportWidth / this.gl.viewportHeight, 0.1, 100.0);
469 |
470 | //Get inverted matrix for volume shader
471 | this.invPMatrix = mat4.create(this.webgl.perspective.matrix);
472 | mat4.inverse(this.invPMatrix);
473 | }
474 |
475 | Volume.prototype.drawAxis = function(alpha) {
476 | this.camera();
477 | this.webgl.use(this.lineprogram);
478 | this.gl.uniform1f(this.lineprogram.uniforms["uAlpha"], alpha);
479 | this.gl.uniform4fv(this.lineprogram.uniforms["uColour"], new Float32Array([1.0, 1.0, 1.0, 0.0]));
480 |
481 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.linePositionBuffer);
482 | this.gl.enableVertexAttribArray(this.lineprogram.attributes["aVertexPosition"]);
483 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexPosition"], this.linePositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0);
484 |
485 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.lineColourBuffer);
486 | this.gl.enableVertexAttribArray(this.lineprogram.attributes["aVertexColour"]);
487 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexColour"], this.lineColourBuffer.itemSize, this.gl.FLOAT, false, 0, 0);
488 |
489 | //Axis position, default centre, use slicer positions if available
490 | var pos = [0.5*this.scaling[0], 0.5*this.scaling[1], 0.5*this.scaling[2]];
491 | if (this.slicer) {
492 | pos = [this.slicer.slices[0]*this.scaling[0],
493 | this.slicer.slices[1]*this.scaling[1],
494 | this.slicer.slices[2]*this.scaling[2]];
495 | }
496 | this.webgl.modelView.translate(pos);
497 | this.webgl.setMatrices();
498 | this.gl.drawArrays(this.gl.LINES, 0, this.linePositionBuffer.numItems);
499 | this.webgl.modelView.translate([-pos[0], -pos[1], -pos[2]]);
500 | }
501 |
502 | Volume.prototype.drawBox = function(alpha) {
503 | this.camera();
504 | this.webgl.use(this.lineprogram);
505 | this.gl.uniform1f(this.lineprogram.uniforms["uAlpha"], alpha);
506 | this.gl.uniform4fv(this.lineprogram.uniforms["uColour"], this.borderColour.rgbaGL());
507 |
508 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.boxPositionBuffer);
509 | this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.boxIndexBuffer);
510 | this.gl.enableVertexAttribArray(this.lineprogram.attributes["aVertexPosition"]);
511 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexPosition"], this.boxPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0);
512 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexColour"], 4, this.gl.UNSIGNED_BYTE, true, 0, 0);
513 |
514 | this.webgl.modelView.scale(this.scaling); //Apply scaling
515 | this.webgl.setMatrices();
516 | this.gl.drawElements(this.gl.LINES, this.boxIndexBuffer.numItems, this.gl.UNSIGNED_SHORT, 0);
517 | }
518 |
519 | Volume.prototype.timeAction = function(action, start) {
520 | if (!window.requestAnimationFrame) return;
521 | var timer = start || new Date().getTime();
522 | function logTime() {
523 | var elapsed = new Date().getTime() - timer;
524 | if (elapsed < 50)
525 | window.requestAnimationFrame(logTime); //Not enough time, assume triggered too early, try again
526 | else {
527 | console.log(action + " took: " + (elapsed / 1000) + " seconds");
528 | /*if (elapsed > 200 && this.quality > 32) {
529 | this.quality = Math.floor(this.quality * 0.5);
530 | OK.debug("Reducing quality to " + this.quality + " samples");
531 | this.draw();
532 | } else if (elapsed < 100 && this.quality < 512 && this.quality >= 128) {
533 | this.quality = this.quality * 2;
534 | OK.debug("Increasing quality to " + this.quality + " samples");
535 | this.draw();
536 | }*/
537 | }
538 | }
539 | window.requestAnimationFrame(logTime);
540 | }
541 |
542 | Volume.prototype.rotateX = function(deg) {
543 | this.rotation(deg, [1,0,0]);
544 | }
545 |
546 | Volume.prototype.rotateY = function(deg) {
547 | this.rotation(deg, [0,1,0]);
548 | }
549 |
550 | Volume.prototype.rotateZ = function(deg) {
551 | this.rotation(deg, [0,0,1]);
552 | }
553 |
554 | Volume.prototype.rotation = function(deg, axis) {
555 | //Quaterion rotate
556 | var arad = deg * Math.PI / 180.0;
557 | var rotation = quat4.fromAngleAxis(arad, axis);
558 | rotation = quat4.normalize(rotation);
559 | this.rotate = quat4.multiply(rotation, this.rotate);
560 | }
561 |
562 | Volume.prototype.zoom = function(factor) {
563 | this.translate[2] += factor * this.modelsize;
564 | }
565 |
566 | Volume.prototype.zoomClip = function(factor) {
567 | //var clip = parseFloat($("nearclip").value) - factor;
568 | //$("nearclip").value = clip;
569 | this.draw();
570 | //OK.debug(clip + " " + $("nearclip").value);
571 | }
572 |
573 | Volume.prototype.click = function(event, mouse) {
574 | this.rotating = false;
575 | this.draw();
576 | return false;
577 | }
578 |
579 | Volume.prototype.move = function(event, mouse) {
580 | this.rotating = false;
581 | if (!mouse.isdown) return true;
582 |
583 | //Switch buttons for translate/rotate
584 | var button = mouse.button;
585 |
586 | switch (button)
587 | {
588 | case 0:
589 | this.rotateY(mouse.deltaX/5.0);
590 | this.rotateX(mouse.deltaY/5.0);
591 | this.rotating = true;
592 | break;
593 | case 1:
594 | this.rotateZ(Math.sqrt(mouse.deltaX*mouse.deltaX + mouse.deltaY*mouse.deltaY)/5.0);
595 | this.rotating = true;
596 | break;
597 | case 2:
598 | var adjust = this.modelsize / 1000; //1/1000th of size
599 | this.translate[0] += mouse.deltaX * adjust;
600 | this.translate[1] -= mouse.deltaY * adjust;
601 | break;
602 | }
603 |
604 | this.draw(true);
605 | return false;
606 | }
607 |
608 | Volume.prototype.wheel = function(event, mouse) {
609 | if (event.shiftKey) {
610 | var factor = event.spin * 0.01;
611 | this.zoomClip(factor);
612 | } else {
613 | var factor = event.spin * 0.05;
614 | this.zoom(factor);
615 | }
616 | this.delayedRender(250); //Delayed high quality render
617 |
618 | return false; //Prevent default
619 | }
620 |
621 | Volume.prototype.pinch = function(event, mouse) {
622 |
623 | var zoom = (event.distance * 0.0001);
624 | console.log(' --> ' + zoom);
625 | this.zoom(zoom);
626 | this.delayedRender(250); //Delayed high quality render
627 | }
628 |
629 | //Delayed high quality render
630 | Volume.prototype.delayedRender = function(time, skipImm) {
631 | if (!skipImm) this.draw(true); //Draw immediately in low quality
632 | //Set timer to draw the final render
633 | if (this.delaytimer) clearTimeout(this.delaytimer);
634 | var that = this;
635 | this.delaytimer = setTimeout(function() {that.draw();}, time);
636 | }
637 |
638 | Volume.prototype.applyBackground = function(bg) {
639 | if (!bg) return;
640 | this.background = new Colour(bg);
641 | var hsv = this.background.HSV();
642 | this.borderColour = hsv.V > 50 ? new Colour(0xff444444) : new Colour(0xffbbbbbb);
643 |
644 | //document.body.style.background = bg;
645 |
646 | //Set canvas background
647 | if (this.properties.usecolourmap)
648 | this.canvas.style.backgroundColor = bg;
649 | else
650 | this.canvas.style.backgroundColor = "black";
651 |
652 |
653 | }
654 |
--------------------------------------------------------------------------------