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();
};