Source: scene/lightmapper.js

Object.assign(pc, function () {

    var maxSize = 2048;
    var maskBaked = 2;
    var maskLightmap = 4;

    var sceneLightmaps = [];
    var sceneLightmapsNode = [];
    var lmCamera;
    var tempVec = new pc.Vec3();
    var bounds = new pc.BoundingBox();
    var lightBounds = new pc.BoundingBox();
    var tempSphere = {};

    var PASS_COLOR = 0;
    var PASS_DIR = 1;

    var passTexName = ["texture_lightMap", "texture_dirLightMap"];
    var passMaterial = [];


    function collectModels(node, nodes, nodesMeshInstances, allNodes) {
        if (!node.enabled) return;

        var i;
        if (node.model && node.model.model && node.model.enabled) {
            if (allNodes) allNodes.push(node);
            if (node.model.lightmapped) {
                if (nodes) {
                    var hasUv1 = true;
                    var meshInstances = node.model.model.meshInstances;
                    for (i = 0; i < meshInstances.length; i++) {
                        if (!meshInstances[i].mesh.vertexBuffer.format.hasUv1) {
                            hasUv1 = false;
                            break;
                        }
                    }
                    if (hasUv1) {

                        var j;
                        var isInstance;
                        var notInstancedMeshInstances = [];
                        for (i = 0; i < meshInstances.length; i++) {
                            isInstance = false;
                            for (j = 0; j < meshInstances.length; j++) {
                                if (i !== j) {
                                    if (meshInstances[i].mesh === meshInstances[j].mesh) {
                                        isInstance = true;
                                    }
                                }
                            }
                            // collect each instance (object with shared VB) as separate "node"
                            if (isInstance) {
                                nodes.push(node);
                                nodesMeshInstances.push([meshInstances[i]]);
                            } else {
                                notInstancedMeshInstances.push(meshInstances[i]);
                            }
                        }

                        // collect all non-shared objects as one "node"
                        if (notInstancedMeshInstances.length > 0) {
                            nodes.push(node);
                            nodesMeshInstances.push(notInstancedMeshInstances);
                        }
                    }
                }
            }
        }
        for (i = 0; i < node._children.length; i++) {
            collectModels(node._children[i], nodes, nodesMeshInstances, allNodes);
        }
    }

    /**
     * @constructor
     * @name pc.Lightmapper
     * @classdesc The lightmapper is used to bake scene lights into textures.
     * @param {pc.GraphicsDevice} device The grahpics device used by the lightmapper.
     * @param {pc.Entity} root The root entity of the scene.
     * @param {pc.Scene} scene The scene to lightmap.
     * @param {pc.ForwardRenderer} renderer The renderer.
     * @param {Array} assets Array of assets to lightmap.
     */
    var Lightmapper = function (device, root, scene, renderer, assets) {
        this.device = device;
        this.root = root;
        this.scene = scene;
        this.renderer = renderer;
        this.assets = assets;

        // #ifdef PROFILER
        this._stats = {
            renderPasses: 0,
            lightmapCount: 0,
            totalRenderTime: 0,
            forwardTime: 0,
            fboTime: 0,
            shadowMapTime: 0,
            compileTime: 0,
            shadersLinked: 0
        };
        // #endif
    };

    Object.assign(Lightmapper.prototype, {
        destroy: function () {
            this.device = null;
            this.root = null;
            this.scene = null;
            this.renderer = null;
            this.assets = null;
        },

        calculateLightmapSize: function (node) {
            var data, parent;
            var sizeMult = this.scene.lightmapSizeMultiplier || 16;
            var scale = tempVec;
            var area = { x: 1, y: 1, z: 1, uv: 1 };

            if (node.model.asset) {
                data = this.assets.get(node.model.asset).data;
                if (data.area) {
                    area.x = data.area.x;
                    area.y = data.area.y;
                    area.z = data.area.z;
                    area.uv = data.area.uv;
                }
            } else if (node.model._area) {
                data = node.model;
                if (data._area) {
                    area.x = data._area.x;
                    area.y = data._area.y;
                    area.z = data._area.z;
                    area.uv = data._area.uv;
                }
            }
            var areaMult = node.model.lightmapSizeMultiplier || 1;
            area.x *= areaMult;
            area.y *= areaMult;
            area.z *= areaMult;

            scale.copy(node.localScale);
            parent = node._parent;
            while (parent) {
                scale.mul(parent.localScale);
                parent = parent._parent;
            }

            // Negatively scaled nodes still need full size lightmaps.
            scale.x = Math.abs(scale.x);
            scale.y = Math.abs(scale.y);
            scale.z = Math.abs(scale.z);

            var totalArea = area.x * scale.y * scale.z +
                            area.y * scale.x * scale.z +
                            area.z * scale.x * scale.y;
            totalArea /= area.uv;
            totalArea = Math.sqrt(totalArea);

            return Math.min(pc.math.nextPowerOfTwo(totalArea * sizeMult), this.scene.lightmapMaxResolution || maxSize);
        },

        /**
         * @function
         * @name pc.Lightmapper#bake
         * @description Generates and applies the lightmaps.
         * @param {pc.Entity} nodes An array of models to render lightmaps for. If not supplied, full scene will be baked.
         * @param {Number} mode Baking mode. Possible values:
         * <ul>
         *     <li>pc.BAKE_COLOR: single color lightmap
         *     <li>pc.BAKE_COLORDIR: single color lightmap + dominant light direction (used for bump/specular)
         * </ul>
         * Only lights with bakeDir=true will be used for generating the dominant light direction.
         */
        bake: function (nodes, mode) {

            // #ifdef PROFILER
            var startTime = pc.now();
            this.device.fire('lightmapper:start', {
                timestamp: startTime,
                target: this
            });
            var stats = this._stats;
            // #endif

            var i, j;
            var device = this.device;
            var scene = this.scene;

            var passCount = 1;
            if (mode === undefined) mode = pc.BAKE_COLORDIR;
            if (mode === pc.BAKE_COLORDIR) passCount = 2;
            var pass;

            // #ifdef PROFILER
            stats.renderPasses = stats.shadowMapTime = stats.forwardTime = 0;
            var startShaders = device._shaderStats.linked;
            var startFboTime = device._renderTargetCreationTime;
            var startCompileTime = device._shaderStats.compileTime;
            // #endif

            var allNodes = [];
            var nodesMeshInstances = [];
            if (!nodes) {
                // ///// Full bake /////

                // delete old lightmaps, if present
                for (i = 0; i < sceneLightmaps.length; i++) {
                    for (j = 0; j < sceneLightmaps[i].length; j++) {
                        sceneLightmaps[i][j].destroy();
                    }
                }
                sceneLightmaps = [];
                sceneLightmapsNode = [];

                // collect
                nodes = [];
                collectModels(this.root, nodes, nodesMeshInstances, allNodes);
            } else {
                // ///// Selected bake /////

                // delete old lightmaps, if present
                var k;
                for (i = sceneLightmapsNode.length - 1; i >= 0; i--) {
                    for (j = 0; j < nodes.length; j++) {
                        if (sceneLightmapsNode[i] === nodes[j]) {
                            for (k = 0; k < sceneLightmaps[i].length; k++) {
                                sceneLightmaps[i][k].destroy();
                            }
                            sceneLightmaps.splice(i, 1);
                            sceneLightmapsNode.splice(i, 1);
                        }
                    }
                }

                // collect
                var _nodes = [];
                for (i = 0; i < nodes.length; i++) {
                    collectModels(nodes[i], _nodes, nodesMeshInstances);
                }
                nodes = _nodes;

                collectModels(this.root, null, null, allNodes);
            }

            if (nodes.length === 0) {
                device.fire('lightmapper:end', {
                    timestamp: pc.now(),
                    target: this
                });

                return;
            }

            // #ifdef PROFILER
            stats.lightmapCount = nodes.length;
            // #endif

            // Disable static preprocessing (lightmapper needs original model draw calls)
            var revertStatic = false;
            if (scene._needsStaticPrepare) {
                scene._needsStaticPrepare = false;
                revertStatic = true;
            }

            // Calculate lightmap sizes and allocate textures
            var texSize = [];
            var lmaps = [[], []];
            var texPool = {};
            var size;
            var tex;
            var blackTex = new pc.Texture(this.device, {
                width: 4,
                height: 4,
                format: pc.PIXELFORMAT_R8_G8_B8_A8,
                rgbm: true
            });
            blackTex.name = 'lightmap';
            for (i = 0; i < nodes.length; i++) {
                size = this.calculateLightmapSize(nodes[i]);
                texSize.push(size);
                for (pass = 0; pass < passCount; pass++) {
                    tex = new pc.Texture(device, {
                        // #ifdef PROFILER
                        profilerHint: pc.TEXHINT_LIGHTMAP,
                        // #endif
                        width: size,
                        height: size,
                        format: pc.PIXELFORMAT_R8_G8_B8_A8,
                        mipmaps: false,
                        rgbm: (pass === PASS_COLOR),
                        minFilter: pc.FILTER_NEAREST,
                        magFilter: pc.FILTER_NEAREST
                    });
                    tex.name = 'lightmap';

                    lmaps[pass].push(tex);
                }

                if (!texPool[size]) {
                    var tex2 = new pc.Texture(device, {
                        // #ifdef PROFILER
                        profilerHint: pc.TEXHINT_LIGHTMAP,
                        // #endif
                        width: size,
                        height: size,
                        format: pc.PIXELFORMAT_R8_G8_B8_A8,
                        mipmaps: false,
                        rgbm: true,
                        minFilter: pc.FILTER_NEAREST,
                        magFilter: pc.FILTER_NEAREST
                    });
                    tex2.name = 'lightmap';

                    var targ2 = new pc.RenderTarget(device, tex2, {
                        depth: false
                    });
                    texPool[size] = targ2;
                }
            }

            var activeComp = scene.layers;
            activeComp._update();

            // Collect bakeable lights
            var lights = [];
            var origMask = [];
            var origShadowMode = [];
            var origEnabled = [];
            var sceneLights = activeComp._lights;
            var mask;
            for (i = 0; i < sceneLights.length; i++) {
                if (sceneLights[i]._enabled) {
                    mask = sceneLights[i]._mask;
                    if ((mask & maskLightmap) !== 0) {
                        origMask.push(mask);
                        origShadowMode.push(sceneLights[i].shadowUpdateMode);
                        sceneLights[i]._mask = 0xFFFFFFFF;
                        sceneLights[i].shadowUpdateMode =
                            sceneLights[i]._type === pc.LIGHTTYPE_DIRECTIONAL ? pc.SHADOWUPDATE_REALTIME : pc.SHADOWUPDATE_THISFRAME;
                        lights.push(sceneLights[i]);
                        sceneLights[i].isStatic = false; // if baked, can't be used as static
                    }
                }
                origEnabled.push(sceneLights[i]._enabled);
                sceneLights[i].enabled = false;
            }


            // Init shaders
            var chunks = pc.shaderChunks;
            var xformUv1 = "#define UV1LAYOUT\n" + chunks.transformVS;
            var bakeLmEnd = chunks.bakeLmEndPS;
            var dilate = chunks.dilatePS;

            var dilateShader = chunks.createShaderFromCode(device, chunks.fullscreenQuadVS, dilate, "lmDilate");
            var constantTexSource = device.scope.resolve("source");
            var constantPixelOffset = device.scope.resolve("pixelOffset");
            var constantBakeDir = device.scope.resolve("bakeDir");

            var pixelOffset = new Float32Array(2);

            var drawCalls = activeComp._meshInstances;

            // update scene matrices
            for (i = 0; i < drawCalls.length; i++) {
                if (drawCalls[i].node) drawCalls[i].node.getWorldTransform();
            }

            // Store scene values
            var origFog = scene.fog;
            var origAmbientR = scene.ambientLight.r;
            var origAmbientG = scene.ambientLight.g;
            var origAmbientB = scene.ambientLight.b;

            scene.fog = pc.FOG_NONE;
            scene.ambientLight.set(0, 0, 0);

            // Create pseudo-camera
            if (!lmCamera) {
                lmCamera = new pc.Camera();
                lmCamera._node = new pc.GraphNode();
                lmCamera.clearColor[0] = 0;
                lmCamera.clearColor[1] = 0;
                lmCamera.clearColor[2] = 0;
                lmCamera.clearColor[3] = 0;
                lmCamera.clearDepth = 1;
                lmCamera.clearFlags = pc.CLEARFLAG_COLOR;
                lmCamera.clearStencil = null;
                lmCamera.frustumCulling = false;
            }

            var node;
            var lm, rcv, m;

            // Disable existing scene lightmaps
            var origShaderDefs = [];
            origShaderDefs.length = sceneLightmapsNode.length;
            var shaderDefs;
            for (node = 0; node < allNodes.length; node++) {
                rcv = allNodes[node].model.model.meshInstances;
                shaderDefs = [];
                for (i = 0; i < rcv.length; i++) {
                    shaderDefs.push(rcv[i]._shaderDefs);
                    rcv[i]._shaderDefs &= ~(pc.SHADERDEF_LM | pc.SHADERDEF_DIRLM);
                }
                for (i = 0; i < sceneLightmapsNode.length; i++) {
                    if (sceneLightmapsNode[i] === allNodes[node]) {
                        origShaderDefs[i] = shaderDefs;
                        break;
                    }
                }
            }

            // Change shadow casting
            var origCastShadows = [];
            var casters = [];
            var meshes;
            for (node = 0; node < allNodes.length; node++) {
                origCastShadows[node] = allNodes[node].model.castShadows;
                allNodes[node].model.castShadows = allNodes[node].model.castShadowsLightmap;
                if (allNodes[node].model.castShadowsLightmap) {
                    meshes = allNodes[node].model.meshInstances;
                    for (i = 0; i < meshes.length; i++) {
                        meshes[i].visibleThisFrame = true;
                        casters.push(meshes[i]);
                    }
                }
            }

            this.renderer.updateCpuSkinMatrices(casters);
            this.renderer.gpuUpdate(casters);

            var origMat = [];

            // Prepare models
            var nodeBounds = [];
            var nodeTarg = [[], []];
            var targ, targTmp, texTmp;
            var light, shadowCam;
            var nodeLightCount = [];
            nodeLightCount.length = nodes.length;

            var lmMaterial;
            for (pass = 0; pass < passCount; pass++) {
                if (!passMaterial[pass]) {
                    lmMaterial = new pc.StandardMaterial();
                    lmMaterial.chunks.transformVS = xformUv1; // draw UV1

                    if (pass === PASS_COLOR) {
                        lmMaterial.chunks.endPS = bakeLmEnd; // encode to RGBM
                        // don't bake ambient
                        lmMaterial.ambient = new pc.Color(0, 0, 0);
                        lmMaterial.ambientTint = true;
                        lmMaterial.lightMap = blackTex;
                    } else {
                        lmMaterial.chunks.basePS = chunks.basePS + "\nuniform sampler2D texture_dirLightMap;\nuniform float bakeDir;\n";
                        lmMaterial.chunks.endPS = chunks.bakeDirLmEndPS;
                    }

                    // avoid writing unrelated things to alpha
                    lmMaterial.chunks.outputAlphaPS = "\n";
                    lmMaterial.chunks.outputAlphaOpaquePS = "\n";
                    lmMaterial.chunks.outputAlphaPremulPS = "\n";
                    lmMaterial.cull = pc.CULLFACE_NONE;
                    lmMaterial.forceUv1 = true; // provide data to xformUv1
                    lmMaterial.update();
                    lmMaterial.updateShader(device, scene);
                    lmMaterial.name = "lmMaterial" + pass;

                    passMaterial[pass] = lmMaterial;
                }
            }

            for (node = 0; node < nodes.length; node++) {
                rcv = nodesMeshInstances[node];
                nodeLightCount[node] = 0;

                // Calculate model AABB
                if (rcv.length > 0) {
                    bounds.copy(rcv[0].aabb);
                    for (i = 0; i < rcv.length; i++) {
                        rcv[i].node.getWorldTransform();
                        bounds.add(rcv[i].aabb);
                    }
                }
                var nbounds = new pc.BoundingBox();
                nbounds.copy(bounds);
                nodeBounds.push(nbounds);

                for (i = 0; i < rcv.length; i++) {
                    // patch meshInstance
                    m = rcv[i];
                    m._shaderDefs &= ~(pc.SHADERDEF_LM | pc.SHADERDEF_DIRLM); // disable LM define, if set, to get bare ambient on first pass
                    m.mask = maskLightmap; // only affected by LM lights
                    m.deleteParameter("texture_lightMap");
                    m.deleteParameter("texture_dirLightMap");

                    // patch material
                    m.setParameter("texture_lightMap", m.material.lightMap ? m.material.lightMap : blackTex);
                    m.setParameter("texture_dirLightMap", blackTex);
                }

                for (pass = 0; pass < passCount; pass++) {
                    lm = lmaps[pass][node];
                    targ = new pc.RenderTarget(device, lm, {
                        depth: false
                    });
                    nodeTarg[pass].push(targ);
                }
            }

            // Disable all bakeable lights
            for (j = 0; j < lights.length; j++)
                lights[j].enabled = false;

            var lightArray = [[], [], []];

            // Accumulate lights into RGBM textures
            var shadersUpdatedOn1stPass = false;
            var shadowMapRendered;
            for (i = 0; i < lights.length; i++) {

                lights[i].enabled = true; // enable next light
                shadowMapRendered = false;

                lights[i]._cacheShadowMap = true;
                if (lights[i]._type !== pc.LIGHTTYPE_DIRECTIONAL) {
                    lights[i]._node.getWorldTransform();
                    lights[i].getBoundingSphere(tempSphere);
                    lightBounds.center = tempSphere.center;
                    lightBounds.halfExtents.x = tempSphere.radius;
                    lightBounds.halfExtents.y = tempSphere.radius;
                    lightBounds.halfExtents.z = tempSphere.radius;
                }
                if (lights[i]._type === pc.LIGHTTYPE_SPOT) {
                    light = lights[i];
                    shadowCam = this.renderer.getShadowCamera(device, light);

                    shadowCam._node.setPosition(light._node.getPosition());
                    shadowCam._node.setRotation(light._node.getRotation());
                    shadowCam._node.rotateLocal(-90, 0, 0);

                    shadowCam.projection = pc.PROJECTION_PERSPECTIVE;
                    shadowCam.nearClip = light.attenuationEnd / 1000;
                    shadowCam.farClip = light.attenuationEnd;
                    shadowCam.aspectRatio = 1;
                    shadowCam.fov = light._outerConeAngle * 2;

                    this.renderer.updateCameraFrustum(shadowCam);
                }

                if (nodesMeshInstances.length > 0) {
                    this.renderer.updateShaders(nodesMeshInstances[0]);
                }

                for (node = 0; node < nodes.length; node++) {

                    rcv = nodesMeshInstances[node];
                    bounds = nodeBounds[node];

                    // Tweak camera to fully see the model, so directional light frustum will also see it
                    if (lights[i]._type === pc.LIGHTTYPE_DIRECTIONAL) {
                        tempVec.copy(bounds.center);
                        tempVec.y += bounds.halfExtents.y;

                        lmCamera._node.setPosition(tempVec);
                        lmCamera._node.setEulerAngles(-90, 0, 0);

                        var frustumSize = Math.max(bounds.halfExtents.x, bounds.halfExtents.z);

                        lmCamera.projection = pc.PROJECTION_ORTHOGRAPHIC;
                        lmCamera.nearClip = 0;
                        lmCamera.farClip = bounds.halfExtents.y * 2;
                        lmCamera.aspectRatio = 1;
                        lmCamera.orthoHeight = frustumSize;
                    } else {
                        if (!lightBounds.intersects(bounds)) {
                            continue;
                        }
                    }

                    if (lights[i]._type === pc.LIGHTTYPE_SPOT) {
                        var nodeVisible = false;
                        for (j = 0; j < rcv.length; j++) {
                            if (this.renderer._isVisible(shadowCam, rcv[j])) {
                                nodeVisible = true;
                                break;
                            }
                        }
                        if (!nodeVisible) {
                            continue;
                        }
                    }

                    if (lights[i]._type === pc.LIGHTTYPE_DIRECTIONAL) {
                        lightArray[pc.LIGHTTYPE_DIRECTIONAL][0] = lights[i];
                        lightArray[pc.LIGHTTYPE_POINT].length = 0;
                        lightArray[pc.LIGHTTYPE_SPOT].length = 0;
                        if (!shadowMapRendered && lights[i].castShadows) {
                            this.renderer.cullDirectionalShadowmap(lights[i], casters, lmCamera, 0);
                            this.renderer.renderShadows(lightArray[pc.LIGHTTYPE_DIRECTIONAL], 0);
                            shadowMapRendered = true;
                        }
                    } else {
                        lightArray[pc.LIGHTTYPE_DIRECTIONAL].length = 0;
                        if (lights[i]._type === pc.LIGHTTYPE_POINT) {
                            lightArray[pc.LIGHTTYPE_POINT][0] = lights[i];
                            lightArray[pc.LIGHTTYPE_SPOT].length = 0;
                            if (!shadowMapRendered && lights[i].castShadows) {
                                this.renderer.cullLocalShadowmap(lights[i], casters);
                                this.renderer.renderShadows(lightArray[pc.LIGHTTYPE_POINT]);
                                shadowMapRendered = true;
                            }
                        } else {
                            lightArray[pc.LIGHTTYPE_POINT].length = 0;
                            lightArray[pc.LIGHTTYPE_SPOT][0] = lights[i];
                            if (!shadowMapRendered && lights[i].castShadows) {
                                this.renderer.cullLocalShadowmap(lights[i], casters);
                                this.renderer.renderShadows(lightArray[pc.LIGHTTYPE_SPOT]);
                                shadowMapRendered = true;
                            }
                        }
                    }

                    // Store original materials
                    for (j = 0; j < rcv.length; j++) {
                        origMat[j] = rcv[j].material;
                    }

                    for (pass = 0; pass < passCount; pass++) {
                        lm = lmaps[pass][node];
                        targ = nodeTarg[pass][node];
                        targTmp = texPool[lm.width];
                        texTmp = targTmp.colorBuffer;

                        if (pass === 0) {
                            shadersUpdatedOn1stPass = scene.updateShaders;
                        } else if (shadersUpdatedOn1stPass) {
                            scene.updateShaders = true;
                        }

                        for (j = 0; j < rcv.length; j++) {
                            rcv[j].material = passMaterial[pass];
                        }
                        if (passCount > 1) {
                            this.renderer.updateShaders(rcv); // update between passes
                        }

                        // ping-ponging output
                        this.renderer.setCamera(lmCamera, targTmp, true);

                        if (pass === PASS_DIR) {
                            constantBakeDir.setValue(lights[i].bakeDir ? 1 : 0);
                        }

                        // console.log("Baking light "+lights[i]._node.name + " on model " + nodes[node].name);

                        this.renderer._forwardTime = 0;
                        this.renderer._shadowMapTime = 0;

                        this.renderer.renderForward(lmCamera,
                                                    rcv, rcv.length,
                                                    lightArray,
                                                    pc.SHADER_FORWARDHDR);

                        // #ifdef PROFILER
                        stats.shadowMapTime += this.renderer._shadowMapTime;
                        stats.forwardTime += this.renderer._forwardTime;
                        stats.renderPasses++;
                        // #endif

                        lmaps[pass][node] = texTmp;
                        nodeTarg[pass][node] = targTmp;
                        texPool[lm.width] = targ;

                        for (j = 0; j < rcv.length; j++) {
                            m = rcv[j];
                            m.setParameter(passTexName[pass], texTmp); // ping-ponging input
                            m._shaderDefs |= pc.SHADERDEF_LM; // force using LM even if material doesn't have it
                        }
                    }

                    nodeLightCount[node]++;

                    // Revert original materials
                    for (j = 0; j < rcv.length; j++) {
                        rcv[j].material = origMat[j];
                    }
                }

                lights[i].enabled = false; // disable that light
                lights[i]._cacheShadowMap = false;
                if (lights[i]._isCachedShadowMap) {
                    lights[i]._destroyShadowMap();
                }
            }


            var sceneLmaps;
            for (node = 0; node < nodes.length; node++) {
                rcv = nodesMeshInstances[node];
                sceneLmaps = [];

                for (pass = 0; pass < passCount; pass++) {
                    lm = lmaps[pass][node];
                    targ = nodeTarg[pass][node];
                    targTmp = texPool[lm.width];
                    texTmp = targTmp.colorBuffer;

                    // Dilate
                    var numDilates2x = 4; // 8 dilates
                    pixelOffset[0] = 1 / lm.width;
                    pixelOffset[1] = 1 / lm.height;
                    constantPixelOffset.setValue(pixelOffset);
                    for (i = 0; i < numDilates2x; i++) {
                        constantTexSource.setValue(lm);
                        pc.drawQuadWithShader(device, targTmp, dilateShader);

                        constantTexSource.setValue(texTmp);
                        pc.drawQuadWithShader(device, targ, dilateShader);
                    }


                    for (i = 0; i < rcv.length; i++) {
                        m = rcv[i];
                        m.mask = maskBaked;

                        // Set lightmap
                        rcv[i].setParameter(passTexName[pass], lm);
                        if (pass === PASS_DIR) rcv[i]._shaderDefs |= pc.SHADERDEF_DIRLM;
                    }
                    sceneLmaps[pass] = lm;

                    // Clean up
                    if (pass === passCount - 1) targ.destroy();
                }

                sceneLightmaps.push(sceneLmaps);
                sceneLightmapsNode.push(nodes[node]);
            }

            for (var key in texPool) {
                if (texPool.hasOwnProperty(key)) {
                    texPool[key].colorBuffer.destroy();
                    texPool[key].destroy();
                }
            }

            // Set up linear filtering
            for (i = 0; i < sceneLightmaps.length; i++) {
                for (j = 0; j < sceneLightmaps[i].length; j++) {
                    tex = sceneLightmaps[i][j];
                    tex.minFilter = pc.FILTER_LINEAR;
                    tex.magFilter = pc.FILTER_LINEAR;
                }
            }

            // Revert shadow casting
            for (node = 0; node < allNodes.length; node++) {
                allNodes[node].model.castShadows = origCastShadows[node];
            }

            // Enable existing scene lightmaps
            for (i = 0; i < origShaderDefs.length; i++) {
                if (origShaderDefs[i]) {
                    rcv = sceneLightmapsNode[i].model.model.meshInstances;
                    for (j = 0; j < rcv.length; j++) {
                        rcv[j]._shaderDefs |= origShaderDefs[i][j] & (pc.SHADERDEF_LM | pc.SHADERDEF_DIRLM);
                    }
                }
            }

            // Enable all lights back
            for (i = 0; i < lights.length; i++) {
                lights[i]._mask = origMask[i];
                lights[i].shadowUpdateMode = origShadowMode[i];
            }

            for (i = 0; i < sceneLights.length; i++) {
                sceneLights[i].enabled = origEnabled[i];
            }

            // Roll back scene stuff
            scene.fog = origFog;
            scene.ambientLight.set(origAmbientR, origAmbientG, origAmbientB);

            // Revert static preprocessing
            if (revertStatic) {
                scene._needsStaticPrepare = true;
            }

            // #ifdef PROFILER
            this.device.fire('lightmapper:end', {
                timestamp: pc.now(),
                target: this
            });

            stats.totalRenderTime = pc.now() - startTime;
            stats.shadersLinked = device._shaderStats.linked - startShaders;
            stats.compileTime = device._shaderStats.compileTime - startCompileTime;
            stats.fboTime = device._renderTargetCreationTime - startFboTime;
            // #endif
        }
    });

    return {
        Lightmapper: Lightmapper
    };
}());