Source: shadows/shadow_caster.js

/*
 * File: shadow_caster.js
 * Renders a colored image representing the shadowCaster on the receiver
 */
"use strict";  // Operate in Strict mode such that variables must be declared before used!

import * as shaderResources from "../core/shader_resources.js";
import SpriteRenderable from "../renderables/sprite_renderable.js";
import Transform from "../utils/transform.js";
import { eLightType } from "../lights/light.js";

// shadowCaster:    must be GameObject referencing at least a LightRenderable
// shadowReceiver:  must be GameObject referencing at least a SpriteRenderable
class ShadowCaster {

    /**
     * @classdesc Renders a colored image representing the shadowCaster on the reciever
     * <p>Found in Chapter 8, page 501 of the textbook </p>
     * Example:
     * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter8/8.7.shadow_shaders/index.html 8.7 Shadow Shaders}
     * @constructor
     * @param {GameObject} shadowCaster - the object casting the shadow, must contain at least a LightRenderable
     * @param {GameObject} shadowReceiver - object receiving the shadow, must contain at least a SpriteRenderable 
     * @returns {ShadowCaster} a new ShadowCaster instance
     */
    constructor(shadowCaster, shadowReceiver) {
        this.mShadowCaster = shadowCaster;
        this.mShadowReceiver = shadowReceiver;
        this.mCasterShader = shaderResources.getShadowCasterShader();
        this.mShadowColor = [0, 0, 0, 0.2];
        this.mSaveXform = new Transform();

        this.kCasterMaxScale = 3;   // Max amount a caster will be scaled
        this.kVerySmall = 0.001;    // 
        this.kDistanceFudge = 0.01; // Dist between caster geometry and receiver: ensure no overlap
        this.kReceiverDistanceFudge = 0.6; // Factor to reduce the projected caster geometry size
    }

    /**
     * Set the color of the shadow
     * @method
     * @param {vec4} c - [R,G,B,A] color array 
     */
    setShadowColor(c) {
        this.mShadowColor = c;
    }

    _computeShadowGeometry(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

        // Region 1: declaring variables
        let cxf = this.mShadowCaster.getXform();
        let rxf = this.mShadowReceiver.getXform();
        // vector from light to caster
        let lgtToCaster = vec3.create();
        let lgtToReceiverZ;
        let receiverToCasterZ;
        let distToCaster, distToReceiver;  // measured along the lgtToCaster vector
        let scale;
        let offset = vec3.fromValues(0, 0, 0);

        receiverToCasterZ = rxf.getZPos() - cxf.getZPos();
        if (aLight.getLightType() === eLightType.eDirectionalLight) {
            // Region 2: Processing a directional light
            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 {
            // Region 3: Processing a point or spot light
            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))) {
                // almost 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);

        // Region 4: Setting casterRenderable xform
        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 this ShadowCaster to the Camera.
     * Interacts with any overlapping Light
     * @method
     * @param {Camera} aCamera - the Camera to draw to 
     */
    draw(aCamera) {
        let casterRenderable = this.mShadowCaster.getRenderable();
        // Step A: save caster xform, shader, and color. and, sets caster to shadow color
        this.mShadowCaster.getXform().cloneTo(this.mSaveXform);
        let s = casterRenderable.swapShader(this.mCasterShader);
        let c = casterRenderable.getColor();
        casterRenderable.setColor(this.mShadowColor);
        let l, lgt;
        // Step B: loop through each light in this array, if shadow casting on the light is on
        // compute the proper shadow offset
        for(l = 0; l < casterRenderable.getNumLights(); l++) {
            lgt = casterRenderable.getLightAt(l);
            if (lgt.isLightOn() && lgt.isLightCastShadow()) {
                // Step C: turn caster into caster geometry, draws as SpriteRenderable
                this.mSaveXform.cloneTo(this.mShadowCaster.getXform());
                if (this._computeShadowGeometry(lgt)) {
                    this.mCasterShader.setCameraAndLights(aCamera, lgt);
                    SpriteRenderable.prototype.draw.call(casterRenderable, aCamera);
                }
            }
        }
        // Step D: restore the original shadow caster
        this.mSaveXform.cloneTo(this.mShadowCaster.getXform());
        casterRenderable.swapShader(s);
        casterRenderable.setColor(c);
    }
}

export default ShadowCaster;