Source: graphics/program-lib/standard.js

var _oldChunkWarn = function (oldName, newName) {
    // #ifdef DEBUG
    console.warn("Shader chunk " + oldName + " is deprecated - override " + newName + " instead");
    // #endif
};

var _oldChunkFloat = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef MAPFLOAT\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkColor = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef MAPCOLOR\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTex = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef MAPTEXTURE\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTexColor = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "#undef MAPTEXTURECOLOR\n#ifdef MAPTEXTURE\n#ifdef MAPCOLOR\n#define MAPTEXTURECOLOR\n#endif\n#endif\n" +
            "#ifdef MAPTEXTURECOLOR\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTexFloat = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "#undef MAPTEXTUREFLOAT\n#ifdef MAPTEXTURE\n#ifdef MAPFLOAT\n#define MAPTEXTUREFLOAT\n#endif\n#endif\n" +
            "#ifdef MAPTEXTUREFLOAT\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkVert = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef MAPVERTEX\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkVertColor = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "#undef MAPVERTEXCOLOR\n#ifdef MAPVERTEX\n#ifdef MAPCOLOR\n#define MAPVERTEXCOLOR\n#endif\n#endif\n" +
            "#ifdef MAPVERTEXCOLOR\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkVertFloat = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "#undef MAPVERTEXFLOAT\n#ifdef MAPVERTEX\n#ifdef MAPFLOAT\n#define MAPVERTEXFLOAT\n#endif\n#endif\n" +
            "#ifdef MAPVERTEXFLOAT\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformSkin = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef SKIN\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformDynbatch = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef DYNAMICBATCH\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformInstanced = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef INSTANCING\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformPixelSnap = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef PIXELSNAP\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformScreenSpace = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef SCREENSPACE\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformScreenSpaceBatch = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "#undef SCREENSPACEBATCH\n#ifdef SCREENSPACE\n#ifdef BATCH\n#define SCREENSPACEBATCH\n#endif\n#endif\n" +
            "#ifdef SCREENSPACEBATCH\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};

var _oldChunkTransformUv1 = function (s, o, p) {
    _oldChunkWarn(p, o);
    return "\n#ifdef UV1LAYOUT\n" + s + "\n#else\n" + pc.shaderChunks[o] + "\n#endif\n";
};


pc.programlib.standard = {

    _oldChunkToNew: {
        aoTexPS: { n: "aoPS", f: _oldChunkTex },
        aoVertPS: { n: "aoPS", f: _oldChunkVert },

        diffuseConstPS: { n: "diffusePS", f: _oldChunkColor },
        diffuseTexPS: { n: "diffusePS", f: _oldChunkTex },
        diffuseTexConstPS: { n: "diffusePS", f: _oldChunkTexColor },
        diffuseVertPS: { n: "diffusePS", f: _oldChunkVert },
        diffuseVertConstPS: { n: "diffusePS", f: _oldChunkVertColor },

        emissiveConstPS: { n: "emissivePS", f: _oldChunkColor },
        emissiveTexPS: { n: "emissivePS", f: _oldChunkTex },
        emissiveTexConstPS: { n: "emissivePS", f: _oldChunkTexColor },
        emissiveTexConstFloatPS: { n: "emissivePS", f: _oldChunkTexFloat },
        emissiveVertPS: { n: "emissivePS", f: _oldChunkVert },
        emissiveVertConstPS: { n: "emissivePS", f: _oldChunkVertColor },
        emissiveVertConstFloatPS: { n: "emissivePS", f: _oldChunkVertFloat },

        glossConstPS: { n: "glossPS", f: _oldChunkFloat },
        glossTexPS: { n: "glossPS", f: _oldChunkTex },
        glossTexConstPS: { n: "glossPS", f: _oldChunkTexFloat },
        glossVertPS: { n: "glossPS", f: _oldChunkVert },
        glossVertConstPS: { n: "glossPS", f: _oldChunkVertFloat },

        metalnessConstPS: { n: "metalnessPS", f: _oldChunkFloat },
        metalnessTexPS: { n: "metalnessPS", f: _oldChunkTex },
        metalnessTexConstPS: { n: "metalnessPS", f: _oldChunkTexFloat },
        metalnessVertPS: { n: "metalnessPS", f: _oldChunkVert },
        metalnessVertConstPS: { n: "metalnessPS", f: _oldChunkVertFloat },

        opacityConstPS: { n: "opacityPS", f: _oldChunkFloat },
        opacityTexPS: { n: "opacityPS", f: _oldChunkTex },
        opacityTexConstPS: { n: "opacityPS", f: _oldChunkTexFloat },
        opacityVertPS: { n: "opacityPS", f: _oldChunkVert },
        opacityVertConstPS: { n: "opacityPS", f: _oldChunkVertFloat },

        specularConstPS: { n: "specularPS", f: _oldChunkColor },
        specularTexPS: { n: "specularPS", f: _oldChunkTex },
        specularTexConstPS: { n: "specularPS", f: _oldChunkTexColor },
        specularVertPS: { n: "specularPS", f: _oldChunkVert },
        specularVertConstPS: { n: "specularPS", f: _oldChunkVertColor },

        transformBatchSkinnedVS: { n: "transformVS", f: _oldChunkTransformDynbatch },
        transformInstancedVS: { n: "transformVS", f: _oldChunkTransformInstanced },
        transformPixelSnapVS: { n: "transformVS", f: _oldChunkTransformPixelSnap },
        transformScreenSpaceVS: { n: "transformVS", f: _oldChunkTransformScreenSpace },
        transformScreenSpaceBatchSkinned: { n: "transformVS", f: _oldChunkTransformScreenSpaceBatch },
        transformSkinned: { n: "transformVS", f: _oldChunkTransformSkin },
        transformUv1: { n: "transformVS", f: _oldChunkTransformUv1 }
    },

    // Shared Sandard Material option structures
    optionsContext: {},
    optionsContextMin: {},

    generateKey: function (options) {
        var buildPropertiesList = function (options) {
            var props = [];
            for (var prop in options) {
                if (options.hasOwnProperty(prop) && prop !== "chunks" && prop !== "lights")
                    props.push(prop);
            }
            return props.sort();
        };
        var props;
        if (options === this.optionsContextMin) {
            if (!this.propsMin) this.propsMin = buildPropertiesList(options);
            props = this.propsMin;
        } else if (options === this.optionsContext) {
            if (!this.props) this.props = buildPropertiesList(options);
            props = this.props;
        } else {
            props = buildPropertiesList(options);
        }

        var key = "standard";

        var i;
        for (i = 0; i < props.length; i++) {
            if (options[props[i]])
                key += props[i] + options[props[i]];
        }

        if (options.chunks) {
            var chunks = [];
            for (var p in options.chunks) {
                if (options.chunks.hasOwnProperty(p)) {
                    chunks.push(p + options.chunks[p]);
                }
            }
            chunks.sort();
            key += chunks;
        }

        if (options.lights) {
            for (i = 0; i < options.lights.length; i++) {
                key += options.lights[i].key;
            }
        }

        return pc.hashCode(key);
    },

    _correctChannel: function (p, chan) {
        if (pc._matTex2D[p] > 0) {
            if (pc._matTex2D[p] < chan.length) {
                return chan.substring(0, pc._matTex2D[p]);
            } else if (pc._matTex2D[p] > chan.length) {
                var str = chan;
                var chr = str.charAt(str.length - 1);
                var addLen = pc._matTex2D[p] - str.length;
                for (var i = 0; i < addLen; i++) str += chr;
                return str;
            }
            return chan;
        }
    },

    _setMapTransform: function (codes, name, id, uv) {
        codes[0] += "uniform vec4 texture_" + name + "MapTransform;\n";

        var checkId = id + uv * 100;
        if (!codes[3][checkId]) {
            codes[1] += "varying vec2 vUV" + uv + "_" + id + ";\n";
            codes[2] += "   vUV" + uv + "_" + id + " = uv" + uv + " * texture_" + name + "MapTransform.xy + texture_" + name + "MapTransform.zw;\n";
            codes[3][checkId] = true;
        }
        return codes;
    },

    // get the value to replace $UV with in Map Shader functions

    /**
     * @private
     * @function
     * @name _getUvSourceExpression
     * @description Get the code with which to to replace '$UV' in the map shader functions
     * @param  {String} transformPropName Name of the transform id in the options block. Usually "basenameTransform"
     * @param  {String} uVPropName        Name of the UV channel in the options block. Usually "basenameUv"
     * @param  {Object} options           The options passed into createShaderDefinition
     * @returns {String}                   The code used to replace "$UV" in the shader code
     */
    _getUvSourceExpression: function (transformPropName, uVPropName, options) {
        var transformId = options[transformPropName];
        var uvChannel = options[uVPropName];

        var expression;

        if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_SLICED) {
            expression = "nineSlicedUv";
        } else if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_TILED) {
            expression = "nineSlicedUv, -1000.0";
        } else {
            if (transformId === 0) {
                expression = "vUv" + uvChannel;
            } else {
                // note: different capitalization!
                expression = "vUV" + uvChannel + "_" + transformId;
            }

            // if heightmap is enabled all maps except the heightmap are offset
            if (options.heightMap && transformPropName !== "heightMapTransform") {
                expression += " + dUvOffset";
            }
        }

        return expression;
    },

    _addMapDef: function (name, enabled) {
        var s = "\n#undef " + name + "\n";
        if (enabled) s += " #define " + name + "\n";
        return s;
    },

    _addMapDefs: function (float, color, vertex, map) {
        var s = "";
        s += this._addMapDef("MAPFLOAT", float);
        s += this._addMapDef("MAPCOLOR", color);
        s += this._addMapDef("MAPVERTEX", vertex);
        s += this._addMapDef("MAPTEXTURE", map);
        return s;
    },
    /**
     * @private
     * @function
     * @name  _addMap
     * @description Add chunk for Map Types (used for all maps except Normal)
     * @param {String} propName      The base name of the map: diffuse | emissive | opacity | light | height | metalness | specular | gloss | ao
     * @param {String} chunkName     The name of the chunk to use. Usually "basenamePS"
     * @param {Object} options       The options passed into to createShaderDefinition
     * @param {Object} chunks        The set of shader chunks to choose from
     * @param {String} samplerFormat Format of texture sampler to use - 0: "texture2DSRGB", 1: "texture2DRGBM", 2: "texture2D"
     * @returns {String} The shader code to support this map
     */
    _addMap: function (propName, chunkName, options, chunks, samplerFormat) {
        var mapPropName = propName + "Map";

        var uVPropName = mapPropName + "Uv";
        var transformPropName = mapPropName + "Transform";
        var channelPropName = mapPropName + "Channel";
        var vertexColorChannelPropName = propName + "VertexColorChannel";
        var tintPropName = propName + "Tint";
        var vertexColorPropName = propName + "VertexColor";

        var tintOption = options[tintPropName];
        var vertexColorOption = options[vertexColorPropName];
        var textureOption = options[mapPropName];

        var subCode = chunks[chunkName];

        if (textureOption) {
            var uv = this._getUvSourceExpression(transformPropName, uVPropName, options);

            subCode = subCode.replace(/\$UV/g, uv).replace(/\$CH/g, options[channelPropName]);

            if (samplerFormat !== undefined) {
                var fmt = samplerFormat === 0 ? "texture2DSRGB" : (samplerFormat === 1 ? "texture2DRGBM" : "texture2D");
                subCode = subCode.replace(/\$texture2DSAMPLE/g, fmt);
            }
        }

        if (vertexColorOption) {
            subCode = subCode.replace(/\$VC/g, options[vertexColorChannelPropName]);
        }

        var isFloatTint = (tintOption === 1);
        var isVecTint = (tintOption === 3);

        subCode = this._addMapDefs(isFloatTint, isVecTint, vertexColorOption, textureOption) + subCode;
        return subCode.replace(/\$/g, "");
    },

    _nonPointShadowMapProjection: function (device, light, shadowCoordArgs) {
        if (!light._normalOffsetBias || light._isVsm) {
            if (light._type === pc.LIGHTTYPE_SPOT) {
                if (light._isPcf && (device.webgl2 || device.extStandardDerivatives)) {
                    return "       getShadowCoordPerspZbuffer" + shadowCoordArgs;
                }
                return "       getShadowCoordPersp" + shadowCoordArgs;
            }
            return "       getShadowCoordOrtho" + shadowCoordArgs;
        }
        if (light._type === pc.LIGHTTYPE_SPOT) {
            if (light._isPcf && (device.webgl2 || device.extStandardDerivatives)) {
                return "       getShadowCoordPerspZbufferNormalOffset" + shadowCoordArgs;
            }
            return "       getShadowCoordPerspNormalOffset" + shadowCoordArgs;
        }
        return "       getShadowCoordOrthoNormalOffset" + shadowCoordArgs;
    },

    _addVaryingIfNeeded: function (code, type, name) {
        return code.indexOf(name) >= 0 ? ("varying " + type + " " + name + ";\n") : "";
    },

    _vsAddTransformCode: function (code, device, chunks, options) {
        code += chunks.transformVS;

        return code;
    },

    _vsAddBaseCode: function (code, device, chunks, options) {
        code += chunks.baseVS;
        if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_SLICED ||
            options.nineSlicedMode === pc.SPRITE_RENDERMODE_TILED) {
            code += chunks.baseNineSlicedVS;
        }

        return code;
    },

    /**
     * @private
     * @function
     * @name _fsAddBaseCode
     * @description Add "Base" Code section to fragment shader
     * @param  {String} code Current fragment shader code
     * @param  {pc.GraphicsDevice} device The graphics device
     * @param  {Object} chunks All available shader chunks
     * @param  {Object} options The Shader Definition options
     * @returns {String} The new fragment shader code (old+new)
     */
    _fsAddBaseCode: function (code, device, chunks, options) {
        code += chunks.basePS;
        if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_SLICED) {
            code += chunks.baseNineSlicedPS;
        } else if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_TILED) {
            code += chunks.baseNineSlicedTiledPS;
        }

        return code;
    },

    /**
     * @private
     * @function
     * @name  _fsAddStartCode
     * @description Add "Start" Code section to fragment shader
     * @param  {String} code  Current fragment shader code
     * @param  {pc.GraphicsDevice} device The graphics device
     * @param  {Object} chunks All available shader chunks
     * @param  {Object} options The Shader Definition options
     * @returns {String} The new fragment shader code (old+new)
     */
    _fsAddStartCode: function (code, device, chunks, options) {
        code += chunks.startPS;

        if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_SLICED) {
            code += chunks.startNineSlicedPS;
        } else if (options.nineSlicedMode === pc.SPRITE_RENDERMODE_TILED) {
            code += chunks.startNineSlicedTiledPS;
        }

        return code;
    },

    createShaderDefinition: function (device, options) {
        var i, p;
        var lighting = options.lights.length > 0;

        if (options.dirLightMap) {
            lighting = true;
            options.useSpecular = true;
        }

        if (options.shadingModel === pc.SPECULAR_PHONG) {
            options.fresnelModel = 0;
            options.specularAntialias = false;
            options.prefilteredCubemap = false;
            options.dpAtlas = false;
            options.ambientSH = false;
        } else {
            options.fresnelModel = (options.fresnelModel === 0) ? pc.FRESNEL_SCHLICK : options.fresnelModel;
        }

        var cubemapReflection = (options.cubeMap || (options.prefilteredCubemap && options.useSpecular)) && !options.sphereMap && !options.dpAtlas;
        var reflections = options.sphereMap || cubemapReflection || options.dpAtlas;
        var useTexCubeLod = options.useTexCubeLod;
        if (options.cubeMap) options.sphereMap = null; // cubeMaps have higher priority
        if (options.dpAtlas) options.prefilteredCubemap = null; // dp has even higher priority
        if (!options.useSpecular) options.specularMap = options.glossMap = null;
        var needsNormal = lighting || reflections || options.ambientSH || options.prefilteredCubemap || options.heightMap;
        var shadowPass = options.pass >= pc.SHADER_SHADOW && options.pass <= 17;

        this.options = options;

        // GENERATE VERTEX SHADER
        var code = '';
        var codeBody = '';

        var varyings = ""; // additional varyings for map transforms

        var chunks = pc.shaderChunks;

        var lightType;
        var shadowCoordArgs;
        var chunk;

        var attributes = {
            vertex_position: pc.SEMANTIC_POSITION
        };

        if (options.chunks) {
            var customChunks = {};
            var newP;
            for (p in chunks) {
                if (chunks.hasOwnProperty(p)) {
                    if (!options.chunks[p]) {
                        customChunks[p] = chunks[p];
                    } else {
                        chunk = options.chunks[p];
                        // scan for attributes in custom code
                        if (chunk.indexOf("vertex_normal") >= 0) {
                            attributes.vertex_normal = pc.SEMANTIC_NORMAL;
                        }
                        if (chunk.indexOf("vertex_tangent") >= 0) {
                            attributes.vertex_tangent = pc.SEMANTIC_TANGENT;
                        }
                        if (chunk.indexOf("vertex_texCoord0") >= 0) {
                            attributes.vertex_texCoord0 = pc.SEMANTIC_TEXCOORD0;
                        }
                        if (chunk.indexOf("vertex_texCoord1") >= 0) {
                            attributes.vertex_texCoord1 = pc.SEMANTIC_TEXCOORD1;
                        }
                        if (chunk.indexOf("vertex_color") >= 0) {
                            attributes.vertex_color = pc.SEMANTIC_COLOR;
                        }
                        if (chunk.indexOf("vertex_boneWeights") >= 0) {
                            attributes.vertex_boneWeights = pc.SEMANTIC_BLENDWEIGHT;
                        }
                        if (chunk.indexOf("vertex_boneIndices") >= 0) {
                            attributes.vertex_boneIndices = pc.SEMANTIC_BLENDINDICES;
                        }
                        customChunks[p] = chunk;
                    }
                }
            }

            for (p in options.chunks) {
                newP = this._oldChunkToNew[p];
                if (newP) {
                    customChunks[newP.n] = newP.f(options.chunks[p], newP.n, p);
                }
            }

            chunks = customChunks;
        }


        // code += chunks.baseVS;
        code = this._vsAddBaseCode(code, device, chunks, options);

        // Allow first shadow coords to be computed in VS
        var mainShadowLight = -1;
        if (!options.noShadow && !options.twoSidedLighting) {
            for (i = 0; i < options.lights.length; i++) {
                lightType = options.lights[i]._type;
                if (options.lights[i].castShadows) {
                    if (lightType === pc.LIGHTTYPE_DIRECTIONAL) {
                        code += "uniform mat4 light" + i + "_shadowMatrixVS;\n";
                        code += "uniform vec3 light" + i + "_shadowParamsVS;\n";
                        code += "uniform vec3 light" + i + (lightType === pc.LIGHTTYPE_DIRECTIONAL ? "_directionVS" : "_positionVS") + ";\n";
                        mainShadowLight = i;
                        break;
                    }
                }
            }
            if (mainShadowLight >= 0) {
                code += chunks.shadowCoordVS;
            }
        }

        codeBody += "   vPositionW    = getWorldPosition();\n";

        if (options.pass === pc.SHADER_DEPTH) {
            code += 'varying float vDepth;\n';
            code += '#ifndef VIEWMATRIX\n';
            code += '#define VIEWMATRIX\n';
            code += 'uniform mat4 matrix_view;\n';
            code += '#endif\n';
            code += '#ifndef CAMERAPLANES\n';
            code += '#define CAMERAPLANES\n';
            code += 'uniform vec4 camera_params;\n\n';
            code += '#endif\n';
            codeBody += "    vDepth = -(matrix_view * vec4(vPositionW,1.0)).z * camera_params.x;\n";
        }

        if (options.useInstancing) {
            attributes.instance_line1 = pc.SEMANTIC_TEXCOORD2;
            attributes.instance_line2 = pc.SEMANTIC_TEXCOORD3;
            attributes.instance_line3 = pc.SEMANTIC_TEXCOORD4;
            attributes.instance_line4 = pc.SEMANTIC_TEXCOORD5;
            code += chunks.instancingVS;
        }

        if (needsNormal) {
            attributes.vertex_normal = pc.SEMANTIC_NORMAL;
            codeBody += "   vNormalW    = dNormalW = getNormal();\n";

            if ((options.sphereMap) && (device.fragmentUniformsCount <= 16)) {
                code += chunks.viewNormalVS;
                codeBody += "   vNormalV    = getViewNormal();\n";
            }

            if ((options.heightMap || options.normalMap) && options.hasTangents) {
                attributes.vertex_tangent = pc.SEMANTIC_TANGENT;
                code += chunks.tangentBinormalVS;
                codeBody += "   vTangentW   = getTangent();\n";
                codeBody += "   vBinormalW  = getBinormal();\n";
            }

            if (mainShadowLight >= 0) {
                lightType = options.lights[mainShadowLight]._type;
                if (lightType === pc.LIGHTTYPE_DIRECTIONAL) {
                    codeBody += "   dLightDirNormW = light" + mainShadowLight + "_directionVS;\n";
                } else {
                    codeBody += "   getLightDirPoint(light" + mainShadowLight + "_positionVS);\n";
                }
                shadowCoordArgs = "(light" + mainShadowLight + "_shadowMatrixVS, light" + mainShadowLight + "_shadowParamsVS);\n";
                codeBody += this._nonPointShadowMapProjection(device, options.lights[mainShadowLight], shadowCoordArgs);
            }
        }

        var useUv = [];
        var useUnmodifiedUv = [];
        var maxUvSets = 2;
        var cname, mname, tname, uname;

        for (p in pc._matTex2D) {
            mname = p + "Map";
            if (options[p + "VertexColor"]) {
                cname = p + "VertexColorChannel";
                options[cname] = this._correctChannel(p, options[cname]);
            }
            if (options[mname]) {
                cname = mname + "Channel";
                tname = mname + "Transform";
                uname = mname + "Uv";
                options[uname] = Math.min(options[uname], maxUvSets - 1);
                options[cname] = this._correctChannel(p, options[cname]);
                var uvSet = options[uname];
                useUv[uvSet] = true;
                useUnmodifiedUv[uvSet] = useUnmodifiedUv[uvSet] || (options[mname] && !options[tname]);
            }
        }

        if (options.forceUv1) useUv[1] = true;

        for (i = 0; i < maxUvSets; i++) {
            if (useUv[i]) {
                attributes["vertex_texCoord" + i] = pc["SEMANTIC_TEXCOORD" + i];
                code += chunks["uv" + i + "VS"];
                codeBody += "   vec2 uv" + i + " = getUv" + i + "();\n";
            }
            if (useUnmodifiedUv[i]) {
                codeBody += "   vUv" + i + " = uv" + i + ";\n";
            }
        }

        var codes = [code, varyings, codeBody, []];

        for (p in pc._matTex2D) {
            mname = p + "Map";
            if (options[mname]) {
                tname = mname + "Transform";
                if (options[tname]) {
                    uname = mname + "Uv";
                    this._setMapTransform(codes, p, options[tname], options[uname]);
                }
            }
        }

        code = codes[0];
        varyings = codes[1];
        codeBody = codes[2];

        if (options.vertexColors) {
            attributes.vertex_color = pc.SEMANTIC_COLOR;
            codeBody += "   vVertexColor = vertex_color;\n";
        }

        if (options.skin) {
            attributes.vertex_boneWeights = pc.SEMANTIC_BLENDWEIGHT;
            attributes.vertex_boneIndices = pc.SEMANTIC_BLENDINDICES;
            code += pc.programlib.skinCode(device, chunks);
            code += "#define SKIN\n";
        } else if (options.useInstancing) {
            code += "#define INSTANCING\n";
        }
        if (options.screenSpace) {
            code += "#define SCREENSPACE\n";
        }
        if (options.pixelSnap) {
            code += "#define PIXELSNAP\n";
        }

        code = this._vsAddTransformCode(code, device, chunks, options);

        if (needsNormal) code += chunks.normalVS;

        code += "\n";

        code += chunks.startVS;
        code += codeBody;
        code += "}";

        var vshader = code;

        var oldVars = varyings;
        varyings = "";
        varyings += this._addVaryingIfNeeded(code, "vec4", "vMainShadowUv");
        varyings += this._addVaryingIfNeeded(code, "vec4", "vVertexColor");
        varyings += this._addVaryingIfNeeded(code, "vec3", "vPositionW");
        varyings += this._addVaryingIfNeeded(code, "vec3", "vNormalV");
        varyings += this._addVaryingIfNeeded(code, "vec3", "vNormalW");
        varyings += this._addVaryingIfNeeded(code, "vec3", "vTangentW");
        varyings += this._addVaryingIfNeeded(code, "vec3", "vBinormalW");
        varyings += this._addVaryingIfNeeded(code, "vec2", "vUv0");
        varyings += this._addVaryingIfNeeded(code, "vec2", "vUv1");
        varyings += oldVars;
        vshader = varyings + vshader;

        var startCode = "";
        if (device.webgl2) {
            startCode = pc.programlib.versionCode(device);
            if (chunks.extensionVS) {
                startCode += chunks.extensionVS + "\n";
            }
            vshader = startCode + chunks.gles3VS + vshader;
        } else {
            if (chunks.extensionVS) {
                startCode = chunks.extensionVS + "\n";
            }
            vshader = startCode + vshader;
        }

        // GENERATE FRAGMENT SHADER
        if (options.forceFragmentPrecision && options.forceFragmentPrecision != "highp" &&
            options.forceFragmentPrecision !== "mediump" && options.forceFragmentPrecision !== "lowp")
            options.forceFragmentPrecision = null;

        if (options.forceFragmentPrecision) {
            if (options.forceFragmentPrecision === "highp" && device.maxPrecision !== "highp") options.forceFragmentPrecision = "mediump";
            if (options.forceFragmentPrecision === "mediump" && device.maxPrecision === "lowp") options.forceFragmentPrecision = "lowp";
        }

        var fshader;
        code = '';

        if (device.webgl2) {
            code += pc.programlib.versionCode(device);
        }

        if (device.extStandardDerivatives && !device.webgl2) {
            code += "#extension GL_OES_standard_derivatives : enable\n\n";
        }
        if (chunks.extensionPS) {
            code += chunks.extensionPS + "\n";
        }

        if (device.webgl2) {
            code += chunks.gles3PS;
        }

        code += options.forceFragmentPrecision ? "precision " + options.forceFragmentPrecision + " float;\n\n" : pc.programlib.precisionCode(device);

        if (options.pass === pc.SHADER_PICK) {
            // ##### PICK PASS #####
            code += "uniform vec4 uColor;";
            code += varyings;
            if (options.alphaTest) {
                code += "float dAlpha;\n";
                code += this._addMap("opacity", "opacityPS", options, chunks);
                code += chunks.alphaTestPS;
            }
            code += pc.programlib.begin();
            if (options.alphaTest) {
                code += "   getOpacity();\n";
                code += "   alphaTest(dAlpha);\n";
            }
            code += "    gl_FragColor = uColor;\n";
            code += pc.programlib.end();
            return {
                attributes: attributes,
                vshader: vshader,
                fshader: code
            };

        } else if (options.pass === pc.SHADER_DEPTH) {
            // ##### SCREEN DEPTH PASS #####
            code += 'varying float vDepth;\n';
            code += varyings;
            code += chunks.packDepthPS;
            if (options.alphaTest) {
                code += "float dAlpha;\n";
                code += this._addMap("opacity", "opacityPS", options, chunks);
                code += chunks.alphaTestPS;
            }
            code += pc.programlib.begin();
            if (options.alphaTest) {
                code += "   getOpacity();\n";
                code += "   alphaTest(dAlpha);\n";
            }
            code += "    gl_FragColor = packFloat(vDepth);\n";
            code += pc.programlib.end();
            return {
                attributes: attributes,
                vshader: vshader,
                fshader: code
            };

        } else if (shadowPass) {
            // ##### SHADOW PASS #####
            var smode = options.pass - pc.SHADER_SHADOW;
            var numShadowModes = 5;
            lightType = Math.floor(smode / numShadowModes);
            var shadowType = smode - lightType * numShadowModes;

            if (device.extStandardDerivatives && !device.webgl2) {
                code += 'uniform vec2 polygonOffset;\n';
            }

            if (shadowType === pc.SHADOW_VSM32) {
                if (device.textureFloatHighPrecision) {
                    code += '#define VSM_EXPONENT 15.0\n\n';
                } else {
                    code += '#define VSM_EXPONENT 5.54\n\n';
                }
            } else if (shadowType === pc.SHADOW_VSM16) {
                code += '#define VSM_EXPONENT 5.54\n\n';
            }

            if (lightType !== pc.LIGHTTYPE_DIRECTIONAL) {
                code += 'uniform vec3 view_position;\n';
                code += 'uniform float light_radius;\n';
            }

            code += varyings;
            if (options.alphaTest) {
                code += "float dAlpha;\n";
                code += this._addMap("opacity", "opacityPS", options, chunks);
                code += chunks.alphaTestPS;
            }

            if (shadowType === pc.SHADOW_PCF3 && (!device.webgl2 || lightType === pc.LIGHTTYPE_POINT)) {
                code += chunks.packDepthPS;
            } else if (shadowType === pc.SHADOW_VSM8) {
                code += "vec2 encodeFloatRG( float v ) {\n";
                code += "    vec2 enc = vec2(1.0, 255.0) * v;\n";
                code += "    enc = fract(enc);\n";
                code += "    enc -= enc.yy * vec2(1.0/255.0, 1.0/255.0);\n";
                code += "    return enc;\n";
                code += "}\n\n";
            }

            code += pc.programlib.begin();

            if (options.alphaTest) {
                code += "   getOpacity();\n";
                code += "   alphaTest(dAlpha);\n";
            }

            var isVsm = shadowType === pc.SHADOW_VSM8 || shadowType === pc.SHADOW_VSM16 || shadowType === pc.SHADOW_VSM32;

            if (lightType === pc.LIGHTTYPE_POINT || (isVsm && lightType !== pc.LIGHTTYPE_DIRECTIONAL)) {
                code += "   float depth = min(distance(view_position, vPositionW) / light_radius, 0.99999);\n";
            } else {
                code += "   float depth = gl_FragCoord.z;\n";
            }

            if (shadowType === pc.SHADOW_PCF3 && (!device.webgl2 || lightType === pc.LIGHTTYPE_POINT)) {
                if (device.extStandardDerivatives && !device.webgl2) {
                    code += "   float minValue = 2.3374370500153186e-10; //(1.0 / 255.0) / (256.0 * 256.0 * 256.0);\n";
                    code += "   depth += polygonOffset.x * max(abs(dFdx(depth)), abs(dFdy(depth))) + minValue * polygonOffset.y;\n";
                    code += "   gl_FragColor = packFloat(depth);\n";
                } else {
                    code += "   gl_FragColor = packFloat(depth);\n";
                }
            } else if (shadowType === pc.SHADOW_PCF3 || shadowType === pc.SHADOW_PCF5) {
                code += "   gl_FragColor = vec4(1.0);\n"; // just the simpliest code, color is not written anyway
            } else if (shadowType === pc.SHADOW_VSM8) {
                code += "   gl_FragColor = vec4(encodeFloatRG(depth), encodeFloatRG(depth*depth));\n";
            } else {
                code += chunks.storeEVSMPS;
            }

            code += pc.programlib.end();

            return {
                attributes: attributes,
                vshader: vshader,
                fshader: code
            };
        }

        if (options.customFragmentShader) {
            // ##### CUSTOM PS #####
            fshader = code + options.customFragmentShader;
            return {
                attributes: attributes,
                vshader: vshader,
                fshader: fshader,
                tag: pc.SHADERTAG_MATERIAL
            };
        }

        // ##### FORWARD/FORWARDHDR PASS #####
        code += varyings;

        // code += chunks.basePS;
        code = this._fsAddBaseCode(code, device, chunks, options);

        var codeBegin = code;
        code = "";

        // FRAGMENT SHADER INPUTS: UNIFORMS
        var numShadowLights = 0;
        var shadowTypeUsed = [];
        var useVsm = false;
        var usePerspZbufferShadow = false;
        var light;
        for (i = 0; i < options.lights.length; i++) {
            light = options.lights[i];
            lightType = light._type;
            code += "uniform vec3 light" + i + "_color;\n";
            if (lightType === pc.LIGHTTYPE_DIRECTIONAL) {
                code += "uniform vec3 light" + i + "_direction;\n";
            } else {
                code += "uniform vec3 light" + i + "_position;\n";
                code += "uniform float light" + i + "_radius;\n";
                if (lightType === pc.LIGHTTYPE_SPOT) {
                    code += "uniform vec3 light" + i + "_direction;\n";
                    code += "uniform float light" + i + "_innerConeAngle;\n";
                    code += "uniform float light" + i + "_outerConeAngle;\n";
                }
            }
            if (light.castShadows && !options.noShadow) {
                code += "uniform mat4 light" + i + "_shadowMatrix;\n";
                if (lightType !== pc.LIGHTTYPE_DIRECTIONAL) {
                    code += "uniform vec4 light" + i + "_shadowParams;\n"; // Width, height, bias, radius
                } else {
                    code += "uniform vec3 light" + i + "_shadowParams;\n"; // Width, height, bias
                }
                if (lightType === pc.LIGHTTYPE_POINT) {
                    code += "uniform samplerCube light" + i + "_shadowMap;\n";
                } else {
                    if (light._isPcf && device.webgl2) {
                        code += "uniform sampler2DShadow light" + i + "_shadowMap;\n";
                    } else {
                        code += "uniform sampler2D light" + i + "_shadowMap;\n";
                    }
                }
                numShadowLights++;
                shadowTypeUsed[light._shadowType] = true;
                if (light._isVsm) useVsm = true;
                if (light._isPcf && (device.webgl2 || device.extStandardDerivatives) && lightType === pc.LIGHTTYPE_SPOT) usePerspZbufferShadow = true;
            }
            if (light._cookie) {
                if (light._cookie._cubemap) {
                    if (lightType === pc.LIGHTTYPE_POINT) {
                        code += "uniform samplerCube light" + i + "_cookie;\n";
                        code += "uniform float light" + i + "_cookieIntensity;\n";
                        if (!light.castShadows || options.noShadow) code += "uniform mat4 light" + i + "_shadowMatrix;\n";
                    }
                } else {
                    if (lightType === pc.LIGHTTYPE_SPOT) {
                        code += "uniform sampler2D light" + i + "_cookie;\n";
                        code += "uniform float light" + i + "_cookieIntensity;\n";
                        if (!light.castShadows || options.noShadow) code += "uniform mat4 light" + i + "_shadowMatrix;\n";
                        if (light._cookieTransform) {
                            code += "uniform vec4 light" + i + "_cookieMatrix;\n";
                            code += "uniform vec2 light" + i + "_cookieOffset;\n";
                        }
                    }
                }
            }
        }

        code += "\n"; // End of uniform declarations


        var tbn;
        if (!options.hasTangents) {
            tbn = chunks.TBNderivativePS;
        } else if (options.fastTbn) {
            tbn = chunks.TBNfastPS;
        } else {
            tbn = chunks.TBNPS;
        }

        if (needsNormal) {
            if (options.normalMap) {
                code += options.packedNormal ? chunks.normalXYPS : chunks.normalXYZPS;

                var transformedNormalMapUv = this._getUvSourceExpression("normalMapTransform", "normalMapUv", options);
                if (options.needsNormalFloat) {
                    code += (options.fastTbn ? chunks.normalMapFloatTBNfastPS : chunks.normalMapFloatPS).replace(/\$UV/g, transformedNormalMapUv);
                } else {
                    code += chunks.normalMapPS.replace(/\$UV/g, transformedNormalMapUv);
                }
                if (!options.hasTangents) tbn = tbn.replace(/\$UV/g, transformedNormalMapUv);
                code += tbn;
            } else {
                code += chunks.normalVertexPS;
            }
        }

        code += pc.programlib.gammaCode(options.gamma, chunks);
        code += pc.programlib.tonemapCode(options.toneMap, chunks);
        code += pc.programlib.fogCode(options.fog, chunks);

        if (options.useRgbm) code += chunks.rgbmPS;
        if (cubemapReflection || options.prefilteredCubemap) {
            code += options.fixSeams ? chunks.fixCubemapSeamsStretchPS : chunks.fixCubemapSeamsNonePS;
        }

        if (needsNormal) {
            code += options.cubeMapProjection > 0 ? chunks.cubeMapProjectBoxPS : chunks.cubeMapProjectNonePS;
            code += options.skyboxIntensity ? chunks.envMultiplyPS : chunks.envConstPS;
        }

        code += this._addMap("diffuse", "diffusePS", options, chunks);
        if (options.blendType !== pc.BLEND_NONE || options.alphaTest || options.alphaToCoverage) {
            code += this._addMap("opacity", "opacityPS", options, chunks);
        }
        code += this._addMap("emissive", "emissivePS", options, chunks, options.emissiveFormat);

        if (options.useSpecular && (lighting || reflections)) {
            if (options.specularAntialias && options.normalMap) {
                if (options.needsNormalFloat && needsNormal) {
                    code += chunks.specularAaToksvigFloatPS;
                } else {
                    code += chunks.specularAaToksvigPS;
                }
            } else {
                code += chunks.specularAaNonePS;
            }

            var specularPropName = options.useMetalness ? "metalness" : "specular";
            code += this._addMap(specularPropName, specularPropName + "PS", options, chunks);
            code += this._addMap("gloss", "glossPS", options, chunks);
            if (options.fresnelModel > 0) {
                if (options.fresnelModel === pc.FRESNEL_SIMPLE) {
                    code += chunks.fresnelSimplePS;
                } else if (options.fresnelModel === pc.FRESNEL_SCHLICK) {
                    code += chunks.fresnelSchlickPS;
                } else if (options.fresnelModel === pc.FRESNEL_COMPLEX) {
                    code += chunks.fresnelComplexPS;
                }
            }
        }

        if (options.heightMap) {
            if (!options.normalMap) {
                var transformedHeightMapUv = this._getUvSourceExpression("heightMapTransform", "heightMapUv", options);
                if (!options.hasTangents) tbn = tbn.replace(/\$UV/g, transformedHeightMapUv);
                code += tbn;
            }
            code += this._addMap("height", "parallaxPS", options, chunks);
        }

        var useAo = options.aoMap || options.aoVertexColor;
        if (useAo) {
            code += this._addMap("ao", "aoPS", options, chunks);
            if (options.occludeSpecular) {
                if (options.occludeSpecular === pc.SPECOCC_AO) {
                    code += options.occludeSpecularFloat ? chunks.aoSpecOccSimplePS : chunks.aoSpecOccConstSimplePS;
                } else {
                    code += options.occludeSpecularFloat ? chunks.aoSpecOccPS : chunks.aoSpecOccConstPS;
                }
            }
        }

        var reflectionDecode = options.rgbmReflection ? "decodeRGBM" : (options.hdrReflection ? "" : "gammaCorrectInput");

        if (options.sphereMap) {
            var scode = device.fragmentUniformsCount > 16 ? chunks.reflectionSpherePS : chunks.reflectionSphereLowPS;
            scode = scode.replace(/\$texture2DSAMPLE/g, options.rgbmReflection ? "texture2DRGBM" : (options.hdrReflection ? "texture2D" : "texture2DSRGB"));
            code += scode;
        } else if (cubemapReflection) {
            if (options.prefilteredCubemap) {
                if (useTexCubeLod) {
                    code += chunks.reflectionPrefilteredCubeLodPS.replace(/\$DECODE/g, reflectionDecode);

                } else {
                    code += chunks.reflectionPrefilteredCubePS.replace(/\$DECODE/g, reflectionDecode);
                }
            } else {
                code += chunks.reflectionCubePS.replace(/\$textureCubeSAMPLE/g,
                                                        options.rgbmReflection ? "textureCubeRGBM" : (options.hdrReflection ? "textureCube" : "textureCubeSRGB"));
            }
        } else if (options.dpAtlas) {
            code += chunks.reflectionDpAtlasPS.replace(/\$texture2DSAMPLE/g, options.rgbmReflection ? "texture2DRGBM" : (options.hdrReflection ? "texture2D" : "texture2DSRGB"));
        }

        if ((cubemapReflection || options.sphereMap || options.dpAtlas) && options.refraction) {
            code += chunks.refractionPS;
        }

        if (numShadowLights > 0) {
            if (shadowTypeUsed[pc.SHADOW_PCF3]) {
                code += chunks.shadowStandardPS;
            }
            if (shadowTypeUsed[pc.SHADOW_PCF5]) {
                code += chunks.shadowStandardGL2PS;
            }
            if (useVsm) {
                code += chunks.shadowVSM_commonPS;
                if (shadowTypeUsed[pc.SHADOW_VSM8]) {
                    code += chunks.shadowVSM8PS;
                }
                if (shadowTypeUsed[pc.SHADOW_VSM16]) {
                    code += device.extTextureHalfFloatLinear ? chunks.shadowEVSMPS.replace(/\$/g, "16") : chunks.shadowEVSMnPS.replace(/\$/g, "16");
                }
                if (shadowTypeUsed[pc.SHADOW_VSM32]) {
                    code += device.extTextureFloatLinear ? chunks.shadowEVSMPS.replace(/\$/g, "32") : chunks.shadowEVSMnPS.replace(/\$/g, "32");
                }
            }

            if (device.webgl2 || device.extStandardDerivatives) {
                // bias is applied on render
            } else {
                code += chunks.biasConstPS;
            }
            code += chunks.shadowCoordPS + chunks.shadowCommonPS;
            if (usePerspZbufferShadow) code += chunks.shadowCoordPerspZbufferPS;

            if (mainShadowLight >= 0) {
                if (shadowTypeUsed[pc.SHADOW_PCF3]) {
                    code += chunks.shadowStandardVSPS;
                }
                if (shadowTypeUsed[pc.SHADOW_PCF5]) {
                    code += chunks.shadowStandardGL2VSPS;
                }
                if (useVsm) {
                    if (shadowTypeUsed[pc.SHADOW_VSM8]) {
                        code += chunks.shadowVSMVSPS.replace(/\$VSM/g, "VSM8").replace(/\$/g, "8");
                    }
                    if (shadowTypeUsed[pc.SHADOW_VSM16]) {
                        code += chunks.shadowVSMVSPS.replace(/\$VSM/g, "VSM16").replace(/\$/g, "16");
                    }
                    if (shadowTypeUsed[pc.SHADOW_VSM32]) {
                        code += chunks.shadowVSMVSPS.replace(/\$VSM/g, "VSM32").replace(/\$/g, "32");
                    }
                }
            }
        }

        if (lighting) code += chunks.lightDiffuseLambertPS;
        var useOldAmbient = false;
        if (options.useSpecular) {
            if (lighting) code += options.shadingModel === pc.SPECULAR_PHONG ? chunks.lightSpecularPhongPS : chunks.lightSpecularBlinnPS;
            if (options.sphereMap || cubemapReflection || options.dpAtlas || (options.fresnelModel > 0)) {
                if (options.fresnelModel > 0) {
                    if (options.conserveEnergy) {
                        code += chunks.combineDiffuseSpecularPS; // this one is correct, others are old stuff
                    } else {
                        code += chunks.combineDiffuseSpecularNoConservePS; // if you don't use environment cubemaps, you may consider this
                    }
                } else {
                    code += chunks.combineDiffuseSpecularOldPS;
                }
            } else {
                if (options.diffuseMap) {
                    code += chunks.combineDiffuseSpecularNoReflPS;
                } else {
                    code += chunks.combineDiffuseSpecularNoReflSeparateAmbientPS;
                    useOldAmbient = true;
                }
            }
        } else {
            code += chunks.combineDiffusePS;
        }

        var addAmbient = true;
        if (options.lightMap || options.lightVertexColor) {
            var lightmapChunkPropName = options.dirLightMap ? 'lightmapDirPS' : 'lightmapSinglePS';
            code += this._addMap("light", lightmapChunkPropName, options, chunks, options.lightMapFormat);
            addAmbient = options.lightMapWithoutAmbient;
        }

        if (addAmbient) {

            var ambientDecode = options.rgbmAmbient ? "decodeRGBM" : (options.hdrAmbient ? "" : "gammaCorrectInput");

            if (options.ambientSH) {
                code += chunks.ambientSHPS;
            } else if (options.prefilteredCubemap) {
                if (useTexCubeLod) {
                    code += chunks.ambientPrefilteredCubeLodPS.replace(/\$DECODE/g, ambientDecode);
                } else {
                    code += chunks.ambientPrefilteredCubePS.replace(/\$DECODE/g, ambientDecode);
                }
            } else {
                code += chunks.ambientConstantPS;
            }
        }

        if (options.ambientTint && !useOldAmbient) {
            code += "uniform vec3 material_ambient;\n";
        }

        if (options.alphaTest) {
            code += chunks.alphaTestPS;
        }

        if (options.msdf) {
            code += chunks.msdfPS;
        }

        if (needsNormal) {
            code += chunks.viewDirPS;
            if (options.useSpecular) {
                code += chunks.reflDirPS;
            }
        }
        var hasPointLights = false;
        var usesLinearFalloff = false;
        var usesInvSquaredFalloff = false;
        var usesSpot = false;
        var usesCookie = false;
        var usesCookieNow;

        // FRAGMENT SHADER BODY

        code = this._fsAddStartCode(code, device, chunks, options);

        if (needsNormal) {
            if (options.twoSidedLighting) {
                code += "   dVertexNormalW = gl_FrontFacing ? vNormalW : -vNormalW;\n";
            } else {
                code += "   dVertexNormalW = vNormalW;\n";
            }
            if ((options.heightMap || options.normalMap) && options.hasTangents) {
                if (options.twoSidedLighting) {
                    code += "   dTangentW = gl_FrontFacing ? vTangentW : -vTangentW;\n";
                    code += "   dBinormalW = gl_FrontFacing ? vBinormalW : -vBinormalW;\n";
                } else {
                    code += "   dTangentW = vTangentW;\n";
                    code += "   dBinormalW = vBinormalW;\n";
                }
            }
        }

        var opacityParallax = false;
        if (options.blendType === pc.BLEND_NONE && !options.alphaTest && !options.alphaToCoverage) {
            code += "   dAlpha = 1.0;\n";
        } else {
            if (options.heightMap && options.opacityMap) {
                opacityParallax = true;
            } else {
                code += "   getOpacity();\n"; // calculate opacity first if there's no parallax+opacityMap, to allow early out
                if (options.alphaTest) {
                    code += "   alphaTest(dAlpha);\n";
                }
            }
        }

        if (needsNormal) {
            code += "   getViewDir();\n";
            if (options.heightMap || options.normalMap) {
                code += "   getTBN();\n";
            }
            if (options.heightMap) {
                code += "   getParallax();\n";
            }

            if (opacityParallax) {
                code += "   getOpacity();\n"; // if there's parallax, calculate opacity after it, to properly distort
                if (options.alphaTest) {
                    code += "   alphaTest(dAlpha);\n";
                }
            }

            code += "   getNormal();\n";
            if (options.useSpecular) code += "   getReflDir();\n";
        }

        code += "   getAlbedo();\n";

        if ((lighting && options.useSpecular) || reflections) {
            code += "   getSpecularity();\n";
            code += "   getGlossiness();\n";
            if (options.fresnelModel > 0) code += "   getFresnel();\n";
        }

        if (addAmbient) {
            code += "   addAmbient();\n";
        }
        if (options.ambientTint && !useOldAmbient) {
            code += "   dDiffuseLight *= material_ambient;\n";
        }
        if (useAo && !options.occludeDirect) {
            code += "    applyAO();\n";
        }
        if (options.lightMap || options.lightVertexColor) {
            code += "   addLightMap();\n";
        }

        if (lighting || reflections) {
            if (cubemapReflection || options.sphereMap || options.dpAtlas) {
                code += "   addReflection();\n";
            }

            if (options.dirLightMap) {
                code += "   addDirLightMap();\n";
            }

            for (i = 0; i < options.lights.length; i++) {
                // The following code is not decoupled to separate shader files, because most of it can be actually changed to achieve different behaviours like:
                // - different falloffs
                // - different shadow coords (point shadows will use drastically different genShadowCoord)
                // - different shadow filter modes

                // getLightDiffuse and getLightSpecular is BRDF itself.

                light = options.lights[i];
                lightType = light._type;
                usesCookieNow = false;

                if (lightType === pc.LIGHTTYPE_DIRECTIONAL) {
                    // directional
                    code += "   dLightDirNormW = light" + i + "_direction;\n";
                    code += "   dAtten = 1.0;\n";
                } else {

                    if (light._cookie) {
                        if (lightType === pc.LIGHTTYPE_SPOT && !light._cookie._cubemap) {
                            usesCookie = true;
                            usesCookieNow = true;
                        } else if (lightType === pc.LIGHTTYPE_POINT && light._cookie._cubemap) {
                            usesCookie = true;
                            usesCookieNow = true;
                        }
                    }

                    code += "   getLightDirPoint(light" + i + "_position);\n";
                    hasPointLights = true;

                    if (usesCookieNow) {
                        if (lightType === pc.LIGHTTYPE_SPOT) {
                            code += "   dAtten3 = getCookie2D" + (light._cookieFalloff ? "" : "Clip") + (light._cookieTransform ? "Xform" : "") + "(light" + i + "_cookie, light" + i + "_shadowMatrix, light" + i + "_cookieIntensity" + (light._cookieTransform ? ", light" + i + "_cookieMatrix, light" + i + "_cookieOffset" : "") + ")." + light._cookieChannel + ";\n";
                        } else {
                            code += "   dAtten3 = getCookieCube(light" + i + "_cookie, light" + i + "_shadowMatrix, light" + i + "_cookieIntensity)." + light._cookieChannel + ";\n";
                        }
                    }

                    if (light._falloffMode === pc.LIGHTFALLOFF_LINEAR) {
                        code += "   dAtten = getFalloffLinear(light" + i + "_radius);\n";
                        usesLinearFalloff = true;
                    } else {
                        code += "   dAtten = getFalloffInvSquared(light" + i + "_radius);\n";
                        usesInvSquaredFalloff = true;
                    }

                    code += "   if (dAtten > 0.00001) {\n"; // BRANCH START

                    if (lightType === pc.LIGHTTYPE_SPOT) {
                        if (!(usesCookieNow && !light._cookieFalloff)) {
                            code += "       dAtten *= getSpotEffect(light" + i + "_direction, light" + i + "_innerConeAngle, light" + i + "_outerConeAngle);\n";
                            usesSpot = true;
                        }
                    }
                }

                code += "       dAtten *= getLightDiffuse();\n";
                if (light.castShadows && !options.noShadow) {

                    var shadowReadMode = null;
                    var evsmExp;
                    if (light._shadowType === pc.SHADOW_VSM8) {
                        shadowReadMode = "VSM8";
                        evsmExp = "0.0";
                    } else if (light._shadowType === pc.SHADOW_VSM16) {
                        shadowReadMode = "VSM16";
                        evsmExp = "5.54";
                    } else if (light._shadowType === pc.SHADOW_VSM32) {
                        shadowReadMode = "VSM32";
                        if (device.textureFloatHighPrecision) {
                            evsmExp = "15.0";
                        } else {
                            evsmExp = "5.54";
                        }
                    } else if (light._shadowType === pc.SHADOW_PCF5) {
                        shadowReadMode = "PCF5x5";
                    } else {
                        shadowReadMode = "PCF3x3";
                    }

                    if (shadowReadMode !== null) {
                        if (lightType === pc.LIGHTTYPE_POINT) {
                            shadowCoordArgs = "(light" + i + "_shadowMap, light" + i + "_shadowParams);\n";
                            if (light._normalOffsetBias) {
                                code += "       normalOffsetPointShadow(light" + i + "_shadowParams);\n";
                            }
                            code += "       dAtten *= getShadowPoint" + shadowReadMode + shadowCoordArgs;
                        } else {
                            if (mainShadowLight === i) {
                                shadowReadMode += "VS";
                            } else {
                                shadowCoordArgs = "(light" + i + "_shadowMatrix, light" + i + "_shadowParams);\n";
                                code += this._nonPointShadowMapProjection(device, options.lights[i], shadowCoordArgs);
                            }
                            if (lightType === pc.LIGHTTYPE_SPOT) shadowReadMode = "Spot" + shadowReadMode;
                            code += "       dAtten *= getShadow" + shadowReadMode + "(light" + i + "_shadowMap, light" + i + "_shadowParams" + (light._isVsm ? ", " + evsmExp : "") + ");\n";
                        }
                    }
                }

                code += "       dDiffuseLight += dAtten * light" + i + "_color" + (usesCookieNow ? " * dAtten3" : "") + ";\n";

                if (options.useSpecular) {
                    code += "       dAtten *= getLightSpecular();\n";
                    code += "       dSpecularLight += dAtten * light" + i + "_color" + (usesCookieNow ? " * dAtten3" : "") + ";\n";
                }

                if (lightType !== pc.LIGHTTYPE_DIRECTIONAL) {
                    code += "   }\n"; // BRANCH END
                }

                code += "\n";
            }

            if ((cubemapReflection || options.sphereMap || options.dpAtlas) && options.refraction) {
                code += "   addRefraction();\n";
            }
        }
        code += "\n";

        if (useAo) {
            if (options.occludeDirect) {
                code += "    applyAO();\n";
            }
            if (options.occludeSpecular) {
                code += "    occludeSpecular();\n";
            }
        }

        code += chunks.endPS;
        if (options.blendType === pc.BLEND_NORMAL || options.blendType === pc.BLEND_ADDITIVEALPHA || options.alphaToCoverage) {
            code += chunks.outputAlphaPS;
        } else if (options.blendType === pc.BLEND_PREMULTIPLIED) {
            code += chunks.outputAlphaPremulPS;
        } else {
            code += chunks.outputAlphaOpaquePS;
        }

        if (options.msdf) {
            code += "   gl_FragColor = applyMsdf(gl_FragColor);\n";
        }

        code += "\n";
        code += pc.programlib.end();

        if (hasPointLights) {
            code = chunks.lightDirPointPS + code;
        }
        if (usesLinearFalloff) {
            code = chunks.falloffLinearPS + code;
        }
        if (usesInvSquaredFalloff) {
            code = chunks.falloffInvSquaredPS + code;
        }
        if (usesSpot) {
            code = chunks.spotPS + code;
        }
        if (usesCookie) {
            code = chunks.cookiePS + code;
        }
        var structCode = "";
        if (code.includes("dReflection")) structCode += "vec4 dReflection;\n";
        if (code.includes("dTBN")) structCode += "mat3 dTBN;\n";
        if (code.includes("dAlbedo")) structCode += "vec3 dAlbedo;\n";
        if (code.includes("dEmission")) structCode += "vec3 dEmission;\n";
        if (code.includes("dNormalW")) structCode += "vec3 dNormalW;\n";
        if (code.includes("dVertexNormalW")) structCode += "vec3 dVertexNormalW;\n";
        if (code.includes("dTangentW")) structCode += "vec3 dTangentW;\n";
        if (code.includes("dBinormalW")) structCode += "vec3 dBinormalW;\n";
        if (code.includes("dViewDirW")) structCode += "vec3 dViewDirW;\n";
        if (code.includes("dReflDirW")) structCode += "vec3 dReflDirW;\n";
        if (code.includes("dDiffuseLight")) structCode += "vec3 dDiffuseLight;\n";
        if (code.includes("dSpecularLight")) structCode += "vec3 dSpecularLight;\n";
        if (code.includes("dLightDirNormW")) structCode += "vec3 dLightDirNormW;\n";
        if (code.includes("dLightDirW")) structCode += "vec3 dLightDirW;\n";
        if (code.includes("dLightPosW")) structCode += "vec3 dLightPosW;\n";
        if (code.includes("dShadowCoord")) structCode += "vec3 dShadowCoord;\n";
        if (code.includes("dNormalMap")) structCode += "vec3 dNormalMap;\n";
        if (code.includes("dSpecularity")) structCode += "vec3 dSpecularity;\n";
        if (code.includes("dUvOffset")) structCode += "vec2 dUvOffset;\n";
        if (code.includes("dGlossiness")) structCode += "float dGlossiness;\n";
        if (code.includes("dAlpha")) structCode += "float dAlpha;\n";
        if (code.includes("dAtten")) structCode += "float dAtten;\n";
        if (code.includes("dAtten3")) structCode += "vec3 dAtten3;\n";
        if (code.includes("dAo")) structCode += "float dAo;\n";
        if (code.includes("dMsdf")) structCode += "vec4 dMsdf;\n";

        code = codeBegin + structCode + code;

        fshader = code;

        return {
            attributes: attributes,
            vshader: vshader,
            fshader: fshader,
            tag: pc.SHADERTAG_MATERIAL
        };
    }
};