API Docs for: 0.6.1
Show:

File: src/collision/Ray.js

module.exports = Ray;

var vec2 = require('../math/vec2');
var RaycastResult = require('../collision/RaycastResult');
var Shape = require('../shapes/Shape');
var AABB = require('../collision/AABB');

/**
 * A line with a start and end point that is used to intersect shapes.
 * @class Ray
 * @constructor
 */
function Ray(options){
    options = options || {};

    /**
     * @property {array} from
     */
    this.from = options.from ? vec2.fromValues(options.from[0], options.from[1]) : vec2.create();

    /**
     * @property {array} to
     */
    this.to = options.to ? vec2.fromValues(options.to[0], options.to[1]) : vec2.create();

    /**
     * @private
     * @property {array} _direction
     */
    this._direction = vec2.create();

    /**
     * The precision of the ray. Used when checking parallelity etc.
     * @property {Number} precision
     */
    this.precision = 0.0001;

    /**
     * Set to true if you want the Ray to take .collisionResponse flags into account on bodies and shapes.
     * @property {Boolean} checkCollisionResponse
     */
    this.checkCollisionResponse = true;

    /**
     * If set to true, the ray skips any hits with normal.dot(rayDirection) < 0.
     * @property {Boolean} skipBackfaces
     */
    this.skipBackfaces = false;

    /**
     * @property {number} collisionMask
     * @default -1
     */
    this.collisionMask = -1;

    /**
     * @property {number} collisionGroup
     * @default -1
     */
    this.collisionGroup = -1;

    /**
     * The intersection mode. Should be Ray.ANY, Ray.ALL or Ray.CLOSEST.
     * @property {number} mode
     */
    this.mode = Ray.ANY;

    /**
     * Current result object.
     * @property {RaycastResult} result
     */
    this.result = new RaycastResult();

    /**
     * Will be set to true during intersectWorld() if the ray hit anything.
     * @property {Boolean} hasHit
     */
    this.hasHit = false;

    /**
     * Current, user-provided result callback. Will be used if mode is Ray.ALL.
     * @property {Function} callback
     */
    this.callback = function(result){};
}
Ray.prototype.constructor = Ray;

Ray.CLOSEST = 1;
Ray.ANY = 2;
Ray.ALL = 4;

var tmpAABB = new AABB();
var tmpArray = [];

/**
 * Do itersection against all bodies in the given World.
 * @method intersectWorld
 * @param  {World} world
 * @param  {object} options
 * @return {Boolean} True if the ray hit anything, otherwise false.
 */
Ray.prototype.intersectWorld = function (world, options) {
    this.mode = options.mode || Ray.ANY;
    this.result = options.result || new RaycastResult();
    this.skipBackfaces = !!options.skipBackfaces;
    this.collisionMask = typeof(options.collisionMask) !== 'undefined' ? options.collisionMask : -1;
    this.collisionGroup = typeof(options.collisionGroup) !== 'undefined' ? options.collisionGroup : -1;
    if(options.from){
        vec2.copy(this.from, options.from);
    }
    if(options.to){
        vec2.copy(this.to, options.to);
    }
    this.callback = options.callback || function(){};
    this.hasHit = false;

    this.result.reset();
    this._updateDirection();

    this.getAABB(tmpAABB);
    tmpArray.length = 0;
    world.broadphase.aabbQuery(world, tmpAABB, tmpArray);
    this.intersectBodies(tmpArray);

    return this.hasHit;
};

var v1 = vec2.create(),
    v2 = vec2.create();

var intersectBody_worldPosition = vec2.create();

/**
 * Shoot a ray at a body, get back information about the hit.
 * @method intersectBody
 * @private
 * @param {Body} body
 * @param {RaycastResult} [result] Deprecated - set the result property of the Ray instead.
 */
Ray.prototype.intersectBody = function (body, result) {

    if(result){
        this.result = result;
        this._updateDirection();
    }
    var checkCollisionResponse = this.checkCollisionResponse;

    if(checkCollisionResponse && !body.collisionResponse){
        return;
    }

    // if((this.collisionGroup & body.collisionMask)===0 || (body.collisionGroup & this.collisionMask)===0){
    //     return;
    // }

    var worldPosition = intersectBody_worldPosition;

    for (var i = 0, N = body.shapes.length; i < N; i++) {
        var shape = body.shapes[i];

        if(checkCollisionResponse && !shape.collisionResponse){
            continue; // Skip
        }

        // Get world angle and position of the shape
        vec2.copy(worldPosition, body.shapeOffsets[i]);
        vec2.rotate(worldPosition, worldPosition, body.angle);
        vec2.add(worldPosition, worldPosition, body.position);
        var worldAngle = body.shapeAngles[i] + body.angle;

        this.intersectShape(
            shape,
            worldAngle,
            worldPosition,
            body
        );

        if(this.result._shouldStop){
            break;
        }
    }
};

/**
 * @method intersectBodies
 * @param {Array} bodies An array of Body objects.
 * @param {RaycastResult} [result] Deprecated
 */
Ray.prototype.intersectBodies = function (bodies, result) {
    if(result){
        this.result = result;
        this._updateDirection();
    }

    for ( var i = 0, l = bodies.length; !this.result._shouldStop && i < l; i ++ ) {
        this.intersectBody(bodies[i]);
    }
};

/**
 * Updates the _direction vector.
 * @private
 * @method _updateDirection
 */
Ray.prototype._updateDirection = function(){
    var d = this._direction;
    vec2.sub(d, this.to, this.from); // this.to.vsub(this.from, this._direction);
    vec2.normalize(d, d); // this._direction.normalize();
};

/**
 * @method intersectShape
 * @private
 * @param {Shape} shape
 * @param {number} angle
 * @param {array} position
 * @param {Body} body
 */
Ray.prototype.intersectShape = function(shape, angle, position, body){
    var from = this.from;


    // Checking boundingSphere
    var distance = distanceFromIntersection(from, this._direction, position);
    if ( distance > shape.boundingSphereRadius ) {
        return;
    }

    var method = this[shape.type];
    if(method){
        method.call(this, shape, angle, position, body);
    }
};

var vector = vec2.create();
var normal = vec2.create();
var intersectPoint = vec2.create();

var a = vec2.create();
var b = vec2.create();
var c = vec2.create();
var d = vec2.create();

var tmpRaycastResult = new RaycastResult();
var intersectRectangle_direction = vec2.create();
var intersectRectangle_rayStart = vec2.create();
var intersectRectangle_worldNormalMin = vec2.create();
var intersectRectangle_worldNormalMax = vec2.create();
var intersectRectangle_hitPointWorld = vec2.create();
var intersectRectangle_boxMin = vec2.create();
var intersectRectangle_boxMax = vec2.create();

/**
 * @method intersectRectangle
 * @private
 * @param  {Shape} shape
 * @param  {number} angle
 * @param  {array} position
 * @param  {Body} body
 */
Ray.prototype.intersectRectangle = function(shape, angle, position, body){
    var tmin = -Number.MAX_VALUE;
    var tmax = Number.MAX_VALUE;

    var direction = intersectRectangle_direction;
    var rayStart = intersectRectangle_rayStart;
    var worldNormalMin = intersectRectangle_worldNormalMin;
    var worldNormalMax = intersectRectangle_worldNormalMax;
    var hitPointWorld = intersectRectangle_hitPointWorld;
    var boxMin = intersectRectangle_boxMin;
    var boxMax = intersectRectangle_boxMax;

    vec2.set(boxMin, -shape.width * 0.5, -shape.height * 0.5);
    vec2.set(boxMax, shape.width * 0.5, shape.height * 0.5);

    // Transform the ray direction and start to local space
    vec2.rotate(direction, this._direction, -angle);
    body.toLocalFrame(rayStart, this.from);

    if (direction[0] !== 0) {
        var tx1 = (boxMin[0] - rayStart[0]) / direction[0];
        var tx2 = (boxMax[0] - rayStart[0]) / direction[0];

        var tminOld = tmin;
        tmin = Math.max(tmin, Math.min(tx1, tx2));
        if(tmin !== tminOld){
            vec2.set(worldNormalMin, tx1 > tx2 ? 1 : -1, 0);
        }

        var tmaxOld = tmax;
        tmax = Math.min(tmax, Math.max(tx1, tx2));
        if(tmax !== tmaxOld){
            vec2.set(worldNormalMax, tx1 < tx2 ? 1 : -1, 0);
        }
    }

    if (direction[1] !== 0) {
        var ty1 = (boxMin[1] - rayStart[1]) / direction[1];
        var ty2 = (boxMax[1] - rayStart[1]) / direction[1];

        var tminOld = tmin;
        tmin = Math.max(tmin, Math.min(ty1, ty2));
        if(tmin !== tminOld){
            vec2.set(worldNormalMin, 0, ty1 > ty2 ? 1 : -1);
        }

        var tmaxOld = tmax;
        tmax = Math.min(tmax, Math.max(ty1, ty2));
        if(tmax !== tmaxOld){
            vec2.set(worldNormalMax, 0, ty1 < ty2 ? 1 : -1);
        }
    }

    if(tmax >= tmin){
        // Hit point
        vec2.set(
            hitPointWorld,
            rayStart[0] + direction[0] * tmin,
            rayStart[1] + direction[1] * tmin
        );

        vec2.rotate(worldNormalMin, worldNormalMin, angle);

        body.toWorldFrame(hitPointWorld, hitPointWorld);

        this.reportIntersection(worldNormalMin, hitPointWorld, shape, body, -1);
        if(this._shouldStop){
            return;
        }

        vec2.rotate(worldNormalMax, worldNormalMax, angle);

        // Hit point
        vec2.set(
            hitPointWorld,
            rayStart[0] + direction[0] * tmax,
            rayStart[1] + direction[1] * tmax
        );
        body.toWorldFrame(hitPointWorld, hitPointWorld);

        this.reportIntersection(worldNormalMax, hitPointWorld, shape, body, -1);
    }
};
Ray.prototype[Shape.RECTANGLE] = Ray.prototype.intersectRectangle;

var intersectPlane_planePointToFrom = vec2.create();
var intersectPlane_dir_scaled_with_t = vec2.create();
var intersectPlane_hitPointWorld = vec2.create();
var intersectPlane_worldNormal = vec2.create();
var intersectPlane_len = vec2.create();

/**
 * @method intersectPlane
 * @private
 * @param  {Shape} shape
 * @param  {number} angle
 * @param  {array} position
 * @param  {Body} body
 */
Ray.prototype.intersectPlane = function(shape, angle, position, body){
    var from = this.from;
    var to = this.to;
    var direction = this._direction;

    var planePointToFrom = intersectPlane_planePointToFrom;
    var dir_scaled_with_t = intersectPlane_dir_scaled_with_t;
    var hitPointWorld = intersectPlane_hitPointWorld;
    var worldNormal = intersectPlane_worldNormal;
    var len = intersectPlane_len;

    // Get plane normal
    vec2.set(worldNormal, 0, 1);
    vec2.rotate(worldNormal, worldNormal, angle);

    vec2.sub(len, from, position); //from.vsub(position, len);
    var planeToFrom = vec2.dot(len, worldNormal); // len.dot(worldNormal);
    vec2.sub(len, to, position); // to.vsub(position, len);
    var planeToTo = vec2.dot(len, worldNormal); // len.dot(worldNormal);

    if(planeToFrom * planeToTo > 0){
        // "from" and "to" are on the same side of the plane... bail out
        return;
    }

    if(vec2.distance(from, to) /* from.distanceTo(to) */ < planeToFrom){
        return;
    }

    var n_dot_dir = vec2.dot(worldNormal, direction); // worldNormal.dot(direction);

    // if (Math.abs(n_dot_dir) < this.precision) {
    //     // No intersection
    //     return;
    // }

    vec2.sub(planePointToFrom, from, position); // from.vsub(position, planePointToFrom);
    var t = -vec2.dot(worldNormal, planePointToFrom) / n_dot_dir; // - worldNormal.dot(planePointToFrom) / n_dot_dir;
    vec2.scale(dir_scaled_with_t, direction, t); // direction.scale(t, dir_scaled_with_t);
    vec2.add(hitPointWorld, from, dir_scaled_with_t); // from.vadd(dir_scaled_with_t, hitPointWorld);

    this.reportIntersection(worldNormal, hitPointWorld, shape, body, -1);
};
Ray.prototype[Shape.PLANE] = Ray.prototype.intersectPlane;

var Ray_intersectSphere_intersectionPoint = vec2.create();
var Ray_intersectSphere_normal = vec2.create();
Ray.prototype.intersectCircle = function(shape, angle, position, body){
    var from = this.from,
        to = this.to,
        r = shape.radius;

    var a = Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2);
    var b = 2 * ((to[0] - from[0]) * (from[0] - position[0]) + (to[1] - from[1]) * (from[1] - position[1]));
    var c = Math.pow(from[0] - position[0], 2) + Math.pow(from[1] - position[1], 2) - Math.pow(r, 2);

    var delta = Math.pow(b, 2) - 4 * a * c;

    var intersectionPoint = Ray_intersectSphere_intersectionPoint;
    var normal = Ray_intersectSphere_normal;

    if(delta < 0){
        // No intersection
        return;

    } else if(delta === 0){
        // single intersection point
        vec2.lerp(intersectionPoint, from, to, delta); // from.lerp(to, delta, intersectionPoint);

        vec2.sub(normal, intersectionPoint, position); // intersectionPoint.vsub(position, normal);
        vec2.normalize(normal,normal); //normal.normalize();

        this.reportIntersection(normal, intersectionPoint, shape, body, -1);

    } else {
        var d1 = (- b - Math.sqrt(delta)) / (2 * a);
        var d2 = (- b + Math.sqrt(delta)) / (2 * a);

        vec2.lerp(intersectionPoint, from, to, d1); // from.lerp(to, d1, intersectionPoint);

        vec2.sub(normal, intersectionPoint, position); // intersectionPoint.vsub(position, normal);
        vec2.normalize(normal,normal); //normal.normalize();

        this.reportIntersection(normal, intersectionPoint, shape, body, -1);

        if(this.result._shouldStop){
            return;
        }

        vec2.lerp(intersectionPoint, from, to, d2); // from.lerp(to, d2, intersectionPoint);

        vec2.sub(normal, intersectionPoint, position); // intersectionPoint.vsub(position, normal);
        vec2.normalize(normal,normal); //normal.normalize();

        this.reportIntersection(normal, intersectionPoint, shape, body, -1);
    }
};
Ray.prototype[Shape.CIRCLE] = Ray.prototype.intersectCircle;

/**
 * Get the AABB of the ray.
 * @method getAABB
 * @param  {AABB} aabb
 */
Ray.prototype.getAABB = function(result){
    var to = this.to;
    var from = this.from;
    result.lowerBound[0] = Math.min(to[0], from[0]);
    result.lowerBound[1] = Math.min(to[1], from[1]);
    result.upperBound[0] = Math.max(to[0], from[0]);
    result.upperBound[1] = Math.max(to[1], from[1]);
};

/**
 * @method reportIntersection
 * @private
 * @param  {array} normal
 * @param  {array} hitPointWorld
 * @param  {Shape} shape
 * @param  {Body} body
 * @return {boolean} True if the intersections should continue
 */
Ray.prototype.reportIntersection = function(normal, hitPointWorld, shape, body, hitFaceIndex){
    var from = this.from;
    var to = this.to;
    var distance = vec2.distance(from, hitPointWorld); // from.distanceTo(hitPointWorld);
    var result = this.result;

    // Skip back faces?
    if(this.skipBackfaces && /* normal.dot(this._direction) */ vec2.dot(normal, this._direction) > 0){
        return;
    }

    result.hitFaceIndex = typeof(hitFaceIndex) !== 'undefined' ? hitFaceIndex : -1;

    switch(this.mode){
    case Ray.ALL:
        this.hasHit = true;
        result.set(
            from,
            to,
            normal,
            hitPointWorld,
            shape,
            body,
            distance
        );
        result.hasHit = true;
        this.callback(result);
        break;

    case Ray.CLOSEST:

        // Store if closer than current closest
        if(distance < result.distance || !result.hasHit){
            this.hasHit = true;
            result.hasHit = true;
            result.set(
                from,
                to,
                normal,
                hitPointWorld,
                shape,
                body,
                distance
            );
        }
        break;

    case Ray.ANY:

        // Report and stop.
        this.hasHit = true;
        result.hasHit = true;
        result.set(
            from,
            to,
            normal,
            hitPointWorld,
            shape,
            body,
            distance
        );
        result._shouldStop = true;
        break;
    }
};

var v0 = vec2.create(),
    intersect = vec2.create();
function distanceFromIntersection(from, direction, position) {

    // v0 is vector from from to position
    vec2.sub(v0, position, from); // position.vsub(from,v0);
    var dot = vec2.dot(v0, direction); // v0.dot(direction);

    // intersect = direction*dot + from
    vec2.scale(intersect, direction, dot); //direction.mult(dot,intersect);
    vec2.add(intersect, intersect, from); // intersect.vadd(from, intersect);

    var distance = vec2.distance(position, intersect); // position.distanceTo(intersect);

    return distance;
}