var fs = require('fs'); var THREE = require('three'); var L3D = require('../../static/js/l3d.min.js'); try { var predictionTables = [ JSON.parse(fs.readFileSync('./geo/mat1.json')), JSON.parse(fs.readFileSync('./geo/mat2.json')), JSON.parse(fs.readFileSync('./geo/mat3.json')), [[1,1,0], [1,2,0], [2,1,0]] ]; } catch (e) { process.stderr.write('No prefetching will be done !'); predictionTables = []; } function isInFrustum(element, planes) { if (element instanceof Array) { var outcodes = []; for (var i = 0; i < element.length; i++) { var vertex = element[i]; var currentOutcode = ""; for (var j = 0; j < planes.length; j++) { var plane = planes[j]; distance = plane.normal.x * vertex.x + plane.normal.y * vertex.y + plane.normal.z * vertex.z + plane.constant; // if (distance < 0) { // exitToContinue = true; // break; // } currentOutcode += distance > 0 ? '0' : '1'; } outcodes.push(parseInt(currentOutcode,2)); } // http://vterrain.org/LOD/culling.html // I have no idea what i'm doing // http://i.kinja-img.com/gawker-media/image/upload/japbcvpavbzau9dbuaxf.jpg // But it seems to work // EDIT : Not, this should be ok http://www.cs.unc.edu/~blloyd/comp770/Lecture07.pdf if ((outcodes[0] | outcodes[1] | outcodes[2]) === 0) { return true; } else if ((outcodes[0] & outcodes[1] & outcodes[2]) !== 0) { return false; } else { // part of the triangle is inside the viewing volume return true; } } } /** * @private */ function bisect(items, x, lo, hi) { var mid; if (typeof(lo) == 'undefined') lo = 0; if (typeof(hi) == 'undefined') hi = items.length; while (lo < hi) { mid = Math.floor((lo + hi) / 2); if (x < items[mid]) hi = mid; else lo = mid + 1; } return lo; } /** * @private */ function insort(items, x) { items.splice(bisect(items, x), 0, x); } /** * @private */ function partialSort(items, k, comparator) { var smallest = items.slice(0, k).sort(), max = smallest[k-1]; for (var i = k, len = items.length; i < len; ++i) { var item = items[i]; var cond = comparator === undefined ? item < max : comparator(item, max) < 0; if (cond) { insort(smallest, item); smallest.length = k; max = smallest[k-1]; } } return smallest; } /** * A class that streams easily a mesh via socket.io * @memberOf geo * @constructor * @param {string} path to the mesh */ geo.MeshStreamer = function(path) { /** * array of array telling if the jth face of the ith mesh has already been sent * * For each mesh, there is an object containing * * @type {Object[]} */ this.meshFaces = []; /** * array of booleans telling if the ith vertex has already been sent * @type {Boolean[]} */ this.vertices = []; /** * array of booleans telling if the ith face has already been sent * @type {Boolean[]} */ this.faces = []; /** * array of booleans telling if the ith normal has already been sent * @type {Boolean[]} */ this.normals = []; /** * array of booleans telling if the ith texCoord has already been sent * @type {Boolean[]} */ this.texCoords = []; /** * Number of element to send by packet * @type {Number} */ this.chunk = 5000; this.previousReco = 0; if (path !== undefined) { this.mesh = geo.availableMeshes[path]; } }; geo.MeshStreamer.prototype.isBackFace = function(camera, face) { var directionCamera = L3D.Tools.diff( L3D.Tools.mul( L3D.Tools.sum( L3D.Tools.sum( this.mesh.vertices[face.a], this.mesh.vertices[face.b] ), this.mesh.vertices[face.c] ), 1/3), camera.position ); var v1 = L3D.Tools.diff(this.mesh.vertices[face.b], this.mesh.vertices[face.a]); var v2 = L3D.Tools.diff(this.mesh.vertices[face.c], this.mesh.vertices[face.a]); var normal = L3D.Tools.cross(v1, v2); return L3D.Tools.dot(directionCamera, normal) > 0; }; /** * Compute a function that can compare two faces * @param {Camera} camera a camera seeing or not face * @returns {function} the function that compares two faces : the higher face is the most interesting for the camera */ geo.MeshStreamer.prototype.faceComparator = function(camera) { var self = this; var direction = { x: camera.target.x - camera.position.x, y: camera.target.y - camera.position.y, z: camera.target.z - camera.position.z }; var norm = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); direction.x /= norm; direction.y /= norm; direction.z /= norm; return function(face1, face2) { var center1 = { x: (self.mesh.vertices[face1.a].x + self.mesh.vertices[face1.b].x + self.mesh.vertices[face1.b].x) / 3, y: (self.mesh.vertices[face1.a].y + self.mesh.vertices[face1.b].y + self.mesh.vertices[face1.b].y) / 3, z: (self.mesh.vertices[face1.a].z + self.mesh.vertices[face1.b].z + self.mesh.vertices[face1.b].z) / 3 }; var dir1 = { x: center1.x - camera.position.x, y: center1.y - camera.position.y, z: center1.z - camera.position.z }; var norm1 = Math.sqrt(dir1.x * dir1.x + dir1.y * dir1.y + dir1.z + dir1.z); dir1.x /= norm1; dir1.y /= norm1; dir1.z /= norm1; var dot1 = direction.x * dir1.x + direction.y * dir1.y + direction.z * dir1.z; var center2 = { x: (self.mesh.vertices[face2.a].x + self.mesh.vertices[face2.b].x + self.mesh.vertices[face2.b].x) / 3, y: (self.mesh.vertices[face2.a].y + self.mesh.vertices[face2.b].y + self.mesh.vertices[face2.b].y) / 3, z: (self.mesh.vertices[face2.a].z + self.mesh.vertices[face2.b].z + self.mesh.vertices[face2.b].z) / 3 }; var dir2 = { x: center2.x - camera.position.x, y: center2.y - camera.position.y, z: center2.z - camera.position.z }; var norm2 = Math.sqrt(dir2.x * dir2.x + dir2.y * dir2.y + dir2.z + dir2.z); dir2.x /= norm2; dir2.y /= norm2; dir2.z /= norm2; var dot2 = direction.x * dir2.x + direction.y * dir2.y + direction.z * dir2.z; // Decreasing order if (dot1 > dot2) { return -1; } if (dot1 < dot2) { return 1; } return 0; }; }; /** * Initialize the socket.io callback * @param {socket} socket the socket to initialize */ geo.MeshStreamer.prototype.start = function(socket) { this.meshIndex = 0; this.socket = socket; var self = this; socket.on('request', function(path, laggy, prefetch) { if (laggy === true) { self.chunk = 1; } self.prefetch = prefetch; self.mesh = geo.availableMeshes[path]; switch (path) { case '/static/data/bobomb/bobomb battlefeild.obj': case '/static/data/bobomb/bobomb battlefeild_sub.obj': self.predictionTable = predictionTables[0]; break; case '/static/data/mountain/coocoolmountain.obj': case '/static/data/mountain/coocoolmountain_sub.obj': self.predictionTable = predictionTables[1]; break; case '/static/data/whomp/Whomps Fortress.obj': case '/static/data/whomp/Whomps Fortress_sub.obj': self.predictionTable = predictionTables[2]; break; default: self.predictionTable = predictionTables[3]; }; if (self.mesh === undefined) { process.stderr.write('Wrong path for model : ' + path); socket.emit('refused'); socket.disconnect(); return; } self.meshFaces = new Array(self.mesh.meshes.length); for (var i = 0; i < self.meshFaces.length; i++) { self.meshFaces[i] = { counter: 0, array: new Array(self.mesh.meshes[i].faces.length) }; } socket.emit('ok'); }); socket.on('materials', function() { var data = self.nextMaterials(); socket.emit('elements', data); }); socket.on('reco', function(recoId) { self.previousReco = recoId + 1; }); socket.on('next', function(_camera) { var cameraFrustum = {}; // Clean camera attribute if (_camera !== null) { cameraFrustum = { position: { x: _camera[0][0], y: _camera[0][1], z: _camera[0][2] }, target: { x: _camera[1][0], y: _camera[1][1], z: _camera[1][2] }, planes: [] }; var fullFrustum = _camera[2]; for (i = 3; i < _camera.length; i++) { cameraFrustum.planes.push({ normal: { x: _camera[i][0], y: _camera[i][1], z: _camera[i][2] }, constant: _camera[i][3] }); } } // Create config for proportions of chunks var config; if (self.prefetch) { config = [{ frustum: cameraFrustum, proportion : 0.5}]; // Find best recommendation var bestReco; var bestScore = -Infinity; var bestIndex = null; if (self.predictionTable !== undefined) { var sum = 0; for (var i = 0; i < self.mesh.recommendations.length; i++) { sum += self.predictionTable[self.previousReco][i]; } for (var i = 0; i < self.mesh.recommendations.length; i++) { if (self.predictionTable[self.previousReco][i] > 0) { config.push({ proportion : self.predictionTable[self.previousReco][i] / (2 * sum), frustum : self.mesh.recommendations[i] }); } } // console.log(config.map(function(o) { return o.proportion; })); } else { // For sponza bestReco = self.mesh.recommendations[0]; } if (!fullFrustum) { // console.log('Frustum and prefetch : ' + (cameraFrustum !== undefined) + ' ' + (bestReco !== undefined)); } else { // console.log('Full frustum fetching (reco clicked)'); config = [{ proportion: 1, frustum: cameraFrustum }]; } } else { config = [{frustum: cameraFrustum, proportion: 1}]; } // Send next elements var oldTime = Date.now(); var next = self.nextElements(config); // console.log(next.configSizes); // console.log('Time to generate chunk : ' + (Date.now() - oldTime) + 'ms'); if (next.data.length === 0) { // If nothing, just serve stuff var tmp = self.nextElements([ // { // proportion: 1, // frustum: cameraFrustum // } ]); next.data = tmp.data; } else if (!self.prefetch && next.size < this.chunk) { // Recompute config var newConfig = []; var sum = 0; for (var i = 0; i < config.length; i++) { // Check if config was full if (next.configSizes[i] < this.chunk * config[i].proportion) { newConfig.push(config[i]); sum += config[i].proportion; } } for (var i = 0; i < newConfig.length; i++) { newConfig[i].proportion /= sum; } // Normalize config probabilities var newData = self.nextElements(newConfig, this.chunk - next.size); next.data = next.data.push.apply(next.data, newData.data); } socket.emit('elements', next.data); if (next.finished) { socket.disconnect(); } }); }; /** * Prepare the array of materials * @return array the array to send with all materials of the current mesh */ geo.MeshStreamer.prototype.nextMaterials = function() { var data = []; data.push(['g', this.mesh.numberOfFaces]); for (var i = 0; i < this.mesh.meshes.length; i++) { var currentMesh = this.mesh.meshes[i]; // Send usemtl data.push([ 'u', currentMesh.material, currentMesh.vertices.length, currentMesh.faces.length, this.mesh.texCoords.length > 0, this.mesh.normals.length > 0 ]); } return data; }; /** * Prepare the next elements * @param {camera} _camera a camera that can be usefull to do smart streaming (stream * only interesting parts according to the camera * @returns {array} an array of elements ready to send */ geo.MeshStreamer.prototype.nextElements = function(config, chunk) { if (chunk === undefined) chunk = this.chunk; var i; var data = []; var configSizes = []; var mightBeCompletetlyFinished = true; // BOOM // if (camera != null) // this.mesh.faces.sort(this.faceComparator(camera)); if (config.length === 0) { config.push({ proportion: 1 }); } totalSize = 0; for (var configIndex = 0; configIndex < config.length; configIndex++) { configSizes[configIndex] = 0; } faceloop: for (var faceIndex = 0; faceIndex < this.mesh.faces.length; faceIndex++) { var currentFace = this.mesh.faces[faceIndex]; if (this.faces[currentFace.index] === true) { continue; } mightBeCompletetlyFinished = false; var vertex1 = this.mesh.vertices[currentFace.a]; var vertex2 = this.mesh.vertices[currentFace.b]; var vertex3 = this.mesh.vertices[currentFace.c]; for (var configIndex = 0; configIndex < config.length; configIndex++) { var currentConfig = config[configIndex]; if ( configSizes[configIndex] < chunk * currentConfig.proportion) { var display = false; var exitToContinue = false; var threeVertices = [vertex1, vertex2, vertex3]; // Frustum culling if (currentConfig.frustum === undefined || (isInFrustum(threeVertices, currentConfig.frustum.planes) && !this.isBackFace(currentConfig.frustum, currentFace))) { // Send face if (!this.vertices[currentFace.a]) { data.push(vertex1.toList()); this.vertices[currentFace.a] = true; configSizes[configIndex]++; } if (!this.vertices[currentFace.b]) { data.push(vertex2.toList()); this.vertices[currentFace.b] = true; configSizes[configIndex]++; } if (!this.vertices[currentFace.c]) { data.push(vertex3.toList()); this.vertices[currentFace.c] = true; configSizes[configIndex]++; } var normal1 = this.mesh.normals[currentFace.aNormal]; var normal2 = this.mesh.normals[currentFace.bNormal]; var normal3 = this.mesh.normals[currentFace.cNormal]; if (normal1 !== undefined && !this.normals[currentFace.aNormal]) { data.push(normal1.toList()); this.normals[currentFace.aNormal] = true; configSizes[configIndex]++; totalSize++; } if (normal2 !== undefined && !this.normals[currentFace.bNormal]) { data.push(normal2.toList()); this.normals[currentFace.bNormal] = true; configSizes[configIndex]++; totalSize++; } if (normal3 !== undefined && !this.normals[currentFace.cNormal]) { data.push(normal3.toList()); this.normals[currentFace.cNormal] = true; configSizes[configIndex]++; totalSize++; } var tex1 = this.mesh.texCoords[currentFace.aTexture]; var tex2 = this.mesh.texCoords[currentFace.bTexture]; var tex3 = this.mesh.texCoords[currentFace.cTexture]; if (tex1 !== undefined && !this.texCoords[currentFace.aTexture]) { data.push(tex1.toList()); this.texCoords[currentFace.aTexture] = true; configSizes[configIndex]++; totalSize++; } if (tex2 !== undefined && !this.texCoords[currentFace.bTexture]) { data.push(tex2.toList()); this.texCoords[currentFace.bTexture] = true; configSizes[configIndex]++; totalSize++; } if (tex3 !== undefined && !this.texCoords[currentFace.cTexture]) { data.push(tex3.toList()); this.texCoords[currentFace.cTexture] = true; configSizes[configIndex]++; totalSize++; } data.push(currentFace.toList()); // this.meshFaces[meshIndex] = this.meshFaces[meshIndex] || []; this.faces[currentFace.index] = true; configSizes[configIndex]+=3; totalSize+=3; // this.meshFaces[meshIndex].counter++; // currentMesh.faceIndex++; // if (totalSize > chunk) { // // console.log(configIndex, sent/(chunk * currentConfig.proportion)); // return {data: data, finsihed:false, configSizes: configSizes, size: totalSize}; // } // Loop on next face continue faceloop; } } if (totalSize > chunk) { // console.log(configIndex, sent/(chunk * currentConfig.proportion)); return {data: data, finsihed:false, configSizes: configSizes, size: totalSize}; } } } return {data: data, finished: mightBeCompletetlyFinished, configSizes: configSizes}; }; geo.MeshStreamer.prototype.isFinished = function(i) { return this.meshFaces[i].counter === this.meshFaces[i].array.length; };