Mercurial > hg > orthanc-stl
view Resources/Nexus/js/nexus.js @ 63:1fd6d0f8fdc9
clarifying the versions of the viewers
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Sat, 15 Jun 2024 16:17:41 +0200 |
parents | 967f947014ac |
children |
line wrap: on
line source
/* Nexus Copyright (c) 2012-2018, Visual Computing Lab, ISTI - CNR All rights reserved. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ Nexus = function() { /* WORKER INITIALIZED ONCE */ var meco; var corto; var scripts = document.getElementsByTagName('script'); var i, j, k; var path; for(i = 0; i < scripts.length; i++) { var attrs = scripts[i].attributes; for(j = 0; j < attrs.length; j++) { var a = attrs[j]; if(a.name != 'src') continue; if(!a.value) continue; if(a.value.search('nexus.js') >= 0) { path = a.value; break; } } } var meco = null; function loadMeco() { meco = new Worker(path.replace('nexus.js', 'meco.js')); meco.onerror = function(e) { console.log(e); } meco.requests = {}; meco.count = 0; meco.postRequest = function(sig, node, patches) { var signature = { texcoords: sig.texcoords ? 1 : 0, colors : sig.colors ? 1 : 0, normals : sig.normals ? 1 : 0, indices : sig.indices ? 1 : 0 }; meco.postMessage({ signature:signature, node:{ nface: node.nface, nvert: node.nvert, buffer:node.buffer, request:this.count}, patches:patches }); this.requests[this.count++] = node; }; meco.onmessage = function(e) { var node = this.requests[e.data.request]; node.buffer = e.data.buffer; readyNode(node); }; } var corto = null; function loadCorto() { corto = new Worker(path.replace('nexus.js', 'corto.js')); corto.requests = {}; corto.count = 0; corto.postRequest = function(node) { corto.postMessage({ buffer: node.buffer, request:this.count, rgba_colors: true, short_normals: true }); this.requests[this.count++] = node; } corto.onmessage = function(e) { var node = this.requests[e.data.request]; node.buffer = e.data.buffer; node.model = e.data.model; readyNode(node); }; } /* UTILITIES */ function getUint64(view) { var s = 0; var lo = view.getUint32(view.offset, true); var hi = view.getUint32(view.offset + 4, true); view.offset += 8; return ((hi * (1 << 32)) + lo); } function getUint32(view) { var s = view.getUint32(view.offset, true); view.offset += 4; return s; } function getUint16(view) { var s = view.getUint16(view.offset, true); view.offset += 2; return s; } function getFloat32(view) { var s = view.getFloat32(view.offset, true); view.offset += 4; return s; } /* MATRIX STUFF */ function vecMul(m, v, r) { var w = m[3]*v[0] + m[7]*v[1] + m[11]*v[2] + m[15]; r[0] = (m[0]*v[0] + m[4]*v[1] + m[8 ]*v[2] + m[12 ])/w; r[1] = (m[1]*v[0] + m[5]*v[1] + m[9 ]*v[2] + m[13 ])/w; r[2] = (m[2]*v[0] + m[6]*v[1] + m[10]*v[2] + m[14])/w; } function matMul(a, b, r) { r[ 0] = a[0]*b[0] + a[4]*b[1] + a[8]*b[2] + a[12]*b[3]; r[ 1] = a[1]*b[0] + a[5]*b[1] + a[9]*b[2] + a[13]*b[3]; r[ 2] = a[2]*b[0] + a[6]*b[1] + a[10]*b[2] + a[14]*b[3]; r[ 3] = a[3]*b[0] + a[7]*b[1] + a[11]*b[2] + a[15]*b[3]; r[ 4] = a[0]*b[4] + a[4]*b[5] + a[8]*b[6] + a[12]*b[7]; r[ 5] = a[1]*b[4] + a[5]*b[5] + a[9]*b[6] + a[13]*b[7]; r[ 6] = a[2]*b[4] + a[6]*b[5] + a[10]*b[6] + a[14]*b[7]; r[ 7] = a[3]*b[4] + a[7]*b[5] + a[11]*b[6] + a[15]*b[7]; r[ 8] = a[0]*b[8] + a[4]*b[9] + a[8]*b[10] + a[12]*b[11]; r[ 9] = a[1]*b[8] + a[5]*b[9] + a[9]*b[10] + a[13]*b[11]; r[10] = a[2]*b[8] + a[6]*b[9] + a[10]*b[10] + a[14]*b[11]; r[11] = a[3]*b[8] + a[7]*b[9] + a[11]*b[10] + a[15]*b[11]; r[12] = a[0]*b[12] + a[4]*b[13] + a[8]*b[14] + a[12]*b[15]; r[13] = a[1]*b[12] + a[5]*b[13] + a[9]*b[14] + a[13]*b[15]; r[14] = a[2]*b[12] + a[6]*b[13] + a[10]*b[14] + a[14]*b[15]; r[15] = a[3]*b[12] + a[7]*b[13] + a[11]*b[14] + a[15]*b[15]; } function matInv(m, t) { var s = 1.0/( m[12]* m[9]*m[6]*m[3]-m[8]*m[13]*m[6]*m[3]-m[12]*m[5]*m[10]*m[3]+m[4]*m[13]*m[10]*m[3]+ m[8]*m[5]*m[14]*m[3]-m[4]*m[9]*m[14]*m[3]-m[12]*m[9]*m[2]*m[7]+m[8]*m[13]*m[2]*m[7]+ m[12]*m[1]*m[10]*m[7]-m[0]*m[13]*m[10]*m[7]-m[8]*m[1]*m[14]*m[7]+m[0]*m[9]*m[14]*m[7]+ m[12]*m[5]*m[2]*m[11]-m[4]*m[13]*m[2]*m[11]-m[12]*m[1]*m[6]*m[11]+m[0]*m[13]*m[6]*m[11]+ m[4]*m[1]*m[14]*m[11]-m[0]*m[5]*m[14]*m[11]-m[8]*m[5]*m[2]*m[15]+m[4]*m[9]*m[2]*m[15]+ m[8]*m[1]*m[6]*m[15]-m[0]*m[9]*m[6]*m[15]-m[4]*m[1]*m[10]*m[15]+m[0]*m[5]*m[10]*m[15] ); t[ 0] = (m[9]*m[14]*m[7]-m[13]*m[10]*m[7]+m[13]*m[6]*m[11]-m[5]*m[14]*m[11]-m[9]*m[6]*m[15]+m[5]*m[10]*m[15])*s; t[ 1] = (m[13]*m[10]*m[3]-m[9]*m[14]*m[3]-m[13]*m[2]*m[11]+m[1]*m[14]*m[11]+m[9]*m[2]*m[15]-m[1]*m[10]*m[15])*s; t[ 2] = (m[5]*m[14]*m[3]-m[13]*m[6]*m[3]+m[13]*m[2]*m[7]-m[1]*m[14]*m[7]-m[5]*m[2]*m[15]+m[1]*m[6]*m[15])*s; t[ 3] = (m[9]*m[6]*m[3]-m[5]*m[10]*m[3]-m[9]*m[2]*m[7]+m[1]*m[10]*m[7]+m[5]*m[2]*m[11]-m[1]*m[6]*m[11])*s; t[ 4] = (m[12]*m[10]*m[7]-m[8]*m[14]*m[7]-m[12]*m[6]*m[11]+m[4]*m[14]*m[11]+m[8]*m[6]*m[15]-m[4]*m[10]*m[15])*s; t[ 5] = (m[8]*m[14]*m[3]-m[12]*m[10]*m[3]+m[12]*m[2]*m[11]-m[0]*m[14]*m[11]-m[8]*m[2]*m[15]+m[0]*m[10]*m[15])*s; t[ 6] = (m[12]*m[6]*m[3]-m[4]*m[14]*m[3]-m[12]*m[2]*m[7]+m[0]*m[14]*m[7]+m[4]*m[2]*m[15]-m[0]*m[6]*m[15])*s; t[ 7] = (m[4]*m[10]*m[3]-m[8]*m[6]*m[3]+m[8]*m[2]*m[7]-m[0]*m[10]*m[7]-m[4]*m[2]*m[11]+m[0]*m[6]*m[11])*s; t[ 8] = (m[8]*m[13]*m[7]-m[12]*m[9]*m[7]+m[12]*m[5]*m[11]-m[4]*m[13]*m[11]-m[8]*m[5]*m[15]+m[4]*m[9]*m[15])*s; t[ 9] = (m[12]*m[9]*m[3]-m[8]*m[13]*m[3]-m[12]*m[1]*m[11]+m[0]*m[13]*m[11]+m[8]*m[1]*m[15]-m[0]*m[9]*m[15])*s; t[10] = (m[4]*m[13]*m[3]-m[12]*m[5]*m[3]+m[12]*m[1]*m[7]-m[0]*m[13]*m[7]-m[4]*m[1]*m[15]+m[0]*m[5]*m[15])*s; t[11] = (m[8]*m[5]*m[3]-m[4]*m[9]*m[3]-m[8]*m[1]*m[7]+m[0]*m[9]*m[7]+m[4]*m[1]*m[11]-m[0]*m[5]*m[11])*s; t[12] = (m[12]*m[9]*m[6]-m[8]*m[13]*m[6]-m[12]*m[5]*m[10]+m[4]*m[13]*m[10]+m[8]*m[5]*m[14]-m[4]*m[9]*m[14])*s; t[13] = (m[8]*m[13]*m[2]-m[12]*m[9]*m[2]+m[12]*m[1]*m[10]-m[0]*m[13]*m[10]-m[8]*m[1]*m[14]+m[0]*m[9]*m[14])*s; t[14] = (m[12]*m[5]*m[2]-m[4]*m[13]*m[2]-m[12]*m[1]*m[6]+m[0]*m[13]*m[6]+m[4]*m[1]*m[14]-m[0]*m[5]*m[14])*s; t[15] = (m[4]*m[9]*m[2]-m[8]*m[5]*m[2]+m[8]*m[1]*m[6]-m[0]*m[9]*m[6]-m[4]*m[1]*m[10]+m[0]*m[5]*m[10])*s; } /* PRIORITY QUEUE */ PriorityQueue = function(max_length) { this.error = new Float32Array(max_length); this.data = new Int32Array(max_length); this.size = 0; } PriorityQueue.prototype = { push: function(data, error) { this.data[this.size] = data; this.error[this.size] = error; this.bubbleUp(this.size); this.size++; }, pop: function() { var result = this.data[0]; this.size--; if(this.size > 0) { this.data[0] = this.data[this.size]; this.error[0] = this.error[this.size]; this.sinkDown(0); } return result; }, bubbleUp: function(n) { var data = this.data[n]; var error = this.error[n]; while (n > 0) { var pN = ((n+1)>>1) -1; var pError = this.error[pN]; if(pError > error) break; //swap this.data[n] = this.data[pN]; this.error[n] = pError; this.data[pN] = data; this.error[pN] = error; n = pN; } }, sinkDown: function(n) { var data = this.data[n]; var error = this.error[n]; while(true) { var child2N = (n + 1) * 2; var child1N = child2N - 1; var swap = -1; if (child1N < this.size) { var child1Error = this.error[child1N]; if(child1Error > error) swap = child1N; } if (child2N < this.size) { var child2Error = this.error[child2N]; if (child2Error > (swap == -1 ? error : child1Error)) swap = child2N; } if (swap == -1) break; this.data[n] = this.data[swap]; this.error[n] = this.error[swap]; this.data[swap] = data; this.error[swap] = error; n = swap; } } }; /* HEADER AND PARSING */ var padding = 256; var Debug = { nodes : false, //color each node culling : false, //visibility culling disabled draw : false, //final rendering call disabled extract : false, //no extraction request : false, //no network requests worker : false //no web workers }; var glP = WebGLRenderingContext.prototype; var attrGlMap = [glP.NONE, glP.BYTE, glP.UNSIGNED_BYTE, glP.SHORT, glP.UNSIGNED_SHORT, glP.INT, glP.UNSIGNED_INT, glP.FLOAT, glP.DOUBLE]; var attrSizeMap = [0, 1, 1, 2, 2, 4, 4, 4, 8]; var targetError = 2.0; var targetFps = 15; var maxPending = 3; var maxBlocked = 3; var maxReqAttempt = 2; var maxCacheSize = 512*(1<<20); //TODO DEBUG var drawBudget = 5*(1<<20); /* MESH DEFINITION */ Mesh = function() { var t = this; t.onLoad = null; t.reqAttempt = 0; } Mesh.prototype = { open: function(url) { var mesh = this; mesh.url = url; mesh.httpRequest( 0, 88, function() { // console.log("Loading header for " + mesh.url); var view = new DataView(this.response); view.offset = 0; mesh.reqAttempt++; var header = mesh.importHeader(view); if(!header) { console.log("Empty header!"); if(mesh.reqAttempt < maxReqAttempt) mesh.open(mesh.url + '?' + Math.random()); // BLINK ENGINE CACHE BUG PATCH return null; } mesh.reqAttempt = 0; for(i in header) mesh[i] = header[i]; mesh.vertex = mesh.signature.vertex; mesh.face = mesh.signature.face; mesh.renderMode = mesh.face.index?["FILL", "POINT"]:["POINT"]; mesh.compressed = (mesh.signature.flags & (2 | 4)); //meco or corto mesh.meco = (mesh.signature.flags & 2); mesh.corto = (mesh.signature.flags & 4); mesh.requestIndex(); }, function() { console.log("Open request error!");}, function() { console.log("Open request abort!");} ); }, httpRequest: function(start, end, load, error, abort, type) { if(!type) type = 'arraybuffer'; var r = new XMLHttpRequest(); r.open('GET', this.url, true); r.responseType = type; r.setRequestHeader("Range", "bytes=" + start + "-" + (end -1)); r.onload = function(){ switch (this.status){ case 0: // console.log("0 response: server unreachable.");//returned in chrome for local files case 206: // console.log("206 response: partial content loaded."); load.bind(this)(); break; case 200: // console.log("200 response: server does not support byte range requests."); } }; r.onerror = error; r.onabort = abort; r.send(); return r; }, requestIndex: function() { var mesh = this; var end = 88 + mesh.nodesCount*44 + mesh.patchesCount*12 + mesh.texturesCount*68; mesh.httpRequest( 88, end, function() { mesh.handleIndex(this.response); }, function() { console.log("Index request error!");}, function() { console.log("Index request abort!");} ); }, handleIndex: function(buffer) { var t = this; var view = new DataView(buffer); view.offset = 0; var n = t.nodesCount; t.noffsets = new Uint32Array(n); t.nvertices = new Uint32Array(n); t.nfaces = new Uint32Array(n); t.nerrors = new Float32Array(n); t.nspheres = new Float32Array(n*5); t.nsize = new Float32Array(n); t.nfirstpatch = new Uint32Array(n); for(i = 0; i < n; i++) { t.noffsets[i] = padding*getUint32(view); //offset t.nvertices[i] = getUint16(view); //verticesCount t.nfaces[i] = getUint16(view); //facesCount t.nerrors[i] = getFloat32(view); view.offset += 8; //skip cone for(k = 0; k < 5; k++) t.nspheres[i*5+k] = getFloat32(view); //sphere + tight t.nfirstpatch[i] = getUint32(view); //first patch } t.sink = n -1; t.patches = new Uint32Array(view.buffer, view.offset, t.patchesCount*3); //noded, lastTriangle, texture t.nroots = t.nodesCount; for(j = 0; j < t.nroots; j++) { for(i = t.nfirstpatch[j]; i < t.nfirstpatch[j+1]; i++) { if(t.patches[i*3] < t.nroots) t.nroots = t.patches[i*3]; } } view.offset += t.patchesCount*12; t.textures = new Uint32Array(t.texturesCount); t.texref = new Uint32Array(t.texturesCount); for(i = 0; i < t.texturesCount; i++) { t.textures[i] = padding*getUint32(view); view.offset += 16*4; //skip proj matrix } t.vsize = 12 + (t.vertex.normal?6:0) + (t.vertex.color?4:0) + (t.vertex.texCoord?8:0); t.fsize = 6; //problem: I have no idea how much space a texture is needed in GPU. 10x factor assumed. var tmptexsize = new Uint32Array(n-1); var tmptexcount = new Uint32Array(n-1); for(var i = 0; i < n-1; i++) { for(var p = t.nfirstpatch[i]; p != t.nfirstpatch[i+1]; p++) { var tex = t.patches[p*3+2]; tmptexsize[i] += t.textures[tex+1] - t.textures[tex]; tmptexcount[i]++; } t.nsize[i] = t.vsize*t.nvertices[i] + t.fsize*t.nfaces[i]; } for(var i = 0; i < n-1; i++) { t.nsize[i] += 10*tmptexsize[i]/tmptexcount[i]; } t.status = new Uint8Array(n); //0 for none, 1 for ready, 2+ for waiting data t.frames = new Uint32Array(n); t.errors = new Float32Array(n); //biggest error of instances t.ibo = new Array(n); t.vbo = new Array(n); t.texids = new Array(n); t.isReady = true; if(t.onLoad) t.onLoad(); }, importAttribute: function(view) { var a = {}; a.type = view.getUint8(view.offset++, true); a.size = view.getUint8(view.offset++, true); a.glType = attrGlMap[a.type]; a.normalized = a.type < 7; a.stride = attrSizeMap[a.type]*a.size; if(a.size == 0) return null; return a; }, importElement: function(view) { var e = []; for(i = 0; i < 8; i++) e[i] = this.importAttribute(view); return e; }, importVertex: function(view) { //enum POSITION, NORMAL, COLOR, TEXCOORD, DATA0 var e = this.importElement(view); var color = e[2]; if(color) { color.type = 2; //unsigned byte color.glType = attrGlMap[2]; } return { position: e[0], normal: e[1], color: e[2], texCoord: e[3], data: e[4] }; }, //enum INDEX, NORMAL, COLOR, TEXCOORD, DATA0 importFace: function(view) { var e = this.importElement(view); var color = e[2]; if(color) { color.type = 2; //unsigned byte color.glType = attrGlMap[2]; } return { index: e[0], normal: e[1], color: e[2], texCoord: e[3], data: e[4] }; }, importSignature: function(view) { var s = {}; s.vertex = this.importVertex(view); s.face = this.importFace(view); s.flags = getUint32(view); return s; }, importHeader: function(view) { var magic = getUint32(view); if(magic != 0x4E787320) return null; var h = {}; h.version = getUint32(view); h.verticesCount = getUint64(view); h.facesCount = getUint64(view); h.signature = this.importSignature(view); h.nodesCount = getUint32(view); h.patchesCount = getUint32(view); h.texturesCount = getUint32(view); h.sphere = { center: [getFloat32(view), getFloat32(view), getFloat32(view)], radius: getFloat32(view) }; return h; } }; Instance = function(gl) { this.gl = gl; this.onLoad = function() {}; this.onUpdate = function() {}; this.drawBudget = drawBudget; this.attributes = { 'position':0, 'normal':1, 'color':2, 'uv':3 }; } Instance.prototype = { open: function(url) { var t = this; t.context = getContext(t.gl); t.modelMatrix = new Float32Array(16); t.viewMatrix = new Float32Array(16); t.projectionMatrix = new Float32Array(16); t.modelView = new Float32Array(16); t.modelViewInv = new Float32Array(16); t.modelViewProj = new Float32Array(16); t.modelViewProjInv = new Float32Array(16); t.planes = new Float32Array(24); t.viewport = new Float32Array(4); t.viewpoint = new Float32Array(4); t.context.meshes.forEach(function(m) { if(m.url == url){ t.mesh = m; t.renderMode = t.mesh.renderMode; t.mode = t.renderMode[0]; t.onLoad(); } }); if(!t.mesh) { t.mesh = new Mesh(); t.mesh.onLoad = function() { t.renderMode = t.mesh.renderMode; t.mode = t.renderMode[0]; t.onLoad(); } t.mesh.open(url); t.context.meshes.push(t.mesh); } }, close: function() { //remove instance from mesh. }, get isReady() { return this.mesh.isReady; }, setPrimitiveMode : function (mode) { this.mode = mode; }, get datasetRadius() { if(!this.isReady) return 1.0; return this.mesh.sphere.radius; }, get datasetCenter() { if(!this.isReady) return [0, 0, 0]; return this.mesh.sphere.center; }, updateView: function(viewport, projection, modelView) { var t = this; for(var i = 0; i < 16; i++) { t.projectionMatrix[i] = projection[i]; t.modelView[i] = modelView[i]; } for(var i = 0; i < 4; i++) t.viewport[i] = viewport[i]; matMul(t.projectionMatrix, t.modelView, t.modelViewProj); matInv(t.modelViewProj, t.modelViewProjInv); matInv(t.modelView, t.modelViewInv); t.viewpoint[0] = t.modelViewInv[12]; t.viewpoint[1] = t.modelViewInv[13]; t.viewpoint[2] = t.modelViewInv[14]; t.viewpoint[3] = 1.0; var m = t.modelViewProj; var mi = t.modelViewProjInv; var p = t.planes; //left, right, bottom, top as Ax + By + Cz + D = 0; p[0] = m[0] + m[3]; p[1] = m[4] + m[7]; p[2] = m[8] + m[11]; p[3] = m[12] + m[15]; p[4] = -m[0] + m[3]; p[5] = -m[4] + m[7]; p[6] = -m[8] + m[11]; p[7] = -m[12] + m[15]; p[8] = m[1] + m[3]; p[9] = m[5] + m[7]; p[10] = m[9] + m[11]; p[11] = m[13] + m[15]; p[12] = -m[1] + m[3]; p[13] = -m[5] + m[7]; p[14] = -m[9] + m[11]; p[15] = -m[13] + m[15]; p[16] = -m[2] + m[3]; p[17] = -m[6] + m[7]; p[18] = -m[10] + m[11]; p[19] = -m[14] + m[15]; p[20] = -m[2] + m[3]; p[21] = -m[6] + m[7]; p[22] = -m[10] + m[11]; p[23] = -m[14] + m[15]; for(var i = 0; i < 16; i+= 4) { var l = Math.sqrt(p[i]*p[i] + p[i+1]*p[i+1] + p[i+2]*p[i+2]); p[i] /= l; p[i+1] /= l; p[i+2] /= l; p[i+3] /= l; } //side is M'(1,0,0,1) - M'(-1,0,0,1) and they lie on the planes var r3 = mi[3] + mi[15]; var r0 = (mi[0] + mi[12 ])/r3; var r1 = (mi[1] + mi[13 ])/r3; var r2 = (mi[2] + mi[14 ])/r3; var l3 = -mi[3] + mi[15]; var l0 = (-mi[0] + mi[12 ])/l3 - r0; var l1 = (-mi[1] + mi[13 ])/l3 - r1; var l2 = (-mi[2] + mi[14 ])/l3 - r2; var side = Math.sqrt(l0*l0 + l1*l1 + l2*l2); //center of the scene is M'*(0, 0, 0, 1) var c0 = mi[12]/mi[15] - t.viewpoint[0]; var c1 = mi[13]/mi[15] - t.viewpoint[1]; var c2 = mi[14]/mi[15] - t.viewpoint[2]; var dist = Math.sqrt(c0*c0 + c1*c1 + c2*c2); t.resolution = (2*side/dist)/ t.viewport[2]; }, traversal : function () { var t = this; if(Debug.extract == true) return; if(!t.isReady) return; var n = t.mesh.nodesCount; t.visited = new Uint8Array(n); t.blocked = new Uint8Array(n); t.selected = new Uint8Array(n); t.visitQueue = new PriorityQueue(n); for(var i = 0; i < t.mesh.nroots; i++) t.insertNode(i); var candidatesCount = 0; t.targetError = t.context.currentError; t.currentError = 1e20; t.drawSize = 0; var nblocked = 0; var requested = 0; while(t.visitQueue.size && nblocked < maxBlocked) { var error = t.visitQueue.error[0]; var node = t.visitQueue.pop(); if ((requested < maxPending) && (t.mesh.status[node] == 0)) { t.context.candidates.push({id: node, instance:t, mesh:t.mesh, frame:t.context.frame, error:error}); requested++; } var blocked = t.blocked[node] || !t.expandNode(node, error); if (blocked) nblocked++; else { t.selected[node] = 1; t.currentError = error; } t.insertChildren(node, blocked); } }, insertNode: function (node) { var t = this; t.visited[node] = 1; var error = t.nodeError(node); if(node > 0 && error < t.targetError) return; //2% speed TODO check if needed var errors = t.mesh.errors; var frames = t.mesh.frames; if(frames[node] != t.context.frame || errors[node] < error) { errors[node] = error; frames[node] = t.context.frame; } t.visitQueue.push(node, error); }, insertChildren : function (node, block) { var t = this; for(var i = t.mesh.nfirstpatch[node]; i < t.mesh.nfirstpatch[node+1]; ++i) { var child = t.mesh.patches[i*3]; if (child == t.mesh.sink) return; if (block) t.blocked[child] = 1; if (!t.visited[child]) t.insertNode(child); } }, expandNode : function (node, error) { var t = this; if(node > 0 && error < t.targetError) { // console.log("Reached error", error, t.targetError); return false; } if(t.drawSize > t.drawBudget) { // console.log("Reached drawsize", t.drawSize, t.drawBudget); return false; } if(t.mesh.status[node] != 1) { //not ready // console.log("Node " + node + " still not loaded (cache?)"); return false; } var sp = t.mesh.nspheres; var off = node*5; if(t.isVisible(sp[off], sp[off+1], sp[off+2], sp[off+3])) //expanded radius t.drawSize += t.mesh.nvertices[node]*0.8; //we are adding half of the new faces. (but we are using the vertices so *2) return true; }, nodeError : function (n, tight) { var t = this; var spheres = t.mesh.nspheres; var b = t.viewpoint; var off = n*5; var cx = spheres[off+0]; var cy = spheres[off+1]; var cz = spheres[off+2]; var r = spheres[off+3]; if(tight) r = spheres[off+4]; var d0 = b[0] - cx; var d1 = b[1] - cy; var d2 = b[2] - cz; var dist = Math.sqrt(d0*d0 + d1*d1 + d2*d2) - r; if (dist < 0.1) dist = 0.1; //resolution is how long is a pixel at distance 1. var error = t.mesh.nerrors[n]/(t.resolution*dist); //in pixels if (!t.isVisible(cx, cy. cz, spheres[off+4])) error /= 1000.0; return error; }, isVisible : function (x, y, z, r) { var p = this.planes; for (i = 0; i < 24; i +=4) { if(p[i]*x + p[i+1]*y + p[i+2]*z +p[i+3] + r < 0) //ax+by+cz+w = 0; return false; } return true; }, renderNodes: function() { var t = this; var m = t.mesh; var gl = t.gl; var attr = t.attributes; var vertexEnabled = gl.getVertexAttrib(attr.position, gl.VERTEX_ATTRIB_ARRAY_ENABLED); var normalEnabled = gl.getVertexAttrib(attr.normal, gl.VERTEX_ATTRIB_ARRAY_ENABLED); var colorEnabled = attr.color >= 0? gl.getVertexAttrib(attr.color, gl.VERTEX_ATTRIB_ARRAY_ENABLED): false; var uvEnabled = attr.uv >= 0? gl.getVertexAttrib(attr.uv, gl.VERTEX_ATTRIB_ARRAY_ENABLED): false; gl.enableVertexAttribArray(attr.position); if(m.vertex.texCoord && attr.uv >= 0) gl.enableVertexAttribArray(attr.uv); if(m.vertex.normal && attr.normal >= 0) gl.enableVertexAttribArray(attr.normal); if(m.vertex.color && attr.color >= 0) gl.enableVertexAttribArray(attr.color); gl.vertexAttrib4fv(2, [0.8, 0.8, 0.8, 1.0]); if(Debug.nodes) { gl.disableVertexAttribArray(2); gl.disableVertexAttribArray(3); } var rendered = 0; var last_texture = -1; for(var n = 0; n < m.nodesCount; n++) { if(!t.selected[n]) continue; if(t.mode != "POINT") { var skip = true; for(var p = m.nfirstpatch[n]; p < m.nfirstpatch[n+1]; p++) { var child = m.patches[p*3]; if(!t.selected[child]) { skip = false; break; } } if(skip) continue; } var sp = t.mesh.nspheres; var off = n*5; if(!t.isVisible(sp[off], sp[off+1], sp[off+2], sp[off+4])) //tight radius continue; gl.bindBuffer(gl.ARRAY_BUFFER, m.vbo[n]); if(t.mode != "POINT") gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, m.ibo[n]); var nv = m.nvertices[n]; gl.vertexAttribPointer(attr.position, 3, gl.FLOAT, false, 12, 0); var offset = nv*12; if(m.vertex.texCoord && attr.uv >= 0) gl.vertexAttribPointer(attr.uv, 2, gl.FLOAT, false, 8, offset), offset += nv*8; if(m.vertex.normal && attr.normal >= 0) gl.vertexAttribPointer(attr.normal, 3, gl.SHORT, true, 6, offset), offset += nv*6; if(m.vertex.color && attr.color >= 0) gl.vertexAttribPointer(attr.color, 4, gl.UNSIGNED_BYTE, true, 4, offset); if (Debug.nodes) { var error = t.nodeError(n, true); var palette = { 1: [1, 1, 1, 1], 2: [0.5, 1, 1, 1], 4: [0, 1, 1, 1], 8: [0, 1, 0.5, 1], 12: [0, 1, 0, 1], 16: [0, 1, 0, 1], 20: [1, 1, 0, 1], 30: [1, 0.5, 0, 1], 1e20: [1, 0, 0, 1] }; for(i in palette) if(i > error) { gl.vertexAttrib4fv(attr.color, palette[i]); break; } // gl.vertexAttrib4fv(2, [(n*200 %255)/255.0, (n*140 %255)/255.0,(n*90 %255)/255.0, 1]); } if (Debug.draw) continue; if(t.mode == "POINT") { var pointsize = Math.ceil(0.30*t.currentError); if(pointsize > 2) pointsize = 2; gl.vertexAttrib1fv(4, [pointsize]); var error = t.nodeError(n); var fraction = (error/t.currentError - 1); if(fraction > 1) fraction = 1; var count = fraction * nv; if(count != 0) { if(m.vertex.texCoord) { var texid = m.patches[m.nfirstpatch[n]*3+2]; if(texid != -1 && texid != last_texture) { //bind texture var tex = m.texids[texid]; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex); } } gl.drawArrays(gl.POINTS, 0, count); rendered += count; } continue; } //concatenate renderings to remove useless calls. except we have textures. var offset = 0; var end = 0; var last = m.nfirstpatch[n+1]-1; for (var p = m.nfirstpatch[n]; p < m.nfirstpatch[n+1]; ++p) { var child = m.patches[p*3]; if(!t.selected[child]) { end = m.patches[p*3+1]; if(p < last) //if textures we do not join. TODO: should actually check for same texture of last one. continue; } if(end > offset) { if(m.vertex.texCoord) { var texid = m.patches[p*3+2]; if(texid != -1 && texid != last_texture) { //bind texture var tex = m.texids[texid]; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex); last_texture = texid; } } gl.drawElements(gl.TRIANGLES, (end - offset) * 3, gl.UNSIGNED_SHORT, offset * 6); rendered += end - offset; } offset = m.patches[p*3+1]; } } t.context.rendered += rendered; if(!vertexEnabled) gl.disableVertexAttribArray(attr.position); if(!normalEnabled && attr.normal >= 0) gl.disableVertexAttribArray(attr.normal); if(!colorEnabled && attr.color >= 0) gl.disableVertexAttribArray(attr.color); if(!uvEnabled && attr.uv >= 0) gl.disableVertexAttribArray(attr.uv); gl.bindBuffer(gl.ARRAY_BUFFER, null); if(t.mode != "POINT") gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); }, render: function() { this.traversal(); this.renderNodes(); } }; //keep track of meshes and which GL they belong to. (no sharing between contexts) var contexts = []; function getContext(gl) { var c = null; if(!gl.isTexture) throw "Something wrong"; contexts.forEach(function(g) { if(g.gl == gl) c = g; }); if(c) return c; c = { gl:gl, meshes:[], frame:0, cacheSize:0, candidates:[], pending:0, targetFps: targetFps, targetError: targetError, currentError: targetError }; contexts.push(c); return c; } function beginFrame(gl, fps) { //each context has a separate frame count. var c = getContext(gl); c.frame++; c.candidates = []; if(fps && c.targetFps) { var r = c.targetFps/fps; if(r > 1.1) c.currentError *= 1.05; if(r < 0.9) c.currentError *= 0.95; if(c.currentError < c.targetError) c.currentError = c.targetError; if(c.currentError > 10) c.currentError = 10; } // console.log("current", c.currentError, "fps", fps, "targetFps", c.targetFps, "rendered", c.rendered); c.rendered = 0; } function endFrame(gl) { updateCache(gl); } function removeNode(context, node) { var n = node.id; var m = node.mesh; if(m.status[n] == 0) return; // console.log("Removing " + m.url + " node: " + n); m.status[n] = 0; if (m.georeq.readyState != 4) m.georeq.abort(); if (m.texreq.readyState != 4) m.texreq.abort(); context.cacheSize -= m.nsize[n]; context.gl.deleteBuffer(m.vbo[n]); context.gl.deleteBuffer(m.ibo[n]); m.vbo[n] = m.ibo[n] = null; if(!m.vertex.texCoord) return; var tex = m.patches[m.nfirstpatch[n]*3+2]; //TODO assuming one texture per node m.texref[tex]--; if(m.texref[tex] == 0 && m.texids[tex]) { context.gl.deleteTexture(m.texids[tex]); m.texids[tex] = null; } } function requestNode(context, node) { var n = node.id; var m = node.mesh; m.status[n] = 2; //pending context.pending++; context.cacheSize += m.nsize[n]; node.reqAttempt = 0; node.context = context; node.nvert = m.nvertices[n]; node.nface = m.nfaces[n]; // console.log("Requesting " + m.url + " node: " + n); requestNodeGeometry(context, node); requestNodeTexture(context, node); } function requestNodeGeometry(context, node) { var n = node.id; var m = node.mesh; m.status[n]++; //pending m.georeq = m.httpRequest( m.noffsets[n], m.noffsets[n+1], function() { loadNodeGeometry(this, context, node); }, function() { // console.log("Geometry request error!"); recoverNode(context, node, 0); }, function() { // console.log("Geometry request abort!"); removeNode(context, node); }, 'arraybuffer' ); } function requestNodeTexture(context, node) { var n = node.id; var m = node.mesh; if(!m.vertex.texCoord) return; var tex = m.patches[m.nfirstpatch[n]*3+2]; m.texref[tex]++; if(m.texids[tex]) return; m.status[n]++; //pending m.texreq = m.httpRequest( m.textures[tex], m.textures[tex+1], function() { loadNodeTexture(this, context, node, tex); }, function() { // console.log("Texture request error!"); recoverNode(context, node, 1); }, function() { // console.log("Texture request abort!"); removeNode(context, node); }, 'blob' ); } function recoverNode(context, node, id) { var n = node.id; var m = node.mesh; if(m.status[n] == 0) return; m.status[n]--; if(node.reqAttempt > maxReqAttempt) { // console.log("Max request limit for " + m.url + " node: " + n); removeNode(context, node); return; } node.reqAttempt++; switch (id){ case 0: requestNodeGeometry(context, node); console.log("Recovering geometry for " + m.url + " node: " + n); break; case 1: requestNodeTexture(context, node); console.log("Recovering texture for " + m.url + " node: " + n); break; } } function loadNodeGeometry(request, context, node) { var n = node.id; var m = node.mesh; if(m.status[n] == 0) return; node.buffer = request.response; if(!m.compressed) readyNode(node); else if(m.meco) { var sig = { texcoords: m.vertex.texCoord, normals:m.vertex.normal, colors:m.vertex.color, indices: m.face.index } var patches = []; for(var k = m.nfirstpatch[n]; k < m.nfirstpatch[n+1]; k++) patches.push(m.patches[k*3+1]); if(!meco) loadMeco(); meco.postRequest(sig, node, patches); } else { if(!corto) loadCorto(); corto.postRequest(node); } } function loadNodeTexture(request, context, node, texid) { var n = node.id; var m = node.mesh; if(m.status[n] == 0) return; var blob = request.response; var urlCreator = window.URL || window.webkitURL; var img = document.createElement('img'); img.onerror = function(e) { console.log("Texture loading error!"); }; img.src = urlCreator.createObjectURL(blob); var gl = context.gl; img.onload = function() { urlCreator.revokeObjectURL(img.src); var flip = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); var tex = m.texids[texid] = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); var s = gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flip); m.status[n]--; if(m.status[n] == 2) { m.status[n]--; //ready node.reqAttempt = 0; node.context.pending--; node.instance.onUpdate(); updateCache(gl); } } } function scramble(n, coords, normals, colors) { while (n > 0) { var i = Math.floor(Math.random() * n); n--; for(var k =0; k < 3; k++) { var v = coords[n*3+k]; coords[n*3+k] = coords[i*3+k]; coords[i*3+k] = v; if(normals) { var v = normals[n*3+k]; normals[n*3+k] = normals[i*3+k]; normals[i*3+k] = v; } if(colors) { var v = colors[n*4+k]; colors[n*4+k] = colors[i*4+k]; colors[i*4+k] = v; } } } } function readyNode(node) { var m = node.mesh; var n = node.id; var nv = m.nvertices[n]; var nf = m.nfaces[n]; var model = node.model; var vertices; var indices; if(!m.corto) { vertices = new Uint8Array(node.buffer, 0, nv*m.vsize); //dangerous alignment 16 bits if we had 1b attribute indices = new Uint8Array(node.buffer, nv*m.vsize, nf*m.fsize); if(nf == 0) { var off = nv*12; var v = new Float32Array(node.buffer, 0, nv*3); if(m.vertex.normal) { var no = new Int16Array(node.buffer, off, nv*3); off += nv*6; } if(m.vertex.color) { var co = new Uint8Array(node.buffer, off, nv*4); } scramble(nv, v, no, co); } } else { indices = node.model.index; vertices = new ArrayBuffer(nv*m.vsize); var v = new Float32Array(vertices, 0, nv*3); v.set(model.position, 0, nv*3); var off = nv*12; if(model.uv) { var uv = new Float32Array(vertices, off, nv*2); uv.set(model.uv); off += nv*8; } if(model.normal) { var no = new Int16Array(vertices, off, nv*3); no.set(model.normal); off += nv*6; } if(model.color) { var co = new Uint8Array(vertices, off, nv*4); co.set(model.color); off += nv*4; } if(nf == 0) scramble(nv, v, no, co); } var gl = node.context.gl; var vbo = m.vbo[n] = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); var ibo = m.ibo[n] = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); m.status[n]--; if(m.status[n] == 2) { m.status[n]--; //ready node.reqAttempt = 0; node.context.pending--; node.instance.onUpdate(); updateCache(gl); } } function updateCache(gl) { var context = getContext(gl); var best = null; context.candidates.forEach(function(e) { if(e.mesh.status[e.id] == 0 && (!best || e.error > best.error)) best = e; }); context.candidates = []; if(!best) return; while(context.cacheSize > maxCacheSize) { var worst = null; //find worst in cache context.meshes.forEach(function(m) { var n = m.nodesCount; for(i = 0; i < n; i++) if(!worst || (m.status[i] == 1 && m.errors[i] < worst.error)) worst = {error: m.errors[i], frame: m.frames[i], mesh:m, id:i}; }); if(!worst || (worst.error >= best.error && worst.frame == best.frame)) return; removeNode(context, worst); } requestNode(context, best); if(context.pending < maxPending) updateCache(gl); } //nodes are loaded asincronously, just update mesh content (VBO) cache size is kept globally. //but this could be messy. function setTargetError(gl, error) { var context = getContext(gl); context.targetError = error; } function setTargetFps(gl, fps) { var context = getContext(gl); context.targetFps = fps; } function setMaxCacheSize(gl, size) { var context = getContext(gl); context.maxCacheSize = size; } return { Mesh: Mesh, Renderer: Instance, Renderable: Instance, Instance:Instance, Debug: Debug, contexts: contexts, beginFrame:beginFrame, endFrame:endFrame, setTargetError:setTargetError, setTargetFps:setTargetFps, setMaxCacheSize:setMaxCacheSize }; }();