Source: Shadows/ShadowCaster.js

/*
 * File: ShadowCaster.js
 * Renders a colored image representing the shadowCaster on the receiver
 */

/*jslint node: true, vars: true, white: true */
/*global gEngine, Renderable, SpriteRenderable, Light, Transform, vec3, Math */
/* find out more about jslint: http://www.jslint.com/help.html */

"use strict";  // Operate in Strict mode such that variables must be declared before used!

// shadowCaster:    must be GameObject referencing at least a LightRenderable  
// shadowReceiver:  must be GameObject referencing at least a SpriteRenderable
/**
 * Default Constructor<p>
 * Renders a colored image representing the shadowCaster on the receiver
 * @param {ShadowCaster} shadowCaster must be GameObject referencing at least a LightRenderable
 * @param {ShadowReceiver} shadowReceiver  must be GameObject referencing at least a SpriteRenderable
 * @returns {ShadowCaster} New instance of ShadowCaster
 * @class ShadowCaster
 */
function ShadowCaster (shadowCaster, shadowReceiver) {
    this.mShadowCaster = shadowCaster;  
    this.mShadowReceiver = shadowReceiver;
    this.mCasterShader = gEngine.DefaultResources.getShadowCasterShader();
    this.mShadowColor = [0, 0, 0, 0.2];
    this.mSaveXform = new Transform();
    
    this.kCasterMaxScale = 3;   // maximum size a caster will be scaled
    this.kVerySmall = 0.001;    // 
    this.kDistanceFudge = 0.01; // Ensure shadow caster geometry is not at the exact same depth as receiver
    this.kReceiverDistanceFudge = 0.6; // Reduce the projection size increase of the caster geometry
}

/**
 * Set the shadow color
 * @param {Float[]} c new Color of shadow [R, G, B, A]
 * @returns {void}
 * @memberOf ShadowCaster
 */
ShadowCaster.prototype.setShadowColor = function (c) {
    this.mShadowColor = c;
};

ShadowCaster.prototype._computeShadowGeometry = function(aLight) {
    // Remember that z-value determines front/back
    //      The camera is located a z=some value, looking towards z=0
    //      The larger the z-value (larger height value) the closer to the camera
    //      If z > camera.Z, will not be visile
    
    // supports casting to the back of a receiver (if receiver is transparent)
    // then you can see shadow from the camera
    // this means, even when:
    //      1. caster is lower than receiver
    //      2. light is lower than the caster
    // it is still possible to cast shadow on receiver
    
    var cxf = this.mShadowCaster.getXform();
    var rxf = this.mShadowReceiver.getXform();
    // vector from light to caster
    var lgtToCaster = vec3.create();
    var lgtToReceiverZ;
    var receiverToCasterZ;
    var distToCaster, distToReceiver;  // measured along the lgtToCaster vector
    var scale;
    var offset = vec3.fromValues(0, 0, 0);
    
    receiverToCasterZ = rxf.getZPos() - cxf.getZPos();
    if (aLight.getLightType() === Light.eLightType.eDirectionalLight) {    
        if (((Math.abs(aLight.getDirection())[2]) < this.kVerySmall) || ((receiverToCasterZ * (aLight.getDirection())[2]) < 0)) {
            return false;   // direction light casting side way or
                            // caster and receiver on different sides of light in Z
        }
        vec3.copy(lgtToCaster, aLight.getDirection());
        vec3.normalize(lgtToCaster, lgtToCaster);
        
        distToReceiver = Math.abs(receiverToCasterZ / lgtToCaster[2]);  // distance measured along lgtToCaster
        scale = Math.abs(1/lgtToCaster[2]);
    } else {    
        vec3.sub(lgtToCaster, cxf.get3DPosition(), aLight.getPosition());
        lgtToReceiverZ = rxf.getZPos() - (aLight.getPosition())[2];
        
        if ((lgtToReceiverZ * lgtToCaster[2]) < 0) {
            return false;  // caster and receiver on different sides of light in Z
        }

        if ((Math.abs(lgtToReceiverZ) < this.kVerySmall) || ((Math.abs(lgtToCaster[2]) < this.kVerySmall))) {
            // alomst the same Z, can't see shadow
            return false;
        }

        distToCaster = vec3.length(lgtToCaster);
        vec3.scale(lgtToCaster, lgtToCaster, 1/distToCaster);  // normalize lgtToCaster
        
        distToReceiver = Math.abs(receiverToCasterZ / lgtToCaster[2]);  // distance measured along lgtToCaster
        scale = (distToCaster + (distToReceiver * this.kReceiverDistanceFudge)) / distToCaster;
    }
    vec3.scaleAndAdd(offset, cxf.get3DPosition(), lgtToCaster, distToReceiver + this.kDistanceFudge);
    
    cxf.setRotationInRad( cxf.getRotationInRad());
    cxf.setPosition(offset[0], offset[1]);
    cxf.setZPos(offset[2]);
    cxf.setWidth(cxf.getWidth() * scale);
    cxf.setHeight(cxf.getHeight() * scale);
    
    return true;
};

/**
 * Draw function called by GameLoop
 * @param {Camera} aCamera Camera to draw too
 * @returns {undefined}
 * @memberOf ShadowCaster
 */
ShadowCaster.prototype.draw = function(aCamera) {
    // loop through each light in this array, if shadow casting on the light is on
    // compute the proper shadow offset
    var casterRenderable = this.mShadowCaster.getRenderable();
    this.mShadowCaster.getXform().cloneTo(this.mSaveXform);
    var s = casterRenderable.swapShader(this.mCasterShader);
    var c = casterRenderable.getColor();
    casterRenderable.setColor(this.mShadowColor);
    var l, lgt;
    for (l = 0; l < casterRenderable.numLights(); l++) {
        lgt = casterRenderable.getLightAt(l);
        if (lgt.isLightOn() && lgt.isLightCastShadow()) {
            this.mSaveXform.cloneTo(this.mShadowCaster.getXform());
            if (this._computeShadowGeometry(lgt)) {
                this.mCasterShader.setLight(lgt);
                SpriteRenderable.prototype.draw.call(casterRenderable, aCamera);
            }
        }
    }
    this.mSaveXform.cloneTo(this.mShadowCaster.getXform());
    casterRenderable.swapShader(s);
    casterRenderable.setColor(c);
};