Source: scene/forward-renderer.js

Object.assign(pc, function () {

    // Global shadowmap resources
    var scaleShift = new pc.Mat4().mul2(
        new pc.Mat4().setTranslate(0.5, 0.5, 0.5),
        new pc.Mat4().setScale(0.5, 0.5, 0.5)
    );

    var opChanId = { r: 1, g: 2, b: 3, a: 4 };

    var pointLightRotations = [
        new pc.Quat().setFromEulerAngles(0, 90, 180),
        new pc.Quat().setFromEulerAngles(0, -90, 180),
        new pc.Quat().setFromEulerAngles(90, 0, 0),
        new pc.Quat().setFromEulerAngles(-90, 0, 0),
        new pc.Quat().setFromEulerAngles(0, 180, 180),
        new pc.Quat().setFromEulerAngles(0, 0, 180)
    ];

    var numShadowModes = 5;
    var shadowMapCache = [{}, {}, {}, {}, {}]; // must be a size of numShadowModes

    var directionalShadowEpsilon = 0.01;
    var pixelOffset = new Float32Array(2);
    var blurScissorRect = { x: 1, y: 1, z: 0, w: 0 };

    var shadowCamView = new pc.Mat4();
    var shadowCamViewProj = new pc.Mat4();
    var c2sc = new pc.Mat4();

    var viewInvMat = new pc.Mat4();
    var viewMat = new pc.Mat4();
    var viewMat3 = new pc.Mat3();
    var viewProjMat = new pc.Mat4();
    var projMat;

    var viewInvL = new pc.Mat4();
    var viewInvR = new pc.Mat4();
    var viewL = new pc.Mat4();
    var viewR = new pc.Mat4();
    var viewPosL = new pc.Vec3();
    var viewPosR = new pc.Vec3();
    var projL, projR;
    var viewMat3L = new pc.Mat4();
    var viewMat3R = new pc.Mat4();
    var viewProjMatL = new pc.Mat4();
    var viewProjMatR = new pc.Mat4();

    var worldMatX = new pc.Vec3();
    var worldMatY = new pc.Vec3();
    var worldMatZ = new pc.Vec3();

    var frustumDiagonal = new pc.Vec3();
    var tempSphere = { center: null, radius: 0 };
    var meshPos;
    var visibleSceneAabb = new pc.BoundingBox();
    var boneTextureSize = [0, 0];
    var boneTexture, instancingData, modelMatrix, normalMatrix;

    var shadowMapCubeCache = {};
    var maxBlurSize = 25;

    var keyA, keyB;

    // The 8 points of the camera frustum transformed to light space
    var frustumPoints = [];
    for (var fp = 0; fp < 8; fp++) {
        frustumPoints.push(new pc.Vec3());
    }

    function _getFrustumPoints(camera, farClip, points) {
        var nearClip = camera._nearClip;
        var fov = camera._fov * Math.PI / 180.0;
        var aspect = camera._aspect;
        var projection = camera._projection;

        var x, y;
        if (projection === pc.PROJECTION_PERSPECTIVE) {
            y = Math.tan(fov / 2.0) * nearClip;
        } else {
            y = camera._orthoHeight;
        }
        x = y * aspect;

        points[0].x = x;
        points[0].y = -y;
        points[0].z = -nearClip;
        points[1].x = x;
        points[1].y = y;
        points[1].z = -nearClip;
        points[2].x = -x;
        points[2].y = y;
        points[2].z = -nearClip;
        points[3].x = -x;
        points[3].y = -y;
        points[3].z = -nearClip;

        if (projection === pc.PROJECTION_PERSPECTIVE) {
            y = Math.tan(fov / 2.0) * farClip;
            x = y * aspect;
        }
        points[4].x = x;
        points[4].y = -y;
        points[4].z = -farClip;
        points[5].x = x;
        points[5].y = y;
        points[5].z = -farClip;
        points[6].x = -x;
        points[6].y = y;
        points[6].z = -farClip;
        points[7].x = -x;
        points[7].y = -y;
        points[7].z = -farClip;

        return points;
    }

    var _sceneAABB_LS = [
        new pc.Vec3(), new pc.Vec3(), new pc.Vec3(), new pc.Vec3(),
        new pc.Vec3(), new pc.Vec3(), new pc.Vec3(), new pc.Vec3()
    ];

    function _getZFromAABBSimple(w2sc, aabbMin, aabbMax, lcamMinX, lcamMaxX, lcamMinY, lcamMaxY) {
        _sceneAABB_LS[0].x = _sceneAABB_LS[1].x = _sceneAABB_LS[2].x = _sceneAABB_LS[3].x = aabbMin.x;
        _sceneAABB_LS[1].y = _sceneAABB_LS[3].y = _sceneAABB_LS[7].y = _sceneAABB_LS[5].y = aabbMin.y;
        _sceneAABB_LS[2].z = _sceneAABB_LS[3].z = _sceneAABB_LS[6].z = _sceneAABB_LS[7].z = aabbMin.z;
        _sceneAABB_LS[4].x = _sceneAABB_LS[5].x = _sceneAABB_LS[6].x = _sceneAABB_LS[7].x = aabbMax.x;
        _sceneAABB_LS[0].y = _sceneAABB_LS[2].y = _sceneAABB_LS[4].y = _sceneAABB_LS[6].y = aabbMax.y;
        _sceneAABB_LS[0].z = _sceneAABB_LS[1].z = _sceneAABB_LS[4].z = _sceneAABB_LS[5].z = aabbMax.z;

        var minz = 9999999999;
        var maxz = -9999999999;
        var z;

        for ( var i = 0; i < 8; ++i ) {
            w2sc.transformPoint( _sceneAABB_LS[i], _sceneAABB_LS[i] );
            z = _sceneAABB_LS[i].z;
            if (z < minz) minz = z;
            if (z > maxz) maxz = z;
        }

        return { min: minz, max: maxz };
    }

    // SHADOW MAPPING SUPPORT FUNCTIONS

    function getShadowFormat(device, shadowType) {
        if (shadowType === pc.SHADOW_VSM32) {
            return pc.PIXELFORMAT_RGBA32F;
        } else if (shadowType === pc.SHADOW_VSM16) {
            return pc.PIXELFORMAT_RGBA16F;
        } else if (shadowType === pc.SHADOW_PCF5) {
            return pc.PIXELFORMAT_DEPTH;
        } else if (shadowType === pc.SHADOW_PCF3 && device.webgl2) {
            return pc.PIXELFORMAT_DEPTH;
        }
        return pc.PIXELFORMAT_R8_G8_B8_A8;
    }

    function getShadowFiltering(device, shadowType) {
        if (shadowType === pc.SHADOW_PCF3 && !device.webgl2) {
            return pc.FILTER_NEAREST;
        } else if (shadowType === pc.SHADOW_VSM32) {
            return device.extTextureFloatLinear ? pc.FILTER_LINEAR : pc.FILTER_NEAREST;
        } else if (shadowType === pc.SHADOW_VSM16) {
            return device.extTextureHalfFloatLinear ? pc.FILTER_LINEAR : pc.FILTER_NEAREST;
        }
        return pc.FILTER_LINEAR;
    }

    function createShadowMap(device, width, height, shadowType) {
        var format = getShadowFormat(device, shadowType);
        var filter = getShadowFiltering(device, shadowType);

        var shadowMap = new pc.Texture(device, {
            // #ifdef PROFILER
            profilerHint: pc.TEXHINT_SHADOWMAP,
            // #endif
            format: format,
            width: width,
            height: height,
            mipmaps: false,
            minFilter: filter,
            magFilter: filter,
            addressU: pc.ADDRESS_CLAMP_TO_EDGE,
            addressV: pc.ADDRESS_CLAMP_TO_EDGE
        });
        shadowMap.name = 'shadowmap';

        if (shadowType === pc.SHADOW_PCF5 || (shadowType === pc.SHADOW_PCF3 && device.webgl2)) {
            shadowMap.compareOnRead = true;
            shadowMap.compareFunc = pc.FUNC_LESS;
            // depthbuffer only
            return new pc.RenderTarget({
                depthBuffer: shadowMap
            });
        }

        // encoded rgba depth
        return new pc.RenderTarget({
            colorBuffer: shadowMap,
            depth: true
        });
    }

    function createShadowCubeMap(device, size) {
        var cubemap = new pc.Texture(device, {
            // #ifdef PROFILER
            profilerHint: pc.TEXHINT_SHADOWMAP,
            // #endif
            format: pc.PIXELFORMAT_R8_G8_B8_A8,
            width: size,
            height: size,
            cubemap: true,
            mipmaps: false,
            minFilter: pc.FILTER_NEAREST,
            magFilter: pc.FILTER_NEAREST,
            addressU: pc.ADDRESS_CLAMP_TO_EDGE,
            addressV: pc.ADDRESS_CLAMP_TO_EDGE
        });
        cubemap.name = 'shadowcube';

        var targets = [];
        var target;
        for (var i = 0; i < 6; i++) {
            target = new pc.RenderTarget({
                colorBuffer: cubemap,
                face: i,
                depth: true
            });
            targets.push(target);
        }
        return targets;
    }

    function gauss(x, sigma) {
        return Math.exp(-(x * x) / (2.0 * sigma * sigma));
    }

    function gaussWeights(kernelSize) {
        if (kernelSize > maxBlurSize) kernelSize = maxBlurSize;
        var sigma = (kernelSize - 1) / (2 * 3);
        var i, values, sum, halfWidth;

        halfWidth = (kernelSize - 1) * 0.5;
        values = new Array(kernelSize);
        sum = 0.0;
        for (i = 0; i < kernelSize; ++i) {
            values[i] = gauss(i - halfWidth, sigma);
            sum += values[i];
        }

        for (i = 0; i < kernelSize; ++i) {
            values[i] /= sum;
        }
        return values;
    }

    function createShadowCamera(device, shadowType, type) {
        // We don't need to clear the color buffer if we're rendering a depth map
        var flags = pc.CLEARFLAG_DEPTH;
        var hwPcf = shadowType === pc.SHADOW_PCF5 || (shadowType === pc.SHADOW_PCF3 && device.webgl2);
        if (type === pc.LIGHTTYPE_POINT) hwPcf = false;
        if (!hwPcf) flags |= pc.CLEARFLAG_COLOR;
        var shadowCam = new pc.Camera();

        if (shadowType >= pc.SHADOW_VSM8 && shadowType <= pc.SHADOW_VSM32) {
            shadowCam.clearColor[0] = 0;
            shadowCam.clearColor[1] = 0;
            shadowCam.clearColor[2] = 0;
            shadowCam.clearColor[3] = 0;
        } else {
            shadowCam.clearColor[0] = 1;
            shadowCam.clearColor[1] = 1;
            shadowCam.clearColor[2] = 1;
            shadowCam.clearColor[3] = 1;
        }

        shadowCam.clearDepth = 1;
        shadowCam.clearFlags = flags;
        shadowCam.clearStencil = null;

        shadowCam._node = new pc.GraphNode();

        return shadowCam;
    }

    function getShadowMapFromCache(device, res, mode, layer) {
        if (!layer) layer = 0;
        var id = layer * 10000 + res;
        var shadowBuffer = shadowMapCache[mode][id];
        if (!shadowBuffer) {
            shadowBuffer = createShadowMap(device, res, res, mode ? mode : pc.SHADOW_PCF3);
            shadowMapCache[mode][id] = shadowBuffer;
        }
        return shadowBuffer;
    }

    function createShadowBuffer(device, light) {
        var shadowBuffer;
        if (light._type === pc.LIGHTTYPE_POINT) {
            if (light._shadowType > pc.SHADOW_PCF3) light._shadowType = pc.SHADOW_PCF3; // no VSM or HW PCF point lights yet
            if (light._cacheShadowMap) {
                shadowBuffer = shadowMapCubeCache[light._shadowResolution];
                if (!shadowBuffer) {
                    shadowBuffer = createShadowCubeMap(device, light._shadowResolution);
                    shadowMapCubeCache[light._shadowResolution] = shadowBuffer;
                }
            } else {
                shadowBuffer = createShadowCubeMap(device, light._shadowResolution);
            }
            light._shadowCamera.renderTarget = shadowBuffer[0];
            light._shadowCubeMap = shadowBuffer;

        } else {

            if (light._cacheShadowMap) {
                shadowBuffer = getShadowMapFromCache(device, light._shadowResolution, light._shadowType);
            } else {
                shadowBuffer = createShadowMap(device, light._shadowResolution, light._shadowResolution, light._shadowType);
            }

            light._shadowCamera.renderTarget = shadowBuffer;
        }
        light._isCachedShadowMap = light._cacheShadowMap;
    }

    function getDepthKey(meshInstance) {
        var material = meshInstance.material;
        var x = meshInstance.skinInstance ? 10 : 0;
        var y = 0;
        if (material.opacityMap) {
            var opChan = material.opacityMapChannel;
            if (opChan) {
                y = opChanId[opChan];
            }
        }
        return x + y;
    }

    /**
     * @private
     * @constructor
     * @name pc.ForwardRenderer
     * @classdesc The forward renderer render scene objects.
     * @description Creates a new forward renderer object.
     * @param {pc.GraphicsDevice} graphicsDevice The graphics device used by the renderer.
     */
    function ForwardRenderer(graphicsDevice) {
        this.device = graphicsDevice;
        var device = this.device;

        this._shadowDrawCalls = 0;
        this._forwardDrawCalls = 0;
        this._skinDrawCalls = 0;
        this._camerasRendered = 0;
        this._materialSwitches = 0;
        this._shadowMapUpdates = 0;
        this._shadowMapTime = 0;
        this._depthMapTime = 0;
        this._forwardTime = 0;
        this._cullTime = 0;
        this._sortTime = 0;
        this._skinTime = 0;
        this._morphTime = 0;
        this._instancingTime = 0;

        // Shaders
        var library = device.getProgramLibrary();
        this.library = library;

        // Uniforms
        var scope = device.scope;
        this.projId = scope.resolve('matrix_projection');
        this.viewId = scope.resolve('matrix_view');
        this.viewId3 = scope.resolve('matrix_view3');
        this.viewInvId = scope.resolve('matrix_viewInverse');
        this.viewProjId = scope.resolve('matrix_viewProjection');
        this.viewPos = new Float32Array(3);
        this.viewPosId = scope.resolve('view_position');
        this.nearClipId = scope.resolve('camera_near');
        this.farClipId = scope.resolve('camera_far');
        this.cameraParamsId = scope.resolve('camera_params');
        this.shadowMapLightRadiusId = scope.resolve('light_radius');

        this.fogColorId = scope.resolve('fog_color');
        this.fogStartId = scope.resolve('fog_start');
        this.fogEndId = scope.resolve('fog_end');
        this.fogDensityId = scope.resolve('fog_density');

        this.modelMatrixId = scope.resolve('matrix_model');
        this.normalMatrixId = scope.resolve('matrix_normal');
        this.poseMatrixId = scope.resolve('matrix_pose[0]');
        this.boneTextureId = scope.resolve('texture_poseMap');
        this.boneTextureSizeId = scope.resolve('texture_poseMapSize');

        this.alphaTestId = scope.resolve('alpha_ref');
        this.opacityMapId = scope.resolve('texture_opacityMap');

        this.ambientId = scope.resolve("light_globalAmbient");
        this.exposureId = scope.resolve("exposure");
        this.skyboxIntensityId = scope.resolve("skyboxIntensity");
        this.lightColorId = [];
        this.lightDir = [];
        this.lightDirId = [];
        this.lightShadowMapId = [];
        this.lightShadowMatrixId = [];
        this.lightShadowParamsId = [];
        this.lightShadowMatrixVsId = [];
        this.lightShadowParamsVsId = [];
        this.lightDirVs = [];
        this.lightDirVsId = [];
        this.lightRadiusId = [];
        this.lightPos = [];
        this.lightPosId = [];
        this.lightInAngleId = [];
        this.lightOutAngleId = [];
        this.lightPosVsId = [];
        this.lightCookieId = [];
        this.lightCookieIntId = [];
        this.lightCookieMatrixId = [];
        this.lightCookieOffsetId = [];

        this.depthMapId = scope.resolve('uDepthMap');
        this.screenSizeId = scope.resolve('uScreenSize');
        this._screenSize = new Float32Array(4);

        this.sourceId = scope.resolve("source");
        this.pixelOffsetId = scope.resolve("pixelOffset");
        this.weightId = scope.resolve("weight[0]");
        var chunks = pc.shaderChunks;
        this.blurVsmShaderCode = [chunks.blurVSMPS, "#define GAUSS\n" + chunks.blurVSMPS];
        var packed = "#define PACKED\n";
        this.blurPackedVsmShaderCode = [packed + this.blurVsmShaderCode[0], packed + this.blurVsmShaderCode[1]];
        this.blurVsmShader = [{}, {}];
        this.blurPackedVsmShader = [{}, {}];
        this.blurVsmWeights = {};

        this.polygonOffsetId = scope.resolve("polygonOffset");
        this.polygonOffset = new Float32Array(2);

        this.fogColor = new Float32Array(3);
        this.ambientColor = new Float32Array(3);
    }

    function mat3FromMat4(m3, m4) {
        m3.data[0] = m4.data[0];
        m3.data[1] = m4.data[1];
        m3.data[2] = m4.data[2];

        m3.data[3] = m4.data[4];
        m3.data[4] = m4.data[5];
        m3.data[5] = m4.data[6];

        m3.data[6] = m4.data[8];
        m3.data[7] = m4.data[9];
        m3.data[8] = m4.data[10];
    }

    Object.assign(ForwardRenderer.prototype, {

        sortCompare: function (drawCallA, drawCallB) {
            if (drawCallA.layer === drawCallB.layer) {
                if (drawCallA.drawOrder && drawCallB.drawOrder) {
                    return drawCallA.drawOrder - drawCallB.drawOrder;
                } else if (drawCallA.zdist && drawCallB.zdist) {
                    return drawCallB.zdist - drawCallA.zdist; // back to front
                } else if (drawCallA.zdist2 && drawCallB.zdist2) {
                    return drawCallA.zdist2 - drawCallB.zdist2; // front to back
                }
            }

            return drawCallB._key[pc.SORTKEY_FORWARD] - drawCallA._key[pc.SORTKEY_FORWARD];
        },

        sortCompareMesh: function (drawCallA, drawCallB) {
            if (drawCallA.layer === drawCallB.layer) {
                if (drawCallA.drawOrder && drawCallB.drawOrder) {
                    return drawCallA.drawOrder - drawCallB.drawOrder;
                } else if (drawCallA.zdist && drawCallB.zdist) {
                    return drawCallB.zdist - drawCallA.zdist; // back to front
                }
            }

            keyA = drawCallA._key[pc.SORTKEY_FORWARD];
            keyB = drawCallB._key[pc.SORTKEY_FORWARD];

            if (keyA === keyB && drawCallA.mesh && drawCallB.mesh) {
                return drawCallB.mesh.id - drawCallA.mesh.id;
            }

            return keyB - keyA;
        },

        depthSortCompare: function (drawCallA, drawCallB) {
            keyA = drawCallA._key[pc.SORTKEY_DEPTH];
            keyB = drawCallB._key[pc.SORTKEY_DEPTH];

            if (keyA === keyB && drawCallA.mesh && drawCallB.mesh) {
                return drawCallB.mesh.id - drawCallA.mesh.id;
            }

            return keyB - keyA;
        },

        lightCompare: function (lightA, lightB) {
            return lightA.key - lightB.key;
        },

        _isVisible: function (camera, meshInstance) {
            if (!meshInstance.visible) return false;

            // custom visibility method on MeshInstance
            if (meshInstance.isVisibleFunc) {
                return meshInstance.isVisibleFunc(camera);
            }

            meshPos = meshInstance.aabb.center;
            if (meshInstance._aabb._radiusVer !== meshInstance._aabbVer) {
                meshInstance._aabb._radius = meshInstance._aabb.halfExtents.length();
                meshInstance._aabb._radiusVer = meshInstance._aabbVer;
            }

            tempSphere.radius = meshInstance._aabb._radius;
            tempSphere.center = meshPos;

            return camera.frustum.containsSphere(tempSphere);
        },

        getShadowCamera: function (device, light) {
            var shadowCam = light._shadowCamera;
            var shadowBuffer;

            if (shadowCam === null) {
                shadowCam = light._shadowCamera = createShadowCamera(device, light._shadowType, light._type);
                createShadowBuffer(device, light);
            } else {
                shadowBuffer = shadowCam.renderTarget;
                if ((shadowBuffer.width !== light._shadowResolution) || (shadowBuffer.height !== light._shadowResolution)) {
                    createShadowBuffer(device, light);
                }
            }

            return shadowCam;
        },

        updateCameraFrustum: function (camera) {
            if (camera.vrDisplay && camera.vrDisplay.presenting) {
                projMat = camera.vrDisplay.combinedProj;
                var parent = camera._node.parent;
                if (parent) {
                    viewMat.copy(parent.getWorldTransform()).mul(camera.vrDisplay.combinedViewInv).invert();
                } else {
                    viewMat.copy(camera.vrDisplay.combinedView);
                }
                viewInvMat.copy(viewMat).invert();
                this.viewInvId.setValue(viewInvMat.data);
                camera.frustum.update(projMat, viewMat);
                return;
            }

            projMat = camera.getProjectionMatrix();
            if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER);

            if (camera.overrideCalculateTransform) {
                camera.calculateTransform(viewInvMat, pc.VIEW_CENTER);
            } else {
                var pos = camera._node.getPosition();
                var rot = camera._node.getRotation();
                viewInvMat.setTRS(pos, rot, pc.Vec3.ONE);
                this.viewInvId.setValue(viewInvMat.data);
            }
            viewMat.copy(viewInvMat).invert();

            camera.frustum.update(projMat, viewMat);
        },

        // make sure colorWrite is set to true to all channels, if you want to fully clear the target
        setCamera: function (camera, target, clear, cullBorder) {
            var vrDisplay = camera.vrDisplay;
            if (!vrDisplay || !vrDisplay.presenting) {
                // Projection Matrix
                projMat = camera.getProjectionMatrix();
                if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER);
                this.projId.setValue(projMat.data);

                // ViewInverse Matrix
                if (camera.overrideCalculateTransform) {
                    camera.calculateTransform(viewInvMat, pc.VIEW_CENTER);
                } else {
                    var pos = camera._node.getPosition();
                    var rot = camera._node.getRotation();
                    viewInvMat.setTRS(pos, rot, pc.Vec3.ONE);
                }
                this.viewInvId.setValue(viewInvMat.data);

                // View Matrix
                viewMat.copy(viewInvMat).invert();
                this.viewId.setValue(viewMat.data);

                // View 3x3
                mat3FromMat4(viewMat3, viewMat);
                this.viewId3.setValue(viewMat3.data);

                // ViewProjection Matrix
                viewProjMat.mul2(projMat, viewMat);
                this.viewProjId.setValue(viewProjMat.data);

                // View Position (world space)
                var cameraPos = camera._node.getPosition();
                this.viewPos[0] = cameraPos.x;
                this.viewPos[1] = cameraPos.y;
                this.viewPos[2] = cameraPos.z;
                this.viewPosId.setValue(this.viewPos);

                camera.frustum.update(projMat, viewMat);
            } else {
                // Projection LR
                projL = vrDisplay.leftProj;
                projR = vrDisplay.rightProj;
                projMat = vrDisplay.combinedProj;
                if (camera.overrideCalculateProjection) {
                    camera.calculateProjection(projL, pc.VIEW_LEFT);
                    camera.calculateProjection(projR, pc.VIEW_RIGHT);
                    camera.calculateProjection(projMat, pc.VIEW_CENTER);
                }

                if (camera.overrideCalculateTransform) {
                    camera.calculateTransform(viewInvL, pc.VIEW_LEFT);
                    camera.calculateTransform(viewInvR, pc.VIEW_RIGHT);
                    camera.calculateTransform(viewInvMat, pc.VIEW_CENTER);
                    viewL.copy(viewInvL).invert();
                    viewR.copy(viewInvR).invert();
                    viewMat.copy(viewInvMat).invert();
                } else {
                    var parent = camera._node.parent;
                    if (parent) {
                        var transform = parent.getWorldTransform();

                        // ViewInverse LR (parent)
                        viewInvL.mul2(transform, vrDisplay.leftViewInv);
                        viewInvR.mul2(transform, vrDisplay.rightViewInv);

                        // View LR (parent)
                        viewL.copy(viewInvL).invert();
                        viewR.copy(viewInvR).invert();

                        // Combined view (parent)
                        viewMat.copy(parent.getWorldTransform()).mul(vrDisplay.combinedViewInv).invert();
                    } else {
                        // ViewInverse LR
                        viewInvL.copy(vrDisplay.leftViewInv);
                        viewInvR.copy(vrDisplay.rightViewInv);

                        // View LR
                        viewL.copy(vrDisplay.leftView);
                        viewR.copy(vrDisplay.rightView);

                        // Combined view
                        viewMat.copy(vrDisplay.combinedView);
                    }
                }

                // View 3x3 LR
                mat3FromMat4(viewMat3L, viewL);
                mat3FromMat4(viewMat3R, viewR);

                // ViewProjection LR
                viewProjMatL.mul2(projL, viewL);
                viewProjMatR.mul2(projR, viewR);

                // View Position LR
                viewPosL.x = viewInvL.data[12];
                viewPosL.y = viewInvL.data[13];
                viewPosL.z = viewInvL.data[14];

                viewPosR.x = viewInvR.data[12];
                viewPosR.y = viewInvR.data[13];
                viewPosR.z = viewInvR.data[14];

                camera.frustum.update(projMat, viewMat);
            }

            // Near and far clip values
            this.nearClipId.setValue(camera._nearClip);
            this.farClipId.setValue(camera._farClip);
            this.cameraParamsId.setValue(camera._shaderParams);

            var device = this.device;
            device.setRenderTarget(target);
            device.updateBegin();

            var rect = camera.getRect();
            var pixelWidth = target ? target.width : device.width;
            var pixelHeight = target ? target.height : device.height;
            var x = Math.floor(rect.x * pixelWidth);
            var y = Math.floor(rect.y * pixelHeight);
            var w = Math.floor(rect.width * pixelWidth);
            var h = Math.floor(rect.height * pixelHeight);
            device.setViewport(x, y, w, h);
            device.setScissor(x, y, w, h);
            if (clear) device.clear(camera._clearOptions); // clear full RT

            rect = camera._scissorRect;
            x = Math.floor(rect.x * pixelWidth);
            y = Math.floor(rect.y * pixelHeight);
            w = Math.floor(rect.width * pixelWidth);
            h = Math.floor(rect.height * pixelHeight);
            device.setScissor(x, y, w, h);

            if (cullBorder) device.setScissor(1, 1, pixelWidth - 2, pixelHeight - 2); // optionally clip borders when rendering
        },

        dispatchGlobalLights: function (scene) {
            var i;
            this.mainLight = -1;

            this.ambientColor[0] = scene.ambientLight.r;
            this.ambientColor[1] = scene.ambientLight.g;
            this.ambientColor[2] = scene.ambientLight.b;
            if (scene.gammaCorrection) {
                for (i = 0; i < 3; i++) {
                    this.ambientColor[i] = Math.pow(this.ambientColor[i], 2.2);
                }
            }
            this.ambientId.setValue(this.ambientColor);
            this.exposureId.setValue(scene.exposure);
            if (scene.skyboxModel) this.skyboxIntensityId.setValue(scene.skyboxIntensity);
        },

        _resolveLight: function (scope, i) {
            var light = "light" + i;
            this.lightColorId[i] = scope.resolve(light + "_color");
            this.lightDir[i] = new Float32Array(3);
            this.lightDirId[i] = scope.resolve(light + "_direction");
            this.lightShadowMapId[i] = scope.resolve(light + "_shadowMap");
            this.lightShadowMatrixId[i] = scope.resolve(light + "_shadowMatrix");
            this.lightShadowParamsId[i] = scope.resolve(light + "_shadowParams");
            this.lightShadowMatrixVsId[i] = scope.resolve(light + "_shadowMatrixVS");
            this.lightShadowParamsVsId[i] = scope.resolve(light + "_shadowParamsVS");
            this.lightDirVs[i] = new Float32Array(3);
            this.lightDirVsId[i] = scope.resolve(light + "_directionVS");
            this.lightRadiusId[i] = scope.resolve(light + "_radius");
            this.lightPos[i] = new Float32Array(3);
            this.lightPosId[i] = scope.resolve(light + "_position");
            this.lightInAngleId[i] = scope.resolve(light + "_innerConeAngle");
            this.lightOutAngleId[i] = scope.resolve(light + "_outerConeAngle");
            this.lightPosVsId[i] = scope.resolve(light + "_positionVS");
            this.lightCookieId[i] = scope.resolve(light + "_cookie");
            this.lightCookieIntId[i] = scope.resolve(light + "_cookieIntensity");
            this.lightCookieMatrixId[i] = scope.resolve(light + "_cookieMatrix");
            this.lightCookieOffsetId[i] = scope.resolve(light + "_cookieOffset");
        },

        dispatchDirectLights: function (dirs, scene, mask) {
            var numDirs = dirs.length;
            var i;
            var directional, wtm;
            var cnt = 0;
            this.mainLight = -1;

            var scope = this.device.scope;

            for (i = 0; i < numDirs; i++) {
                if (!(dirs[i]._mask & mask)) continue;

                directional = dirs[i];
                wtm = directional._node.getWorldTransform();

                if (!this.lightColorId[cnt]) {
                    this._resolveLight(scope, cnt);
                }

                this.lightColorId[cnt].setValue(scene.gammaCorrection ? directional._linearFinalColor : directional._finalColor);

                // Directionals shine down the negative Y axis
                wtm.getY(directional._direction).scale(-1);
                directional._direction.normalize();
                this.lightDir[cnt][0] = directional._direction.x;
                this.lightDir[cnt][1] = directional._direction.y;
                this.lightDir[cnt][2] = directional._direction.z;
                this.lightDirId[cnt].setValue(this.lightDir[cnt]);

                if (directional.castShadows) {
                    var shadowMap = directional._isPcf && this.device.webgl2 ?
                        directional._shadowCamera.renderTarget.depthBuffer :
                        directional._shadowCamera.renderTarget.colorBuffer;

                    // make bias dependent on far plane because it's not constant for direct light
                    var bias;
                    if (directional._isVsm) {
                        bias = -0.00001 * 20;
                    } else {
                        bias = (directional.shadowBias / directional._shadowCamera._farClip) * 100;
                        if (!this.device.webgl2 && this.device.extStandardDerivatives) bias *= -100;
                    }
                    var normalBias = directional._isVsm ?
                        directional.vsmBias / (directional._shadowCamera._farClip / 7.0) :
                        directional._normalOffsetBias;

                    this.lightShadowMapId[cnt].setValue(shadowMap);
                    this.lightShadowMatrixId[cnt].setValue(directional._shadowMatrix.data);
                    var params = directional._rendererParams;
                    if (params.length !== 3) params.length = 3;
                    params[0] = directional._shadowResolution;
                    params[1] = normalBias;
                    params[2] = bias;
                    this.lightShadowParamsId[cnt].setValue(params);
                    if (this.mainLight < 0) {
                        this.lightShadowMatrixVsId[cnt].setValue(directional._shadowMatrix.data);
                        this.lightShadowParamsVsId[cnt].setValue(params);
                        directional._direction.normalize();
                        this.lightDirVs[cnt][0] = directional._direction.x;
                        this.lightDirVs[cnt][1] = directional._direction.y;
                        this.lightDirVs[cnt][2] = directional._direction.z;
                        this.lightDirVsId[cnt].setValue(this.lightDirVs[cnt]);
                        this.mainLight = i;
                    }
                }
                cnt++;
            }
            return cnt;
        },

        dispatchPointLight: function (scene, scope, point, cnt) {
            var wtm = point._node.getWorldTransform();

            if (!this.lightColorId[cnt]) {
                this._resolveLight(scope, cnt);
            }

            this.lightRadiusId[cnt].setValue(point.attenuationEnd);
            this.lightColorId[cnt].setValue(scene.gammaCorrection ? point._linearFinalColor : point._finalColor);
            wtm.getTranslation(point._position);
            this.lightPos[cnt][0] = point._position.x;
            this.lightPos[cnt][1] = point._position.y;
            this.lightPos[cnt][2] = point._position.z;
            this.lightPosId[cnt].setValue(this.lightPos[cnt]);

            if (point.castShadows) {
                var shadowMap = point._shadowCamera.renderTarget.colorBuffer;
                this.lightShadowMapId[cnt].setValue(shadowMap);
                var params = point._rendererParams;
                if (params.length !== 4) params.length = 4;
                params[0] = point._shadowResolution;
                params[1] = point._normalOffsetBias;
                params[2] = point.shadowBias;
                params[3] = 1.0 / point.attenuationEnd;
                this.lightShadowParamsId[cnt].setValue(params);
            }
            if (point._cookie) {
                this.lightCookieId[cnt].setValue(point._cookie);
                this.lightShadowMatrixId[cnt].setValue(wtm.data);
                this.lightCookieIntId[cnt].setValue(point.cookieIntensity);
            }
        },

        dispatchSpotLight: function (scene, scope, spot, cnt) {
            var wtm = spot._node.getWorldTransform();

            if (!this.lightColorId[cnt]) {
                this._resolveLight(scope, cnt);
            }

            this.lightInAngleId[cnt].setValue(spot._innerConeAngleCos);
            this.lightOutAngleId[cnt].setValue(spot._outerConeAngleCos);
            this.lightRadiusId[cnt].setValue(spot.attenuationEnd);
            this.lightColorId[cnt].setValue(scene.gammaCorrection ? spot._linearFinalColor : spot._finalColor);
            wtm.getTranslation(spot._position);
            this.lightPos[cnt][0] = spot._position.x;
            this.lightPos[cnt][1] = spot._position.y;
            this.lightPos[cnt][2] = spot._position.z;
            this.lightPosId[cnt].setValue(this.lightPos[cnt]);
            // Spots shine down the negative Y axis
            wtm.getY(spot._direction).scale(-1);
            spot._direction.normalize();
            this.lightDir[cnt][0] = spot._direction.x;
            this.lightDir[cnt][1] = spot._direction.y;
            this.lightDir[cnt][2] = spot._direction.z;
            this.lightDirId[cnt].setValue(this.lightDir[cnt]);

            if (spot.castShadows) {
                var bias;
                if (spot._isVsm) {
                    bias = -0.00001 * 20;
                } else {
                    bias = spot.shadowBias * 20; // approx remap from old bias values
                    if (!this.device.webgl2 && this.device.extStandardDerivatives) bias *= -100;
                }
                var normalBias = spot._isVsm ?
                    spot.vsmBias / (spot.attenuationEnd / 7.0) :
                    spot._normalOffsetBias;

                var shadowMap = spot._isPcf && this.device.webgl2 ?
                    spot._shadowCamera.renderTarget.depthBuffer :
                    spot._shadowCamera.renderTarget.colorBuffer;
                this.lightShadowMapId[cnt].setValue(shadowMap);
                this.lightShadowMatrixId[cnt].setValue(spot._shadowMatrix.data);
                var params = spot._rendererParams;
                if (params.length !== 4) params.length = 4;
                params[0] = spot._shadowResolution;
                params[1] = normalBias;
                params[2] = bias;
                params[3] = 1.0 / spot.attenuationEnd;
                this.lightShadowParamsId[cnt].setValue(params);
            }
            if (spot._cookie) {
                this.lightCookieId[cnt].setValue(spot._cookie);
                if (!spot.castShadows) {
                    var shadowCam = this.getShadowCamera(this.device, spot);
                    var shadowCamNode = shadowCam._node;

                    shadowCamNode.setPosition(spot._node.getPosition());
                    shadowCamNode.setRotation(spot._node.getRotation());
                    shadowCamNode.rotateLocal(-90, 0, 0);

                    shadowCam.projection = pc.PROJECTION_PERSPECTIVE;
                    shadowCam.aspectRatio = 1;
                    shadowCam.fov = spot._outerConeAngle * 2;

                    shadowCamView.setTRS(shadowCamNode.getPosition(), shadowCamNode.getRotation(), pc.Vec3.ONE).invert();
                    shadowCamViewProj.mul2(shadowCam.getProjectionMatrix(), shadowCamView);
                    spot._shadowMatrix.mul2(scaleShift, shadowCamViewProj);
                }
                this.lightShadowMatrixId[cnt].setValue(spot._shadowMatrix.data);
                this.lightCookieIntId[cnt].setValue(spot.cookieIntensity);
                if (spot._cookieTransform) {
                    spot._cookieTransformUniform[0] = spot._cookieTransform.x;
                    spot._cookieTransformUniform[1] = spot._cookieTransform.y;
                    spot._cookieTransformUniform[2] = spot._cookieTransform.z;
                    spot._cookieTransformUniform[3] = spot._cookieTransform.w;
                    this.lightCookieMatrixId[cnt].setValue(spot._cookieTransformUniform);
                    spot._cookieOffsetUniform[0] = spot._cookieOffset.x;
                    spot._cookieOffsetUniform[1] = spot._cookieOffset.y;
                    this.lightCookieOffsetId[cnt].setValue(spot._cookieOffsetUniform);
                }
            }
        },

        dispatchLocalLights: function (sortedLights, scene, mask, usedDirLights, staticLightList) {
            var i;
            var point, spot;

            var pnts = sortedLights[pc.LIGHTTYPE_POINT];
            var spts = sortedLights[pc.LIGHTTYPE_SPOT];

            var numDirs = usedDirLights;
            var numPnts = pnts.length;
            var numSpts = spts.length;
            var cnt = numDirs;

            var scope = this.device.scope;

            for (i = 0; i < numPnts; i++) {
                point = pnts[i];
                if (!(point._mask & mask)) continue;
                if (point.isStatic) continue;
                this.dispatchPointLight(scene, scope, point, cnt);
                cnt++;
            }

            var staticId = 0;
            if (staticLightList) {
                point = staticLightList[staticId];
                while (point && point._type === pc.LIGHTTYPE_POINT) {
                    this.dispatchPointLight(scene, scope, point, cnt);
                    cnt++;
                    staticId++;
                    point = staticLightList[staticId];
                }
            }

            for (i = 0; i < numSpts; i++) {
                spot = spts[i];
                if (!(spot._mask & mask)) continue;
                if (spot.isStatic) continue;
                this.dispatchSpotLight(scene, scope, spot, cnt);
                cnt++;
            }

            if (staticLightList) {
                spot = staticLightList[staticId];
                while (spot && spot._type === pc.LIGHTTYPE_SPOT) {
                    this.dispatchSpotLight(scene, scope, spot, cnt);
                    cnt++;
                    staticId++;
                    spot = staticLightList[staticId];
                }
            }
        },

        cull: function (camera, drawCalls, visibleList) {
            // #ifdef PROFILER
            var cullTime = pc.now();
            var numDrawCallsCulled = 0;
            // #endif

            var visibleLength = 0;
            var i, drawCall, visible;
            var drawCallsCount = drawCalls.length;

            var cullingMask = camera.cullingMask || 0xFFFFFFFF; // if missing assume camera's default value

            if (!camera.frustumCulling) {
                for (i = 0; i < drawCallsCount; i++) {
                    // need to copy array anyway because sorting will happen and it'll break original draw call order assumption
                    drawCall = drawCalls[i];
                    if (!drawCall.visible && !drawCall.command) continue;

                    // if the object's mask AND the camera's cullingMask is zero then the game object will be invisible from the camera
                    if (drawCall.mask && (drawCall.mask & cullingMask) === 0) continue;

                    visibleList[visibleLength] = drawCall;
                    visibleLength++;
                    drawCall.visibleThisFrame = true;
                }
                return visibleLength;
            }

            for (i = 0; i < drawCallsCount; i++) {
                drawCall = drawCalls[i];
                if (!drawCall.command) {
                    if (!drawCall.visible) continue; // use visible property to quickly hide/show meshInstances
                    visible = true;

                    // if the object's mask AND the camera's cullingMask is zero then the game object will be invisible from the camera
                    if (drawCall.mask && (drawCall.mask & cullingMask) === 0) continue;

                    if (drawCall.cull) {
                        visible = this._isVisible(camera, drawCall);
                        // #ifdef PROFILER
                        numDrawCallsCulled++;
                        // #endif
                    }

                    if (visible) {
                        visibleList[visibleLength] = drawCall;
                        visibleLength++;
                        drawCall.visibleThisFrame = true;
                    }
                } else {
                    visibleList[visibleLength] = drawCall;
                    visibleLength++;
                    drawCall.visibleThisFrame = true;
                }
            }

            // #ifdef PROFILER
            this._cullTime += pc.now() - cullTime;
            this._numDrawCallsCulled += numDrawCallsCulled;
            // #endif

            return visibleLength;
        },

        cullLights: function (camera, lights) {
            var i, light, type;
            for (i = 0; i < lights.length; i++) {
                light = lights[i];
                type = light._type;
                if (light.castShadows && light._enabled && light.shadowUpdateMode !== pc.SHADOWUPDATE_NONE) {
                    if (type !== pc.LIGHTTYPE_DIRECTIONAL) {
                        light.getBoundingSphere(tempSphere);
                        if (!camera.frustum.containsSphere(tempSphere)) continue;
                        light.visibleThisFrame = true;
                    }
                }
            }
        },

        updateCpuSkinMatrices: function (drawCalls) {
            var drawCallsCount = drawCalls.length;
            if (drawCallsCount === 0) return;

            // #ifdef PROFILER
            var skinTime = pc.now();
            // #endif

            var i, skin;
            for (i = 0; i < drawCallsCount; i++) {
                skin = drawCalls[i].skinInstance;
                if (skin) {
                    skin.updateMatrices(drawCalls[i].node);
                    skin._dirty = true;
                }
            }

            // #ifdef PROFILER
            this._skinTime += pc.now() - skinTime;
            // #endif
        },

        updateGpuSkinMatrices: function (drawCalls) {
            // #ifdef PROFILER
            var skinTime = pc.now();
            // #endif

            var i, skin;
            var drawCallsCount = drawCalls.length;
            for (i = 0; i < drawCallsCount; i++) {
                if (!drawCalls[i].visibleThisFrame) continue;
                skin = drawCalls[i].skinInstance;
                if (skin) {
                    if (skin._dirty) {
                        skin.updateMatrixPalette();
                        skin._dirty = false;
                    }
                }
            }

            // #ifdef PROFILER
            this._skinTime += pc.now() - skinTime;
            // #endif
        },

        updateMorphedBounds: function (drawCalls) {
            // #ifdef PROFILER
            var morphTime = pc.now();
            // #endif

            var i, morph;
            var drawCallsCount = drawCalls.length;
            for (i = 0; i < drawCallsCount; i++) {
                morph = drawCalls[i].morphInstance;
                if (morph && morph._dirty) {
                    morph.updateBounds(drawCalls[i].mesh);
                }
            }
            // #ifdef PROFILER
            this._morphTime += pc.now() - morphTime;
            // #endif
        },

        updateMorphing: function (drawCalls) {
            // #ifdef PROFILER
            var morphTime = pc.now();
            // #endif

            var i, morph;
            var drawCallsCount = drawCalls.length;
            for (i = 0; i < drawCallsCount; i++) {
                if (!drawCalls[i].visibleThisFrame) continue;
                morph = drawCalls[i].morphInstance;
                if (morph && morph._dirty) {
                    morph.update(drawCalls[i].mesh);
                    morph._dirty = false;
                }
            }
            // #ifdef PROFILER
            this._morphTime += pc.now() - morphTime;
            // #endif
        },

        setBaseConstants: function (device, material) {
            // Cull mode
            device.setCullMode(material.cull);
            // Alpha test
            if (material.opacityMap) {
                this.opacityMapId.setValue(material.opacityMap);
                this.alphaTestId.setValue(material.alphaTest);
            }
        },

        setSkinning: function (device, meshInstance, material) {
            if (meshInstance.skinInstance) {
                this._skinDrawCalls++;
                if (device.supportsBoneTextures) {
                    boneTexture = meshInstance.skinInstance.boneTexture;
                    this.boneTextureId.setValue(boneTexture);
                    boneTextureSize[0] = boneTexture.width;
                    boneTextureSize[1] = boneTexture.height;
                    this.boneTextureSizeId.setValue(boneTextureSize);
                } else {
                    this.poseMatrixId.setValue(meshInstance.skinInstance.matrixPalette);
                }
            }
        },

        drawInstance: function (device, meshInstance, mesh, style, normal) {
            instancingData = meshInstance.instancingData;
            if (instancingData) {
                this._instancedDrawCalls++;
                this._removedByInstancing += instancingData.count;
                device.setVertexBuffer(instancingData._buffer, 1, instancingData.offset);
                device.draw(mesh.primitive[style], instancingData.count);
                if (instancingData._buffer === pc._autoInstanceBuffer) {
                    meshInstance.instancingData = null;
                    return instancingData.count - 1;
                }
            } else {
                modelMatrix = meshInstance.node.worldTransform;
                this.modelMatrixId.setValue(modelMatrix.data);

                if (normal) {
                    normalMatrix = meshInstance.node.normalMatrix;
                    if (meshInstance.node._dirtyNormal) {
                        modelMatrix.invertTo3x3(normalMatrix);
                        normalMatrix.transpose();
                        meshInstance.node._dirtyNormal = false;
                    }
                    this.normalMatrixId.setValue(normalMatrix.data);
                }

                device.draw(mesh.primitive[style]);
                return 0;
            }
        },

        // used for stereo
        drawInstance2: function (device, meshInstance, mesh, style) {
            instancingData = meshInstance.instancingData;
            if (instancingData) {
                this._instancedDrawCalls++;
                this._removedByInstancing += instancingData.count;
                device.setVertexBuffer(instancingData._buffer, 1, instancingData.offset);
                device.draw(mesh.primitive[style], instancingData.count);
                if (instancingData._buffer === pc._autoInstanceBuffer) {
                    meshInstance.instancingData = null;
                    return instancingData.count - 1;
                }
            } else {
                // matrices are already set
                device.draw(mesh.primitive[style]);
                return 0;
            }
        },

        renderShadows: function (lights, cameraPass) {
            var device = this.device;
            // #ifdef PROFILER
            var shadowMapStartTime = pc.now();
            // #endif
            var i, j, light, shadowShader, type, shadowCam, shadowCamNode, pass, passes, shadowType, smode;
            var numInstances;
            var meshInstance, mesh, material;
            var style;
            var settings;
            var visibleList, visibleLength;

            var passFlag = 1 << pc.SHADER_SHADOW;
            var paramName, parameter, parameters;

            for (i = 0; i < lights.length; i++) {
                light = lights[i];
                type = light._type;

                if (!light.castShadows || !light._enabled) continue;

                if (!light._shadowCamera) {
                    this.getShadowCamera(device, light); // fix accessing non-existing shadow map/camera when the light was created/applied, but shadowmap was never initialized
                }

                if (light.shadowUpdateMode !== pc.SHADOWUPDATE_NONE && light.visibleThisFrame) {
                    var cameraPos;
                    shadowCam = this.getShadowCamera(device, light);
                    shadowCamNode = shadowCam._node;
                    pass = 0;
                    passes = 1;

                    if (type === pc.LIGHTTYPE_DIRECTIONAL) {
                        if (light._visibleLength[cameraPass] < 0) continue; // prevent light from rendering more than once for this camera
                        settings = light._visibleCameraSettings[cameraPass];
                        shadowCamNode.setPosition(settings.x, settings.y, settings.z);
                        shadowCam.orthoHeight = settings.orthoHeight;
                        shadowCam.farClip = settings.farClip;
                        pass = cameraPass;

                    } else if (type === pc.LIGHTTYPE_SPOT) {
                        cameraPos = shadowCamNode.getPosition();
                        this.viewPos[0] = cameraPos.x;
                        this.viewPos[1] = cameraPos.y;
                        this.viewPos[2] = cameraPos.z;
                        this.viewPosId.setValue(this.viewPos);
                        this.shadowMapLightRadiusId.setValue(light.attenuationEnd);

                    } else if (type === pc.LIGHTTYPE_POINT) {
                        cameraPos = shadowCamNode.getPosition();
                        this.viewPos[0] = cameraPos.x;
                        this.viewPos[1] = cameraPos.y;
                        this.viewPos[2] = cameraPos.z;
                        this.viewPosId.setValue(this.viewPos);
                        this.shadowMapLightRadiusId.setValue(light.attenuationEnd);
                        passes = 6;

                    }

                    if (type !== pc.LIGHTTYPE_POINT) {
                        shadowCamView.setTRS(shadowCamNode.getPosition(), shadowCamNode.getRotation(), pc.Vec3.ONE).invert();
                        shadowCamViewProj.mul2(shadowCam.getProjectionMatrix(), shadowCamView);
                        light._shadowMatrix.mul2(scaleShift, shadowCamViewProj);
                    }

                    if (device.webgl2) {
                        if (type === pc.LIGHTTYPE_POINT) {
                            device.setDepthBias(false);
                        } else {
                            device.setDepthBias(true);
                            device.setDepthBiasValues(light.shadowBias * -1000.0, light.shadowBias * -1000.0);
                        }
                    } else if (device.extStandardDerivatives) {
                        if (type === pc.LIGHTTYPE_POINT) {
                            this.polygonOffset[0] = 0;
                            this.polygonOffset[1] = 0;
                            this.polygonOffsetId.setValue(this.polygonOffset);
                        } else {
                            this.polygonOffset[0] = light.shadowBias * -1000.0;
                            this.polygonOffset[1] = light.shadowBias * -1000.0;
                            this.polygonOffsetId.setValue(this.polygonOffset);
                        }
                    }

                    if (light.shadowUpdateMode === pc.SHADOWUPDATE_THISFRAME) light.shadowUpdateMode = pc.SHADOWUPDATE_NONE;

                    this._shadowMapUpdates += passes;

                    // Set standard shadowmap states
                    device.setBlending(false);
                    device.setDepthWrite(true);
                    device.setDepthTest(true);
                    if (light._isPcf && device.webgl2 && type !== pc.LIGHTTYPE_POINT) {
                        device.setColorWrite(false, false, false, false);
                    } else {
                        device.setColorWrite(true, true, true, true);
                    }

                    if (pass) {
                        passes = pass + 1; // predefined single pass
                    } else {
                        pass = 0; // point light passes
                    }

                    while (pass < passes) {
                        if (type === pc.LIGHTTYPE_POINT) {
                            shadowCamNode.setRotation(pointLightRotations[pass]);
                            shadowCam.renderTarget = light._shadowCubeMap[pass];
                        }

                        this.setCamera(shadowCam, shadowCam.renderTarget, true, type !== pc.LIGHTTYPE_POINT);

                        visibleList = light._visibleList[pass];
                        visibleLength = light._visibleLength[pass];

                        // Sort shadow casters
                        shadowType = light._shadowType;
                        smode = shadowType + type * numShadowModes;

                        // Render
                        for (j = 0, numInstances = visibleLength; j < numInstances; j++) {
                            meshInstance = visibleList[j];
                            mesh = meshInstance.mesh;
                            material = meshInstance.material;

                            // set basic material states/parameters
                            this.setBaseConstants(device, material);
                            this.setSkinning(device, meshInstance, material);

                            if (material.dirty) {
                                material.updateUniforms();
                                material.dirty = false;
                            }

                            if (material.chunks) {
                                // Uniforms I (shadow): material
                                parameters = material.parameters;
                                for (paramName in parameters) {
                                    parameter = parameters[paramName];
                                    if (parameter.passFlags & passFlag) {
                                        if (!parameter.scopeId) {
                                            parameter.scopeId = device.scope.resolve(paramName);
                                        }
                                        parameter.scopeId.setValue(parameter.data);
                                    }
                                }
                                this.setCullMode(true, false, meshInstance);

                                // Uniforms II (shadow): meshInstance overrides
                                parameters = meshInstance.parameters;
                                for (paramName in parameters) {
                                    parameter = parameters[paramName];
                                    if (parameter.passFlags & passFlag) {
                                        if (!parameter.scopeId) {
                                            parameter.scopeId = device.scope.resolve(paramName);
                                        }
                                        parameter.scopeId.setValue(parameter.data);
                                    }
                                }
                            }

                            // set shader
                            shadowShader = meshInstance._shader[pc.SHADER_SHADOW + smode];
                            if (!shadowShader) {
                                this.updateShader(meshInstance, meshInstance._shaderDefs, null, pc.SHADER_SHADOW + smode);
                                shadowShader = meshInstance._shader[pc.SHADER_SHADOW + smode];
                                meshInstance._key[pc.SORTKEY_DEPTH] = getDepthKey(meshInstance);
                            }
                            device.setShader(shadowShader);
                            // set buffers
                            style = meshInstance.renderStyle;
                            device.setVertexBuffer((meshInstance.morphInstance && meshInstance.morphInstance._vertexBuffer) ?
                                meshInstance.morphInstance._vertexBuffer : mesh.vertexBuffer, 0);
                            device.setIndexBuffer(mesh.indexBuffer[style]);
                            // draw
                            j += this.drawInstance(device, meshInstance, mesh, style);
                            this._shadowDrawCalls++;
                        }
                        pass++;
                        if (type === pc.LIGHTTYPE_DIRECTIONAL) light._visibleLength[cameraPass] = -1; // prevent light from rendering more than once for this camera
                    } // end pass

                    if (light._isVsm) {
                        var filterSize = light._vsmBlurSize;
                        if (filterSize > 1) {
                            var origShadowMap = shadowCam.renderTarget;
                            var tempRt = getShadowMapFromCache(device, light._shadowResolution, light._shadowType, 1);

                            var isVsm8 = light._shadowType === pc.SHADOW_VSM8;
                            var blurMode = light.vsmBlurMode;
                            var blurShader = (isVsm8 ? this.blurPackedVsmShader : this.blurVsmShader)[blurMode][filterSize];
                            if (!blurShader) {
                                this.blurVsmWeights[filterSize] = gaussWeights(filterSize);

                                var blurVS = pc.shaderChunks.fullscreenQuadVS;
                                var blurFS = "#define SAMPLES " + filterSize + "\n";
                                if (isVsm8) {
                                    blurFS += this.blurPackedVsmShaderCode[blurMode];
                                } else {
                                    blurFS += this.blurVsmShaderCode[blurMode];
                                }
                                var blurShaderName = "blurVsm" + blurMode + "" + filterSize + "" + isVsm8;
                                blurShader = pc.shaderChunks.createShaderFromCode(this.device, blurVS, blurFS, blurShaderName);

                                if (isVsm8) {
                                    this.blurPackedVsmShader[blurMode][filterSize] = blurShader;
                                } else {
                                    this.blurVsmShader[blurMode][filterSize] = blurShader;
                                }
                            }

                            blurScissorRect.z = light._shadowResolution - 2;
                            blurScissorRect.w = blurScissorRect.z;

                            // Blur horizontal
                            this.sourceId.setValue(origShadowMap.colorBuffer);
                            pixelOffset[0] = 1 / light._shadowResolution;
                            pixelOffset[1] = 0;
                            this.pixelOffsetId.setValue(pixelOffset);
                            if (blurMode === pc.BLUR_GAUSSIAN) this.weightId.setValue(this.blurVsmWeights[filterSize]);
                            pc.drawQuadWithShader(device, tempRt, blurShader, null, blurScissorRect);

                            // Blur vertical
                            this.sourceId.setValue(tempRt.colorBuffer);
                            pixelOffset[1] = pixelOffset[0];
                            pixelOffset[0] = 0;
                            this.pixelOffsetId.setValue(pixelOffset);
                            pc.drawQuadWithShader(device, origShadowMap, blurShader, null, blurScissorRect);
                        }
                    }
                }
            }

            if (device.webgl2) {
                device.setDepthBias(false);
            } else if (device.extStandardDerivatives) {
                this.polygonOffset[0] = 0;
                this.polygonOffset[1] = 0;
                this.polygonOffsetId.setValue(this.polygonOffset);
            }

            // #ifdef PROFILER
            this._shadowMapTime += pc.now() - shadowMapStartTime;
            // #endif
        },

        updateShader: function (meshInstance, objDefs, staticLightList, pass, sortedLights) {
            meshInstance.material._scene = this.scene;
            meshInstance.material.updateShader(this.device, this.scene, objDefs, staticLightList, pass, sortedLights);
            meshInstance._shader[pass] = meshInstance.material.shader;
        },

        setCullMode: function (cullFaces, flip, drawCall) {
            var material = drawCall.material;
            var mode = pc.CULLFACE_NONE;
            if (cullFaces) {
                var flipFaces = 1;

                if (material.cull > pc.CULLFACE_NONE && material.cull < pc.CULLFACE_FRONTANDBACK) {
                    if (drawCall.flipFaces)
                        flipFaces *= -1;

                    if (flip)
                        flipFaces *= -1;

                    var wt = drawCall.node.worldTransform;
                    wt.getX(worldMatX);
                    wt.getY(worldMatY);
                    wt.getZ(worldMatZ);
                    worldMatX.cross(worldMatX, worldMatY);
                    if (worldMatX.dot(worldMatZ) < 0)
                        flipFaces *= -1;
                }

                if (flipFaces < 0) {
                    mode = material.cull === pc.CULLFACE_FRONT ? pc.CULLFACE_BACK : pc.CULLFACE_FRONT;
                } else {
                    mode = material.cull;
                }
            }
            this.device.setCullMode(mode);
        },

        renderForward: function (camera, drawCalls, drawCallsCount, sortedLights, pass, cullingMask, drawCallback, layer) {
            var device = this.device;
            var scene = this.scene;
            var vrDisplay = camera.vrDisplay;
            var passFlag = 1 << pass;

            var lightHash = layer ? layer._lightHash : 0;

            // #ifdef PROFILER
            var forwardStartTime = pc.now();
            // #endif

            var i, drawCall, mesh, material, objDefs, variantKey, lightMask, style, usedDirLights;
            var prevMaterial = null, prevObjDefs, prevLightMask, prevStatic;
            var paramName, parameter, parameters;
            var stencilFront, stencilBack;

            var halfWidth = device.width * 0.5;

            // Render the scene
            for (i = 0; i < drawCallsCount; i++) {

                drawCall = drawCalls[i];
                if (cullingMask && drawCall.mask && !(cullingMask & drawCall.mask)) continue; // apply visibility override

                if (drawCall.command) {
                    // We have a command
                    drawCall.command();
                } else {

                    // #ifdef PROFILER
                    if (camera === pc.skipRenderCamera) {
                        if (pc._skipRenderCounter >= pc.skipRenderAfter) continue;
                        pc._skipRenderCounter++;
                    }
                    if (layer) {
                        if (layer._skipRenderCounter >= layer.skipRenderAfter) continue;
                        layer._skipRenderCounter++;
                    }
                    // #endif

                    // We have a mesh instance
                    mesh = drawCall.mesh;
                    material = drawCall.material;
                    objDefs = drawCall._shaderDefs;
                    lightMask = drawCall.mask;

                    this.setSkinning(device, drawCall, material);

                    if (material && material === prevMaterial && objDefs !== prevObjDefs) {
                        prevMaterial = null; // force change shader if the object uses a different variant of the same material
                    }

                    if (drawCall.isStatic || prevStatic) {
                        prevMaterial = null;
                    }

                    if (material !== prevMaterial) {
                        this._materialSwitches++;

                        if (material.dirty) {
                            material.updateUniforms();
                            material.dirty = false;
                        }

                        if (!drawCall._shader[pass] || drawCall._shaderDefs !== objDefs || drawCall._lightHash !== lightHash) {
                            if (!drawCall.isStatic) {
                                variantKey = pass + "_" + objDefs + "_" + lightHash;
                                drawCall._shader[pass] = material.variants[variantKey];
                                if (!drawCall._shader[pass]) {
                                    this.updateShader(drawCall, objDefs, null, pass, sortedLights);
                                    material.variants[variantKey] = drawCall._shader[pass];
                                }
                            } else {
                                this.updateShader(drawCall, objDefs, drawCall._staticLightList, pass, sortedLights);
                            }
                            drawCall._shaderDefs = objDefs;
                            drawCall._lightHash = lightHash;
                        }

                        // #ifdef DEBUG
                        if (!device.setShader(drawCall._shader[pass])) {
                            console.error('Error in material "' + material.name + '" with flags ' + objDefs);
                            drawCall.material = scene.defaultMaterial;
                        }
                        // #else
                        device.setShader(drawCall._shader[pass]);
                        // #endif

                        // Uniforms I: material
                        parameters = material.parameters;
                        for (paramName in parameters) {
                            parameter = parameters[paramName];
                            if (parameter.passFlags & passFlag) {
                                if (!parameter.scopeId) {
                                    parameter.scopeId = device.scope.resolve(paramName);
                                }
                                parameter.scopeId.setValue(parameter.data);
                            }
                        }

                        if (!prevMaterial || lightMask !== prevLightMask) {
                            usedDirLights = this.dispatchDirectLights(sortedLights[pc.LIGHTTYPE_DIRECTIONAL], scene, lightMask);
                            this.dispatchLocalLights(sortedLights, scene, lightMask, usedDirLights, drawCall._staticLightList);
                        }

                        this.alphaTestId.setValue(material.alphaTest);

                        device.setBlending(material.blend);
                        if (material.blend) {
                            if (material.separateAlphaBlend) {
                                device.setBlendFunctionSeparate(material.blendSrc, material.blendDst, material.blendSrcAlpha, material.blendDstAlpha);
                                device.setBlendEquationSeparate(material.blendEquation, material.blendAlphaEquation);
                            } else {
                                device.setBlendFunction(material.blendSrc, material.blendDst);
                                device.setBlendEquation(material.blendEquation);
                            }
                        }
                        device.setColorWrite(material.redWrite, material.greenWrite, material.blueWrite, material.alphaWrite);
                        device.setDepthWrite(material.depthWrite);
                        device.setDepthTest(material.depthTest);
                        device.setAlphaToCoverage(material.alphaToCoverage);

                        if (material.depthBias || material.slopeDepthBias) {
                            device.setDepthBias(true);
                            device.setDepthBiasValues(material.depthBias, material.slopeDepthBias);
                        } else {
                            device.setDepthBias(false);
                        }
                    }

                    this.setCullMode(camera._cullFaces, camera._flipFaces, drawCall);

                    stencilFront = drawCall.stencilFront || material.stencilFront;
                    stencilBack = drawCall.stencilBack || material.stencilBack;

                    if (stencilFront || stencilBack) {
                        device.setStencilTest(true);
                        if (stencilFront === stencilBack) {
                            // identical front/back stencil
                            device.setStencilFunc(stencilFront.func, stencilFront.ref, stencilFront.readMask);
                            device.setStencilOperation(stencilFront.fail, stencilFront.zfail, stencilFront.zpass, stencilFront.writeMask);
                        } else {
                            // separate
                            if (stencilFront) {
                                // set front
                                device.setStencilFuncFront(stencilFront.func, stencilFront.ref, stencilFront.readMask);
                                device.setStencilOperationFront(stencilFront.fail, stencilFront.zfail, stencilFront.zpass, stencilFront.writeMask);
                            } else {
                                // default front
                                device.setStencilFuncFront(pc.FUNC_ALWAYS, 0, 0xFF);
                                device.setStencilOperationFront(pc.STENCILOP_KEEP, pc.STENCILOP_KEEP, pc.STENCILOP_KEEPP, 0xFF);
                            }
                            if (stencilBack) {
                                // set back
                                device.setStencilFuncBack(stencilBack.func, stencilBack.ref, stencilBack.readMask);
                                device.setStencilOperationBack(stencilBack.fail, stencilBack.zfail, stencilBack.zpass, stencilBack.writeMask);
                            } else {
                                // default back
                                device.setStencilFuncBack(pc.FUNC_ALWAYS, 0, 0xFF);
                                device.setStencilOperationBack(pc.STENCILOP_KEEP, pc.STENCILOP_KEEP, pc.STENCILOP_KEEP, 0xFF);
                            }
                        }
                    } else {
                        device.setStencilTest(false);
                    }

                    // Uniforms II: meshInstance overrides
                    parameters = drawCall.parameters;
                    for (paramName in parameters) {
                        parameter = parameters[paramName];
                        if (parameter.passFlags & passFlag) {
                            if (!parameter.scopeId) {
                                parameter.scopeId = device.scope.resolve(paramName);
                            }
                            parameter.scopeId.setValue(parameter.data);
                        }
                    }

                    device.setVertexBuffer((drawCall.morphInstance && drawCall.morphInstance._vertexBuffer) ?
                        drawCall.morphInstance._vertexBuffer : mesh.vertexBuffer, 0);
                    style = drawCall.renderStyle;
                    device.setIndexBuffer(mesh.indexBuffer[style]);

                    if (drawCallback) {
                        drawCallback(drawCall, i);
                    }

                    if (vrDisplay && vrDisplay.presenting) {
                        // Left
                        device.setViewport(0, 0, halfWidth, device.height);
                        this.projId.setValue(projL.data);
                        this.viewInvId.setValue(viewInvL.data);
                        this.viewId.setValue(viewL.data);
                        this.viewId3.setValue(viewMat3L.data);
                        this.viewProjId.setValue(viewProjMatL.data);
                        this.viewPos[0] = viewPosL.x;
                        this.viewPos[1] = viewPosL.y;
                        this.viewPos[2] = viewPosL.z;
                        this.viewPosId.setValue(this.viewPos);
                        i += this.drawInstance(device, drawCall, mesh, style, true);
                        this._forwardDrawCalls++;

                        // Right
                        device.setViewport(halfWidth, 0, halfWidth, device.height);
                        this.projId.setValue(projR.data);
                        this.viewInvId.setValue(viewInvR.data);
                        this.viewId.setValue(viewR.data);
                        this.viewId3.setValue(viewMat3R.data);
                        this.viewProjId.setValue(viewProjMatR.data);
                        this.viewPos[0] = viewPosR.x;
                        this.viewPos[1] = viewPosR.y;
                        this.viewPos[2] = viewPosR.z;
                        this.viewPosId.setValue(this.viewPos);
                        i += this.drawInstance2(device, drawCall, mesh, style);
                        this._forwardDrawCalls++;
                    } else {
                        i += this.drawInstance(device, drawCall, mesh, style, true);
                        this._forwardDrawCalls++;
                    }

                    // Unset meshInstance overrides back to material values if next draw call will use the same material
                    if (i < drawCallsCount - 1 && drawCalls[i + 1].material === material) {
                        for (paramName in parameters) {
                            parameter = material.parameters[paramName];
                            if (parameter) {
                                if (!parameter.scopeId) {
                                    parameter.scopeId = device.scope.resolve(paramName);
                                }
                                parameter.scopeId.setValue(parameter.data);
                            }
                        }
                    }

                    prevMaterial = material;
                    prevObjDefs = objDefs;
                    prevLightMask = lightMask;
                    prevStatic = drawCall.isStatic;
                }
            }
            device.updateEnd();

            // #ifdef PROFILER
            this._forwardTime += pc.now() - forwardStartTime;
            // #endif
        },

        setupInstancing: function (device) {
            if (!pc._instanceVertexFormat) {
                var formatDesc = [
                    { semantic: pc.SEMANTIC_TEXCOORD2, components: 4, type: pc.TYPE_FLOAT32 },
                    { semantic: pc.SEMANTIC_TEXCOORD3, components: 4, type: pc.TYPE_FLOAT32 },
                    { semantic: pc.SEMANTIC_TEXCOORD4, components: 4, type: pc.TYPE_FLOAT32 },
                    { semantic: pc.SEMANTIC_TEXCOORD5, components: 4, type: pc.TYPE_FLOAT32 }
                ];
                pc._instanceVertexFormat = new pc.VertexFormat(device, formatDesc);
            }
            if (device.enableAutoInstancing) {
                if (!pc._autoInstanceBuffer) {
                    pc._autoInstanceBuffer = new pc.VertexBuffer(device, pc._instanceVertexFormat, device.autoInstancingMaxObjects, pc.BUFFER_DYNAMIC);
                    pc._autoInstanceBufferData = new Float32Array(pc._autoInstanceBuffer.lock());
                }
            }
        },

        revertStaticMeshes: function (meshInstances) {
            var i;
            var drawCalls = meshInstances;
            var drawCallsCount = drawCalls.length;
            var drawCall;
            var newDrawCalls = [];

            var prevStaticSource;
            for (i = 0; i < drawCallsCount; i++) {
                drawCall = drawCalls[i];
                if (drawCall._staticSource) {
                    if (drawCall._staticSource !== prevStaticSource) {
                        newDrawCalls.push(drawCall._staticSource);
                        prevStaticSource = drawCall._staticSource;
                    }
                } else {
                    newDrawCalls.push(drawCall);
                }
            }

            // Set array to new
            meshInstances.length = newDrawCalls.length;
            for (i = 0; i < newDrawCalls.length; i++) {
                meshInstances[i] = newDrawCalls[i];
            }
        },

        prepareStaticMeshes: function (meshInstances, lights) {
            // #ifdef PROFILER
            var prepareTime = pc.now();
            var searchTime = 0;
            var subSearchTime = 0;
            var triAabbTime = 0;
            var subTriAabbTime = 0;
            var writeMeshTime = 0;
            var subWriteMeshTime = 0;
            var combineTime = 0;
            var subCombineTime = 0;
            // #endif

            var i, j, k, v, s, index;

            var device = this.device;
            var scene = this.scene;
            var drawCalls = meshInstances;
            var drawCallsCount = drawCalls.length;
            var drawCall, light;

            var newDrawCalls = [];
            var mesh;
            var indices, verts, numTris, elems, vertSize, offsetP, baseIndex;
            var _x, _y, _z;
            var minx, miny, minz, maxx, maxy, maxz;
            var minv, maxv;
            var minVec = new pc.Vec3();
            var maxVec = new pc.Vec3();
            var localLightBounds = new pc.BoundingBox();
            var invMatrix = new pc.Mat4();
            var triLightComb = [];
            var triLightCombUsed;
            var indexBuffer, vertexBuffer;
            var combIndices, combIbName, combIb;
            var lightTypePass;
            var lightAabb = [];
            var aabb;
            var triBounds = [];
            var staticLights = [];
            var bit;
            var lht;
            for (i = 0; i < drawCallsCount; i++) {
                drawCall = drawCalls[i];
                if (!drawCall.isStatic) {
                    newDrawCalls.push(drawCall);
                } else {
                    aabb = drawCall.aabb;
                    staticLights.length = 0;
                    for (lightTypePass = pc.LIGHTTYPE_POINT; lightTypePass <= pc.LIGHTTYPE_SPOT; lightTypePass++) {
                        for (j = 0; j < lights.length; j++) {
                            light = lights[j];
                            if (light._type !== lightTypePass) continue;
                            if (light._enabled) {
                                if (light._mask & drawCall.mask) {
                                    if (light.isStatic) {
                                        if (!lightAabb[j]) {
                                            lightAabb[j] = new pc.BoundingBox();
                                            // light.getBoundingBox(lightAabb[j]); // box from sphere seems to give better granularity
                                            light._node.getWorldTransform();
                                            light.getBoundingSphere(tempSphere);
                                            lightAabb[j].center.copy(tempSphere.center);
                                            lightAabb[j].halfExtents.x = tempSphere.radius;
                                            lightAabb[j].halfExtents.y = tempSphere.radius;
                                            lightAabb[j].halfExtents.z = tempSphere.radius;
                                        }
                                        if (!lightAabb[j].intersects(aabb)) continue;
                                        staticLights.push(j);
                                    }
                                }
                            }
                        }
                    }

                    if (staticLights.length === 0) {
                        newDrawCalls.push(drawCall);
                        continue;
                    }

                    mesh = drawCall.mesh;
                    vertexBuffer = mesh.vertexBuffer;
                    indexBuffer = mesh.indexBuffer[drawCall.renderStyle];
                    indices = indexBuffer.bytesPerIndex === 2 ? new Uint16Array(indexBuffer.lock()) : new Uint32Array(indexBuffer.lock());
                    numTris = mesh.primitive[drawCall.renderStyle].count / 3;
                    baseIndex = mesh.primitive[drawCall.renderStyle].base;
                    elems = vertexBuffer.format.elements;
                    vertSize = vertexBuffer.format.size / 4; // / 4 because float
                    verts = new Float32Array(vertexBuffer.storage);

                    for (k = 0; k < elems.length; k++) {
                        if (elems[k].name === pc.SEMANTIC_POSITION) {
                            offsetP = elems[k].offset / 4; // / 4 because float
                        }
                    }

                    // #ifdef PROFILER
                    subTriAabbTime = pc.now();
                    // #endif

                    triLightComb.length = numTris;
                    for (k = 0; k < numTris; k++) {
                        // triLightComb[k] = ""; // uncomment to remove 32 lights limit
                        triLightComb[k] = 0; // comment to remove 32 lights limit
                    }
                    triLightCombUsed = false;

                    triBounds.length = numTris * 6;
                    for (k = 0; k < numTris; k++) {
                        minx = Number.MAX_VALUE;
                        miny = Number.MAX_VALUE;
                        minz = Number.MAX_VALUE;
                        maxx = -Number.MAX_VALUE;
                        maxy = -Number.MAX_VALUE;
                        maxz = -Number.MAX_VALUE;
                        for (v = 0; v < 3; v++) {
                            index = indices[k * 3 + v + baseIndex];
                            index = index * vertSize + offsetP;
                            _x = verts[index];
                            _y = verts[index + 1];
                            _z = verts[index + 2];
                            if (_x < minx) minx = _x;
                            if (_y < miny) miny = _y;
                            if (_z < minz) minz = _z;
                            if (_x > maxx) maxx = _x;
                            if (_y > maxy) maxy = _y;
                            if (_z > maxz) maxz = _z;
                        }
                        index = k * 6;
                        triBounds[index] = minx;
                        triBounds[index + 1] = miny;
                        triBounds[index + 2] = minz;
                        triBounds[index + 3] = maxx;
                        triBounds[index + 4] = maxy;
                        triBounds[index + 5] = maxz;
                    }
                    // #ifdef PROFILER
                    triAabbTime += pc.now() - subTriAabbTime;
                    // #endif

                    // #ifdef PROFILER
                    subSearchTime = pc.now();
                    // #endif
                    for (s = 0; s < staticLights.length; s++) {
                        j = staticLights[s];
                        light = lights[j];

                        invMatrix.copy(drawCall.node.worldTransform).invert();
                        localLightBounds.setFromTransformedAabb(lightAabb[j], invMatrix);
                        minv = localLightBounds.getMin();
                        maxv = localLightBounds.getMax();
                        bit = 1 << s;

                        for (k = 0; k < numTris; k++) {
                            index = k * 6;
                            if ((triBounds[index] <= maxv.x) && (triBounds[index + 3] >= minv.x) &&
                                (triBounds[index + 1] <= maxv.y) && (triBounds[index + 4] >= minv.y) &&
                                (triBounds[index + 2] <= maxv.z) && (triBounds[index + 5] >= minv.z)) {

                                // triLightComb[k] += j + "_";  // uncomment to remove 32 lights limit
                                triLightComb[k] |= bit; // comment to remove 32 lights limit
                                triLightCombUsed = true;
                            }
                        }
                    }
                    // #ifdef PROFILER
                    searchTime += pc.now() - subSearchTime;
                    // #endif

                    if (triLightCombUsed) {

                        // #ifdef PROFILER
                        subCombineTime = pc.now();
                        // #endif

                        combIndices = {};
                        for (k = 0; k < numTris; k++) {
                            j = k * 3 + baseIndex; // can go beyond 0xFFFF if base was non-zero?
                            combIbName = triLightComb[k];
                            if (!combIndices[combIbName]) combIndices[combIbName] = [];
                            combIb = combIndices[combIbName];
                            combIb.push(indices[j]);
                            combIb.push(indices[j + 1]);
                            combIb.push(indices[j + 2]);
                        }

                        // #ifdef PROFILER
                        combineTime += pc.now() - subCombineTime;
                        // #endif

                        // #ifdef PROFILER
                        subWriteMeshTime = pc.now();
                        // #endif

                        for (combIbName in combIndices) {
                            combIb = combIndices[combIbName];
                            var ib = new pc.IndexBuffer(device, indexBuffer.format, combIb.length, indexBuffer.usage);
                            var ib2 = ib.bytesPerIndex === 2 ? new Uint16Array(ib.lock()) : new Uint32Array(ib.lock());
                            ib2.set(combIb);
                            ib.unlock();

                            minx = Number.MAX_VALUE;
                            miny = Number.MAX_VALUE;
                            minz = Number.MAX_VALUE;
                            maxx = -Number.MAX_VALUE;
                            maxy = -Number.MAX_VALUE;
                            maxz = -Number.MAX_VALUE;
                            for (k = 0; k < combIb.length; k++) {
                                index = combIb[k];
                                _x = verts[index * vertSize + offsetP];
                                _y = verts[index * vertSize + offsetP + 1];
                                _z = verts[index * vertSize + offsetP + 2];
                                if (_x < minx) minx = _x;
                                if (_y < miny) miny = _y;
                                if (_z < minz) minz = _z;
                                if (_x > maxx) maxx = _x;
                                if (_y > maxy) maxy = _y;
                                if (_z > maxz) maxz = _z;
                            }
                            minVec.set(minx, miny, minz);
                            maxVec.set(maxx, maxy, maxz);
                            var chunkAabb = new pc.BoundingBox();
                            chunkAabb.setMinMax(minVec, maxVec);

                            var mesh2 = new pc.Mesh();
                            mesh2.vertexBuffer = vertexBuffer;
                            mesh2.indexBuffer[0] = ib;
                            mesh2.primitive[0].type = pc.PRIMITIVE_TRIANGLES;
                            mesh2.primitive[0].base = 0;
                            mesh2.primitive[0].count = combIb.length;
                            mesh2.primitive[0].indexed = true;
                            mesh2.aabb = chunkAabb;

                            var instance = new pc.MeshInstance(drawCall.node, mesh2, drawCall.material);
                            instance.isStatic = drawCall.isStatic;
                            instance.visible = drawCall.visible;
                            instance.layer = drawCall.layer;
                            instance.castShadow = drawCall.castShadow;
                            instance._receiveShadow = drawCall._receiveShadow;
                            instance.cull = drawCall.cull;
                            instance.pick = drawCall.pick;
                            instance.mask = drawCall.mask;
                            instance.parameters = drawCall.parameters;
                            instance._shaderDefs = drawCall._shaderDefs;
                            instance._staticSource = drawCall;

                            if (drawCall._staticLightList) {
                                instance._staticLightList = drawCall._staticLightList; // add forced assigned lights
                            } else {
                                instance._staticLightList = [];
                            }

                            // uncomment to remove 32 lights limit
                            // var lnames = combIbName.split("_");
                            // lnames.length = lnames.length - 1;
                            // for(k = 0; k < lnames.length; k++) {
                            //     instance._staticLightList[k] = lights[parseInt(lnames[k])];
                            // }

                            // comment to remove 32 lights limit
                            for (k = 0; k < staticLights.length; k++) {
                                bit = 1 << k;
                                if (combIbName & bit) {
                                    lht = lights[staticLights[k]];
                                    if (instance._staticLightList.indexOf(lht) < 0) {
                                        instance._staticLightList.push(lht);
                                    }
                                }
                            }

                            instance._staticLightList.sort(this.lightCompare);

                            newDrawCalls.push(instance);
                        }

                        // #ifdef PROFILER
                        writeMeshTime += pc.now() - subWriteMeshTime;
                        // #endif
                    } else {
                        newDrawCalls.push(drawCall);
                    }
                }
            }
            // Set array to new
            meshInstances.length = newDrawCalls.length;
            for (i = 0; i < newDrawCalls.length; i++) {
                meshInstances[i] = newDrawCalls[i];
            }
            // #ifdef PROFILER
            scene._stats.lastStaticPrepareFullTime = pc.now() - prepareTime;
            scene._stats.lastStaticPrepareSearchTime = searchTime;
            scene._stats.lastStaticPrepareWriteTime = writeMeshTime;
            scene._stats.lastStaticPrepareTriAabbTime = triAabbTime;
            scene._stats.lastStaticPrepareCombineTime = combineTime;
            // #endif
        },

        updateShaders: function (drawCalls) {
            // #ifdef PROFILER
            var time = pc.now();
            // #endif

            var i;
            // Collect materials
            var materials = [];
            for (i = 0; i < drawCalls.length; i++) {
                var drawCall = drawCalls[i];
                if (drawCall.material !== undefined) {
                    if (materials.indexOf(drawCall.material) === -1) {
                        materials.push(drawCall.material);
                    }
                }
            }
            // Clear material shaders
            for (i = 0; i < materials.length; i++) {
                var mat = materials[i];
                if (mat.updateShader !== pc.Material.prototype.updateShader) {
                    mat.clearVariants();
                    mat.shader = null;
                }
            }

            // #ifdef PROFILER
            this.scene._stats.updateShadersTime += pc.now() - time;
            // #endif
        },

        updateLitShaders: function (drawCalls) {
            // #ifdef PROFILER
            var time = pc.now();
            // #endif

            for (var i = 0; i < drawCalls.length; i++) {
                var drawCall = drawCalls[i];
                if (drawCall.material !== undefined) {
                    var mat = drawCall.material;
                    if (mat.updateShader !== pc.Material.prototype.updateShader) {
                        if (mat.useLighting === false || (mat.emitter && !mat.emitter.lighting)) {
                            // skip unlit standard and particles materials
                            continue;
                        }
                        mat.clearVariants();
                        mat.shader = null;
                    }
                }
            }

            // #ifdef PROFILER
            this.scene._stats.updateShadersTime += pc.now() - time;
            // #endif
        },

        beginFrame: function (comp) {
            var device = this.device;
            var scene = this.scene;
            var meshInstances = comp._meshInstances;
            var lights = comp._lights;

            if (scene.updateSkybox) {
                scene._updateSkybox(device);
                scene.updateSkybox = false;
            }

            // Update shaders if needed
            // all mesh instances (TODO: ideally can update less if only lighting changed)
            if (scene.updateShaders) {
                this.updateShaders(meshInstances);
                scene.updateShaders = false;
                scene.updateLitShaders = false;
                scene._shaderVersion++;
            } else if (scene.updateLitShaders) {
                this.updateLitShaders(meshInstances);
                scene.updateLitShaders = false;
                scene._shaderVersion++;
            }

            // Update all skin matrices to properly cull skinned objects (but don't update rendering data yet)
            this.updateCpuSkinMatrices(meshInstances);
            this.updateMorphedBounds(meshInstances);

            var i;
            var len = meshInstances.length;
            for (i = 0; i < len; i++) {
                meshInstances[i].visibleThisFrame = false;
            }

            len = lights.length;
            for (i = 0; i < len; i++) {
                lights[i].visibleThisFrame = lights[i]._type === pc.LIGHTTYPE_DIRECTIONAL;
            }
        },

        beginLayers: function (comp) {
            var scene = this.scene;
            var len = comp.layerList.length;
            var layer;
            var i, j;
            var shaderVersion = this.scene._shaderVersion;
            for (i = 0; i < len; i++) {
                comp.layerList[i]._postRenderCounter = 0;
            }
            var transparent;
            for (i = 0; i < len; i++) {
                layer = comp.layerList[i];
                layer._shaderVersion = shaderVersion;
                // #ifdef PROFILER
                layer._skipRenderCounter = 0;
                layer._forwardDrawCalls = 0;
                layer._shadowDrawCalls = 0;
                layer._renderTime = 0;
                // #endif

                layer._preRenderCalledForCameras = 0;
                layer._postRenderCalledForCameras = 0;
                transparent = comp.subLayerList[i];
                if (transparent) {
                    layer._postRenderCounter |= 2;
                } else {
                    layer._postRenderCounter |= 1;
                }
                layer._postRenderCounterMax = layer._postRenderCounter;

                for (j = 0; j < layer.cameras.length; j++) {
                    // Create visible arrays for every camera inside each layer if not present
                    if (!layer.instances.visibleOpaque[j]) layer.instances.visibleOpaque[j] = new pc.VisibleInstanceList();
                    if (!layer.instances.visibleTransparent[j]) layer.instances.visibleTransparent[j] = new pc.VisibleInstanceList();
                    // Mark visible arrays as not processed yet
                    layer.instances.visibleOpaque[j].done = false;
                    layer.instances.visibleTransparent[j].done = false;
                }

                // remove visible lists if cameras have been removed, remove one per frame
                if (layer.cameras.length < layer.instances.visibleOpaque.length) {
                    layer.instances.visibleOpaque.splice(layer.cameras.length, 1);
                }

                if (layer.cameras.length < layer.instances.visibleTransparent.length) {
                    layer.instances.visibleTransparent.splice(layer.cameras.length, 1);
                }

                // Generate static lighting for meshes in this layer if needed
                if (layer._needsStaticPrepare && layer._staticLightHash) {
                    // TODO: reuse with the same staticLightHash
                    if (layer._staticPrepareDone) {
                        this.revertStaticMeshes(layer.opaqueMeshInstances);
                        this.revertStaticMeshes(layer.transparentMeshInstances);
                    }
                    this.prepareStaticMeshes(layer.opaqueMeshInstances, layer._lights);
                    this.prepareStaticMeshes(layer.transparentMeshInstances, layer._lights);
                    comp._dirty = true;
                    scene.updateShaders = true;
                    layer._needsStaticPrepare = false;
                    layer._staticPrepareDone = true;
                }
            }
        },

        cullLocalShadowmap: function (light, drawCalls) {
            var i, type, shadowCam, shadowCamNode, passes, pass, numInstances, meshInstance, visibleList, vlen, visible;
            var lightNode;
            type = light._type;
            if (type === pc.LIGHTTYPE_DIRECTIONAL) return;
            light.visibleThisFrame = true; // force light visibility if function was manually called

            shadowCam = this.getShadowCamera(this.device, light);

            shadowCam.projection = pc.PROJECTION_PERSPECTIVE;
            shadowCam.nearClip = light.attenuationEnd / 1000;
            shadowCam.farClip = light.attenuationEnd;
            shadowCam.aspectRatio = 1;
            if (type === pc.LIGHTTYPE_SPOT) {
                shadowCam.fov = light._outerConeAngle * 2;
                passes = 1;
            } else {
                shadowCam.fov = 90;
                passes = 6;
            }
            shadowCamNode = shadowCam._node;
            lightNode = light._node;
            shadowCamNode.setPosition(lightNode.getPosition());
            if (type === pc.LIGHTTYPE_SPOT) {
                shadowCamNode.setRotation(lightNode.getRotation());
                shadowCamNode.rotateLocal(-90, 0, 0); // Camera's look down negative Z, and directional lights point down negative Y // TODO: remove eulers
            }

            for (pass = 0; pass < passes; pass++) {

                if (type === pc.LIGHTTYPE_POINT) {
                    shadowCamNode.setRotation(pointLightRotations[pass]);
                    shadowCam.renderTarget = light._shadowCubeMap[pass];
                }

                this.updateCameraFrustum(shadowCam);

                visibleList = light._visibleList[pass];
                if (!visibleList) {
                    visibleList = light._visibleList[pass] = [];
                }
                light._visibleLength[pass] = 0;
                vlen = 0;
                for (i = 0, numInstances = drawCalls.length; i < numInstances; i++) {
                    meshInstance = drawCalls[i];
                    visible = true;
                    if (meshInstance.cull) {
                        visible = this._isVisible(shadowCam, meshInstance);
                    }
                    if (visible) {
                        visibleList[vlen] = meshInstance;
                        vlen++;
                        meshInstance.visibleThisFrame = true;
                    }
                }
                light._visibleLength[pass] = vlen;

                if (visibleList.length !== vlen) {
                    visibleList.length = vlen;
                }
                visibleList.sort(this.depthSortCompare); // sort shadowmap drawcalls here, not in render
            }
        },


        cullDirectionalShadowmap: function (light, drawCalls, camera, pass) {
            var i, shadowCam, shadowCamNode, lightNode, frustumSize, vlen, visibleList;
            var unitPerTexel, delta, p;
            var minx, miny, minz, maxx, maxy, maxz, centerx, centery;
            var visible, numInstances;
            var meshInstance;
            var emptyAabb;
            var drawCallAabb;
            var device = this.device;
            light.visibleThisFrame = true; // force light visibility if function was manually called

            shadowCam = this.getShadowCamera(device, light);
            shadowCamNode = shadowCam._node;
            lightNode = light._node;

            shadowCamNode.setPosition(lightNode.getPosition());
            shadowCamNode.setRotation(lightNode.getRotation());
            shadowCamNode.rotateLocal(-90, 0, 0); // Camera's look down negative Z, and directional lights point down negative Y

            // Positioning directional light frustum I
            // Construct light's orthographic frustum around camera frustum
            // Use very large near/far planes this time

            // 1. Get the frustum of the camera
            _getFrustumPoints(camera, light.shadowDistance || camera._farClip, frustumPoints);

            // 2. Figure out the maximum diagonal of the frustum in light's projected space.
            frustumSize = frustumDiagonal.sub2( frustumPoints[0], frustumPoints[6] ).length();
            frustumSize = Math.max( frustumSize, frustumDiagonal.sub2( frustumPoints[4], frustumPoints[6] ).length() );

            // 3. Transform the 8 corners of the camera frustum into the shadow camera's view space
            shadowCamView.copy( shadowCamNode.getWorldTransform() ).invert();
            c2sc.copy( shadowCamView ).mul( camera._node.worldTransform );
            for (i = 0; i < 8; i++) {
                c2sc.transformPoint(frustumPoints[i], frustumPoints[i]);
            }

            // 4. Come up with a bounding box (in light-space) by calculating the min
            // and max X, Y, and Z values from your 8 light-space frustum coordinates.
            minx = miny = minz = 1000000;
            maxx = maxy = maxz = -1000000;
            for (i = 0; i < 8; i++) {
                p = frustumPoints[i];
                if (p.x < minx) minx = p.x;
                if (p.x > maxx) maxx = p.x;
                if (p.y < miny) miny = p.y;
                if (p.y > maxy) maxy = p.y;
                if (p.z < minz) minz = p.z;
                if (p.z > maxz) maxz = p.z;
            }

            // 5. Enlarge the light's frustum so that the frustum will be the same size
            // no matter how the view frustum moves.
            // And also snap the frustum to align with shadow texel. ( Avoid shadow shimmering )
            unitPerTexel = frustumSize / light._shadowResolution;
            delta = (frustumSize - (maxx - minx)) * 0.5;
            minx = Math.floor( (minx - delta) / unitPerTexel ) * unitPerTexel;
            delta = (frustumSize - (maxy - miny)) * 0.5;
            miny = Math.floor( (miny - delta) / unitPerTexel ) * unitPerTexel;
            maxx = minx + frustumSize;
            maxy = miny + frustumSize;

            // 6. Use your min and max values to create an off-center orthographic projection.
            centerx = (maxx + minx) * 0.5;
            centery = (maxy + miny) * 0.5;
            shadowCamNode.translateLocal(centerx, centery, 100000);

            shadowCam.projection = pc.PROJECTION_ORTHOGRAPHIC;
            shadowCam.nearClip = 0;
            shadowCam.farClip = 200000;
            shadowCam.aspectRatio = 1; // The light's frustum is a cuboid.
            shadowCam.orthoHeight = frustumSize * 0.5;

            this.updateCameraFrustum(shadowCam);

            // Cull shadow casters and find their AABB
            emptyAabb = true;
            visibleList = light._visibleList[pass];
            if (!visibleList) {
                visibleList = light._visibleList[pass] = [];
            }
            vlen = light._visibleLength[pass] = 0;

            for (i = 0, numInstances = drawCalls.length; i < numInstances; i++) {
                meshInstance = drawCalls[i];
                visible = true;
                if (meshInstance.cull) {
                    visible = this._isVisible(shadowCam, meshInstance);
                }
                if (visible) {
                    visibleList[vlen] = meshInstance;
                    vlen++;
                    meshInstance.visibleThisFrame = true;

                    drawCallAabb = meshInstance.aabb;
                    if (emptyAabb) {
                        visibleSceneAabb.copy(drawCallAabb);
                        emptyAabb = false;
                    } else {
                        visibleSceneAabb.add(drawCallAabb);
                    }
                }
            }
            light._visibleLength[pass] = vlen;

            if (visibleList.length !== vlen) {
                visibleList.length = vlen;
            }
            visibleList.sort(this.depthSortCompare); // sort shadowmap drawcalls here, not in render

            // Positioning directional light frustum II
            // Fit clipping planes tightly around visible shadow casters

            // 1. Calculate minz/maxz based on casters' AABB
            var z = _getZFromAABBSimple( shadowCamView, visibleSceneAabb.getMin(), visibleSceneAabb.getMax(), minx, maxx, miny, maxy );

            // Always use the scene's aabb's Z value
            // Otherwise object between the light and the frustum won't cast shadow.
            maxz = z.max;
            if (z.min > minz) minz = z.min;

            // 2. Fix projection
            shadowCamNode.setPosition(lightNode.getPosition());
            shadowCamNode.translateLocal(centerx, centery, maxz + directionalShadowEpsilon);
            shadowCam.farClip = maxz - minz;

            // Save projection variables to use in rendering later
            var settings = light._visibleCameraSettings[pass];
            if (!settings) {
                settings = light._visibleCameraSettings[pass] = {};
            }
            var lpos = shadowCamNode.getPosition();
            settings.x = lpos.x;
            settings.y = lpos.y;
            settings.z = lpos.z;
            settings.orthoHeight = shadowCam.orthoHeight;
            settings.farClip = shadowCam.farClip;
        },


        gpuUpdate: function (drawCalls) {
            // skip everything with visibleThisFrame === false
            this.updateGpuSkinMatrices(drawCalls);
            this.updateMorphing(drawCalls);
        },

        clearView: function (camera, target, options) {
            camera = camera.camera;
            var device = this.device;
            device.setRenderTarget(target);
            device.updateBegin();

            device.setColorWrite(true, true, true, true);
            device.setDepthWrite(true);

            var rect = camera.getRect();
            var pixelWidth = target ? target.width : device.width;
            var pixelHeight = target ? target.height : device.height;
            var x = Math.floor(rect.x * pixelWidth);
            var y = Math.floor(rect.y * pixelHeight);
            var w = Math.floor(rect.width * pixelWidth);
            var h = Math.floor(rect.height * pixelHeight);
            device.setViewport(x, y, w, h);
            device.setScissor(x, y, w, h);

            device.clear(options ? options : camera._clearOptions); // clear full RT
        },

        setSceneConstants: function () {
            var i;
            var device = this.device;
            var scene = this.scene;

            // Set up ambient/exposure
            this.dispatchGlobalLights(scene);

            // Set up the fog
            if (scene.fog !== pc.FOG_NONE) {
                this.fogColor[0] = scene.fogColor.r;
                this.fogColor[1] = scene.fogColor.g;
                this.fogColor[2] = scene.fogColor.b;
                if (scene.gammaCorrection) {
                    for (i = 0; i < 3; i++) {
                        this.fogColor[i] = Math.pow(this.fogColor[i], 2.2);
                    }
                }
                this.fogColorId.setValue(this.fogColor);
                if (scene.fog === pc.FOG_LINEAR) {
                    this.fogStartId.setValue(scene.fogStart);
                    this.fogEndId.setValue(scene.fogEnd);
                } else {
                    this.fogDensityId.setValue(scene.fogDensity);
                }
            }

            // Set up screen size // should be RT size?
            this._screenSize[0] = device.width;
            this._screenSize[1] = device.height;
            this._screenSize[2] = 1 / device.width;
            this._screenSize[3] = 1 / device.height;
            this.screenSizeId.setValue(this._screenSize);
        },

        renderComposition: function (comp) {
            var device = this.device;
            var camera;
            var renderedRt = comp._renderedRt;
            var renderedByCam = comp._renderedByCam;
            var renderedLayer = comp._renderedLayer;
            var i, layer, transparent, cameras, j, rt, k, processedThisCamera, processedThisCameraAndLayer, processedThisCameraAndRt;

            this.beginLayers(comp);

            // Update static layer data, if something's changed
            var updated = comp._update();
            if (updated & pc.COMPUPDATED_LIGHTS) {
                this.scene.updateLitShaders = true;
            }

            // #ifdef PROFILER
            if (updated & pc.COMPUPDATED_LIGHTS || !this.scene._statsUpdated) {
                var stats = this.scene._stats;
                stats.lights = comp._lights.length;
                stats.dynamicLights = 0;
                stats.bakedLights = 0;
                var l;
                for (i = 0; i < stats.lights; i++) {
                    l = comp._lights[i];
                    if (l._enabled) {
                        if ((l._mask & pc.MASK_DYNAMIC) || (l._mask & pc.MASK_BAKED)) { // if affects dynamic or baked objects in real-time
                            stats.dynamicLights++;
                        }
                        if (l._mask & pc.MASK_LIGHTMAP) { // if baked into lightmaps
                            stats.bakedLights++;
                        }
                    }
                }
            }
            if (updated & pc.COMPUPDATED_INSTANCES || !this.scene._statsUpdated) {
                this.scene._stats.meshInstances = comp._meshInstances.length;
            }
            this.scene._statsUpdated = true;
            // #endif

            // Single per-frame calculations
            this.beginFrame(comp);
            this.setSceneConstants();

            // Camera culling (once for each camera + layer)
            // Also applies meshInstance.visible and camera.cullingMask
            var renderedLength = 0;
            var objects, drawCalls, visible;
            for (i = 0; i < comp.layerList.length; i++) {
                layer = comp.layerList[i];
                if (!layer.enabled || !comp.subLayerEnabled[i]) continue;
                transparent = comp.subLayerList[i];
                objects = layer.instances;

                cameras = layer.cameras;
                for (j = 0; j < cameras.length; j++) {
                    camera = cameras[j];
                    if (!camera) continue;
                    camera.frameBegin(layer.renderTarget);
                    drawCalls = transparent ? layer.transparentMeshInstances : layer.opaqueMeshInstances;

                    processedThisCamera = false;
                    processedThisCameraAndLayer = false;
                    for (k = 0; k < renderedLength; k++) {
                        if (renderedByCam[k] === camera) {
                            processedThisCamera = true;
                            if (renderedLayer[k] === layer) {
                                processedThisCameraAndLayer = true;
                                break;
                            }
                        }
                    }
                    if (!processedThisCamera) {
                        this.updateCameraFrustum(camera.camera); // update camera frustum once
                        this._camerasRendered++;
                    }
                    if (!processedThisCameraAndLayer) {
                        // cull each layer's lights once with each camera
                        // lights aren't collected anywhere, but marked as visible
                        this.cullLights(camera.camera, layer._lights);
                    }
                    if (!processedThisCamera || !processedThisCameraAndLayer) {
                        renderedByCam[renderedLength] = camera;
                        renderedLayer[renderedLength] = layer;
                        renderedLength++;
                    }

                    // cull mesh instances
                    // collected into layer arrays
                    // shared objects are only culled once
                    visible = transparent ? objects.visibleTransparent[j] : objects.visibleOpaque[j];
                    if (!visible.done) {
                        if (layer.onPreCull) {
                            layer.onPreCull(j);
                        }

                        visible.length = this.cull(camera.camera, drawCalls, visible.list);
                        visible.done = true;

                        if (layer.onPostCull) {
                            layer.onPostCull(j);
                        }

                    }

                    camera.frameEnd();
                }
            }

            // Shadowmap culling for directional and visible local lights
            // collected into light._visibleList
            // objects are also globally marked as visible
            // Also sets up local shadow camera matrices
            var light, casters;

            // Local lights
            // culled once for the whole frame

            // #ifdef PROFILER
            var cullTime = pc.now();
            // #endif

            for (i = 0; i < comp._lights.length; i++) {
                light = comp._lights[i];
                if (!light.visibleThisFrame) continue;
                if (light._type === pc.LIGHTTYPE_DIRECTIONAL) continue;
                if (!light.castShadows || !light._enabled || light.shadowUpdateMode === pc.SHADOWUPDATE_NONE) continue;
                casters = comp._lightShadowCasters[i];
                this.cullLocalShadowmap(light, casters);
            }

            // Directional lights
            // culled once for each camera
            renderedLength = 0;
            var globalLightCounter = -1;
            for (i = 0; i < comp._lights.length; i++) {
                light = comp._lights[i];
                if (light._type !== pc.LIGHTTYPE_DIRECTIONAL) continue;
                globalLightCounter++;
                if (!light.castShadows || !light._enabled || light.shadowUpdateMode === pc.SHADOWUPDATE_NONE) continue;
                casters = comp._lightShadowCasters[i];
                cameras = comp._globalLightCameras[globalLightCounter];
                for (j = 0; j < cameras.length; j++) {
                    this.cullDirectionalShadowmap(light, casters, cameras[j].camera, comp._globalLightCameraIds[globalLightCounter][j]);
                }
            }

            // #ifdef PROFILER
            this._cullTime += pc.now() - cullTime;
            // #endif

            // Can call script callbacks here and tell which objects are visible

            // GPU update for all visible objects
            this.gpuUpdate(comp._meshInstances);

            // Shadow render for all local visible culled lights
            this.renderShadows(comp._sortedLights[pc.LIGHTTYPE_SPOT]);
            this.renderShadows(comp._sortedLights[pc.LIGHTTYPE_POINT]);

            // Rendering
            renderedLength = 0;
            var cameraPass;
            var sortTime, draws, drawTime;
            for (i = 0; i < comp._renderList.length; i++) {
                layer = comp.layerList[comp._renderList[i]];
                if (!layer.enabled || !comp.subLayerEnabled[comp._renderList[i]]) continue;
                objects = layer.instances;
                transparent = comp.subLayerList[comp._renderList[i]];
                cameraPass = comp._renderListCamera[i];
                camera = layer.cameras[cameraPass];

                // #ifdef PROFILER
                drawTime = pc.now();
                // #endif

                if (camera) camera.frameBegin(layer.renderTarget);

                // Call prerender callback if there's one
                if (!transparent && layer.onPreRenderOpaque) {
                    layer.onPreRenderOpaque(cameraPass);
                } else if (transparent && layer.onPreRenderTransparent) {
                    layer.onPreRenderTransparent(cameraPass);
                }

                // Called for the first sublayer and for every camera
                if (!(layer._preRenderCalledForCameras & (1 << cameraPass))) {
                    if (layer.onPreRender) layer.onPreRender(cameraPass);
                    layer._preRenderCalledForCameras |= 1 << cameraPass;
                    if (layer.overrideClear) {
                        this.clearView(camera, layer.renderTarget, layer._clearOptions);
                    }
                }

                if (camera) {
                    // Each camera must only clear each render target once
                    rt = layer.renderTarget;
                    processedThisCameraAndRt = false;
                    for (k = 0; k < renderedLength; k++) {
                        if (renderedRt[k] === rt && renderedByCam[k] === camera) {
                            processedThisCameraAndRt = true;
                            break;
                        }
                    }

                    if (!processedThisCameraAndRt) {
                        // clear once per camera + RT
                        if (!layer.overrideClear) this.clearView(camera, layer.renderTarget); // TODO: deprecate camera.renderTarget?
                        renderedRt[renderedLength] = rt;
                        renderedByCam[renderedLength] = camera;
                        renderedLength++;
                    }

                    // Render directional shadows once for each camera (will reject more than 1 attempt in this function)

                    // #ifdef PROFILER
                    draws = this._shadowDrawCalls;
                    // #endif
                    this.renderShadows(layer._sortedLights[pc.LIGHTTYPE_DIRECTIONAL], cameraPass);
                    // #ifdef PROFILER
                    layer._shadowDrawCalls += this._shadowDrawCalls - draws;
                    // #endif

                    // #ifdef PROFILER
                    sortTime = pc.now();
                    // #endif

                    layer._sortVisible(transparent, camera.node, cameraPass);

                     // #ifdef PROFILER
                    this._sortTime += pc.now() - sortTime;
                     // #endif

                    visible = transparent ? objects.visibleTransparent[cameraPass] : objects.visibleOpaque[cameraPass];

                    // Set the not very clever global variable which is only useful when there's just one camera
                    this.scene._activeCamera = camera.camera;

                    // Set camera shader constants, viewport, scissor, render target
                    this.setCamera(camera.camera, layer.renderTarget);

                    // #ifdef PROFILER
                    draws = this._forwardDrawCalls;
                    // #endif
                    this.renderForward(camera.camera,
                                       visible.list,
                                       visible.length,
                                       layer._sortedLights,
                                       layer.shaderPass,
                                       layer.cullingMask,
                                       layer.onDrawCall,
                                       layer);
                    // #ifdef PROFILER
                    layer._forwardDrawCalls += this._forwardDrawCalls - draws;
                    // #endif

                    // Revert temp frame stuff
                    device.setColorWrite(true, true, true, true);
                    device.setStencilTest(false); // don't leak stencil state
                    device.setAlphaToCoverage(false); // don't leak a2c state
                    device.setDepthBias(false);

                    camera.frameEnd();
                }

                // Call postrender callback if there's one
                if (!transparent && layer.onPostRenderOpaque) {
                    layer.onPostRenderOpaque(cameraPass);
                } else if (transparent && layer.onPostRenderTransparent) {
                    layer.onPostRenderTransparent(cameraPass);
                }
                if (layer.onPostRender && !(layer._postRenderCalledForCameras & (1 << cameraPass))) {
                    layer._postRenderCounter &= ~(transparent ? 2 : 1);
                    if (layer._postRenderCounter === 0) {
                        layer.onPostRender(cameraPass);
                        layer._postRenderCalledForCameras |= 1 << cameraPass;
                        layer._postRenderCounter = layer._postRenderCounterMax;
                    }
                }

               // #ifdef PROFILER
                layer._renderTime += pc.now() - drawTime;
                // #endif
            }
        }
    });

    return {
        ForwardRenderer: ForwardRenderer,
        gaussWeights: gaussWeights
    };
}());