#

API Docs for: 0.6.0
Show: Inherited Protected Private Deprecated

# File: src/shapes/Heightfield.js

    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 = {};
    };

    /**
     * @method updateMinValue
     * Update the .minValue property
     */
    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: do it better
        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
            );
            verts[4].set(
                0.75 * elementSize,
                -0.25 * elementSize,
                -h
            );
            verts[5].set(
                -0.25 * elementSize,
                0.75  * elementSize,
                -h
            );

            // 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
            );
            verts[4].set(
                -0.75 * elementSize,
                0.25 * elementSize,
                - h
            );
            verts[5].set(
                0.25 * elementSize,
                -0.75 * elementSize,
                - h
            );

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