UNPKG

babylon-navigation-mesh

Version:

A toolkit to move on navigation mesh with BABYLONJS

1,622 lines (1,281 loc) 1.87 MB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Navigation = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){ "use strict"; var Class = require("abitbol"); var BABYLON = require("babylonjs"); var BinaryHeap = require("./BinaryHeap.js"); var Astar = Class.$extend({ init: function init(graph) { for (var x = 0; x < graph.length; x++) { //for(var x in graph) { var node = graph[x]; node.f = 0; node.g = 0; node.h = 0; node.cost = 1.0; node.visited = false; node.closed = false; node.parent = null; } }, cleanUp: function cleanUp(graph) { for (var x = 0; x < graph.length; x++) { var node = graph[x]; delete node.f; delete node.g; delete node.h; delete node.cost; delete node.visited; delete node.closed; delete node.parent; } }, heap: function heap() { return new BinaryHeap(function (node) { return node.f; }); }, search: function search(graph, start, end) { this.init(graph); //heuristic = heuristic || astar.manhattan; var openHeap = this.heap(); openHeap.push(start); while (openHeap.size() > 0) { // Grab the lowest f(x) to process next. Heap keeps this sorted for us. var currentNode = openHeap.pop(); // End case -- result has been found, return the traced path. if (currentNode === end) { var curr = currentNode; var ret = []; while (curr.parent) { ret.push(curr); curr = curr.parent; } this.cleanUp(ret); return ret.reverse(); } // Normal case -- move currentNode from open to closed, process each of its neighbours. currentNode.closed = true; // Find all neighbours for the current node. Optionally find diagonal neighbours as well (false by default). var neighbours = this.neighbours(graph, currentNode); for (var i = 0, il = neighbours.length; i < il; i++) { var neighbour = neighbours[i]; if (neighbour.closed) { // Not a valid node to process, skip to next neighbour. continue; } // The g score is the shortest distance from start to current node. // We need to check if the path we have arrived at this neighbour is the shortest one we have seen yet. var gScore = currentNode.g + neighbour.cost; var beenVisited = neighbour.visited; if (!beenVisited || gScore < neighbour.g) { // Found an optimal (so far) path to this node. Take score for node to see how good it is. neighbour.visited = true; neighbour.parent = currentNode; if (!neighbour.centroid || !end.centroid) debugger; neighbour.h = neighbour.h || this.heuristic(neighbour.centroid, end.centroid); neighbour.g = gScore; neighbour.f = neighbour.g + neighbour.h; if (!beenVisited) { // Pushing to heap will put it in proper place based on the 'f' value. openHeap.push(neighbour); } else { // Already seen the node, but since it has been rescored we need to reorder it in the heap openHeap.rescoreElement(neighbour); } } } } // No result was found - empty array signifies failure to find path. return []; }, heuristic: function heuristic(pos1, pos2) { return BABYLON.Vector3.DistanceSquared(pos1, pos2); }, neighbours: function neighbours(graph, node) { var ret = []; for (var e = 0; e < node.neighbours.length; e++) { ret.push(graph[node.neighbours[e]]); } return ret; } }); module.exports = Astar; },{"./BinaryHeap.js":2,"abitbol":5,"babylonjs":7}],2:[function(require,module,exports){ "use strict"; var Class = require("abitbol"); var BinaryHeap = Class.$extend({ __init__: function __init__(scoreFunction) { this.content = []; this.scoreFunction = scoreFunction || new function () {}(); }, push: function push(element) { // Add the new element to the end of the array. this.content.push(element); // Allow it to sink down. this.sinkDown(this.content.length - 1); }, pop: function pop() { // Store the first element so we can return it later. var result = this.content[0]; // Get the element at the end of the array. var end = this.content.pop(); // If there are any elements left, put the end element at the // start, and let it bubble up. if (this.content.length > 0) { this.content[0] = end; this.bubbleUp(0); } return result; }, remove: function remove(node) { var i = this.content.indexOf(node); // When it is found, the process seen in 'pop' is repeated // to fill up the hole. var end = this.content.pop(); if (i !== this.content.length - 1) { this.content[i] = end; if (this.scoreFunction(end) < this.scoreFunction(node)) { this.sinkDown(i); } else { this.bubbleUp(i); } } }, size: function size() { return this.content.length; }, rescoreElement: function rescoreElement(node) { this.sinkDown(this.content.indexOf(node)); }, sinkDown: function sinkDown(n) { // Fetch the element that has to be sunk. var element = this.content[n]; // When at 0, an element can not sink any further. while (n > 0) { // Compute the parent element's index, and fetch it. var parentN = (n + 1 >> 1) - 1, parent = this.content[parentN]; // Swap the elements if the parent is greater. if (this.scoreFunction(element) < this.scoreFunction(parent)) { this.content[parentN] = element; this.content[n] = parent; // Update 'n' to continue at the new position. n = parentN; } // Found a parent that is less, no need to sink any further. else { break; } } }, bubbleUp: function bubbleUp(n) { // Look up the target element and its score. var length = this.content.length, element = this.content[n], elemScore = this.scoreFunction(element); while (true) { // Compute the indices of the child elements. var child2N = n + 1 << 1, child1N = child2N - 1; // This is used to store the new position of the element, // if any. var swap = null; // If the first child exists (is inside the array)... if (child1N < length) { // Look it up and compute its score. var child1 = this.content[child1N], child1Score = this.scoreFunction(child1); // If the score is less than our element's, we need to swap. if (child1Score < elemScore) swap = child1N; } // Do the same checks for the other child. if (child2N < length) { var child2 = this.content[child2N], child2Score = this.scoreFunction(child2); if (child2Score < (swap === null ? elemScore : child1Score)) { swap = child2N; } } // If the element needs to be moved, swap it, and continue. if (swap !== null) { this.content[n] = this.content[swap]; this.content[swap] = element; n = swap; } // Otherwise, we are done. else { break; } } } }); module.exports = BinaryHeap; },{"abitbol":5}],3:[function(require,module,exports){ "use strict"; var Class = require("abitbol"); var BABYLON = require("babylonjs"); var Channel = Class.$extend({ __init__: function __init__() { this.portals = []; }, push: function push(p1, p2) { if (p2 === undefined) p2 = p1; this.portals.push({ left: p1, right: p2 }); }, _vequal: function _vequal(a, b) { return BABYLON.Vector3.DistanceSquared(a, b) < 0.00001; }, _triarea2: function _triarea2(a, b, c) { var ax = b.x - a.x; var az = b.z - a.z; var bx = c.x - a.x; var bz = c.z - a.z; return bx * az - ax * bz; }, stringPull: function stringPull() { var portals = this.portals; var pts = []; // Init scan state var portalApex, portalLeft, portalRight; var apexIndex = 0, leftIndex = 0, rightIndex = 0; portalApex = portals[0].left; portalLeft = portals[0].left; portalRight = portals[0].right; // Add start point. pts.push(portalApex); for (var i = 1; i < portals.length; i++) { var left = portals[i].left; var right = portals[i].right; // Update right vertex. if (this._triarea2(portalApex, portalRight, right) >= 0.0) { if (this._vequal(portalApex, portalRight) || this._triarea2(portalApex, portalLeft, right) < 0.0) { // Tighten the funnel. portalRight = right; rightIndex = i; } else { // Right over left, insert left to path and restart scan from portal left point. pts.push(portalLeft); // Make current left the new apex. portalApex = portalLeft; apexIndex = leftIndex; // Reset portal portalLeft = portalApex; portalRight = portalApex; leftIndex = apexIndex; rightIndex = apexIndex; // Restart scan i = apexIndex; continue; } } // Update left vertex. if (this._triarea2(portalApex, portalLeft, left) <= 0.0) { if (this._vequal(portalApex, portalLeft) || this._triarea2(portalApex, portalRight, left) > 0.0) { // Tighten the funnel. portalLeft = left; leftIndex = i; } else { // Left over right, insert right to path and restart scan from portal right point. pts.push(portalRight); // Make current right the new apex. portalApex = portalRight; apexIndex = rightIndex; // Reset portal portalLeft = portalApex; portalRight = portalApex; leftIndex = apexIndex; rightIndex = apexIndex; // Restart scan i = apexIndex; continue; } } } if (pts.length === 0 || !this._vequal(pts[pts.length - 1], portals[portals.length - 1].left)) { // Append last point to path. pts.push(portals[portals.length - 1].left); } this.path = pts; return pts; } }); module.exports = Channel; },{"abitbol":5,"babylonjs":7}],4:[function(require,module,exports){ "use strict"; var Class = require("abitbol"); var _ = require("lodash"); var Astar = require("./Astar.js"); var Channel = require("./Channel.js"); var BABYLON = require("babylonjs"); /** * This component generates screenshots of 3D models, in order to preview them when they are rendered. * * @class Focus3D * @constructor */ var Navigation = Class.$extend({ __init__: function __init__() { this.zoneNodes = {}; this.astar = new Astar(); this.yTolerance = 1; }, buildNodes: function buildNodes(mesh) { var navigationMesh = this._buildNavigationMesh(mesh.geometry); var zoneNodes = this._groupNavMesh(navigationMesh); return zoneNodes; }, setZoneData: function setZoneData(zone, data) { this.zoneNodes[zone] = data; }, setHeightTolerance: function setHeightTolerance(tolerance) { this.yTolerance = tolerance; }, getGroup: function getGroup(zone, position) { if (!this.zoneNodes[zone]) { return null; } var closestNodeGroup = null; var distance = Infinity; _.each(this.zoneNodes[zone].groups, function (group, index) { _.each(group, function (node) { var measuredDistance = BABYLON.Vector3.DistanceSquared(node.centroid, position); if (measuredDistance < distance) { closestNodeGroup = index; distance = measuredDistance; } }); }); return closestNodeGroup; }, getRandomNode: function getRandomNode(zone, group, nearPosition, nearRange) { if (!this.zoneNodes[zone]) return new BABYLON.Vector3(); nearPosition = nearPosition || null; nearRange = nearRange || 0; var candidates = []; var polygons = this.zoneNodes[zone].groups[group]; _.each(polygons, function (p) { if (nearPosition && nearRange) { if (BABYLON.Vector3.DistanceSquared(nearPosition, p.centroid) < nearRange * nearRange) { candidates.push(p.centroid); } } else { candidates.push(p.centroid); } }); return _.sample(candidates) || new BABYLON.Vector3(); }, projectOnNavmesh: function projectOnNavmesh(position, zone, group) { var allNodes = this.zoneNodes[zone].groups[group]; var vertices = this.zoneNodes[zone].vertices; var closestNode = null; var distance = Infinity; var finalProj = null, proj = null, node = null, measuredDistance = 0; for (var i = 0; i < allNodes.length; i++) { node = allNodes[i]; proj = this._getProjectionOnNode(position, node, vertices); measuredDistance = BABYLON.Vector3.DistanceSquared(proj, position); if (measuredDistance < distance) { distance = measuredDistance; //this.meshes[3].position.copyFrom(proj); finalProj = proj; closestNode = node; } } return finalProj; }, _projectPointOnPlane: function _projectPointOnPlane(point, plane) { var coef = BABYLON.Vector3.Dot(point, plane.normal) + plane.d; var proj = point.subtract(plane.normal.scale(coef)); return proj; }, _getProjectionOnNode: function _getProjectionOnNode(position, node, vertices) { var A = this.getVectorFrom(vertices, node.vertexIds[0]); var B = this.getVectorFrom(vertices, node.vertexIds[1]); var C = this.getVectorFrom(vertices, node.vertexIds[2]); var u = B.subtract(A); var v = C.subtract(A); var n = BABYLON.Vector3.Cross(u, v).normalize(); var plane = { normal: n, d: -BABYLON.Vector3.Dot(A, n) }; var p = this._projectPointOnPlane(position, plane); // Compute barycentric coordinates (u, v, w) for // point p with respect to triangle (a, b, c) var barycentric = function barycentric(p, a, b, c) { var ret = {}; var v0 = c.subtract(a), v1 = b.subtract(a), v2 = p.subtract(a); var d00 = BABYLON.Vector3.Dot(v0, v0); var d01 = BABYLON.Vector3.Dot(v0, v1); var d02 = BABYLON.Vector3.Dot(v0, v2); var d11 = BABYLON.Vector3.Dot(v1, v1); var d12 = BABYLON.Vector3.Dot(v1, v2); var denom = d00 * d11 - d01 * d01; ret.u = (d11 * d02 - d01 * d12) / denom; ret.v = (d00 * d12 - d01 * d02) / denom; ret.w = 1 - ret.u - ret.v; return ret; }; var bary = barycentric(p, A, B, C); bary.u = Math.min(Math.max(bary.u, 0), 1); bary.v = Math.min(Math.max(bary.v, 0), 1); if (bary.u + bary.v >= 1) { var sum = bary.u + bary.v; bary.u /= sum; bary.v /= sum; } var proj = A.add(B.subtract(A).scale(bary.v).add(C.subtract(A).scale(bary.u))); return proj; }, findPath: function findPath(startPosition, targetPosition, zone, group) { var allNodes = this.zoneNodes[zone].groups[group]; var vertices = this.zoneNodes[zone].vertices; var startingNode = null; for (var i = 0; i < allNodes.length; i++) { if (this._isVectorInPolygon(startPosition, allNodes[i], vertices)) { startingNode = allNodes[i]; break; } } var endNode = null; for (var i = 0; i < allNodes.length; i++) { if (this._isVectorInPolygon(targetPosition, allNodes[i], vertices)) { endNode = allNodes[i]; break; } } // If we can't find any node, theres no path to target if (!startingNode || !endNode) { return null; } if (startingNode.id != endNode.id) { // if the starting node and target node are at the same polygon skip searching and funneling as there is no obstacle. var paths = this.astar.search(allNodes, startingNode, endNode); } else { vectors = []; vectors.push(new BABYLON.Vector3(targetPosition.x, targetPosition.y, targetPosition.z)); return vectors; } var getPortalFromTo = function getPortalFromTo(a, b) { for (var i = 0; i < a.neighbours.length; i++) { if (a.neighbours[i] === b.id) { return a.portals[i]; } } }; // We got the corridor // Now pull the rope var channel = new Channel(); channel.push(startPosition); for (var i = 0; i < paths.length; i++) { var polygon = paths[i]; var nextPolygon = paths[i + 1]; if (nextPolygon) { var portals = getPortalFromTo(polygon, nextPolygon); channel.push(this.getVectorFrom(vertices, portals[0]), this.getVectorFrom(vertices, portals[1])); } } channel.push(targetPosition); channel.stringPull(); var vectors = []; channel.path.forEach(function (c) { var vec = new BABYLON.Vector3(c.x, c.y, c.z); // console.log(vec.clone().sub(startPosition).length()); // Ensure the intermediate steps aren't too close to the start position // var dist = vec.clone().sub(startPosition).lengthSq(); // if (dist > 0.01 * 0.01) { vectors.push(vec); // } }); // We don't need the first one, as we already know our start position vectors.shift(); return vectors; }, _isPointInPoly: function _isPointInPoly(poly, pt) { for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) { (poly[i].z <= pt.z && pt.z < poly[j].z || poly[j].z <= pt.z && pt.z < poly[i].z) && pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x && (c = !c); }return c; }, _isVectorInPolygon: function _isVectorInPolygon(vector, polygon, vertices) { // reference point will be the centroid of the polygon // We need to rotate the vector as well as all the points which the polygon uses var lowestPoint = 100000; var highestPoint = -100000; var polygonVertices = []; _.each(polygon.vertexIds, function (vId) { var point = this.getVectorFrom(vertices, vId); lowestPoint = Math.min(point.y, lowestPoint); highestPoint = Math.max(point.y, highestPoint); polygonVertices.push(point); }.bind(this)); if (vector.y < highestPoint + this.yTolerance && vector.y > lowestPoint - this.yTolerance && this._isPointInPoly(polygonVertices, vector)) { return true; } return false; }, _computeCentroids: function _computeCentroids(geometry) { var centroids = []; var indices = geometry.getIndices(); var vertices = geometry.getVerticesData(BABYLON.VertexBuffer.PositionKind); var c = new BABYLON.Vector3(0, 0, 0); for (var f = 0; f < indices.length; f += 3) { var p1 = this.getVectorFrom(vertices, indices[f]); var p2 = this.getVectorFrom(vertices, indices[f + 1]); var p3 = this.getVectorFrom(vertices, indices[f + 2]); c.copyFromFloats(0, 0, 0); c.addInPlace(p1); c.addInPlace(p2); c.addInPlace(p3); c.scaleInPlace(1 / 3); centroids.push(c.clone()); } geometry.centroids = centroids; }, _roundNumber: function _roundNumber(number, decimals) { var newnumber = new Number(number + '').toFixed(parseInt(decimals)); return parseFloat(newnumber); }, _mergeVertexIds: function _mergeVertexIds(aList, bList) { var sharedVertices = []; aList.forEach(function (vId) { if (_.includes(bList, vId)) { sharedVertices.push(vId); } }); if (sharedVertices.length < 2) return []; // console.log("TRYING aList:", aList, ", bList:", bList, ", sharedVertices:", sharedVertices); if (_.includes(sharedVertices, aList[0]) && _.includes(sharedVertices, aList[aList.length - 1])) { // Vertices on both edges are bad, so shift them once to the left aList.push(aList.shift()); } if (_.includes(sharedVertices, bList[0]) && _.includes(sharedVertices, bList[bList.length - 1])) { // Vertices on both edges are bad, so shift them once to the left bList.push(bList.shift()); } // Again! sharedVertices = []; aList.forEach(function (vId) { if (_.includes(bList, vId)) { sharedVertices.push(vId); } }); var clockwiseMostSharedVertex = sharedVertices[1]; var counterClockwiseMostSharedVertex = sharedVertices[0]; var cList = _.clone(aList); while (cList[0] !== clockwiseMostSharedVertex) { cList.push(cList.shift()); } var c = 0; var temp = _.clone(bList); while (temp[0] !== counterClockwiseMostSharedVertex) { temp.push(temp.shift()); if (c++ > 10) break; } // Shave temp.shift(); temp.pop(); cList = cList.concat(temp); // console.log("aList:", aList, ", bList:", bList, ", cList:", cList, ", sharedVertices:", sharedVertices); return cList; }, _setPolygonCentroid: function _setPolygonCentroid(polygon, navigationMesh) { var sum = new BABYLON.Vector3(0, 0, 0); var vertices = navigationMesh.vertices; _.each(polygon.vertexIds, function (vId) { sum.x += vertices[vId * 3]; sum.y += vertices[vId * 3 + 1]; sum.z += vertices[vId * 3 + 2]; }); sum.scaleInPlace(1 / polygon.vertexIds.length); polygon.centroid.copyFrom(sum); }, getVectorFrom: function getVectorFrom(vertices, id, _vector) { if (_vector) { _vector.copyFromFloats(vertices[id * 3], vertices[id * 3 + 1], vertices[id * 3 + 2]); return _vector; } return new BABYLON.Vector3(vertices[id * 3], vertices[id * 3 + 1], vertices[id * 3 + 2]); }, _cleanPolygon: function _cleanPolygon(polygon, navigationMesh) { var newVertexIds = []; var vertices = navigationMesh.vertices; for (var i = 0; i < polygon.vertexIds.length; i++) { var vertex = this.getVectorFrom(vertices, polygon.vertexIds[i]); var nextVertexId, previousVertexId; var nextVertex, previousVertex; // console.log("nextVertex: ", nextVertex); if (i === 0) { nextVertexId = polygon.vertexIds[1]; previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 1]; } else if (i === polygon.vertexIds.length - 1) { nextVertexId = polygon.vertexIds[0]; previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 2]; } else { nextVertexId = polygon.vertexIds[i + 1]; previousVertexId = polygon.vertexIds[i - 1]; } nextVertex = this.getVectorFrom(vertices, nextVertexId); previousVertex = this.getVectorFrom(vertices, previousVertexId); var a = nextVertex.clone().sub(vertex); var b = previousVertex.clone().sub(vertex); var angle = a.angleTo(b); // console.log(angle); if (angle > Math.PI - 0.01 && angle < Math.PI + 0.01) { // Unneccesary vertex // console.log("Unneccesary vertex: ", polygon.vertexIds[i]); // console.log("Angle between "+previousVertexId+", "+polygon.vertexIds[i]+" "+nextVertexId+" was: ", angle); // Remove the neighbours who had this vertex var goodNeighbours = []; polygon.neighbours.forEach(function (neighbour) { if (!_.includes(neighbour.vertexIds, polygon.vertexIds[i])) { goodNeighbours.push(neighbour); } }); polygon.neighbours = goodNeighbours; // TODO cleanup the list of vertices and rebuild vertexIds for all polygons } else { newVertexIds.push(polygon.vertexIds[i]); } } // console.log("New vertexIds: ", newVertexIds); polygon.vertexIds = newVertexIds; this._setPolygonCentroid(polygon, navigationMesh); }, _isConvex: function _isConvex(polygon, navigationMesh) { var vertices = navigationMesh.vertices; if (polygon.vertexIds.length < 3) return false; var convex = true; var total = 0; var results = []; for (var i = 0; i < polygon.vertexIds.length; i++) { var vertex = this.getVectorFrom(vertices, polygon.vertexIds[i]); var nextVertex, previousVertex; // console.log("nextVertex: ", nextVertex); if (i === 0) { nextVertex = this.getVectorFrom(vertices, polygon.vertexIds[1]); previousVertex = this.getVectorFrom(vertices, polygon.vertexIds[polygon.vertexIds.length - 1]); } else if (i === polygon.vertexIds.length - 1) { nextVertex = this.getVectorFrom(vertices, polygon.vertexIds[0]); previousVertex = this.getVectorFrom(vertices, polygon.vertexIds[polygon.vertexIds.length - 2]); } else { nextVertex = this.getVectorFrom(vertices, polygon.vertexIds[i + 1]); previousVertex = this.getVectorFrom(vertices, polygon.vertexIds[i - 1]); } var a = nextVertex.clone().sub(vertex); var b = previousVertex.clone().sub(vertex); var angle = a.angleTo(b); total += angle; // console.log(angle); if (angle === Math.PI || angle === 0) return false; var r = BABYLON.Vector3.Cross(a, b).y; results.push(r); // console.log("pushed: ", r); } // if ( total > (polygon.vertexIds.length-2)*Math.PI ) return false; results.forEach(function (r) { if (r === 0) convex = false; }); if (results[0] > 0) { results.forEach(function (r) { if (r < 0) convex = false; }); } else { results.forEach(function (r) { if (r > 0) convex = false; }); } // console.log("allowed: "+total+", max: "+(polygon.vertexIds.length-2)*Math.PI); // if ( total > (polygon.vertexIds.length-2)*Math.PI ) convex = false; // console.log("Convex: "+(convex ? "true": "false")); return convex; }, _buildPolygonGroups: function _buildPolygonGroups(navigationMesh) { var polygons = navigationMesh.polygons; var polygonGroups = []; var groupCount = 0; var spreadGroupId = function spreadGroupId(polygon) { _.each(polygon.neighbours, function (neighbour) { if (_.isUndefined(neighbour.group)) { neighbour.group = polygon.group; spreadGroupId(neighbour); } }); }; _.each(polygons, function (polygon) { if (_.isUndefined(polygon.group)) { polygon.group = groupCount++; // Spread it spreadGroupId(polygon); } if (!polygonGroups[polygon.group]) polygonGroups[polygon.group] = []; polygonGroups[polygon.group].push(polygon); }); console.log("Groups built: ", polygonGroups.length); return polygonGroups; }, _array_intersect: function _array_intersect() { var i, shortest, nShortest, n, len, ret = [], obj = {}, nOthers; nOthers = arguments.length - 1; nShortest = arguments[0].length; shortest = 0; for (i = 0; i <= nOthers; i++) { n = arguments[i].length; if (n < nShortest) { shortest = i; nShortest = n; } } for (i = 0; i <= nOthers; i++) { n = i === shortest ? 0 : i || shortest; //Read the shortest array first. Read the first array instead of the shortest len = arguments[n].length; for (var j = 0; j < len; j++) { var elem = arguments[n][j]; if (obj[elem] === i - 1) { if (i === nOthers) { ret.push(elem); obj[elem] = 0; } else { obj[elem] = i; } } else if (i === 0) { obj[elem] = 0; } } } return ret; }, _buildPolygonNeighbours: function _buildPolygonNeighbours(polygon, navigationMesh) { polygon.neighbours = []; // All other nodes that contain at least two of our vertices are our neighbours for (var i = 0, len = navigationMesh.polygons.length; i < len; i++) { if (polygon === navigationMesh.polygons[i]) continue; // Don't check polygons that are too far, since the intersection tests take a long time if (BABYLON.Vector3.DistanceSquared(polygon.centroid, navigationMesh.polygons[i].centroid) > 100 * 100) continue; var matches = this._array_intersect(polygon.vertexIds, navigationMesh.polygons[i].vertexIds); // var matches = _.intersection(polygon.vertexIds, navigationMesh.polygons[i].vertexIds); if (matches.length >= 2) { polygon.neighbours.push(navigationMesh.polygons[i]); } } }, _buildPolygonsFromGeometry: function _buildPolygonsFromGeometry(geometry) { var polygons = []; var vertices = geometry.getVerticesData(BABYLON.VertexBuffer.PositionKind); var indices = geometry.getIndices(); var polygonId = 1; console.log("Vertices:", vertices.length / 3, "polygons:", indices.length / 3); // Convert the faces into a custom format that supports more than 3 vertices for (var i = 0; i < indices.length; i += 3) { var a = this.getVectorFrom(vertices, indices[i]); var b = this.getVectorFrom(vertices, indices[i + 1]); var c = this.getVectorFrom(vertices, indices[i + 2]); var normal = BABYLON.Vector3.Cross(b.subtract(a), b.subtract(c)).normalize(); polygons.push({ id: polygonId++, vertexIds: [indices[i], indices[i + 1], indices[i + 2]], centroid: geometry.centroids[i / 3], normal: normal, neighbours: [] }); } var navigationMesh = { polygons: polygons, vertices: vertices }; // Build a list of adjacent polygons _.each(polygons, function (polygon) { this._buildPolygonNeighbours(polygon, navigationMesh); }.bind(this)); return navigationMesh; }, _cleanNavigationMesh: function _cleanNavigationMesh(navigationMesh) { var polygons = navigationMesh.polygons; var vertices = navigationMesh.vertices; // Remove steep triangles var up = new BABYLON.Vector3(0, 1, 0); polygons = _.filter(polygons, function (polygon) { var angle = Math.acos(BABYLON.Vector3.Dot(up, polygon.normal)); return angle < Math.PI / 4; }); // Remove unnecessary edges using the Hertel-Mehlhorn algorithm // 1. Find a pair of adjacent nodes (i.e., two nodes that share an edge between them) // whose normals are nearly identical (i.e., their surfaces face the same direction). var newPolygons = []; _.each(polygons, function (polygon) { if (polygon.toBeDeleted) return; var keepLooking = true; while (keepLooking) { keepLooking = false; _.each(polygon.neighbours, function (otherPolygon) { if (polygon === otherPolygon) return; if (Math.acos(BABYLON.Vector3.Dot(polygon.normal, otherPolygon.normal)) < 0.01) { // That's pretty equal alright! // Merge otherPolygon with polygon var testPolygon = { vertexIds: this._mergeVertexIds(polygon.vertexIds, otherPolygon.vertexIds), neighbours: polygon.neighbours, normal: polygon.normal.clone(), centroid: polygon.centroid.clone() }; this._cleanPolygon(testPolygon, navigationMesh); if (this._isConvex(testPolygon, navigationMesh)) { otherPolygon.toBeDeleted = true; // Inherit the neighbours from the to be merged polygon, except ourself _.each(otherPolygon.neighbours, function (otherPolygonNeighbour) { // Set this poly to be merged to be no longer our neighbour otherPolygonNeighbour.neighbours = _.without(otherPolygonNeighbour.neighbours, otherPolygon); if (otherPolygonNeighbour !== polygon) { // Tell the old Polygon's neighbours about the new neighbour who has merged otherPolygonNeighbour.neighbours.push(polygon); } else { // For ourself, we don't need to know about ourselves // But we inherit the old neighbours polygon.neighbours = polygon.neighbours.concat(otherPolygon.neighbours); polygon.neighbours = _.uniq(polygon.neighbours); // Without ourselves in it! polygon.neighbours = _.without(polygon.neighbours, polygon); } }); polygon.vertexIds = this._mergeVertexIds(polygon.vertexIds, otherPolygon.vertexIds); this._cleanPolygon(polygon, navigationMesh); keepLooking = true; } } }.bind(this)); } if (!polygon.toBeDeleted) { newPolygons.push(polygon); } }); var isUsed = function isUsed(vId) { var contains = false; _.each(newPolygons, function (p) { if (!contains && _.includes(p.vertexIds, vId)) { contains = true; } }); return contains; }; // Clean vertices for (var i = 0; i < vertices.length; i++) { if (!isUsed(i)) { // Decrement all vertices that are higher than i _.each(newPolygons, function (p) { for (var j = 0; j < p.vertexIds.length; j++) { if (p.vertexIds[j] > i) { p.vertexIds[j]--; } } }); vertices.splice(i, 1); i--; } } navigationMesh.polygons = newPolygons; navigationMesh.vertices = vertices; }, _buildNavigationMesh: function _buildNavigationMesh(geometry) { // Prepare geometry this._computeCentroids(geometry); this._mergeVertices(geometry); // BABYLON.GeometryUtils.triangulateQuads(geometry); // console.log("vertices:", geometry.vertices.length, "polygons:", geometry.faces.length); var navigationMesh = this._buildPolygonsFromGeometry(geometry); // cleanNavigationMesh(navigationMesh); // console.log("Pre-clean:", navigationMesh.polygons.length, "polygons,", navigationMesh.vertices.length, "vertices."); // console.log("") // console.log("Vertices:", navigationMesh.vertices.length, "polygons,", navigationMesh.polygons.length, "vertices."); return navigationMesh; }, _mergeVertices: function _mergeVertices(geometry) { var verticesMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique) var unique = [], changes = []; var v, key; var precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001 var precision = Math.pow(10, precisionPoints); var indices; var ind = geometry.getIndices(), vert = geometry.getVerticesData(BABYLON.VertexBuffer.PositionKind); for (var i = 0; i < vert.length; i += 3) { v = new BABYLON.Vector3(vert[i], vert[i + 1], vert[i + 2]); key = Math.round(v.x * precision) + '_' + Math.round(v.y * precision) + '_' + Math.round(v.z * precision); if (verticesMap[key] === undefined) { verticesMap[key] = i / 3; unique.push(v.clone()); changes[i / 3] = unique.length - 1; } else { //console.log('Duplicate vertex found. ', i, ' could be using ', verticesMap[key]); changes[i / 3] = changes[verticesMap[key]]; } } // if faces are completely degenerate after merging vertices, we // have to remove them from the geometry. var faceIndicesToRemove = []; for (i = 0; i < ind.length; i += 3) { ind[i] = changes[ind[i]]; ind[i + 1] = changes[ind[i + 1]]; ind[i + 2] = changes[ind[i + 2]]; indices = [ind[i], ind[i + 1], ind[i + 2]]; var dupIndex = -1; // if any duplicate vertices are found in a Face3 // we have to remove the face as nothing can be saved for (var n = 0; n < 3; n++) { if (indices[n] === indices[(n + 1) % 3]) { dupIndex = n; faceIndicesToRemove.push(i); break; } } } for (i = faceIndicesToRemove.length - 1; i >= 0; i--) { var idx = faceIndicesToRemove[i]; ind.splice(idx, 3); } // Use unique set of vertices var diff = vert.length / 3 - unique.length; vert = []; for (i = 0; i < unique.length; i++) { vert.push(unique[i].x, unique[i].y, unique[i].z); } geometry.setIndices(ind); geometry.setVerticesData(BABYLON.VertexBuffer.PositionKind, vert); return diff; }, _getSharedVerticesInOrder: function _getSharedVerticesInOrder(a, b) { var aList = a.vertexIds; var bList = b.vertexIds; var sharedVertices = []; _.each(aList, function (vId) { if (_.includes(bList, vId)) { sharedVertices.push(vId); } }); if (sharedVertices.length < 2) return []; // console.log("TRYING aList:", aList, ", bList:", bList, ", sharedVertices:", sharedVertices); if (_.includes(sharedVertices, aList[0]) && _.includes(sharedVertices, aList[aList.length - 1])) { // Vertices on both edges are bad, so shift them once to the left aList.push(aList.shift()); } if (_.includes(sharedVertices, bList[0]) && _.includes(sharedVertices, bList[bList.length - 1])) { // Vertices on both edges are bad, so shift them once to the left bList.push(bList.shift()); } // Again! sharedVertices = []; _.each(aList, function (vId) { if (_.includes(bList, vId)) { sharedVertices.push(vId); } }); return sharedVertices; }, _groupNavMesh: function _groupNavMesh(navigationMesh) { var saveObj = {}; _.each(navigationMesh.vertices, function (v) { v = this._roundNumber(v, 2); }.bind(this)); saveObj.vertices = navigationMesh.vertices; var groups = this._buildPolygonGroups(navigationMesh); saveObj.groups = []; var findPolygonIndex = function findPolygonIndex(group, p) { for (var i = 0; i < group.length; i++) { if (p === group[i]) return i; } }; _.each(groups, function (group) { var newGroup = []; _.each(group, function (p) { var neighbours = []; _.each(p.neighbours, function (n) { neighbours.push(findPolygonIndex(group, n)); }); // Build a portal list to each neighbour var portals = []; _.each(p.neighbours, function (n) { portals.push(this._getSharedVerticesInOrder(p, n)); }.bind(this)); p.centroid.x = this._roundNumber(p.centroid.x, 2); p.centroid.y = this._roundNumber(p.centroid.y, 2); p.centroid.z = this._roundNumber(p.centroid.z, 2); newGroup.push({ id: findPolygonIndex(group, p), neighbours: neighbours, vertexIds: p.vertexIds, centroid: p.centroid, portals: portals }); }.bind(this)); saveObj.groups.push(newGroup); }.bind(this)); return saveObj; } }); module.exports = Navigation; },{"./Astar.js":1,"./Channel.js":3,"abitbol":5,"babylonjs":7,"lodash":8}],5:[function(require,module,exports){ "use strict"; var extractAnnotations = require("./annotation.js"); var _disableConstructor = false; // Inherit from a class without calling its constructor. function inherit(SuperClass) { _disableConstructor = true; var __class__ = new SuperClass(); _disableConstructor = false; return __class__; } // Checks if the given function uses abitbol special properties ($super, $name,...) function usesSpecialProperty(fn) { return Boolean(fn.toString().match(/.*(\$super|\$name|\$computedPropertyName).*/)); } var Class = function () {}; Object.defineProperty(Class, "$class", { enumerable: false, value: Class }); Object.defineProperty(Class, "$map", { enumerable: false, value: { attributes: {}, methods: {}, computedProperties: {} } }); Object.defineProperty(Class, "$extend", { enumerable: false, value: function (properties) { var _superClass = this; var _classMap = JSON.parse(JSON.stringify(_superClass.$map)); // not pretty :s // New class var __class__ = function () { if (_disableConstructor) { return; } // Abitbol special properties Object.defineProperty(this, "$class", { enumerable: false, value: __class__ }); Object.defineProperty(this, "$map", { enumerable: false, value: _classMap }); Object.defineProperty(this, "$data", { enumerable: false, value: {} }); // Computed properties for (var property in _classMap.computedProperties) { Object.defineProperty(this, property, { enumerable: true, configurable: false, get: (_classMap.computedProperties[property].get !== undefined) ? (function (accessorName) { return function () { return this[accessorName].apply(this, arguments); }; })(_classMap.computedProperties[property].get) : undefined, // jshint ignore:line set: (_classMap.computedProperties[property].set !== undefined) ? (function (mutatorName) { return function () { return this[mutatorName].apply(this, arguments); }; })(_classMap.computedProperties[property].set) : undefined // jshint ignore:line }); } // Bind this for (var method in _classMap.methods) { this[method] = this[method].bind(this); } // Call the constructor if any if (this.__init__) { this.__init__.apply(this, arguments); } return this; }; // Inheritance __class__.prototype = inherit(this.$class); properties = properties || {}; var property; var computedPropertyName; var annotations; var i; var mixin; // Copy properties from mixins if (properties.__include__) { for (i = properties.__include__.length - 1 ; i >= 0 ; i--) { mixin = properties.__include__[i]; for (property in mixin) { if (property == "__classvars__") { continue; } else if (properties[property] === undefined) { properties[property] = mixin[property]; } } // Merging mixin's static properties if (mixin.__classvars__) { if (!properties.__classvars__) { properties.__classvars__ = {}; } for (property in mixin.__classvars__) { if (properties.__classvars__[property] === undefined) { properties.__classvars__[property] = mixin.__classvars__[property]; } } } } } // Add properties for (property in properties || {}) { if (property == "__include__" || property == "__classvars__") { continue; } if (typeof properties[property] == "function") { computedPropertyName = undefined; _classMap.methods[property] = {annotations: {}}; // Accessors / Mutators if (property.indexOf("get") === 0) { computedPropertyName = property.slice(3, 4).toLowerCase() + property.slice(4, property.length); if (!_classMap.computedProperties[computedPropertyName]) { _classMap.computedProperties[computedPropertyName] = {annotations: {}}; } _classMap.computedProperties[computedPropertyName].get = property; } else if (property.indexOf("set") === 0) { computedPropertyName = property.slice(3, 4).toLowerCase() + property.slice(4, property.length); if (!_classMap.computedProperties[computedPropertyName]) { _classMap.computedProperties[computedPropertyName] = {annotations: {}}; } _classMap.computedProperties[computedPropertyName].set = property; } else if (property.indexOf("has") === 0) { computedPropertyName = property.slice(3, 4).toLowerCase() + property.slice(4, property.length); if (!_classMap.computedProperties[computedPropertyName]) { _classMap.computedProperties[computedPropertyName] = {annotations: {}}; } _classMap.computedProperties[computedPropertyName].get = property; } else if (property.indexOf("is") === 0) { computedPropertyName = property.slice(2, 3).toLowerCase() + property.slice(3, property.length); if (!_classMap.computedProperties[computedPropertyName]) { _classMap.computedProperties[computedPropertyName] = {annotations: {}}; } _classMap.computedProperties[computedPropertyName].get = property; } // Annotations annotations = extractAnnotations(properties[property]); for (var annotation in annotations) { _classMap.methods[property].annotations[annotation] = annotations[annotation]; if (computedPropertyName) { _classMap.computedProperties[computedPropertyName] .annotations[annotation] = annotations[annotation]; } } // Wrapped method if (usesSpecialProperty(properties[property])) { __class__.prototype[property] = (function (method, propertyName, computedPropertyName) { return function () { var _oldSuper = this.$super; var _oldName = this.$name; var _oldComputedPropertyName = this.$computedPropertyName; this.$super = _superClass.prototype[propertyName]; this.$name = propertyName; this.$computedPropertyName = computedPropertyName; try { return method.apply(this, arguments); } finally { if (_oldSuper) { this.$super = _oldSuper; } else { delete this.$super; } if (_oldName) { this.$name = _oldName; } else { delete this.$name; } if (_oldComputedPropertyName) { this.$computedPropertyName = _oldComputedPropertyName; } else { delete this.$computedPropertyName; } } }; })(properties[property], property, computedPropertyName); // jshint ignore:line // Simple methods } else { __class__.prototype[property] = properties[property]; } } else { _classMap.attributes[property] = true; __class__.prototype[property] = properties[property]; } } // Copy super class static properties var scStaticProps = Object.getOwnPropertyNames(_superClass); // Removes caller, callee and arguments from the list (strict mode) // Removes non enumerable Abitbol properties too scStaticProps = scStaticProps.filter(function (value) { return (["caller", "callee", "arguments", "$class", "$extend", "$map"].indexOf(value) == -1); }); for (i = 0 ; i < scStaticProps.length ; i++) { if (__class__[scStaticProps[i]] === undefined) { __class__[scStaticProps[i]] = _superClass[scStaticProps[i]]; } } // Add static properties if (properties.__classvars__) { for (property in properties.__classvars__) { __class__[property] = properties.__classvars__[property]; } } // Add abitbol static properties Object.defineProperty(__class__, "$class", { enumerable: false, value: __class__ }); Object.defineProperty(__class__, "$extend", { enumerable: false, value: Class.$extend }); Object.defineProperty(__class__, "$map", { enumerable: false, value: _classMap }); return __class__; } }); module.exports = Class; },{"./annotation.js":6}],6:[function(require,module,exports){ "use strict"; function cleanJs(js) { // remove function fn(param) { // or fn(param) { // or (param) => { var c; var p = 0; for (var i = 0 ; i < js.length ; i++) { c = js[i]; if (c == "(") { ++p; } else if (c == ")") { --p; } else if (c == "{" && p === 0) { js = js.slice(i + 1); break; } } // remove comments (not super safe but should work in most cases) js = js.replace(/\/\*(.|\r|\n)*?\*\//g, ""); js = js.replace(/\/\/.*?\r?\n