Source: scene/light.js

Object.assign(pc, function () {

    var spotCenter = new pc.Vec3();
    var spotEndPoint = new pc.Vec3();
    var tmpVec = new pc.Vec3();

    var chanId = { r: 0, g: 1, b: 2, a: 3 };

    /**
     * @private
     * @constructor
     * @name pc.Light
     * @classdesc A light.
     */
    var Light = function Light() {
        // Light properties (defaults)
        this._type = pc.LIGHTTYPE_DIRECTIONAL;
        this._color = new pc.Color(0.8, 0.8, 0.8);
        this._intensity = 1;
        this._castShadows = false;
        this._enabled = false;
        this._mask = 1;
        this.isStatic = false;
        this.key = 0;
        this.bakeDir = true;

        // Point and spot properties
        this.attenuationStart = 10;
        this.attenuationEnd = 10;
        this._falloffMode = 0;
        this._shadowType = pc.SHADOW_PCF3;
        this._vsmBlurSize = 11;
        this.vsmBlurMode = pc.BLUR_GAUSSIAN;
        this.vsmBias = 0.01 * 0.25;
        this._cookie = null; // light cookie texture (2D for spot, cubemap for point)
        this.cookieIntensity = 1;
        this._cookieFalloff = true;
        this._cookieChannel = "rgb";
        this._cookieTransform = null; // 2d rotation/scale matrix (spot only)
        this._cookieTransformUniform = new Float32Array(4);
        this._cookieOffset = null; // 2d position offset (spot only)
        this._cookieOffsetUniform = new Float32Array(2);
        this._cookieTransformSet = false;
        this._cookieOffsetSet = false;

        // Spot properties
        this._innerConeAngle = 40;
        this._outerConeAngle = 45;

        // Cache of light property data in a format more friendly for shader uniforms
        this._finalColor = new Float32Array([0.8, 0.8, 0.8]);
        var c = Math.pow(this._finalColor[0], 2.2);
        this._linearFinalColor = new Float32Array([c, c, c]);

        this._position = new pc.Vec3(0, 0, 0);
        this._direction = new pc.Vec3(0, 0, 0);
        this._innerConeAngleCos = Math.cos(this._innerConeAngle * Math.PI / 180);
        this._outerConeAngleCos = Math.cos(this._outerConeAngle * Math.PI / 180);

        // Shadow mapping resources
        this._shadowCamera = null;
        this._shadowMatrix = new pc.Mat4();
        this.shadowDistance = 40;
        this._shadowResolution = 1024;
        this.shadowBias = -0.0005;
        this._normalOffsetBias = 0.0;
        this.shadowUpdateMode = pc.SHADOWUPDATE_REALTIME;

        this._scene = null;
        this._node = null;
        this._rendererParams = [];

        this._isVsm = false;
        this._isPcf = true;
        this._cacheShadowMap = false;
        this._isCachedShadowMap = false;

        this._visibleLength = [0]; // lengths of passes in culledList
        this._visibleList = [[]]; // culled mesh instances per pass (1 for spot, 6 for point, cameraCount for directional)
        this._visibleCameraSettings = []; // camera settings used in each directional light pass
    };

    Object.assign(Light.prototype, {
        destroy: function () {
            this._destroyShadowMap();
        },

        /**
         * @private
         * @function
         * @name pc.Light#clone
         * @description Duplicates a light node but does not 'deep copy' the hierarchy.
         * @returns {pc.Light} A cloned Light.
         */
        clone: function () {
            var clone = new pc.Light();

            // Clone Light properties
            clone.type = this._type;
            clone.setColor(this._color);
            clone.intensity = this._intensity;
            clone.castShadows = this.castShadows;
            clone.enabled = this._enabled;

            // Point and spot properties
            clone.attenuationStart = this.attenuationStart;
            clone.attenuationEnd = this.attenuationEnd;
            clone.falloffMode = this._falloffMode;
            clone.shadowType = this._shadowType;
            clone.vsmBlurSize = this._vsmBlurSize;
            clone.vsmBlurMode = this.vsmBlurMode;
            clone.vsmBias = this.vsmBias;
            clone.shadowUpdateMode = this.shadowUpdateMode;
            clone.mask = this._mask;

            // Spot properties
            clone.innerConeAngle = this._innerConeAngle;
            clone.outerConeAngle = this._outerConeAngle;

            // Shadow properties
            clone.shadowBias = this.shadowBias;
            clone.normalOffsetBias = this._normalOffsetBias;
            clone.shadowResolution = this._shadowResolution;
            clone.shadowDistance = this.shadowDistance;

            // Cookies properties
            // clone.cookie = this._cookie;
            // clone.cookieIntensity = this.cookieIntensity;
            // clone.cookieFalloff = this._cookieFalloff;
            // clone.cookieChannel = this._cookieChannel;
            // clone.cookieTransform = this._cookieTransform;
            // clone.cookieOffset = this._cookieOffset;

            return clone;
        },

        getColor: function () {
            return this._color;
        },

        getBoundingSphere: function (sphere) {
            if (this._type === pc.LIGHTTYPE_SPOT) {
                var range = this.attenuationEnd;
                var angle = this._outerConeAngle;
                var f = Math.cos(angle * pc.math.DEG_TO_RAD);
                var node = this._node;

                spotCenter.copy(node.up);
                spotCenter.scale(-range * 0.5 * f);
                spotCenter.add(node.getPosition());
                sphere.center = spotCenter;

                spotEndPoint.copy(node.up);
                spotEndPoint.scale(-range);

                tmpVec.copy(node.right);
                tmpVec.scale(Math.sin(angle * pc.math.DEG_TO_RAD) * range);
                spotEndPoint.add(tmpVec);

                sphere.radius = spotEndPoint.length() * 0.5;

            } else if (this._type === pc.LIGHTTYPE_POINT) {
                sphere.center = this._node.getPosition();
                sphere.radius = this.attenuationEnd;
            }
        },

        getBoundingBox: function (box) {
            if (this._type === pc.LIGHTTYPE_SPOT) {
                var range = this.attenuationEnd;
                var angle = this._outerConeAngle;
                var node = this._node;

                var scl = Math.abs( Math.sin(angle * pc.math.DEG_TO_RAD) * range );

                box.center.set(0, -range * 0.5, 0);
                box.halfExtents.set(scl, range * 0.5, scl);

                box.setFromTransformedAabb(box, node.getWorldTransform());

            } else if (this._type === pc.LIGHTTYPE_POINT) {
                box.center.copy(this._node.getPosition());
                box.halfExtents.set(this.attenuationEnd, this.attenuationEnd, this.attenuationEnd);
            }
        },

        _updateFinalColor: function () {
            var color = this._color;
            var r = color.r;
            var g = color.g;
            var b = color.b;

            var i = this._intensity;

            var finalColor = this._finalColor;
            var linearFinalColor = this._linearFinalColor;

            finalColor[0] = r * i;
            finalColor[1] = g * i;
            finalColor[2] = b * i;
            if (i >= 1) {
                linearFinalColor[0] = Math.pow(r, 2.2) * i;
                linearFinalColor[1] = Math.pow(g, 2.2) * i;
                linearFinalColor[2] = Math.pow(b, 2.2) * i;
            } else {
                linearFinalColor[0] = Math.pow(finalColor[0], 2.2);
                linearFinalColor[1] = Math.pow(finalColor[1], 2.2);
                linearFinalColor[2] = Math.pow(finalColor[2], 2.2);
            }
        },

        setColor: function () {
            var r, g, b;
            if (arguments.length === 1) {
                r = arguments[0].r;
                g = arguments[0].g;
                b = arguments[0].b;
            } else if (arguments.length === 3) {
                r = arguments[0];
                g = arguments[1];
                b = arguments[2];
            }

            this._color.set(r, g, b);

            this._updateFinalColor();
        },

        _destroyShadowMap: function () {
            if (this._shadowCamera) {
                if (!this._isCachedShadowMap) {
                    var rt = this._shadowCamera.renderTarget;
                    var i;
                    if (rt) {
                        if (rt.length) {
                            for (i = 0; i < rt.length; i++) {
                                if (rt[i].colorBuffer) rt[i].colorBuffer.destroy();
                                rt[i].destroy();
                            }
                        } else {
                            if (rt.colorBuffer) rt.colorBuffer.destroy();
                            if (rt.depthBuffer) rt.depthBuffer.destroy();
                            rt.destroy();
                        }
                    }
                }
                this._shadowCamera.renderTarget = null;
                this._shadowCamera = null;
                this._shadowCubeMap = null;
                if (this.shadowUpdateMode === pc.SHADOWUPDATE_NONE) {
                    this.shadowUpdateMode = pc.SHADOWUPDATE_THISFRAME;
                }
            }
        },

        updateShadow: function () {
            if (this.shadowUpdateMode !== pc.SHADOWUPDATE_REALTIME) {
                this.shadowUpdateMode = pc.SHADOWUPDATE_THISFRAME;
            }
        },

        updateKey: function () {
            // Key definition:
            // Bit
            // 31      : sign bit (leave)
            // 29 - 30 : type
            // 28      : cast shadows
            // 25 - 27 : shadow type
            // 23 - 24 : falloff mode
            // 22      : normal offset bias
            // 21      : cookie
            // 20      : cookie falloff
            // 18 - 19 : cookie channel R
            // 16 - 17 : cookie channel G
            // 14 - 15 : cookie channel B
            // 12      : cookie transform
            var key =
                   (this._type                                << 29) |
                   ((this._castShadows ? 1 : 0)               << 28) |
                   (this._shadowType                          << 25) |
                   (this._falloffMode                         << 23) |
                   ((this._normalOffsetBias !== 0.0 ? 1 : 0)  << 22) |
                   ((this._cookie ? 1 : 0)                    << 21) |
                   ((this._cookieFalloff ? 1 : 0)             << 20) |
                   (chanId[this._cookieChannel.charAt(0)]     << 18) |
                   ((this._cookieTransform ? 1 : 0)           << 12);

            if (this._cookieChannel.length === 3) {
                key |= (chanId[this._cookieChannel.charAt(1)] << 16);
                key |= (chanId[this._cookieChannel.charAt(2)] << 14);
            }

            if (key !== this.key && this._scene !== null) {
                this._scene.layers._dirtyLights = true;
            }

            this.key = key;
        }
    });

    Object.defineProperty(Light.prototype, 'enabled', {
        get: function () {
            return this._type;
        },
        set: function (value) {
            if (this._type === value)
                return;

            this._enabled = value;
        }
    });

    Object.defineProperty(Light.prototype, 'type', {
        get: function () {
            return this._type;
        },
        set: function (value) {
            if (this._type === value)
                return;

            this._type = value;
            this._destroyShadowMap();
            this.updateKey();

            var stype = this._shadowType;
            this._shadowType = null;
            this.shadowType = stype; // refresh shadow type; switching from direct/spot to point and back may change it
        }
    });

    Object.defineProperty(Light.prototype, 'mask', {
        get: function () {
            return this._mask;
        },
        set: function (value) {
            if (this._mask === value)
                return;

            this._mask = value;
        }
    });

    Object.defineProperty(Light.prototype, 'shadowType', {
        get: function () {
            return this._shadowType;
        },
        set: function (value) {
            if (this._shadowType === value)
                return;

            var device = pc.Application.getApplication().graphicsDevice;

            if (this._type === pc.LIGHTTYPE_POINT)
                value = pc.SHADOW_PCF3; // VSM or HW PCF for point lights is not supported yet

            if (value === pc.SHADOW_PCF5 && !device.webgl2) {
                value = pc.SHADOW_PCF3; // fallback from HW PCF to old PCF
            }

            if (value === pc.SHADOW_VSM32 && !device.textureFloatRenderable) // fallback from vsm32 to vsm16
                value = pc.SHADOW_VSM16;

            if (value === pc.SHADOW_VSM16 && !device.textureHalfFloatRenderable) // fallback from vsm16 to vsm8
                value = pc.SHADOW_VSM8;

            this._isVsm = value >= pc.SHADOW_VSM8 && value <= pc.SHADOW_VSM32;
            this._isPcf = value === pc.SHADOW_PCF5 || value === pc.SHADOW_PCF3;

            this._shadowType = value;
            this._destroyShadowMap();
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'castShadows', {
        get: function () {
            return this._castShadows && this._mask !== pc.MASK_LIGHTMAP && this._mask !== 0;
        },
        set: function (value) {
            if (this._castShadows === value)
                return;

            this._castShadows = value;
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'shadowResolution', {
        get: function () {
            return this._shadowResolution;
        },
        set: function (value) {
            if (this._shadowResolution === value)
                return;

            var device = pc.Application.getApplication().graphicsDevice;
            if (this._type === pc.LIGHTTYPE_POINT) {
                value = Math.min(value, device.maxCubeMapSize);
            } else {
                value = Math.min(value, device.maxTextureSize);
            }
            this._shadowResolution = value;
        }
    });

    Object.defineProperty(Light.prototype, 'vsmBlurSize', {
        get: function () {
            return this._vsmBlurSize;
        },
        set: function (value) {
            if (this._vsmBlurSize === value)
                return;

            if (value % 2 === 0) value++; // don't allow even size
            this._vsmBlurSize = value;
        }
    });

    Object.defineProperty(Light.prototype, 'normalOffsetBias', {
        get: function () {
            return this._normalOffsetBias;
        },
        set: function (value) {
            if (this._normalOffsetBias === value)
                return;

            if ((!this._normalOffsetBias && value) || (this._normalOffsetBias && !value)) {
                this.updateKey();
            }
            this._normalOffsetBias = value;
        }
    });

    Object.defineProperty(Light.prototype, 'falloffMode', {
        get: function () {
            return this._falloffMode;
        },
        set: function (value) {
            if (this._falloffMode === value)
                return;

            this._falloffMode = value;
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'innerConeAngle', {
        get: function () {
            return this._innerConeAngle;
        },
        set: function (value) {
            if (this._innerConeAngle === value)
                return;

            this._innerConeAngle = value;
            this._innerConeAngleCos = Math.cos(value * Math.PI / 180);
        }
    });

    Object.defineProperty(Light.prototype, 'outerConeAngle', {
        get: function () {
            return this._outerConeAngle;
        },
        set: function (value) {
            if (this._outerConeAngle === value)
                return;

            this._outerConeAngle = value;
            this._outerConeAngleCos = Math.cos(value * Math.PI / 180);
        }
    });

    Object.defineProperty(Light.prototype, 'intensity', {
        get: function () {
            return this._intensity;
        },
        set: function (value) {
            if (this._intensity !== value) {
                this._intensity = value;
                this._updateFinalColor();
            }
        }
    });

    Object.defineProperty(Light.prototype, 'cookie', {
        get: function () {
            return this._cookie;
        },
        set: function (value) {
            if (this._cookie === value)
                return;

            this._cookie = value;
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'cookieFalloff', {
        get: function () {
            return this._cookieFalloff;
        },
        set: function (value) {
            if (this._cookieFalloff === value)
                return;

            this._cookieFalloff = value;
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'cookieChannel', {
        get: function () {
            return this._cookieChannel;
        },
        set: function (value) {
            if (this._cookieChannel === value)
                return;

            if (value.length < 3) {
                var chr = value.charAt(value.length - 1);
                var addLen = 3 - value.length;
                for (var i = 0; i < addLen; i++)
                    value += chr;
            }
            this._cookieChannel = value;
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'cookieTransform', {
        get: function () {
            return this._cookieTransform;
        },
        set: function (value) {
            if (this._cookieTransform === value)
                return;

            this._cookieTransform = value;
            this._cookieTransformSet = !!value;
            if (value && !this._cookieOffset) {
                this.cookieOffset = new pc.Vec2(); // using transform forces using offset code
                this._cookieOffsetSet = false;
            }
            this.updateKey();
        }
    });

    Object.defineProperty(Light.prototype, 'cookieOffset', {
        get: function () {
            return this._cookieOffset;
        },
        set: function (value) {
            if (this._cookieOffset === value)
                return;

            var xformNew = !!(this._cookieTransformSet || value);
            if (xformNew && !value && this._cookieOffset) {
                this._cookieOffset.set(0, 0);
            } else {
                this._cookieOffset = value;
            }
            this._cookieOffsetSet = !!value;
            if (value && !this._cookieTransform) {
                this.cookieTransform = new pc.Vec4(1, 1, 0, 0); // using offset forces using matrix code
                this._cookieTransformSet = false;
            }
            this.updateKey();
        }
    });


    return {
        Light: Light
    };
}());