UNPKG

cannon

Version:

A lightweight 3D physics engine written in JavaScript.

485 lines (417 loc) 13.4 kB
var Shape = require('./Shape'); var ConvexPolyhedron = require('./ConvexPolyhedron'); var Vec3 = require('../math/Vec3'); var Utils = require('../utils/Utils'); module.exports = Heightfield; /** * Heightfield shape class. Height data is given as an array. These data points are spread out evenly with a given distance. * @class Heightfield * @extends Shape * @constructor * @param {Array} data An array of Y values that will be used to construct the terrain. * @param {object} options * @param {Number} [options.minValue] Minimum value of the data points in the data array. Will be computed automatically if not given. * @param {Number} [options.maxValue] Maximum value. * @param {Number} [options.elementSize=0.1] World spacing between the data points in X direction. * @todo Should be possible to use along all axes, not just y * * @example * // Generate some height data (y-values). * var data = []; * for(var i = 0; i < 1000; i++){ * var y = 0.5 * Math.cos(0.2 * i); * data.push(y); * } * * // Create the heightfield shape * var heightfieldShape = new Heightfield(data, { * elementSize: 1 // Distance between the data points in X and Y directions * }); * var heightfieldBody = new Body(); * heightfieldBody.addShape(heightfieldShape); * world.addBody(heightfieldBody); */ function Heightfield(data, options){ options = Utils.defaults(options, { maxValue : null, minValue : null, elementSize : 1 }); /** * An array of numbers, or height values, that are spread out along the x axis. * @property {array} data */ this.data = data; /** * Max value of the data * @property {number} maxValue */ this.maxValue = options.maxValue; /** * Max value of the data * @property {number} minValue */ this.minValue = options.minValue; /** * The width of each element * @property {number} elementSize * @todo elementSizeX and Y */ this.elementSize = options.elementSize; if(options.minValue === null){ this.updateMinValue(); } if(options.maxValue === null){ this.updateMaxValue(); } this.cacheEnabled = true; Shape.call(this); this.pillarConvex = new ConvexPolyhedron(); this.pillarOffset = new Vec3(); this.type = Shape.types.HEIGHTFIELD; this.updateBoundingSphereRadius(); // "i_j_isUpper" => { convex: ..., offset: ... } // for example: // _cachedPillars["0_2_1"] this._cachedPillars = {}; } Heightfield.prototype = new Shape(); /** * Call whenever you change the data array. * @method update */ Heightfield.prototype.update = function(){ this._cachedPillars = {}; }; /** * Update the .minValue property * @method updateMinValue */ Heightfield.prototype.updateMinValue = function(){ var data = this.data; var minValue = data[0][0]; for(var i=0; i !== data.length; i++){ for(var j=0; j !== data[i].length; j++){ var v = data[i][j]; if(v < minValue){ minValue = v; } } } this.minValue = minValue; }; /** * Update the .maxValue property * @method updateMaxValue */ Heightfield.prototype.updateMaxValue = function(){ var data = this.data; var maxValue = data[0][0]; for(var i=0; i !== data.length; i++){ for(var j=0; j !== data[i].length; j++){ var v = data[i][j]; if(v > maxValue){ maxValue = v; } } } this.maxValue = maxValue; }; /** * Set the height value at an index. Don't forget to update maxValue and minValue after you're done. * @method setHeightValueAtIndex * @param {integer} xi * @param {integer} yi * @param {number} value */ Heightfield.prototype.setHeightValueAtIndex = function(xi, yi, value){ var data = this.data; data[xi][yi] = value; // Invalidate cache this.clearCachedConvexTrianglePillar(xi, yi, false); if(xi > 0){ this.clearCachedConvexTrianglePillar(xi - 1, yi, true); this.clearCachedConvexTrianglePillar(xi - 1, yi, false); } if(yi > 0){ this.clearCachedConvexTrianglePillar(xi, yi - 1, true); this.clearCachedConvexTrianglePillar(xi, yi - 1, false); } if(yi > 0 && xi > 0){ this.clearCachedConvexTrianglePillar(xi - 1, yi - 1, true); } }; /** * Get max/min in a rectangle in the matrix data * @method getRectMinMax * @param {integer} iMinX * @param {integer} iMinY * @param {integer} iMaxX * @param {integer} iMaxY * @param {array} [result] An array to store the results in. * @return {array} The result array, if it was passed in. Minimum will be at position 0 and max at 1. */ Heightfield.prototype.getRectMinMax = function (iMinX, iMinY, iMaxX, iMaxY, result) { result = result || []; // Get max and min of the data var data = this.data, max = this.minValue; // Set first value for(var i = iMinX; i <= iMaxX; i++){ for(var j = iMinY; j <= iMaxY; j++){ var height = data[i][j]; if(height > max){ max = height; } } } result[0] = this.minValue; result[1] = max; }; /** * Get the index of a local position on the heightfield. The indexes indicate the rectangles, so if your terrain is made of N x N height data points, you will have rectangle indexes ranging from 0 to N-1. * @method getIndexOfPosition * @param {number} x * @param {number} y * @param {array} result Two-element array * @param {boolean} clamp If the position should be clamped to the heightfield edge. * @return {boolean} */ Heightfield.prototype.getIndexOfPosition = function (x, y, result, clamp) { // Get the index of the data points to test against var w = this.elementSize; var data = this.data; var xi = Math.floor(x / w); var yi = Math.floor(y / w); result[0] = xi; result[1] = yi; if(clamp){ // Clamp index to edges if(xi < 0){ xi = 0; } if(yi < 0){ yi = 0; } if(xi >= data.length - 1){ xi = data.length - 1; } if(yi >= data[0].length - 1){ yi = data[0].length - 1; } } // Bail out if we are out of the terrain if(xi < 0 || yi < 0 || xi >= data.length-1 || yi >= data[0].length-1){ return false; } return true; }; Heightfield.prototype.getHeightAt = function(x, y, edgeClamp){ var idx = []; this.getIndexOfPosition(x, y, idx, edgeClamp); // TODO: get upper or lower triangle, then use barycentric interpolation to get the height in the triangle. var minmax = []; this.getRectMinMax(idx[0], idx[1] + 1, idx[0], idx[1] + 1, minmax); return (minmax[0] + minmax[1]) / 2; // average }; Heightfield.prototype.getCacheConvexTrianglePillarKey = function(xi, yi, getUpperTriangle){ return xi + '_' + yi + '_' + (getUpperTriangle ? 1 : 0); }; Heightfield.prototype.getCachedConvexTrianglePillar = function(xi, yi, getUpperTriangle){ return this._cachedPillars[this.getCacheConvexTrianglePillarKey(xi, yi, getUpperTriangle)]; }; Heightfield.prototype.setCachedConvexTrianglePillar = function(xi, yi, getUpperTriangle, convex, offset){ this._cachedPillars[this.getCacheConvexTrianglePillarKey(xi, yi, getUpperTriangle)] = { convex: convex, offset: offset }; }; Heightfield.prototype.clearCachedConvexTrianglePillar = function(xi, yi, getUpperTriangle){ delete this._cachedPillars[this.getCacheConvexTrianglePillarKey(xi, yi, getUpperTriangle)]; }; /** * Get a triangle in the terrain in the form of a triangular convex shape. * @method getConvexTrianglePillar * @param {integer} i * @param {integer} j * @param {boolean} getUpperTriangle */ Heightfield.prototype.getConvexTrianglePillar = function(xi, yi, getUpperTriangle){ var result = this.pillarConvex; var offsetResult = this.pillarOffset; if(this.cacheEnabled){ var data = this.getCachedConvexTrianglePillar(xi, yi, getUpperTriangle); if(data){ this.pillarConvex = data.convex; this.pillarOffset = data.offset; return; } result = new ConvexPolyhedron(); offsetResult = new Vec3(); this.pillarConvex = result; this.pillarOffset = offsetResult; } var data = this.data; var elementSize = this.elementSize; var faces = result.faces; // Reuse verts if possible result.vertices.length = 6; for (var i = 0; i < 6; i++) { if(!result.vertices[i]){ result.vertices[i] = new Vec3(); } } // Reuse faces if possible faces.length = 5; for (var i = 0; i < 5; i++) { if(!faces[i]){ faces[i] = []; } } var verts = result.vertices; var h = (Math.min( data[xi][yi], data[xi+1][yi], data[xi][yi+1], data[xi+1][yi+1] ) - this.minValue ) / 2 + this.minValue; if (!getUpperTriangle) { // Center of the triangle pillar - all polygons are given relative to this one offsetResult.set( (xi + 0.25) * elementSize, // sort of center of a triangle (yi + 0.25) * elementSize, h // vertical center ); // Top triangle verts verts[0].set( -0.25 * elementSize, -0.25 * elementSize, data[xi][yi] - h ); verts[1].set( 0.75 * elementSize, -0.25 * elementSize, data[xi + 1][yi] - h ); verts[2].set( -0.25 * elementSize, 0.75 * elementSize, data[xi][yi + 1] - h ); // bottom triangle verts verts[3].set( -0.25 * elementSize, -0.25 * elementSize, -h-1 ); verts[4].set( 0.75 * elementSize, -0.25 * elementSize, -h-1 ); verts[5].set( -0.25 * elementSize, 0.75 * elementSize, -h-1 ); // top triangle faces[0][0] = 0; faces[0][1] = 1; faces[0][2] = 2; // bottom triangle faces[1][0] = 5; faces[1][1] = 4; faces[1][2] = 3; // -x facing quad faces[2][0] = 0; faces[2][1] = 2; faces[2][2] = 5; faces[2][3] = 3; // -y facing quad faces[3][0] = 1; faces[3][1] = 0; faces[3][2] = 3; faces[3][3] = 4; // +xy facing quad faces[4][0] = 4; faces[4][1] = 5; faces[4][2] = 2; faces[4][3] = 1; } else { // Center of the triangle pillar - all polygons are given relative to this one offsetResult.set( (xi + 0.75) * elementSize, // sort of center of a triangle (yi + 0.75) * elementSize, h // vertical center ); // Top triangle verts verts[0].set( 0.25 * elementSize, 0.25 * elementSize, data[xi + 1][yi + 1] - h ); verts[1].set( -0.75 * elementSize, 0.25 * elementSize, data[xi][yi + 1] - h ); verts[2].set( 0.25 * elementSize, -0.75 * elementSize, data[xi + 1][yi] - h ); // bottom triangle verts verts[3].set( 0.25 * elementSize, 0.25 * elementSize, - h-1 ); verts[4].set( -0.75 * elementSize, 0.25 * elementSize, - h-1 ); verts[5].set( 0.25 * elementSize, -0.75 * elementSize, - h-1 ); // Top triangle faces[0][0] = 0; faces[0][1] = 1; faces[0][2] = 2; // bottom triangle faces[1][0] = 5; faces[1][1] = 4; faces[1][2] = 3; // +x facing quad faces[2][0] = 2; faces[2][1] = 5; faces[2][2] = 3; faces[2][3] = 0; // +y facing quad faces[3][0] = 3; faces[3][1] = 4; faces[3][2] = 1; faces[3][3] = 0; // -xy facing quad faces[4][0] = 1; faces[4][1] = 4; faces[4][2] = 5; faces[4][3] = 2; } result.computeNormals(); result.computeEdges(); result.updateBoundingSphereRadius(); this.setCachedConvexTrianglePillar(xi, yi, getUpperTriangle, result, offsetResult); }; Heightfield.prototype.calculateLocalInertia = function(mass, target){ target = target || new Vec3(); target.set(0, 0, 0); return target; }; Heightfield.prototype.volume = function(){ return Number.MAX_VALUE; // The terrain is infinite }; Heightfield.prototype.calculateWorldAABB = function(pos, quat, min, max){ // TODO: do it properly min.set(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE); max.set(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); }; Heightfield.prototype.updateBoundingSphereRadius = function(){ // Use the bounding box of the min/max values var data = this.data, s = this.elementSize; this.boundingSphereRadius = new Vec3(data.length * s, data[0].length * s, Math.max(Math.abs(this.maxValue), Math.abs(this.minValue))).norm(); };