#if UNITY_EDITOR

// Disable 'obsolete' warnings
#pragma warning disable 0618

using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using System.Reflection;

public class ftBuildGraphics : ScriptableWizard
{
    const int UVGBFLAG_NORMAL = 1;
    const int UVGBFLAG_FACENORMAL = 2;
    const int UVGBFLAG_ALBEDO = 4;
    const int UVGBFLAG_EMISSIVE = 8;
    const int UVGBFLAG_POS = 16;
    const int UVGBFLAG_SMOOTHPOS = 32;
    const int UVGBFLAG_TANGENT = 64;
    const int UVGBFLAG_TERRAIN = 128;
    const int UVGBFLAG_RESERVED = 256;

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void InitShaders();

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void LoadScene(string path);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    private static extern void SetAlbedos(int count, IntPtr[] tex);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    private static extern int CopyAlbedos();

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void FreeAlbedoCopies();

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    private static extern void SetAlphas(int count, IntPtr[] tex, float[] alphaRefs, int[] alphaChannels, int numLODs, bool flip);

    [DllImport ("unityLib11")]
    private static extern void SaveTexture(IntPtr tex, string path);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void SaveSky(IntPtr tex, float rx, float ry, float rz, float ux, float uy, float uz, float fx, float fy, float fz, string path, bool isLinear, bool doubleLDR);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void SaveCookie(IntPtr tex, string path);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern int ftRenderUVGBuffer();

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void SetUVGBFlags(int flags);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void SetFixPos(bool enabled);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern void SetCompression(bool enabled);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern int ftGenerateAlphaBuffer();

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern int SaveGBufferMap(IntPtr tex, string path, bool compressed);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern int SaveGBufferMapFromRAM(byte[] tex, int size, string path, bool compressed);

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern int GetABGErrorCode();

    [DllImport ("frender", CallingConvention=CallingConvention.Cdecl)]
    public static extern int GetUVGBErrorCode();

    [DllImport ("uvrepack", CallingConvention=CallingConvention.Cdecl)]
    public static extern int uvrLoad(float[] inputUVs, int numVerts, int[] inputIndices, int numIndices);

    [DllImport ("uvrepack", CallingConvention=CallingConvention.Cdecl)]
    public static extern int uvrRepack(float padding, int resolution);

    [DllImport ("uvrepack", CallingConvention=CallingConvention.Cdecl)]
    public static extern int uvrUnload();

    static int voffset, soffset, ioffset;

    static public string scenePath = "";

    static BufferedBinaryWriterFloat fvbfull, fvbtrace, fvbtraceTex, fvbtraceUV0;
    static BufferedBinaryWriterInt fib;
    static BinaryWriter fscene, fmesh, flmid, fseamfix, fsurf, fmatid, fmatide, fmatidh, fmatideb, falphaid, fib32, fhmaps;
    static BinaryWriter[] fib32lod;
    static BinaryWriter[] falphaidlod;

    public static ftLightmapsStorage.ImplicitLightmapData tempStorage = new ftLightmapsStorage.ImplicitLightmapData();

    public static float texelsPerUnit = 20;
    public static int minAutoResolution = 16;
    public static int maxAutoResolution = 4096;
    public static bool mustBePOT = true;
    public static bool autoAtlas = true;
    public static bool unwrapUVs = true;
    public static bool forceDisableUnwrapUVs = false;
    public static bool exportShaderColors = true;
    public static int atlasPaddingPixels = 3;
    public static bool atlasCountPriority = false;
    public static bool splitByScene = false;
    public static bool uvPaddingMax = false;
    public static bool exportTerrainAsHeightmap = true;
    public static bool exportTerrainTrees = false;
    public static bool uvgbHeightmap = true;

    public static bool texelsPerUnitPerMap = false;
    public static float mainLightmapScale = 1;
    public static float maskLightmapScale = 1;
    public static float dirLightmapScale = 1;

    const float atlasScaleUpValue = 1.01f;
    const int atlasMaxTries = 100;
    const float alphaInstanceThreshold = 5.0f / 255.0f;

    const bool flipAlpha = true;

    public static string overwriteExtensionCheck = ".hdr";
    public static bool overwriteWarning = false;
    public static bool overwriteWarningSelectedOnly = false;
    public static bool memoryWarning = false;
    public static bool modifyLightmapStorage = true;
    public static bool validateLightmapStorageImmutability = true;
    public static bool sceneNeedsToBeRebuilt = false;
    //public static int unityVersionMajor = 0;
    //public static int unityVersionMinor = 0;

    static int areaLightCounter = -2;
    public static int sceneLodsUsed = 0;

    static GameObject lmgroupHolder;
    static BakeryLightmapGroup lightProbeLMGroup = null;
    static BakeryLightmapGroup volumeLMGroup = null;

    static List<GameObject> terrainObjectList;
    static List<Terrain> terrainObjectToActual;
    static List<Texture> terrainObjectToHeightMap;
    static IntPtr[] terrainObjectToHeightMapPtr;
    static List<float> terrainObjectToBounds;
    static List<int> terrainObjectToLMID;
    static List<float> terrainObjectToBoundsUV;
    static List<int> terrainObjectToFlags;
    static List<List<float[]>> terrainObjectToHeightMips;
    //static List<List<Vector3[]>> terrainObjectToNormalMips;
    //static List<Vector3[]> terrainObjectToNormalMip0;
    static List<GameObject> temporaryAreaLightMeshList;
    static List<BakeryLightMesh> temporaryAreaLightMeshList2;
    static List<GameObject> treeObjectList;

    static Dictionary<GameObject,int> cmp_objToLodLevel;
    static Dictionary<GameObject, float> cmp_holderObjArea;

    public static List<float> vbtraceTexPosNormalArray; // global vbTraceTex.bin positions/normals
    public static List<float> vbtraceTexUVArray; // global vbTraceTex.bin UVs
    public static float[] vbtraceTexUVArrayLOD; // global vbTraceTex.bin LOD UVs

    public static List<Renderer> atlasOnlyObj;
    public static List<Vector4> atlasOnlyScaleOffset;
    public static List<int> atlasOnlySize;
    public static List<int> atlasOnlyID;

    public static ftGlobalStorage.AtlasPacker atlasPacker = ftGlobalStorage.AtlasPacker.xatlas;

    public static bool forceAllAreaLightsSelfshadow = false;

    public static bool postPacking = true;
    public static bool holeFilling = false;

    static int floatOverWarnCount = 0;
    const int maxFloatOverWarn = 10;

    static ftGlobalStorage gstorage;

    static public void DebugLogError(string text)
    {
        ftRenderLightmap.DebugLogError(text);
    }

    class AtlasNode
    {
        public AtlasNode child0, child1;
        public Rect rc;
        public GameObject obj;
        bool leaf = true;

        public AtlasNode Insert(GameObject o, Rect rect)
        {
            if (!leaf)
            {
                var newNode = child0.Insert(o, rect);
                if (newNode != null) return newNode;

                return child1.Insert(o, rect);
            }
            else
            {
                if (obj != null) return null;

                var fits = (rect.width <= rc.width && rect.height <= rc.height);
                if (!fits) return null;

                var fitsExactly = (rect.width == rc.width && rect.height == rc.height);
                if (fitsExactly)
                {
                    obj = o;
                    return this;
                }

                child0 = new AtlasNode();
                child1 = new AtlasNode();

                var dw = rc.width - rect.width;
                var dh = rc.height - rect.height;

                if (dw > dh)
                {
                    child0.rc = new Rect(rc.x, rc.y, rect.width, rc.height);
                    child1.rc = new Rect(rc.x + rect.width, rc.y, rc.width - rect.width, rc.height);
                }
                else
                {
                    child0.rc = new Rect(rc.x, rc.y, rc.width, rect.height);
                    child1.rc = new Rect(rc.x, rc.y + rect.height, rc.width, rc.height - rect.height);
                }
                leaf = false;

                return child0.Insert(o, rect);
            }
        }

        public void GetMax(ref float maxx, ref float maxy)
        {
            if (obj != null)
            {
                if ((rc.x + rc.width) > maxx) maxx = rc.x + rc.width;
                if ((rc.y + rc.height) > maxy) maxy = rc.y + rc.height;
            }
            if (child0 != null) child0.GetMax(ref maxx, ref maxy);
            if (child1 != null) child1.GetMax(ref maxx, ref maxy);
        }

        public void Transform(float offsetx, float offsety, float scalex, float scaley)
        {
            rc.x *= scalex;
            rc.y *= scaley;
            rc.x += offsetx;
            rc.y += offsety;
            rc.width *= scalex;
            rc.height *= scaley;
            if (child0 != null) child0.Transform(offsetx, offsety, scalex, scaley);
            if (child1 != null) child1.Transform(offsetx, offsety, scalex, scaley);
        }
    };

    static ftBuildGraphics()
    {
        //ftRenderLightmap.PatchPath();
        //var unityVer = Application.unityVersion.Split('.');
        //unityVersionMajor = Int32.Parse(unityVer[0]);
        //unityVersionMinor = Int32.Parse(unityVer[1]);
    }

    static void exportVBPos(BinaryWriter f, Transform t, Mesh m, Vector3[] vertices)
    {
        for(int i=0;i<vertices.Length;i++)
        {
            Vector3 pos = vertices[i];//t.(vertices[i]);
            f.Write(pos.x);
            f.Write(pos.y);
            f.Write(pos.z);
        }
    }

    static void exportVBTrace(BufferedBinaryWriterFloat f, Mesh m, Vector3[] vertices, Vector3[] normals)
    {
        for(int i=0;i<vertices.Length;i++)
        {
            Vector3 pos = vertices[i];//t.(vertices[i]);
            f.Write(pos.x);
            f.Write(pos.y);
            f.Write(pos.z);

            Vector3 normal = normals[i];//t.TransformDirection(normals[i]);
            f.Write(normal.x);
            f.Write(normal.y);
            f.Write(normal.z);
        }
    }

    static BakeryLightmapGroup GetLMGroupFromObjectExplicit(GameObject obj, ExportSceneData data)
    {
        lmgroupHolder = null;
        var lmgroupSelector = obj.GetComponent<BakeryLightmapGroupSelector>(); // if object has explicit lmgroup
        if (lmgroupSelector == null)
        {
            // if parents have explicit lmgroup
            var t2 = obj.transform.parent;
            while(lmgroupSelector == null && t2 != null)
            {
                lmgroupSelector = t2.GetComponent<BakeryLightmapGroupSelector>();
                t2 = t2.parent;
            }
        }
        BakeryLightmapGroup lmgroup = null;
        if (lmgroupSelector != null)
        {
            lmgroup = lmgroupSelector.lmgroupAsset as BakeryLightmapGroup;
            lmgroupHolder = lmgroupSelector.gameObject;

            var so = new SerializedObject(obj.GetComponent<Renderer>());
            var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;
            if (scaleInLm == 0.0f) lmgroup = data.autoVertexGroup;
                //null; // ignore lightmaps when scaleInLightmap == 0
        }
        return lmgroup;
    }

    static BakeryLightmapGroup GetLMGroupFromObject(GameObject obj, ExportSceneData data)
    {
        UnityEngine.Object lmgroupObj = null;
        BakeryLightmapGroup lmgroup = null;
        lmgroupHolder = null;

        var lmgroupSelector = obj.GetComponent<BakeryLightmapGroupSelector>(); // if object has explicit lmgroup
        tempStorage.implicitGroupMap.TryGetValue(obj, out lmgroupObj); // or implicit
        lmgroup = (BakeryLightmapGroup)lmgroupObj;
        lmgroupHolder = obj;
        if (lmgroupSelector == null && lmgroup == null)
        {
            // if parents have explicit/implicit lmgroup
            var t2 = obj.transform.parent;
            while(lmgroupSelector == null && lmgroup == null && t2 != null)
            {
                lmgroupSelector = t2.GetComponent<BakeryLightmapGroupSelector>();
                tempStorage.implicitGroupMap.TryGetValue(t2.gameObject, out lmgroupObj);
                lmgroup = (BakeryLightmapGroup)lmgroupObj;
                lmgroupHolder = t2.gameObject;
                t2 = t2.parent;
            }
        }
        if (lmgroupSelector != null)
        {
            lmgroup = lmgroupSelector.lmgroupAsset as BakeryLightmapGroup;
        }

        if (lmgroup != null)
        {
            var r = obj.GetComponent<Renderer>();
            if (r)
            {
                var so = new SerializedObject(r);
                var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;
                if (scaleInLm == 0.0f) lmgroup = data.autoVertexGroup;
                // null; // ignore lightmaps when scaleInLightmap == 0
            }
        }
        else
        {
            lmgroupHolder = null;
        }

        return lmgroup;
    }

    // use by ftRenderLightmap
    public static BakeryLightmapGroup GetLMGroupFromObject(GameObject obj)
    {
        UnityEngine.Object lmgroupObj = null;
        BakeryLightmapGroup lmgroup = null;
        lmgroupHolder = null;

        var lmgroupSelector = obj.GetComponent<BakeryLightmapGroupSelector>(); // if object has explicit lmgroup
        tempStorage.implicitGroupMap.TryGetValue(obj, out lmgroupObj); // or implicit
        lmgroup = (BakeryLightmapGroup)lmgroupObj;
        lmgroupHolder = obj;
        if (lmgroupSelector == null && lmgroup == null)
        {
            // if parents have explicit/implicit lmgroup
            var t2 = obj.transform.parent;
            while(lmgroupSelector == null && lmgroup == null && t2 != null)
            {
                lmgroupSelector = t2.GetComponent<BakeryLightmapGroupSelector>();
                tempStorage.implicitGroupMap.TryGetValue(t2.gameObject, out lmgroupObj);
                lmgroup = (BakeryLightmapGroup)lmgroupObj;
                lmgroupHolder = t2.gameObject;
                t2 = t2.parent;
            }
        }
        if (lmgroupSelector != null)
        {
            lmgroup = lmgroupSelector.lmgroupAsset as BakeryLightmapGroup;
        }

        if (lmgroup != null)
        {
            var r = obj.GetComponent<Renderer>();
            if (r)
            {
                var so = new SerializedObject(r);
                var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;
                if (scaleInLm == 0.0f) lmgroup = null;
                // null; // ignore lightmaps when scaleInLightmap == 0
            }
        }
        else
        {
            lmgroupHolder = null;
        }

        return lmgroup;
    }

    public static void exportVBTraceTexAttribs(List<float> arrPosNormal, List<float> arrUV,
     Vector3[] vertices, Vector3[] normals, Vector2[] uv2, int lmid, bool vertexBake, GameObject obj)
    {
        for(int i=0;i<vertices.Length;i++)
        {
            Vector3 pos = vertices[i];//t.(vertices[i]);
            arrPosNormal.Add(pos.x);
            arrPosNormal.Add(pos.y);
            arrPosNormal.Add(pos.z);

            Vector3 normal = normals[i];//t.TransformDirection(normals[i]);
            arrPosNormal.Add(normal.x);
            arrPosNormal.Add(normal.y);
            arrPosNormal.Add(normal.z);

            float u = 0;
            float v = 0;

            if (lmid < 0)
            {
                //u = lmid * 10;
                if (uv2.Length>0)
                {
                    u = Mathf.Clamp(uv2[i].x, 0, 0.99999f);
                    v = Mathf.Clamp(1.0f - uv2[i].y, 0, 0.99999f);
                }
            }
            else
            {
                if (uv2.Length>0 && !vertexBake)
                {
                    u = Mathf.Clamp(uv2[i].x, 0, 0.99999f);
                    v = Mathf.Clamp(uv2[i].y, 0, 0.99999f);
                }
                else if (vertexBake)
                {
                    u = uv2[i].x;
                    v = uv2[i].y - 1.0f;
                }
            }

            float origU = u;
            if (lmid >= 0)
            {
                u += lmid * 10;
                if ((int)u > lmid*10)
                {
                    // attempt fixing float overflow
                    u = (lmid*10+1) - (lmid*10+1)*0.0000002f;
                    if ((int)u > lmid*10)
                    {
                        if (floatOverWarnCount < maxFloatOverWarn)
                        {
                            Debug.LogError("Float overflow for " + obj.name + " (U: " + origU + ", LMID: " + lmid + ")");
                            floatOverWarnCount++;
                        }
                    }
                }
            }
            else
            {
                u = lmid * 10 - u;
                if ((int)u != lmid*10)
                {
                    u = -u;
                    lmid = -lmid;
                    u = (lmid*10+1) - (lmid*10+1)*0.0000002f;
                    if ((int)u > lmid*10)
                    {
                        if (floatOverWarnCount < maxFloatOverWarn)
                        {
                            Debug.LogError("Float overflow for " + obj.name + " (U: " + origU + ", LMID: " + lmid + ")");
                            floatOverWarnCount++;
                        }
                    }
                    u = -u;
                    lmid = -lmid;
                }
            }

            arrUV.Add(u);
            arrUV.Add(v);
        }
    }

    static void exportVBTraceUV0(BufferedBinaryWriterFloat f, Vector2[] uvs, int vertCount)
    {
        if (uvs.Length == 0)
        {
            for(int i=0;i<vertCount;i++)
            {
                f.Write(0.0f);
                f.Write(0.0f);
            }
        }
        else
        {
            for(int i=0;i<vertCount;i++)
            {
                f.Write(uvs[i].x);
                f.Write(uvs[i].y);
            }
        }
    }

    static void exportVBBasic(BinaryWriter f, Transform t, Mesh m, Vector3[] vertices, Vector3[] normals, Vector2[] uv2)
    {
        for(int i=0;i<vertices.Length;i++)
        {
            Vector3 pos = vertices[i];//t.(vertices[i]);
            f.Write(pos.x);
            f.Write(pos.y);
            f.Write(pos.z);

            Vector3 normal = normals[i];//t.TransformDirection(normals[i]);
            f.Write(normal.x);
            f.Write(normal.y);
            f.Write(normal.z);

            if (uv2.Length>0)
            {
                f.Write(uv2[i].x);
                f.Write(uv2[i].y);
            }
            else
            {
                f.Write(0.0f);
                f.Write(0.0f);
            }
        }
    }

    // Either I'm dumb, or it's impossible to make generics work with it (only worked in .NET 3.5)
    class BufferedBinaryWriterFloat
    {
        [StructLayout(LayoutKind.Explicit)]
        public class ReinterpretBuffer
        {
           [FieldOffset(0)]
           public byte[] bytes;
           [FieldOffset(0)]
           public float[] elements;
       }

        BinaryWriter f;
        ReinterpretBuffer buffer;
        int bufferPtr;
        int bufferSize;
        int elementSize;

        public BufferedBinaryWriterFloat(BinaryWriter b, int elemSize = 4, int buffSizeInFloats = 1024*1024)
        {
            f = b;
            buffer = new ReinterpretBuffer();
            buffer.bytes = new byte[buffSizeInFloats * elemSize];
            bufferPtr = 0;
            bufferSize = buffSizeInFloats;
            elementSize = elemSize;
        }

        void Flush()
        {
            if (bufferPtr == 0) return;
            f.Write(buffer.bytes, 0, bufferPtr * elementSize);
            bufferPtr = 0;
        }

        public void Write(float x)
        {
            buffer.elements[bufferPtr] = x;
            bufferPtr++;
            if (bufferPtr == bufferSize) Flush();
        }

        public void Close()
        {
            Flush();
            f.Close();
        }
    }
    class BufferedBinaryWriterInt
    {
        [StructLayout(LayoutKind.Explicit)]
        public class ReinterpretBuffer
        {
           [FieldOffset(0)]
           public byte[] bytes;
           [FieldOffset(0)]
           public int[] elements;
       }

        BinaryWriter f;
        ReinterpretBuffer buffer;
        int bufferPtr;
        int bufferSize;
        int elementSize;

        public BufferedBinaryWriterInt(BinaryWriter b, int elemSize = 4, int buffSizeInFloats = 1024*1024)
        {
            f = b;
            buffer = new ReinterpretBuffer();
            buffer.bytes = new byte[buffSizeInFloats * elemSize];
            bufferPtr = 0;
            bufferSize = buffSizeInFloats;
            elementSize = elemSize;
        }

        void Flush()
        {
            if (bufferPtr == 0) return;
            f.Write(buffer.bytes, 0, bufferPtr * elementSize);
            bufferPtr = 0;
        }

        public void Write(int x)
        {
            buffer.elements[bufferPtr] = x;
            bufferPtr++;
            if (bufferPtr == bufferSize) Flush();
        }

        public void Close()
        {
            Flush();
            f.Close();
        }
    }

    static void exportVBFull(BufferedBinaryWriterFloat f, Vector3[] vertices, Vector3[] normals, Vector4[] tangents, Vector2[] uv, Vector2[] uv2)
    {
        bool hasUV = uv.Length > 0;
        bool hasUV2 = uv2.Length > 0;

        for(int i=0;i<vertices.Length;i++)
        {
            Vector3 pos = vertices[i];//t.(vertices[i]);
            f.Write(pos.x);
            f.Write(pos.y);
            f.Write(pos.z);

            Vector3 normal = normals[i];//t.TransformDirection(normals[i]);
            f.Write(normal.x);
            f.Write(normal.y);
            f.Write(normal.z);

            if (tangents == null)
            {
                f.Write(0.0f);
                f.Write(0.0f);
                f.Write(0.0f);
                f.Write(0.0f);
            }
            else
            {
                Vector4 tangent = tangents[i];
                f.Write(tangent.x);
                f.Write(tangent.y);
                f.Write(tangent.z);
                f.Write(tangent.w);
            }

            if (hasUV)
            {
                f.Write(uv[i].x);
                f.Write(uv[i].y);
            }
            else
            {
                f.Write(0.0f);
                f.Write(0.0f);
            }

            if (hasUV2)
            {
                f.Write(uv2[i].x);
                f.Write(uv2[i].y);
            }
            else
            {
                f.Write(0.0f);
                f.Write(0.0f);
            }
        }
    }

    static int exportIB(BufferedBinaryWriterInt f, int[] indices, bool isFlipped, bool is32Bit, int offset, BinaryWriter falphaid, ushort alphaID)
    {
        //var indices = m.GetTriangles(i);
        for(int j=0;j<indices.Length;j+=3)
        {
            if (!isFlipped)
            {
                if (is32Bit)
                {
                    f.Write(indices[j] + offset);
                    f.Write(indices[j+1] + offset);
                    f.Write(indices[j+2] + offset);
                }
                else
                {
                    f.Write(indices[j]);
                    f.Write(indices[j+1]);
                    f.Write(indices[j+2]);
                }
            }
            else
            {
                if (is32Bit)
                {
                    f.Write(indices[j+2] + offset);
                    f.Write(indices[j+1] + offset);
                    f.Write(indices[j] + offset);
                }
                else
                {
                    f.Write(indices[j+2]);
                    f.Write(indices[j+1]);
                    f.Write(indices[j]);
                }
            }

            if (falphaid!=null) falphaid.Write(alphaID);
        }
        return indices.Length;
    }

    // returns mesh area if requested
    static void exportIB32(List<int> indicesOpaque, List<int> indicesTransparent, List<int> indicesLMID,
        int[] indices, bool isFlipped, int offset, int indexOffsetLMID, BinaryWriter falphaid,
        ushort alphaID)
    {
        //var indices = m.GetTriangles(i);
        var indicesOut = alphaID == 0xFFFF ? indicesOpaque : indicesTransparent;

        int indexA, indexB, indexC;

        for(int j=0;j<indices.Length;j+=3)
        {
            if (!isFlipped)
            {
                indexA = indices[j];
                indexB = indices[j + 1];
                indexC = indices[j + 2];
            }
            else
            {
                indexA = indices[j + 2];
                indexB = indices[j + 1];
                indexC = indices[j];
            }

            indicesOut.Add(indexA + offset);
            indicesOut.Add(indexB + offset);
            indicesOut.Add(indexC + offset);

            if (indicesLMID != null)
            {
                indicesLMID.Add(indexA + indexOffsetLMID);
                indicesLMID.Add(indexB + indexOffsetLMID);
                indicesLMID.Add(indexC + indexOffsetLMID);
            }

            if (alphaID != 0xFFFF) falphaid.Write(alphaID);
        }
    }


    static void exportSurfs(BinaryWriter f, int[][] indices, int subMeshCount)// Mesh m)
    {
        int offset = ioffset;
        for(int i=0;i<subMeshCount;i++) {
            int size = indices[i].Length;//m.GetTriangles(i).Length;
            f.Write(offset);
            f.Write(size);
            offset += size;// * 2;
        }
        soffset += subMeshCount;
    }

    static void exportMesh(BinaryWriter f, Mesh m)
    {
        f.Write(voffset);
        f.Write(soffset);
        f.Write((ushort)m.subMeshCount);
        f.Write((ushort)0);
    }

    static int exportLMID(BinaryWriter f, GameObject obj, BakeryLightmapGroup lmgroup)
    {
        var areaLight =  obj.GetComponent<BakeryLightMesh>();
        if (areaLight == null)
        {
            int index = temporaryAreaLightMeshList.IndexOf(obj);
            if (index >= 0)
            {
                areaLight = temporaryAreaLightMeshList2[index];
            }
        }
        if (areaLight != null)
        {
            f.Write(areaLightCounter);
            areaLight.lmid = areaLightCounter;
            areaLightCounter--;
            return areaLightCounter;
        }
        else if (lmgroup != null)
        {
            f.Write(lmgroup.id);
            return lmgroup.id;
        }
        else
        {
            f.Write(0xFFFFFFFF);
            return -1;
        }
    }

    static Vector2[] GenerateVertexBakeUVs(int voffset, int vlength, int totalVertexCount)
    {
        int atlasTexSize = (int)Mathf.Ceil(Mathf.Sqrt((float)totalVertexCount));
        atlasTexSize = (int)Mathf.Ceil(atlasTexSize / (float)ftRenderLightmap.tileSize) * ftRenderLightmap.tileSize;
        var uvs = new Vector2[vlength];
        float mul = 1.0f / atlasTexSize;
        float add = mul * 0.5f;
        for(int i=0; i<vlength; i++)
        {
            int x = (i + voffset) % atlasTexSize;
            int y = (i + voffset) / atlasTexSize;
            uvs[i] = new Vector2(x * mul + add, y * mul + add);// - 1.0f);
        }
        return uvs;
    }

    public static Mesh GetSharedMesh(Renderer mr)
    {
        if (mr == null) return null;
        var mrSkin = mr as SkinnedMeshRenderer;
        var mf = mr.gameObject.GetComponent<MeshFilter>();
        return mrSkin != null ? mrSkin.sharedMesh : (mf != null ? mf.sharedMesh : null);
    }

    public static Mesh GetSharedMeshBaked(GameObject obj)
    {
        var mrSkin = obj.GetComponent<SkinnedMeshRenderer>();
        if (mrSkin != null)
        {
            var baked = new Mesh();
            mrSkin.BakeMesh(baked);
            return baked;
        }
        var mf = obj.GetComponent<MeshFilter>();
        return (mf != null ? mf.sharedMesh : null);
    }

    public static Mesh GetSharedMesh(GameObject obj)
    {
        var mrSkin = obj.GetComponent<SkinnedMeshRenderer>();
        var mf = obj.GetComponent<MeshFilter>();
        return mrSkin != null ? mrSkin.sharedMesh : (mf != null ? mf.sharedMesh : null);
    }

    public static Mesh GetSharedMeshSkinned(GameObject obj, out bool isSkin)
    {
        var mrSkin = obj.GetComponent<SkinnedMeshRenderer>();
        Mesh mesh;
        if (mrSkin != null)
        {
            mesh = new Mesh();
            mrSkin.BakeMesh(mesh);
            isSkin = mrSkin.bones.Length > 0; // blendshape-only don't need scale?
        }
        else
        {
            isSkin = false;
            var mf = obj.GetComponent<MeshFilter>();
            if (mf == null) return null;
            mesh = mf.sharedMesh;
        }
        return mesh;
    }

    static GameObject TestPackAsSingleSquare(GameObject holder)
    {
        var t = holder.transform;
        var p = t.parent;
        while(p != null)
        {
            if (p.GetComponent<BakeryPackAsSingleSquare>() != null) return p.gameObject;
            p = p.parent;
        }
        return holder;
    }

    static bool ModelUVsOverlap(ModelImporter importer, ftGlobalStorage store)
    {
        if (importer.generateSecondaryUV) return true;

        var path = importer.assetPath;
        /*for(int i=0; i<storages.Length; i++)
        {
            var store = storages[i];
            if (store == null) continue;
            var index = store.assetList.IndexOf(path);
            if (index < 0) continue;

            if (store.uvOverlapAssetList[index] == 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }*/
        var index = store.assetList.IndexOf(path);
        if (index >= 0 && index < store.uvOverlapAssetList.Count)
        {
            if (store.uvOverlapAssetList[index] == 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        var newAsset = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)) as GameObject;
        ftModelPostProcessor.CheckUVOverlap(newAsset, path);

        /*for(int i=0; i<storages.Length; i++)
        {
            var store = storages[i];
            var index = store.assetList.IndexOf(path);
            if (index < 0) continue;

            if (store.uvOverlapAssetList[index] == 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }*/
        index = store.assetList.IndexOf(path);
        if (index >= 0)
        {
            if (store.uvOverlapAssetList[index] == 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        return true;
    }

    static bool NeedsTangents(BakeryLightmapGroup lmgroup, bool tangentSHLights)
    {
        // RNM requires tangents
        if (ftRenderLightmap.renderDirMode == ftRenderLightmap.RenderDirMode.RNM ||
            (lmgroup!=null && lmgroup.renderDirMode == BakeryLightmapGroup.RenderDirMode.RNM)) return true;

        // SH requires tangents only if there is a SH skylight
        if (!tangentSHLights) return false;

        if (ftRenderLightmap.renderDirMode == ftRenderLightmap.RenderDirMode.SH ||
            (lmgroup!=null && lmgroup.renderDirMode == BakeryLightmapGroup.RenderDirMode.SH)) return true;

        return false;
    }

    static long GetTime()
    {
        return System.DateTime.Now.Ticks / System.TimeSpan.TicksPerMillisecond;
    }

    static public string progressBarText;
    static public float progressBarPercent = 0;
    //static bool progressBarEnabled = false;
    static public bool userCanceled = false;
    //static IEnumerator progressFunc;
    static EditorWindow activeWindow;
    static void ProgressBarInit(string startText, EditorWindow window = null)
    {
        progressBarText = startText;
        //progressBarEnabled = true;
        ftRenderLightmap.simpleProgressBarShow("Bakery", progressBarText, progressBarPercent, 0);
    }
    static void ProgressBarShow(string text, float percent)
    {
        progressBarText = text;
        progressBarPercent = percent;
        ftRenderLightmap.simpleProgressBarShow("Bakery", progressBarText, progressBarPercent, 0);
        userCanceled = ftRenderLightmap.simpleProgressBarCancelled();
    }

    public static void FreeTemporaryAreaLightMeshes()
    {
        if (temporaryAreaLightMeshList != null)
        {
            for(int i=0; i<temporaryAreaLightMeshList.Count; i++)
            {
                if (temporaryAreaLightMeshList[i] != null)
                {
                    //var mr = temporaryAreaLightMeshList[i].GetComponent<Renderer>();
                    //if (mr != null) DestroyImmediate(mr);
                    //var mf = temporaryAreaLightMeshList[i].GetComponent<MeshFilter>();
                    //if (mf != null) DestroyImmediate(mf);
                    DestroyImmediate(temporaryAreaLightMeshList[i]);
                }
            }
            temporaryAreaLightMeshList = null;
        }
    }

    public static void ProgressBarEnd(bool removeTempObjects)// bool isError = true)
    {
        if (removeTempObjects)
        {
            if (terrainObjectList != null)
            {
                for(int i=0; i<terrainObjectList.Count; i++)
                {
                    if (terrainObjectList[i] != null) DestroyImmediate(terrainObjectList[i]);
                }
                terrainObjectList = null;
            }

            if (treeObjectList != null)
            {
                for(int i=0; i<treeObjectList.Count; i++)
                {
                    if (treeObjectList[i] != null) DestroyImmediate(treeObjectList[i]);
                }
                treeObjectList = null;
            }

            //if (isError)
            {
                FreeTemporaryAreaLightMeshes();
            }
        }

        //progressBarEnabled = false;
        ftRenderLightmap.simpleProgressBarEnd();
    }
    void OnInspectorUpdate()
    {
        Repaint();
    }
    static void CloseAllFiles()
    {
        if (fscene != null) fscene.Close();
        if (fmesh != null) fmesh.Close();
        if (flmid != null) flmid.Close();
        if (fseamfix != null) fseamfix.Close();
        if (fsurf != null) fsurf.Close();
        if (fmatid != null) fmatid.Close();
        if (fmatide != null) fmatide.Close();
        if (fmatideb != null) fmatideb.Close();
        if (fmatidh != null) fmatidh.Close();
        if (falphaid != null) falphaid.Close();
        if (fvbfull != null) fvbfull.Close();
        if (fvbtrace != null) fvbtrace.Close();
        if (fvbtraceTex != null) fvbtraceTex.Close();
        if (fvbtraceUV0 != null) fvbtraceUV0.Close();
        if (fib != null) fib.Close();
        if (fib32 != null) fib32.Close();
        if (fhmaps != null) fhmaps.Close();
        if (fib32lod != null)
        {
            for(int i=0; i<fib32lod.Length; i++) fib32lod[i].Close();
        }
        if (falphaidlod != null)
        {
            for(int i=0; i<falphaidlod.Length; i++) falphaidlod[i].Close();
        }
        fvbfull = fvbtrace = fvbtraceTex = fvbtraceUV0 = null;
        fib = null;
        fscene = fmesh = flmid = fsurf = fmatid = fmatide = fmatideb = falphaid  = fib32 = fseamfix = fmatidh = fhmaps = null;
        fib32lod = falphaidlod = null;
    }

    static bool CheckUnwrapError()
    {
        if (ftModelPostProcessor.unwrapError)
        {

            DebugLogError("Unity failed unwrapping some models. See console for details. Last failed model: " + ftModelPostProcessor.lastUnwrapErrorAsset+"\nModel will now be reverted to original state.");

            int mstoreIndex = gstorage.modifiedAssetPathList.IndexOf(ftModelPostProcessor.lastUnwrapErrorAsset);
            if (mstoreIndex < 0)
            {
                Debug.LogError("Failed to find failed asset?");
            }
            else
            {
                gstorage.ClearAssetModifications(mstoreIndex);
            }

            ftModelPostProcessor.unwrapError = false;
            CloseAllFiles();
            userCanceled = true;
            ProgressBarEnd(true);
            return true;
        }
        return false;
    }

    static Mesh BuildAreaLightMesh(Light areaLight)
    {
        var mesh = new Mesh();

        var verts = new Vector3[4];

        Vector2 areaSize = ftLightMeshInspector.GetAreaLightSize(areaLight);

        verts[0] = new Vector3(-0.5f * areaSize.x, -0.5f * areaSize.y, 0);
        verts[1] = new Vector3(0.5f * areaSize.x, -0.5f * areaSize.y, 0);
        verts[2] = new Vector3(0.5f * areaSize.x, 0.5f * areaSize.y, 0);
        verts[3] = new Vector3(-0.5f * areaSize.x, 0.5f * areaSize.y, 0);

        var uvs = new Vector2[4];
        uvs[0] = new Vector2(0, 0);
        uvs[1] = new Vector2(1, 0);
        uvs[2] = new Vector2(1, 1);
        uvs[3] = new Vector2(0, 1);

        var indices = new int[6];

        indices[0] = 0;
        indices[1] = 1;
        indices[2] = 2;

        indices[3] = 0;
        indices[4] = 2;
        indices[5] = 3;

        var normals = new Vector3[4];
        var n = Vector3.forward;// -areaLight.transform.forward; // transformation will be applied later
        for(int i=0; i<4; i++) normals[i] = n;

        mesh.vertices = verts;
        mesh.triangles = indices;
        mesh.normals = normals;
        mesh.uv = uvs;

        return mesh;
    }

    static bool CheckForTangentSHLights()
    {
        var All2 = FindObjectsOfType(typeof(BakerySkyLight));
        for(int i=0; i<All2.Length; i++)
        {
            var obj = All2[i] as BakerySkyLight;
            if (!obj.enabled) continue;
            if (!obj.gameObject.activeInHierarchy) continue;
            if (obj.tangentSH)
            {
                return true;
            }
        }
        return false;
    }

    public static void CreateSceneFolder()
    {
        if (scenePath == "" || !Directory.Exists(scenePath))
        {
            // Default scene path is TEMP/frender
            var tempDir = System.Environment.GetEnvironmentVariable("TEMP", System.EnvironmentVariableTarget.Process);
            scenePath = tempDir + "\\frender";
            if (!Directory.Exists(scenePath)) Directory.CreateDirectory(scenePath);
        }
    }

    static int CorrectLMGroupID(int id, BakeryLightmapGroup lmgroup, List<BakeryLightmapGroup> groupList)
    {
        id = id < 0 ? -1 : id;
        if (lmgroup != null && lmgroup.parentName != null && lmgroup.parentName.Length > 0 && lmgroup.parentName != "|")
        {
            for(int g=0; g<groupList.Count; g++)
            {
                if (groupList[g].name == lmgroup.parentName)
                {
                    id = g;
                    break;
                }
            }
        }
        return id;
    }

    static void InitSceneStorage(ExportSceneData data)
    {
        var storages = data.storages;
        var sceneToID = data.sceneToID;

        bool first = true;
        for(int i=0; i<storages.Length; i++)
        {
            var scene = SceneManager.GetSceneAt(i);
            if (!scene.isLoaded) continue;
            var gg = ftLightmaps.FindInScene("!ftraceLightmaps", scene);
            storages[i] = gg.GetComponent<ftLightmapsStorage>();
            if (modifyLightmapStorage)
            {
                /*
                storages[i].bakedRenderers = new List<Renderer>();
                storages[i].bakedIDs = new List<int>();
                storages[i].bakedScaleOffset = new List<Vector4>();
                storages[i].bakedVertexOffset = new List<int>();
                storages[i].bakedVertexColorMesh = new List<Mesh>();
                storages[i].bakedRenderersTerrain = new List<Terrain>();
                storages[i].bakedIDsTerrain = new List<int>();
                storages[i].bakedScaleOffsetTerrain = new List<Vector4>();
                */
                storages[i].hasEmissive = new List<bool>();
                storages[i].lmGroupLODResFlags = null;
                storages[i].lmGroupMinLOD = null;
                storages[i].lmGroupLODMatrix = null;
                storages[i].nonBakedRenderers = new List<Renderer>();
            }
            if (first)
            {
                data.firstNonNullStorage = i;
                first = false;
            }
            storages[i].implicitGroups = new List<UnityEngine.Object>();
            storages[i].implicitGroupedObjects = new List<GameObject>();
            sceneToID[scene] = i;
        }

        //var go = GameObject.Find("!ftraceLightmaps");
        //data.settingsStorage = go.GetComponent<ftLightmapsStorage>();
    }

    static void InitSceneStorage2(ExportSceneData data)
    {
        var storages = data.storages;
        for(int i=0; i<storages.Length; i++)
        {
            var scene = SceneManager.GetSceneAt(i);
            if (!scene.isLoaded) continue;
            if (modifyLightmapStorage)
            {
                storages[i].bakedRenderers = new List<Renderer>();
                storages[i].bakedIDs = new List<int>();
                storages[i].bakedScaleOffset = new List<Vector4>();
                storages[i].bakedVertexOffset = new List<int>();
                storages[i].bakedVertexColorMesh = new List<Mesh>();
                storages[i].bakedRenderersTerrain = new List<Terrain>();
                storages[i].bakedIDsTerrain = new List<int>();
                storages[i].bakedScaleOffsetTerrain = new List<Vector4>();
            }
        }
    }

    static IEnumerator CreateLightProbeLMGroup(ExportSceneData data)
    {
        var storages = data.storages;
        var sceneToID = data.sceneToID;
        var lmBounds = data.lmBounds;
        var groupList = data.groupList;

        var probes = LightmapSettings.lightProbes;
        if (probes == null)
        {
            DebugLogError("No probes in LightingDataAsset");
            yield break;
        }
        var positions = probes.positions;

        int atlasTexSize = (int)Mathf.Ceil(Mathf.Sqrt((float)probes.count));
        atlasTexSize = (int)Mathf.Ceil(atlasTexSize / (float)ftRenderLightmap.tileSize) * ftRenderLightmap.tileSize;
        var uvpos = new float[atlasTexSize * atlasTexSize * 4];
        var uvnormal = new byte[atlasTexSize * atlasTexSize * 4];

        for(int i=0; i<probes.count; i++)
        {
            int x = i % atlasTexSize;
            int y = i / atlasTexSize;
            int index = y * atlasTexSize + x;
            uvpos[index * 4] =     positions[i].x;
            uvpos[index * 4 + 1] = positions[i].y;
            uvpos[index * 4 + 2] = positions[i].z;
            uvpos[index * 4 + 3] = 1.0f;
            uvnormal[index * 4 + 1] = 255;
            uvnormal[index * 4 + 3] = 255;
        }

        var posFile = new byte[128 + uvpos.Length * 4];
        System.Buffer.BlockCopy(ftDDS.ddsHeaderFloat4, 0, posFile, 0, 128);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posFile, 12, 4);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posFile, 16, 4);
        System.Buffer.BlockCopy(uvpos, 0, posFile, 128, uvpos.Length * 4);
        SaveGBufferMapFromRAM(posFile, posFile.Length, scenePath + "/uvpos_probes" + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"), ftRenderLightmap.compressedGBuffer);
        GL.IssuePluginEvent(8);
        yield return null;

        var posNormal = new byte[128 + uvnormal.Length];
        System.Buffer.BlockCopy(ftDDS.ddsHeaderRGBA8, 0, posNormal, 0, 128);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posNormal, 12, 4);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posNormal, 16, 4);
        System.Buffer.BlockCopy(uvnormal, 0, posNormal, 128, uvnormal.Length);
        SaveGBufferMapFromRAM(posNormal, posNormal.Length, scenePath + "/uvnormal_probes" + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"), ftRenderLightmap.compressedGBuffer);
        GL.IssuePluginEvent(8);
        yield return null;

        lightProbeLMGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();
        lightProbeLMGroup.name = "probes";
        lightProbeLMGroup.probes = true;
        lightProbeLMGroup.isImplicit = true;
        lightProbeLMGroup.resolution = 256;
        lightProbeLMGroup.bitmask = 1;
        lightProbeLMGroup.mode = BakeryLightmapGroup.ftLMGroupMode.Vertex;
        lightProbeLMGroup.id = data.lmid;
        lightProbeLMGroup.totalVertexCount = probes.count;
        lightProbeLMGroup.vertexCounter = 0;
        lightProbeLMGroup.renderDirMode = BakeryLightmapGroup.RenderDirMode.ProbeSH;
        groupList.Add(lightProbeLMGroup);
        lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(10000,10000,10000)));
        data.lmid++;

        storages[sceneToID[EditorSceneManager.GetActiveScene()]].implicitGroups.Add(lightProbeLMGroup);
        storages[sceneToID[EditorSceneManager.GetActiveScene()]].implicitGroupedObjects.Add(null);
    }

    static IEnumerator CreateVolumeLMGroup(ExportSceneData data)
    {
        ftRenderLightmap.hasAnyVolumes = false;

        var vols = ftRenderLightmap.FindBakeableVolumes();
        if (vols.Length == 0) yield break;

        ftRenderLightmap.hasAnyVolumes = true;

        var storages = data.storages;
        var sceneToID = data.sceneToID;
        var lmBounds = data.lmBounds;
        var groupList = data.groupList;

        int numTotalProbes = 0;
        for(int v=0; v<vols.Length; v++)
        {
            numTotalProbes += vols[v].resolutionX * vols[v].resolutionY * vols[v].resolutionZ;
        }

        var positions = new Vector3[numTotalProbes];
        int i = 0;
        Vector3 halfVoxelSize = Vector3.one;
        for(int v=0; v<vols.Length; v++)
        {
            var vol = vols[v];
            int rx = vol.resolutionX;
            int ry = vol.resolutionY;
            int rz = vol.resolutionZ;
            var bmin = vol.bounds.min;
            var bmax = vol.bounds.max;
            halfVoxelSize = bmax - bmin;
            halfVoxelSize = new Vector3(halfVoxelSize.x/rx, halfVoxelSize.y/ry, halfVoxelSize.z/rz) * 0.5f;
            float lx, ly, lz;
            for(int z=0; z<rz; z++)
            {
                lz = Mathf.Lerp(bmin.z, bmax.z, z/(float)rz) + halfVoxelSize.z;
                for(int y=0; y<ry; y++)
                {
                    ly = Mathf.Lerp(bmin.y, bmax.y, y/(float)ry) + halfVoxelSize.y;
                    for(int x=0; x<rx; x++)
                    {
                        lx = Mathf.Lerp(bmin.x, bmax.x, x/(float)rx) + halfVoxelSize.x;
                        positions[i] = new Vector3(lx, ly, lz);
                        i++;
                    }
                }
            }
        }

        int atlasTexSize = (int)Mathf.Ceil(Mathf.Sqrt((float)numTotalProbes));
        atlasTexSize = (int)Mathf.Ceil(atlasTexSize / (float)ftRenderLightmap.tileSize) * ftRenderLightmap.tileSize;
        var uvpos = new float[atlasTexSize * atlasTexSize * 4];
        var uvnormal = new byte[atlasTexSize * atlasTexSize * 4];

        for(i=0; i<numTotalProbes; i++)
        {
            int x = i % atlasTexSize;
            int y = i / atlasTexSize;
            int index = y * atlasTexSize + x;
            uvpos[index * 4] =     positions[i].x;
            uvpos[index * 4 + 1] = positions[i].y;
            uvpos[index * 4 + 2] = positions[i].z;
            uvpos[index * 4 + 3] = 1.0f;
            uvnormal[index * 4 + 1] = 255;
            uvnormal[index * 4 + 3] = 255;
        }

        var posFile = new byte[128 + uvpos.Length * 4];
        System.Buffer.BlockCopy(ftDDS.ddsHeaderFloat4, 0, posFile, 0, 128);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posFile, 12, 4);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posFile, 16, 4);
        System.Buffer.BlockCopy(uvpos, 0, posFile, 128, uvpos.Length * 4);
        SaveGBufferMapFromRAM(posFile, posFile.Length, scenePath + "/uvpos_volumes" + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"), ftRenderLightmap.compressedGBuffer);
        GL.IssuePluginEvent(8);
        yield return null;

        var posNormal = new byte[128 + uvnormal.Length];
        System.Buffer.BlockCopy(ftDDS.ddsHeaderRGBA8, 0, posNormal, 0, 128);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posNormal, 12, 4);
        System.Buffer.BlockCopy(BitConverter.GetBytes(atlasTexSize), 0, posNormal, 16, 4);
        System.Buffer.BlockCopy(uvnormal, 0, posNormal, 128, uvnormal.Length);
        SaveGBufferMapFromRAM(posNormal, posNormal.Length, scenePath + "/uvnormal_volumes" + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"), ftRenderLightmap.compressedGBuffer);
        GL.IssuePluginEvent(8);
        yield return null;

        volumeLMGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();
        volumeLMGroup.name = "volumes";
        volumeLMGroup.probes = true;
        volumeLMGroup.fixPos3D = true;
        volumeLMGroup.voxelSize = halfVoxelSize * 2; // incorrect... should be different for every probe
        volumeLMGroup.isImplicit = true;
        volumeLMGroup.resolution = 256;
        volumeLMGroup.bitmask = 1;
        volumeLMGroup.mode = BakeryLightmapGroup.ftLMGroupMode.Vertex;
        volumeLMGroup.id = data.lmid;
        volumeLMGroup.totalVertexCount = numTotalProbes;
        volumeLMGroup.vertexCounter = 0;
        volumeLMGroup.renderDirMode = BakeryLightmapGroup.RenderDirMode.ProbeSH;
        groupList.Add(volumeLMGroup);
        lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(10000,10000,10000)));
        data.lmid++;

        storages[sceneToID[EditorSceneManager.GetActiveScene()]].implicitGroups.Add(volumeLMGroup);
        storages[sceneToID[EditorSceneManager.GetActiveScene()]].implicitGroupedObjects.Add(null);
    }

    static void CollectExplicitLMGroups(ExportSceneData data)
    {
        var groupList = data.groupList;
        var lmBounds = data.lmBounds;

        // Find explicit LMGroups
        // (Also init lmBounds and LMID)
        var groupSelectors = new List<BakeryLightmapGroupSelector>(FindObjectsOfType(typeof(BakeryLightmapGroupSelector)) as BakeryLightmapGroupSelector[]);
        for(int i=0; i<groupSelectors.Count; i++)
        {
            var lmgroup = groupSelectors[i].lmgroupAsset as BakeryLightmapGroup;
            if (lmgroup == null) continue;
            if (!groupList.Contains(lmgroup))
            {
                lmgroup.id = data.lmid;
                lmgroup.sceneLodLevel = -1;
                lmgroup.sceneName = "";
                groupList.Add(lmgroup);
                lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(0,0,0)));
                data.lmid++;
            }
            EditorUtility.SetDirty(lmgroup);
        }
    }

    static bool CheckForMultipleSceneStorages(GameObject obj, ExportSceneData data)
    {
        var sceneHasStorage = data.sceneHasStorage;

        // Check for storage count in each scene
        if (obj.name == "!ftraceLightmaps")
        {
            if (!sceneHasStorage.ContainsKey(obj.scene))
            {
                sceneHasStorage[obj.scene] = true;
            }
            else
            {
                return ExportSceneValidationMessage("Scene " + obj.scene.name + " has multiple lightmap storage objects. This is not currently supported. Make sure you don't bake a scene with already baked prefabs.");
            }
        }
        return true;
    }

    static void ConvertTerrain(GameObject obj)
    {
        var terr = obj.GetComponent<Terrain>();
        if (terr == null) return;
        if (!terr.enabled) return;

        if (!obj.activeInHierarchy) return;
        if ((obj.hideFlags & (HideFlags.DontSave|HideFlags.HideAndDontSave)) != 0) return; // skip temp objects
        if (obj.tag == "EditorOnly") return; // skip temp objects
        if ((GameObjectUtility.GetStaticEditorFlags(obj) & StaticEditorFlags.ContributeGI) == 0) return; // skip dynamic

        var so = new SerializedObject(terr);
        var scaleInLmTerr = so.FindProperty("m_ScaleInLightmap").floatValue;

        var terrParent = new GameObject();
        SceneManager.MoveGameObjectToScene(terrParent, obj.scene);
        terrParent.transform.parent = obj.transform.parent;
        var expGroup = obj.GetComponent<BakeryLightmapGroupSelector>();
        if (expGroup != null)
        {
            var expGroup2 = terrParent.AddComponent<BakeryLightmapGroupSelector>();
            expGroup2.lmgroupAsset = expGroup.lmgroupAsset;
            expGroup2.instanceResolutionOverride = expGroup.instanceResolutionOverride;
            expGroup2.instanceResolution = expGroup.instanceResolution;
        }
        terrParent.name = "__ExportTerrainParent";
        terrainObjectList.Add(terrParent);
        terrainObjectToActual.Add(terr);

        var tdata = terr.terrainData;
        int res = tdata.heightmapResolution;
        var heightmap = tdata.GetHeights(0, 0, res, res);
        var uvscale = new Vector2(1,1) / (res-1);
        var uvoffset = new Vector2(0,0);
        var gposOffset = obj.transform.position;
        float scaleX = tdata.size.x / (res-1);
        float scaleY = tdata.size.y;
        float scaleZ = tdata.size.z / (res-1);

        int patchRes = res;
        while(patchRes > 254) patchRes = 254;//patchRes /= 2;
        int numVerts = patchRes * patchRes;
        int numPatches = (int)Mathf.Ceil(res / (float)patchRes);

        // Gen terrain texture
        var oldMat = terr.materialTemplate;
        var oldMatType = terr.materialType;
        var oldPos = obj.transform.position;
        var unlitTerrainMat = new Material(Shader.Find("Hidden/ftUnlitTerrain"));
            //unlitTerrainMat = AssetDatabase.LoadAssetAtPath("Assets/Bakery/ftUnlitTerrain.mat", typeof(Material)) as Material;
        terr.materialTemplate = unlitTerrainMat;
        terr.materialType = Terrain.MaterialType.Custom;
        obj.transform.position = new Vector3(-10000, -10000, -10000); // let's hope it's not the worst idea
        var tempCamGO = new GameObject();
        tempCamGO.transform.parent = obj.transform;
        tempCamGO.transform.localPosition = new Vector3(tdata.size.x * 0.5f, scaleY + 1, tdata.size.z * 0.5f);
        tempCamGO.transform.eulerAngles = new Vector3(90,0,0);
        var tempCam = tempCamGO.AddComponent<Camera>();
        tempCam.orthographic = true;
        tempCam.orthographicSize = Mathf.Max(tdata.size.x, tdata.size.z) * 0.5f;
        tempCam.aspect = Mathf.Max(tdata.size.x, tdata.size.z) / Mathf.Min(tdata.size.x, tdata.size.z);
        tempCam.enabled = false;
        tempCam.clearFlags = CameraClearFlags.SolidColor;
        tempCam.backgroundColor = new Color(0,0,0,0);
        tempCam.targetTexture =
            new RenderTexture(tdata.baseMapResolution, tdata.baseMapResolution, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
        var tex = new Texture2D(tdata.baseMapResolution, tdata.baseMapResolution, TextureFormat.ARGB32, true, false);
        RenderTexture.active = tempCam.targetTexture;
        tempCam.Render();
        terr.materialTemplate = oldMat;
        terr.materialType = oldMatType;
        obj.transform.position = oldPos;
        RenderTexture.active = tempCam.targetTexture;
        tex.ReadPixels(new Rect(0,0,tdata.baseMapResolution, tdata.baseMapResolution), 0, 0, true);
        tex.Apply();
        unlitTerrainMat.mainTexture = tex;
        Graphics.SetRenderTarget(null);
        DestroyImmediate(tempCamGO);

        if (exportTerrainAsHeightmap)
        {
            var hmap = new BinaryWriter(File.Open(scenePath + "/height" + terrainObjectToHeightMap.Count + ".dds", FileMode.Create));
            if (ftRenderLightmap.clientMode) ftClient.serverFileList.Add("height" + terrainObjectToHeightMap.Count + ".dds");

            hmap.Write(ftDDS.ddsHeaderR32F);
            var bytes = new byte[res * res * 4];

            // Normalize heights
            float maxHeight = 0;
            float height;
            for(int y=0; y<res; y++)
            {
                for(int x=0; x<res; x++)
                {
                    height = heightmap[x,y];
                    if (height > maxHeight) maxHeight = height;
                }
            }
            maxHeight = Mathf.Max(maxHeight, 0.0001f);
            float invMaxHeight = 1.0f / maxHeight;
            for(int y=0; y<res; y++)
            {
                for(int x=0; x<res; x++)
                {
                    heightmap[x,y] *= invMaxHeight;
                }
            }
            float aabbHeight = maxHeight * scaleY;
            //Debug.Log("aabbHeight: " + aabbHeight);

            // First mip is the real heightmap
            System.Buffer.BlockCopy(heightmap, 0, bytes, 0, bytes.Length);
            hmap.Write(bytes);

            var htex = new Texture2D(res, res, TextureFormat.RFloat, false, true);
            htex.LoadRawTextureData(bytes);
            htex.Apply();
            terrainObjectToHeightMap.Add(htex);

            // min
            terrainObjectToBounds.Add(obj.transform.position.x);
            terrainObjectToBounds.Add(obj.transform.position.y);
            terrainObjectToBounds.Add(obj.transform.position.z);
            // max
            terrainObjectToBounds.Add(obj.transform.position.x + tdata.size.x);
            terrainObjectToBounds.Add(obj.transform.position.y + aabbHeight);
            terrainObjectToBounds.Add(obj.transform.position.z + tdata.size.z);

            // filled later
            terrainObjectToLMID.Add(0);
            terrainObjectToBoundsUV.Add(0);
            terrainObjectToBoundsUV.Add(0);
            terrainObjectToBoundsUV.Add(0);
            terrainObjectToBoundsUV.Add(0);

            terrainObjectToFlags.Add(terr.castShadows ? 1 : 0);

            // Second mip is max() of 3x3 corners
            float[] floats = null;
            float[] floatsPrev = null;
            float[] floatsTmp;
            int mipCount = 1;
            int mipRes = res / 2;
            //int prevRes = res;
            float h00, h10, h01, h11;
            /*Vector3 n00, n10, n01, n11;
            Vector3[] normals = null;
            Vector3[] normalsPrev = null;

            var origNormals = new Vector3[res * res];
            for(int y=0; y<res; y++)
            {
                for(int x=0; x<res; x++)
                {
                    origNormals[y*res+x] = data.GetInterpolatedNormal(x / (float)res, y / (float)res);
                }
            }*/
            int terrIndex = terrainObjectToHeightMap.Count - 1;//terrainObjectToNormalMip0.Count;
            //terrainObjectToNormalMip0.Add(origNormals);
            //normalsPrev = origNormals;
            terrainObjectToHeightMips.Add(new List<float[]>());
            //terrainObjectToNormalMips.Add(new List<Vector3[]>());

            if (mipRes > 0)
            {
                floats = new float[mipRes * mipRes];
                //normals = new Vector3[mipRes * mipRes];
                for(int y=0; y<mipRes; y++)
                {
                    for(int x=0; x<mipRes; x++)
                    {
                        /*h00 = heightmap[y*2,x*2];
                        h10 = heightmap[y*2,x*2+1];
                        h01 = heightmap[y*2+1,x*2];
                        h11 = heightmap[y*2+1,x*2+1];
                        height = h00 > h10 ? h00 : h10;
                        height = height > h01 ? height : h01;
                        height = height > h11 ? height : h11;
                        floats[y*mipRes+x] = height;*/

                        float maxVal = 0;
                        for(int yy=0; yy<3; yy++)
                        {
                            for(int xx=0; xx<3; xx++)
                            {
                                float val = heightmap[y*2+yy, x*2+xx];
                                if (val > maxVal) maxVal = val;
                            }
                        }
                        floats[y*mipRes+x] = maxVal;

                        //n00 = normalsPrev[y*2 * res + x*2];
                        //n10 = normalsPrev[y*2 * res + x*2+1];
                        //n01 = normalsPrev[(y*2+1) * res + x*2];
                        //n11 = normalsPrev[(y*2+1) * res + x*2+1];
                        //normals[y*mipRes+x] = (n00 + n10 + n01 + n11);
                    }
                }

                System.Buffer.BlockCopy(floats, 0, bytes, 0, mipRes*mipRes*4);
                hmap.Write(bytes, 0, mipRes*mipRes*4);

                float[] storedMip = new float[mipRes*mipRes];
                System.Buffer.BlockCopy(floats, 0, storedMip, 0, mipRes*mipRes*4);
                terrainObjectToHeightMips[terrIndex].Add(storedMip);
                //terrainObjectToNormalMips[terrIndex].Add(normals);

                mipCount++;
                mipRes /= 2;
            }

            // Next mips are regular max() of 4 texels
            while(mipRes > 1)
            {
                if (floatsPrev == null)
                {
                    floatsPrev = floats;
                    floats = new float[mipRes * mipRes];
                }

                //normalsPrev = normals;
                //normals = new Vector3[mipRes * mipRes];

                for(int y=0; y<mipRes; y++)
                {
                    for(int x=0; x<mipRes; x++)
                    {
                        h00 = floatsPrev[y*2 * mipRes*2 + x*2];
                        h10 = floatsPrev[y*2 * mipRes*2 + x*2+1];
                        h01 = floatsPrev[(y*2+1) * mipRes*2 + x*2];
                        h11 = floatsPrev[(y*2+1) * mipRes*2 + x*2+1];
                        height = h00 > h10 ? h00 : h10;
                        height = height > h01 ? height : h01;
                        height = height > h11 ? height : h11;
                        floats[y*mipRes+x] = height;

                        //n00 = normalsPrev[y*2 * mipRes*2 + x*2];
                        //n10 = normalsPrev[y*2 * mipRes*2 + x*2+1];
                        //n01 = normalsPrev[(y*2+1) * mipRes*2 + x*2];
                        //n11 = normalsPrev[(y*2+1) * mipRes*2 + x*2+1];
                        //normals[y*mipRes+x] = (n00 + n10 + n01 + n11);
                    }
                }

                System.Buffer.BlockCopy(floats, 0, bytes, 0, mipRes*mipRes*4);
                hmap.Write(bytes, 0, mipRes*mipRes*4);

                float[] storedMip = new float[mipRes*mipRes];
                System.Buffer.BlockCopy(floats, 0, storedMip, 0, mipRes*mipRes*4);
                terrainObjectToHeightMips[terrIndex].Add(storedMip);
                //terrainObjectToNormalMips[terrIndex].Add(normals);

                mipCount++;
                mipRes /= 2;

                floatsTmp = floatsPrev;
                floatsPrev = floats;
                floats = floatsTmp;
            }

            hmap.BaseStream.Seek(12, SeekOrigin.Begin);
            hmap.Write(res);
            hmap.Write(res);
            hmap.BaseStream.Seek(28, SeekOrigin.Begin);
            hmap.Write(mipCount);
            hmap.Close();

            // Create dummy plane for packing/albedo/emissive purposes
            var mesh = new Mesh();
            mesh.vertices = new Vector3[] { gposOffset,
                                            gposOffset + new Vector3(tdata.size.x, 0, 0),
                                            gposOffset + new Vector3(tdata.size.x, 0, tdata.size.z),
                                            gposOffset + new Vector3(0, 0, tdata.size.z) };
            mesh.triangles = new int[]{0,1,2, 2,3,0};
            mesh.normals = new Vector3[]{Vector3.up, Vector3.up, Vector3.up, Vector3.up};
            mesh.uv = new Vector2[]{new Vector2(0,0), new Vector2(1,0), new Vector2(1,1), new Vector2(0,1)};
            mesh.uv2 = mesh.uv;

            var terrGO = new GameObject();
            terrGO.name = "__ExportTerrain";
            GameObjectUtility.SetStaticEditorFlags(terrGO, StaticEditorFlags.ContributeGI);
            terrGO.transform.parent = terrParent.transform;
            var mf = terrGO.AddComponent<MeshFilter>();
            var mr = terrGO.AddComponent<MeshRenderer>();
            mf.sharedMesh = mesh;
#if UNITY_2019_3_OR_NEWER
            // using standard materialTemplates in 2019.3 doesn't work
            mr.sharedMaterial = unlitTerrainMat;
#else
            mr.sharedMaterial = (terr.materialTemplate == null) ? unlitTerrainMat : terr.materialTemplate;
#endif

            terrGO.transform.position = obj.transform.position;

            var so2 = new SerializedObject(mr);
            so2.FindProperty("m_ScaleInLightmap").floatValue = scaleInLmTerr;
            so2.ApplyModifiedProperties();

            //terrainObjectList.Add(terrGO);
            //terrainObjectToActual.Add(terr);
        }
        else
        {
            for (int patchX=0; patchX<numPatches; patchX++)
            {
                for (int patchY=0; patchY<numPatches; patchY++)
                {
                    int patchResX = patchX < numPatches-1 ? patchRes : (res - patchRes*(numPatches-1));
                    int patchResY = patchY < numPatches-1 ? patchRes : (res - patchRes*(numPatches-1));
                    if (patchX < numPatches-1) patchResX += 1;
                    if (patchY < numPatches-1) patchResY += 1;
                    numVerts = patchResX * patchResY;
                    var posOffset = gposOffset + new Vector3(patchX*patchRes*scaleX, 0, patchY*patchRes*scaleZ);

                    var positions = new Vector3[numVerts];
                    var uvs = new Vector2[numVerts];
                    var normals = new Vector3[numVerts];
                    var indices = new int[(patchResX-1) * (patchResY-1) * 2 * 3];
                    int vertOffset = 0;
                    int indexOffset = 0;

                    for (int x=0;x<patchResX;x++)
                    {
                        for (int y=0;y<patchResY;y++)
                        {
                            int gx = x + patchX * patchRes;
                            int gy = y + patchY * patchRes;

                            int index = x * patchResY + y;
                            float height = heightmap[gy,gx];

                            positions[index] = new Vector3(x * scaleX, height * scaleY, y * scaleZ) + posOffset;
                            uvs[index] = new Vector2(gx * uvscale.x + uvoffset.x, gy * uvscale.y + uvoffset.y);

                            normals[index] = tdata.GetInterpolatedNormal(gx / (float)res, gy / (float)res);

                            if (x < patchResX-1 && y < patchResY-1)
                            {
                                indices[indexOffset] = vertOffset + patchResY + 1;
                                indices[indexOffset + 1] = vertOffset + patchResY;
                                indices[indexOffset + 2] = vertOffset;

                                indices[indexOffset + 3] = vertOffset + 1;
                                indices[indexOffset + 4] = vertOffset + patchResY + 1;
                                indices[indexOffset + 5] = vertOffset;

                                indexOffset += 6;
                            }

                            vertOffset++;
                        }
                    }

                    var mesh = new Mesh();
                    mesh.vertices = positions;
                    mesh.triangles = indices;
                    mesh.normals = normals;
                    mesh.uv = uvs;
                    mesh.uv2 = uvs;

                    var terrGO = new GameObject();
                    terrGO.name = "__ExportTerrain";
                    GameObjectUtility.SetStaticEditorFlags(terrGO, StaticEditorFlags.ContributeGI);
                    terrGO.transform.parent = terrParent.transform;
                    var mf = terrGO.AddComponent<MeshFilter>();
                    var mr = terrGO.AddComponent<MeshRenderer>();
                    mf.sharedMesh = mesh;
#if UNITY_2019_3_OR_NEWER
                    // using standard materialTemplates in 2019.3 doesn't work
                    mr.sharedMaterial = unlitTerrainMat;
#else
                    mr.sharedMaterial = (terr.materialTemplate == null) ? unlitTerrainMat : terr.materialTemplate;
#endif

                    var so2 = new SerializedObject(mr);
                    so2.FindProperty("m_ScaleInLightmap").floatValue = scaleInLmTerr;
                    so2.ApplyModifiedProperties();

                    mr.shadowCastingMode = terr.castShadows ? UnityEngine.Rendering.ShadowCastingMode.On : UnityEngine.Rendering.ShadowCastingMode.Off;

                    terrainObjectList.Add(terrGO);
                    terrainObjectToActual.Add(terr);
                }
            }
        }

        if (exportTerrainTrees && terr.drawTreesAndFoliage)
        {
            var trees = tdata.treeInstances;
            for (int t = 0; t < trees.Length; t++)
            {
                Vector3 pos = Vector3.Scale(trees[t].position, tdata.size) + obj.transform.position;

                var treeProt = tdata.treePrototypes[trees[t].prototypeIndex];
                var prefab = treeProt.prefab;

                var newObj = GameObject.Instantiate(prefab, pos, Quaternion.AngleAxis(trees[t].rotation, Vector3.up)) as GameObject;
                newObj.name = "__Export" + newObj.name;
                treeObjectList.Add(newObj);

                var lodGroup = newObj.GetComponent<LODGroup>();
                if (lodGroup == null)
                {
                    var renderers = newObj.GetComponentsInChildren<Renderer>();
                    for(int r=0; r<renderers.Length; r++)
                    {
                        GameObjectUtility.SetStaticEditorFlags(renderers[r].gameObject, StaticEditorFlags.ContributeGI);
                        var s = new SerializedObject(renderers[r]);
                        s.FindProperty("m_ScaleInLightmap").floatValue = 0;
                        s.ApplyModifiedProperties();
                    }
                }
                else
                {
                    var lods = lodGroup.GetLODs();
                    for (int tl = 0; tl < lods.Length; tl++)
                    {
                        for (int h = 0; h < lods[tl].renderers.Length; h++)
                        {
                            GameObjectUtility.SetStaticEditorFlags(lods[tl].renderers[h].gameObject, tl==0 ? StaticEditorFlags.ContributeGI : 0);
                            if (tl == 0)
                            {
                                var s = new SerializedObject(lods[tl].renderers[h]);
                                s.FindProperty("m_ScaleInLightmap").floatValue = 0;
                                s.ApplyModifiedProperties();
                            }
                        }
                    }
                }

                var xform = newObj.transform;
                xform.localScale = new Vector3(trees[t].widthScale, trees[t].heightScale, trees[t].widthScale);
            }
        }
    }

    static bool ConvertUnityAreaLight(GameObject obj)
    {
        // Add temporary meshes to area lights
        var areaLightMesh = obj.GetComponent<BakeryLightMesh>();
        if (areaLightMesh != null)
        {
            var areaLight = obj.GetComponent<Light>();
            var mr = obj.GetComponent<Renderer>();
            var mf = obj.GetComponent<MeshFilter>();

            if (!forceAllAreaLightsSelfshadow)
            {
                if (!areaLightMesh.selfShadow) return true; // no selfshadow - ignore mesh export
            }
            if (areaLight != null && ftLightMeshInspector.IsArea(areaLight) && (mr == null || mf == null))
            {
                var areaObj = new GameObject();
                mf = areaObj.AddComponent<MeshFilter>();
                mf.sharedMesh = BuildAreaLightMesh(areaLight);
                mr = areaObj.AddComponent<MeshRenderer>();

                var props = new MaterialPropertyBlock();
                props.SetColor("_Color", areaLightMesh.color);
                props.SetFloat("intensity", areaLightMesh.intensity);
                if (areaLightMesh.texture != null) props.SetTexture("_MainTex", areaLightMesh.texture);
                mr.SetPropertyBlock(props);
                GameObjectUtility.SetStaticEditorFlags(areaObj, StaticEditorFlags.ContributeGI);
                temporaryAreaLightMeshList.Add(areaObj);
                temporaryAreaLightMeshList2.Add(areaLightMesh);

                var xformSrc = obj.transform;
                var xformDest = areaObj.transform;
                xformDest.position = xformSrc.position;
                xformDest.rotation = xformSrc.rotation;
                var srcMtx = xformSrc.localToWorldMatrix;
                xformDest.localScale = new Vector3(srcMtx.GetColumn(0).magnitude, srcMtx.GetColumn(1).magnitude, srcMtx.GetColumn(2).magnitude);

                return true; // mesh created
            }
        }
        return false; // not Light Mesh
    }

    static void MapObjectsToSceneLODs(ExportSceneData data, UnityEngine.Object[] objects)
    {
        var objToLodLevel = data.objToLodLevel;
        var objToLodLevelVisible = data.objToLodLevelVisible;

        const int maxSceneLodLevels = 100;
        var sceneLodUsed = new int[maxSceneLodLevels];
        for(int i=0; i<maxSceneLodLevels; i++) sceneLodUsed[i] = -1;
        var lodGroups = Resources.FindObjectsOfTypeAll(typeof(LODGroup));
        var lodLevelsInLodGroup = new List<int>[lodGroups.Length];
        var localLodLevelsInLodGroup = new List<int>[lodGroups.Length];
        int lcounter = -1;
        foreach(LODGroup lodgroup in lodGroups)
        {
            lcounter++;
            if (!lodgroup.enabled) continue;
            var obj = lodgroup.gameObject;
            if (obj == null) continue;
            if (!obj.activeInHierarchy) continue;
            var path = AssetDatabase.GetAssetPath(obj);
            if (path != "") continue; // must belond to scene
            if ((obj.hideFlags & (HideFlags.DontSave|HideFlags.HideAndDontSave)) != 0) continue; // skip temp objects
            if (obj.tag == "EditorOnly") continue; // skip temp objects

            var lods = lodgroup.GetLODs();
            if (lods.Length == 0) continue;

            for(int i=0; i<lods.Length; i++)
            {
                var lodRenderers = lods[i].renderers;
                if (lodRenderers.Length == 0) continue;

                bool lightmappedLOD = false;
                for(int j=0; j<lodRenderers.Length; j++)
                {
                    var r = lodRenderers[j];
                    if (r == null) continue;
                    if (!r.enabled) continue;
                    if (!r.gameObject.activeInHierarchy) continue;
                    if ((r.gameObject.hideFlags & (HideFlags.DontSave|HideFlags.HideAndDontSave)) != 0) continue; // skip temp objects
                    if (r.gameObject.tag == "EditorOnly") continue; // skip temp objects
                    if ((GameObjectUtility.GetStaticEditorFlags(r.gameObject) & StaticEditorFlags.ContributeGI) == 0) continue; // skip dynamic
                    var mr = r.gameObject.GetComponent<Renderer>();
                    var sharedMesh = GetSharedMesh(mr);
                    if (mr == null || sharedMesh == null) continue; // must have visible mesh
                    var mrEnabled = mr.enabled || r.gameObject.GetComponent<BakeryAlwaysRender>() != null;
                    if (!mrEnabled) continue;
                    //if (mf.sharedMesh == null) continue;

                    var so = new SerializedObject(mr);
                    var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;
                    if (scaleInLm == 0) continue;

                    lightmappedLOD = true;
                    break;
                }
                if (!lightmappedLOD) continue;

                var lodDist = i == 0 ? 0 : (int)Mathf.Clamp((1.0f-lods[i-1].screenRelativeTransitionHeight) * (maxSceneLodLevels-1), 0, maxSceneLodLevels-1);
                if (sceneLodUsed[lodDist] < 0)
                {
                    sceneLodUsed[lodDist] = sceneLodsUsed;
                    sceneLodsUsed++;
                }
                int newLodLevel = sceneLodUsed[lodDist];

                if (lodLevelsInLodGroup[lcounter] == null)
                {
                    lodLevelsInLodGroup[lcounter] = new List<int>();
                    localLodLevelsInLodGroup[lcounter] = new List<int>();
                }
                if (lodLevelsInLodGroup[lcounter].IndexOf(newLodLevel) < 0)
                {
                    lodLevelsInLodGroup[lcounter].Add(newLodLevel);
                    localLodLevelsInLodGroup[lcounter].Add(i);
                }

                for(int j=0; j<lodRenderers.Length; j++)
                {
                    var r = lodRenderers[j];
                    if (r == null) continue;
                    int existingLodLevel = -1;
                    if (objToLodLevel.ContainsKey(r.gameObject)) existingLodLevel = objToLodLevel[r.gameObject];
                    if (existingLodLevel < newLodLevel)
                    {
                        objToLodLevel[r.gameObject] = existingLodLevel < 0 ? newLodLevel : existingLodLevel; // set to lowest LOD

                        // Collect LOD levels where this object is visible
                        List<int> visList;
                        if (!objToLodLevelVisible.TryGetValue(r.gameObject, out visList)) objToLodLevelVisible[r.gameObject] = visList = new List<int>();
                        visList.Add(newLodLevel);
                    }
                }
            }
        }

        // Sort scene LOD levels
        int counter = 0;
        var unsortedLodToSortedLod = new int[maxSceneLodLevels];
        for(int i=0; i<maxSceneLodLevels; i++)
        {
            int unsorted = sceneLodUsed[i];
            if (unsorted >= 0)
            {
                unsortedLodToSortedLod[unsorted] = counter;
                sceneLodUsed[i] = counter;
                counter++;
            }
        }
        var keys = new GameObject[objToLodLevel.Count];
        counter = 0;
        foreach(var pair in objToLodLevel)
        {
            keys[counter] = pair.Key;
            counter++;
        }
        foreach(var key in keys)
        {
            int unsorted = objToLodLevel[key];
            objToLodLevel[key] = unsortedLodToSortedLod[unsorted];
            var visList = objToLodLevelVisible[key];
            for(int j=0; j<visList.Count; j++)
            {
                visList[j] = unsortedLodToSortedLod[visList[j]];
            }
        }
        for(int i=0; i<lodLevelsInLodGroup.Length; i++)
        {
            if (lodLevelsInLodGroup[i] == null) continue;
            var levels = lodLevelsInLodGroup[i];
            for(int j=0; j<levels.Count; j++)
            {
                levels[j] = unsortedLodToSortedLod[levels[j]];
            }
        }

        // Fill LOD gaps
        for(int i=0; i<lodLevelsInLodGroup.Length; i++)
        {
            if (lodLevelsInLodGroup[i] == null) continue;
            var levels = lodLevelsInLodGroup[i];
            var localLevels = localLodLevelsInLodGroup[i];
            var lgroup = lodGroups[i] as LODGroup;
            var lods = lgroup.GetLODs();
            for(int j=0; j<levels.Count; j++)
            {
                int level = levels[j];
                int localLevel = localLevels[j];
                int nextLevel = (j == levels.Count-1) ? (sceneLodsUsed-1) : levels[j+1];
                if (nextLevel - level > 1)
                {
                    var lodRenderers = lods[localLevel].renderers;
                    for(int k=0; k<lodRenderers.Length; k++)
                    {
                        var r = lodRenderers[k];
                        if (r == null) continue;

                        var visList = objToLodLevelVisible[r.gameObject];
                        for(int l=level+1; l<nextLevel; l++)
                        {
                            visList.Add(l);
                        }
                    }
                }
            }
        }

        Debug.Log("Scene LOD levels: " + sceneLodsUsed);

        // Init scene LOD index buffers
        data.indicesOpaqueLOD = new List<int>[sceneLodsUsed];
        data.indicesTransparentLOD = new List<int>[sceneLodsUsed];
        for(int i=0; i<sceneLodsUsed; i++)
        {
            data.indicesOpaqueLOD[i] = new List<int>();
            data.indicesTransparentLOD[i] = new List<int>();
        }

        // Sort objects by scene-wide LOD level
        if (sceneLodsUsed > 0)
        {
            Array.Sort(objects, delegate(UnityEngine.Object a, UnityEngine.Object b)
            {
                if (a == null || b == null) return 0;
                int lodLevelA = -1;
                int lodLevelB = -1;
                if (!objToLodLevel.TryGetValue((GameObject)a, out lodLevelA)) lodLevelA = -1;
                if (!objToLodLevel.TryGetValue((GameObject)b, out lodLevelB)) lodLevelB = -1;
                return lodLevelA.CompareTo(lodLevelB);
            });
        }
    }

    static bool FilterObjects(ExportSceneData data, UnityEngine.Object[] objects)
    {
        var objToLodLevel = data.objToLodLevel;
        var groupList = data.groupList;
        var lmBounds = data.lmBounds;
        var storages = data.storages;
        var sceneToID = data.sceneToID;
        var objsToWrite = data.objsToWrite;
        var objsToWriteNames = data.objsToWriteNames;
        var objsToWriteLightmapped = data.objsToWriteLightmapped;
        var objsToWriteGroup = data.objsToWriteGroup;
        var objsToWriteHolder = data.objsToWriteHolder;
        var objsToWriteVerticesUV = data.objsToWriteVerticesUV;
        var objsToWriteVerticesUV2 = data.objsToWriteVerticesUV2;
        var objsToWriteIndices = data.objsToWriteIndices;

        var prop = new MaterialPropertyBlock();
        foreach(GameObject obj in objects)
        {
            if (obj == null) continue;
            if (!obj.activeInHierarchy) continue;
            var path = AssetDatabase.GetAssetPath(obj);
            if (path != "") continue; // must belond to scene
            if ((obj.hideFlags & (HideFlags.DontSave|HideFlags.HideAndDontSave)) != 0) continue; // skip temp objects
            if (obj.tag == "EditorOnly") continue; // skip temp objects

            var areaLight = obj.GetComponent<BakeryLightMesh>();
            if (areaLight == null)
            {
                int areaIndex = temporaryAreaLightMeshList.IndexOf(obj);
                if (areaIndex >= 0) areaLight = temporaryAreaLightMeshList2[areaIndex];
            }

            if (areaLight != null)
            {
                if (!forceAllAreaLightsSelfshadow)
                {
                    if (!areaLight.selfShadow) continue;
                }
            }
            var mr = obj.GetComponent<Renderer>();

            if (mr as MeshRenderer == null && mr as SkinnedMeshRenderer == null)
            {
                // must be MR or SMR
                continue;
            }

            var sharedMesh = GetSharedMesh(mr);
            if (sharedMesh == null) continue; // must have visible mesh

            // Remove previous lightmap
#if UNITY_2018_1_OR_NEWER
            if (mr.HasPropertyBlock())
            {
                // Reset shader props
                mr.GetPropertyBlock(prop);
                prop.SetFloat("bakeryLightmapMode", 0);
                mr.SetPropertyBlock(prop);
            }
#else
            mr.GetPropertyBlock(prop);
            if (!prop.isEmpty)
            {
                prop.SetFloat("bakeryLightmapMode", 0);
                mr.SetPropertyBlock(prop);
            }
#endif
            if (((GameObjectUtility.GetStaticEditorFlags(obj) & StaticEditorFlags.ContributeGI) == 0) && areaLight==null)
            {
                mr.lightmapIndex = 0xFFFF;
                continue; // skip dynamic
            }

            var mrEnabled = mr.enabled || obj.GetComponent<BakeryAlwaysRender>() != null;
            if (!mrEnabled && areaLight == null) continue;

            var so = new SerializedObject(obj.GetComponent<Renderer>());
            var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;

            BakeryLightmapGroup group = null;
            if (scaleInLm > 0)
            {
                group = GetLMGroupFromObjectExplicit(obj, data);
                if (group != null)
                {
                    // Set LOD level for explicit group
                    int lodLevel;
                    if (!objToLodLevel.TryGetValue(obj, out lodLevel)) lodLevel = -1;

                    if (!postPacking)
                    {
                        if (group.sceneLodLevel == -1)
                        {
                            group.sceneLodLevel = lodLevel;
                        }
                        else
                        {
                            if (!ExportSceneValidationMessage("Multiple LOD levels in " + group.name + ", this is only supported when xatlas is set as the atlas packer and post-packing is enabled.")) return false;
                        }
                    }

                    if (splitByScene) group.sceneName = obj.scene.name;

                    // New explicit Pack Atlas holder selection
                    if (!group.isImplicit && group.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
                    {
                        lmgroupHolder = obj; // by default pack each object
                        lmgroupHolder = TestPackAsSingleSquare(lmgroupHolder);
                        var prefabParent = PrefabUtility.GetPrefabParent(obj) as GameObject;
                        if (prefabParent != null)
                        {
                            var ptype = PrefabUtility.GetPrefabType(prefabParent);
                            if (ptype == PrefabType.ModelPrefab)
                            {
                                // but if object is a part of prefab/model
                                var sharedMesh2 = GetSharedMesh(obj);
                                var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(sharedMesh2)) as ModelImporter;
                                if (importer != null && !ModelUVsOverlap(importer, gstorage))
                                {
                                    // or actually just non-overlapping model,
                                    // then pack it as a whole

                                    // find topmost asset parent
                                    var t = prefabParent.transform;
                                    while(t.parent != null) t = t.parent;
                                    var assetTopMost = t.gameObject;

                                    // find topmost scene instance parent
                                    var g = obj;
                                    while(PrefabUtility.GetPrefabParent(g) as GameObject != assetTopMost && g.transform.parent != null)
                                    {
                                        g = g.transform.parent.gameObject;
                                    }
                                    lmgroupHolder = g;
                                }
                            }
                        }
                    }
                }
            }
            else
            {
                if (data.autoVertexGroup == null)
                {
                    data.autoVertexGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();
                    data.autoVertexGroup.name = obj.scene.name + "_VLM";
                    data.autoVertexGroup.isImplicit = true;
                    data.autoVertexGroup.resolution = 256;
                    data.autoVertexGroup.bitmask = 1;
                    data.autoVertexGroup.mode = BakeryLightmapGroup.ftLMGroupMode.Vertex;
                    data.autoVertexGroup.id = data.lmid;
                    groupList.Add(data.autoVertexGroup);
                    lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(0,0,0)));
                    data.lmid++;
                }
                group = data.autoVertexGroup;

                storages[sceneToID[obj.scene]].implicitGroupedObjects.Add(obj);
                storages[sceneToID[obj.scene]].implicitGroups.Add(data.autoVertexGroup);
                tempStorage.implicitGroupMap[obj] = data.autoVertexGroup;

                storages[sceneToID[obj.scene]].nonBakedRenderers.Add(mr);
            }

            bool vertexBake = (group != null && group.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex);
            // must have UVs or be arealight or vertexbaked
            var uv = sharedMesh.uv;
            var uv2 = sharedMesh.uv2;
            if (uv.Length == 0 && uv2.Length == 0 && areaLight==null && !vertexBake) continue;

            var usedUVs = uv2.Length == 0 ? uv : uv2;
            //bool validUVs = true;
            for(int v=0; v<usedUVs.Length; v++)
            {
                if (usedUVs[v].x < -0.0001f || usedUVs[v].x > 1.0001f || usedUVs[v].y < -0.0001f || usedUVs[v].y > 1.0001f)
                {
                    Debug.LogWarning("Mesh " + sharedMesh.name + " on object " + obj.name + " possibly has incorrect UVs (UV2: " + (uv2.Length == 0 ? "no" : "yes")+", U: " + usedUVs[v].x + ", V: " + usedUVs[v].y + ")");
                    //validUVs = false;
                    break;
                }
            }
            //if (!validUVs) continue;

            if (vertexBake)
            {
                group.totalVertexCount = 0;
                group.vertexCounter = 0;
            }

            objsToWrite.Add(obj);
            objsToWriteNames.Add(obj.name);
            objsToWriteLightmapped.Add((scaleInLm > 0 && areaLight == null) ? true : false);
            objsToWriteGroup.Add(group);
            objsToWriteHolder.Add(lmgroupHolder);

            objsToWriteVerticesUV.Add(uv);
            objsToWriteVerticesUV2.Add(uv2);
            var inds = new int[sharedMesh.subMeshCount][];
            for(int n=0; n<inds.Length; n++) inds[n] = sharedMesh.GetTriangles(n);
            objsToWriteIndices.Add(inds);
        }

        return true;
    }

    static void CalculateVertexCountForVertexGroups(ExportSceneData data)
    {
        var objsToWrite = data.objsToWrite;
        var objsToWriteGroup = data.objsToWriteGroup;

        // Calculate total vertex count for vertex-baked groups
        for(int i=0; i<objsToWrite.Count; i++)
        {
            var lmgroup = objsToWriteGroup[i];
            if (lmgroup == null || lmgroup.mode != BakeryLightmapGroup.ftLMGroupMode.Vertex) continue;
            var sharedMesh = GetSharedMesh(objsToWrite[i]);
            lmgroup.totalVertexCount += sharedMesh.vertexCount;
        }
    }

    static void CreateAutoAtlasLMGroups(ExportSceneData data, bool renderTextures, bool atlasOnly)
    {
        var objsToWrite = data.objsToWrite;
        var objsToWriteLightmapped = data.objsToWriteLightmapped;
        var objsToWriteGroup = data.objsToWriteGroup;
        var objsToWriteHolder = data.objsToWriteHolder;
        var objToLodLevel = data.objToLodLevel;
        var storages = data.storages;
        var sceneToID = data.sceneToID;
        var groupList = data.groupList;
        var lmBounds = data.lmBounds;
        var autoAtlasGroups = data.autoAtlasGroups;
        var autoAtlasGroupRootNodes = data.autoAtlasGroupRootNodes;

        // Create implicit temp LMGroups.
        // If object is a part of prefab, and if UVs are not generated in Unity, group is only addded to the topmost object (aka holder).

        // Implicit groups are added on every static object without ftLMGroupSelector.
        // (Also init lmBounds and LMID as well)
        // if autoAtlas == false: new group for every holder.
        // if autoAtlas == true: single group for all holders (will be split later).
        for(int i=0; i<objsToWrite.Count; i++)
        {
            if (!objsToWriteLightmapped[i]) continue; // skip objects with scaleInLM == 0
            if (objsToWriteGroup[i] != null) continue; // skip if already has lightmap assigned
            var obj = objsToWrite[i];

            var holder = obj; // holder is object itself (packed individually)
            holder = TestPackAsSingleSquare(holder);
            var prefabParent = PrefabUtility.GetPrefabParent(obj) as GameObject;
            if (prefabParent != null) // object is part of prefab
            {
                // unity doesn't generate non-overlapping UVs for the whole model, only submeshes
                // // if importer == null, it's an actual prefab, not model <-- not really; importer points to mesh's prefab, not real
                // importer of a mesh is always model asset
                // importers of components never exist
                // at least check the prefab type
                var ptype = PrefabUtility.GetPrefabType(prefabParent);
                if (ptype == PrefabType.ModelPrefab)
                {
                    var sharedMesh = GetSharedMesh(obj);
                    var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(sharedMesh)) as ModelImporter;

                    if (importer != null && !ModelUVsOverlap(importer, gstorage))
                    {
                        // find topmost asset parent
                        var t = prefabParent.transform;
                        while(t.parent != null) t = t.parent;
                        var assetTopMost = t.gameObject;

                        // find topmost scene instance parent
                        var g = obj;
                        var assetG = PrefabUtility.GetPrefabParent(g) as GameObject;
                        while(assetG != assetTopMost && g.transform.parent != null && assetG.transform.parent != null)
                        {
                            var g2 = g.transform.parent.gameObject;
                            var assetG2 = assetG.transform.parent.gameObject;

                            if (PrefabUtility.GetPrefabParent(g2) != assetG2) break; // avoid using parents which don't belong to this model

                            g = g2;
                            assetG = assetG2;
                        }
                        var sceneTopMost = g;
                        holder = sceneTopMost; // holder is topmost model object (non-overlapped UVs)

                        int lodLevel;
                        if (objToLodLevel.TryGetValue(obj, out lodLevel)) holder = obj; // separated if used in LOD
                    }
                }
            }
            else if (obj.name == "__ExportTerrain")
            {
                holder = obj.transform.parent.gameObject; // holder is terrain parent

                int lodLevel;
                if (objToLodLevel.TryGetValue(obj, out lodLevel)) holder = obj; // separated if used in LOD
            }

            if (!storages[sceneToID[holder.scene]].implicitGroupedObjects.Contains(holder))
            {
                BakeryLightmapGroup newGroup;
                if (autoAtlas && autoAtlasGroups.Count > 0)
                {
                    newGroup = autoAtlasGroups[0];
                }
                else
                {
                    newGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();

                    // Make sure first lightmap is always LM0, not LM1, if probes are used
                    int lmNum = storages[sceneToID[holder.scene]].implicitGroups.Count;
                    if (ftRenderLightmap.lightProbeMode == ftRenderLightmap.LightProbeMode.L1 && ftRenderLightmap.hasAnyProbes && renderTextures && !atlasOnly) lmNum--;

                    newGroup.name = holder.scene.name + "_LM" + autoAtlasGroups.Count;//lmNum;
                    newGroup.isImplicit = true;
                    newGroup.resolution = 256;
                    newGroup.bitmask = 1;
                    newGroup.area = 0;
                    newGroup.mode = autoAtlas ? BakeryLightmapGroup.ftLMGroupMode.PackAtlas : BakeryLightmapGroup.ftLMGroupMode.OriginalUV;

                    newGroup.id = data.lmid;
                    groupList.Add(newGroup);
                    lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(0,0,0)));
                    data.lmid++;

                    if (autoAtlas)
                    {
                        autoAtlasGroups.Add(newGroup);
                        var rootNode = new AtlasNode();
                        rootNode.rc = new Rect(0, 0, 1, 1);
                        autoAtlasGroupRootNodes.Add(rootNode);
                    }
                }
                storages[sceneToID[holder.scene]].implicitGroupedObjects.Add(holder);

                storages[sceneToID[holder.scene]].implicitGroups.Add(newGroup);
                //Debug.LogError("Add "+(storages[sceneToID[holder.scene]].implicitGroups.Count-1)+" "+newGroup.name);

                tempStorage.implicitGroupMap[holder] = newGroup;
                if (splitByScene) newGroup.sceneName = holder.scene.name;
            }

            if (!tempStorage.implicitGroupMap.ContainsKey(holder))
            {
                // happens with modifyLightmapStorage == false
                var gholders = storages[sceneToID[holder.scene]].implicitGroupedObjects;
                var grs = storages[sceneToID[holder.scene]].implicitGroups;
                for(int g=0; g<gholders.Count; g++)
                {
                    if (gholders[g] == holder)
                    {
                        tempStorage.implicitGroupMap[holder] = grs[g];
                        break;
                    }
                }
            }

            objsToWriteGroup[i] = (BakeryLightmapGroup)tempStorage.implicitGroupMap[holder];
            objsToWriteHolder[i] = holder;
        }
    }

    static void TransformVertices(ExportSceneData data, bool tangentSHLights, int onlyID = -1)
    {
        var objsToWrite = data.objsToWrite;
        var objsToWriteGroup = data.objsToWriteGroup;
        var objsToWriteVerticesPosW = data.objsToWriteVerticesPosW;
        var objsToWriteVerticesNormalW = data.objsToWriteVerticesNormalW;
        var objsToWriteVerticesTangentW = data.objsToWriteVerticesTangentW;

        int startIndex = 0;
        int endIndex = objsToWrite.Count-1;

        if (onlyID >= 0)
        {
            startIndex = onlyID;
            endIndex = onlyID;
        }

        // Transform vertices to world space
        for(int i=startIndex; i<=endIndex; i++)
        {
            var obj = objsToWrite[i];
            var lmgroup = objsToWriteGroup[i];
            bool isSkin;
            var m = GetSharedMeshSkinned(obj, out isSkin);
            var vertices = m.vertices;
            var tform = obj.transform;

            while(objsToWriteVerticesPosW.Count <= i)
            {
                objsToWriteVerticesPosW.Add(null);
                objsToWriteVerticesNormalW.Add(null);
            }

            objsToWriteVerticesPosW[i] = new Vector3[vertices.Length];
            if (isSkin)
            {
                var lossyScale = tform.lossyScale;
                var inverseWorldScale = new Vector3(1.0f/lossyScale.x, 1.0f/lossyScale.y, 1.0f/lossyScale.z);
                for(int t=0; t<vertices.Length; t++)
                {
                    vertices[t].Scale(inverseWorldScale);
                }
            }
            for(int t=0; t<vertices.Length; t++)
            {
                objsToWriteVerticesPosW[i][t] = tform.TransformPoint(vertices[t]);
            }
            var normals = m.normals;
            objsToWriteVerticesNormalW[i] = new Vector3[vertices.Length];
            var nbuff = objsToWriteVerticesNormalW[i];
            var localScale = obj.transform.localScale;
            bool flipX = localScale.x < 0;
            bool flipY = localScale.y < 0;
            bool flipZ = localScale.z < 0;
            if (lmgroup != null && lmgroup.flipNormal)
            {
                flipX = !flipX;
                flipY = !flipY;
                flipZ = !flipZ;
            }
            for(int t=0; t<vertices.Length; t++)
            {
                if (normals.Length == 0)
                {
                    nbuff[t] = Vector3.up;
                }
                else
                {
                    nbuff[t] = normals[t];
                    if (flipX) nbuff[t].x *= -1;
                    if (flipY) nbuff[t].y *= -1;
                    if (flipZ) nbuff[t].z *= -1;
                    nbuff[t] = tform.TransformDirection(nbuff[t]);
                }
            }
            if (NeedsTangents(lmgroup, tangentSHLights))
            {
                var tangents = m.tangents;
                while(objsToWriteVerticesTangentW.Count <= i) objsToWriteVerticesTangentW.Add(null);
                objsToWriteVerticesTangentW[i] = new Vector4[vertices.Length];
                var tbuff = objsToWriteVerticesTangentW[i];
                Vector3 tangent = Vector3.zero;
                for(int t=0; t<vertices.Length; t++)
                {
                    if (tangents.Length == 0)
                    {
                        tbuff[t] = Vector3.right;
                    }
                    else
                    {
                        tangent.Set(flipX ? -tangents[t].x : tangents[t].x,
                                    flipY ? -tangents[t].y : tangents[t].y,
                                    flipZ ? -tangents[t].z : tangents[t].z);
                        tangent = tform.TransformDirection(tangent);
                        tbuff[t] = new Vector4(tangent.x, tangent.y, tangent.z, tangents[t].w);
                    }
                }
            }
        }
    }

    static void CalculateUVPadding(ExportSceneData data, AdjustUVPaddingData adata)
    {
        var meshToPaddingMap = adata.meshToPaddingMap;
        var meshToObjIDs = adata.meshToObjIDs;

        float smallestMapScale = 1;
        float colorScale = 1.0f / (1 << (int)((1.0f - ftBuildGraphics.mainLightmapScale) * 6));
        float maskScale = 1.0f / (1 << (int)((1.0f - ftBuildGraphics.maskLightmapScale) * 6));
        float dirScale = 1.0f / (1 << (int)((1.0f - ftBuildGraphics.dirLightmapScale) * 6));
        smallestMapScale = Mathf.Min(colorScale, maskScale);
        smallestMapScale = Mathf.Min(smallestMapScale, dirScale);

        var objsToWrite = data.objsToWrite;
        var objsToWriteGroup = data.objsToWriteGroup;
        var objsToWriteVerticesPosW = data.objsToWriteVerticesPosW;
        var objsToWriteIndices = data.objsToWriteIndices;
        var objsToWriteHolder = data.objsToWriteHolder;

        // Calculate every implicit mesh area and convert to proper padding value
        var explicitGroupTotalArea = new Dictionary<int, float>();
        var objsWithExplicitGroupPadding = new List<int>();
        var objsWithExplicitGroupPaddingWidth = new List<float>();

        for(int i=0; i<objsToWrite.Count; i++)
        {
            var lmgroup = objsToWriteGroup[i];
            if (lmgroup == null) continue;
            var prefabParent = PrefabUtility.GetPrefabParent(objsToWrite[i]) as GameObject;
            if (prefabParent == null) continue;
            var sharedMesh = GetSharedMesh(objsToWrite[i]);
            var assetPath = AssetDatabase.GetAssetPath(sharedMesh);
            var importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
            if (importer == null || !importer.generateSecondaryUV) continue;
            // user doesn't care much about UVs - adjust

            var m = sharedMesh;
            var vpos = objsToWriteVerticesPosW[i];
            float area = 0;
            var inds = objsToWriteIndices[i];
            for(int k=0;k<m.subMeshCount;k++) {
                var indices = inds[k];// m.GetTriangles(k);
                int indexA, indexB, indexC;
                for(int j=0;j<indices.Length;j+=3)
                {
                    indexA = indices[j];
                    indexB = indices[j + 1];
                    indexC = indices[j + 2];

                    var v1 = vpos[indexA];
                    var v2 = vpos[indexB];
                    var v3 = vpos[indexC];
                    area += Vector3.Cross(v2 - v1, v3 - v1).magnitude;
                }
            }
            var so = new SerializedObject(objsToWrite[i].GetComponent<Renderer>());
            var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;
            area *= scaleInLm;

            float width = Mathf.Sqrt(area);
            float twidth = 1;
            if (lmgroup.isImplicit)
            {
                twidth = width * texelsPerUnit;
            }
            else
            {
                float currentArea;
                if (!explicitGroupTotalArea.TryGetValue(lmgroup.id, out currentArea)) currentArea = 0;
                explicitGroupTotalArea[lmgroup.id] = currentArea + area;

                var holder = objsToWriteHolder[i];
                BakeryLightmapGroupSelector comp = null;
                if (holder != null) comp = holder.GetComponent<BakeryLightmapGroupSelector>();
                if (comp != null && comp.instanceResolutionOverride)
                {
                    // Explicit holder size
                    twidth = width * comp.instanceResolution;
                }
                else
                {
                    // Texel size in atlas - can't calculate at this point
                    objsWithExplicitGroupPadding.Add(i);
                    objsWithExplicitGroupPaddingWidth.Add(width);
                    continue;
                }
            }
            float requiredPadding = 4 * (1024.0f / (twidth * smallestMapScale));
            int requiredPaddingClamped = (int)Mathf.Clamp(requiredPadding, 1, 256);

            int existingPadding = 0;
            meshToPaddingMap.TryGetValue(m, out existingPadding);
            meshToPaddingMap[m] = Math.Max(requiredPaddingClamped, existingPadding); // select largest padding among instances

            List<int> arr;
            if (!meshToObjIDs.TryGetValue(m, out arr))
            {
                meshToObjIDs[m] = arr = new List<int>();
            }
            if (!arr.Contains(i)) arr.Add(i);
        }

        for(int j=0; j<objsWithExplicitGroupPadding.Count; j++)
        {
            int i = objsWithExplicitGroupPadding[j];
            float width = objsWithExplicitGroupPaddingWidth[j];
            var lmgroup = objsToWriteGroup[i];
            float totalArea = explicitGroupTotalArea[lmgroup.id];
            float twidth = (width / Mathf.Sqrt(totalArea)) * lmgroup.resolution;
            var m = GetSharedMesh(objsToWrite[i]);

            // Following is copy-pasted from the loop above
            float requiredPadding = 4 * (1024.0f / (twidth * smallestMapScale));
            int requiredPaddingClamped = (int)Mathf.Clamp(requiredPadding, 1, 256);

            int existingPadding = 0;
            meshToPaddingMap.TryGetValue(m, out existingPadding);
            meshToPaddingMap[m] = Math.Max(requiredPaddingClamped, existingPadding); // select largest padding among instances

            List<int> arr;
            if (!meshToObjIDs.TryGetValue(m, out arr))
            {
                meshToObjIDs[m] = arr = new List<int>();
            }
            if (!arr.Contains(i)) arr.Add(i);
        }
    }

    static void ResetPaddingStorageData(ExportSceneData data)
    {
        var storages = data.storages;

        // Reset scene padding backup
        for(int s=0; s<storages.Length; s++)
        {
            var str = storages[s];
            if (str == null) continue;
            str.modifiedAssetPathList = new List<string>();
            str.modifiedAssets = new List<ftGlobalStorage.AdjustedMesh>();
        }
    }

    static void StoreNewUVPadding(ExportSceneData data, AdjustUVPaddingData adata)
    {
        var meshToPaddingMap = adata.meshToPaddingMap;
        var meshToObjIDs = adata.meshToObjIDs;
        var dirtyAssetList = adata.dirtyAssetList;
        var dirtyObjList = adata.dirtyObjList;
        var storages = data.storages;

        foreach(var pair in meshToPaddingMap)
        {
            var m = pair.Key;
            var requiredPaddingClamped = pair.Value;
            var assetPath = AssetDatabase.GetAssetPath(m);

            var ids = meshToObjIDs[m];

            //for(int s=0; s<sceneCount; s++)
            {
                var objStorage = gstorage;// == null ? storages[0] : gstorage;// storages[s];
                int mstoreIndex = objStorage.modifiedAssetPathList.IndexOf(assetPath);
                int ind = -1;
                var mname = m.name;
                if (mstoreIndex >= 0) ind = objStorage.modifiedAssets[mstoreIndex].meshName.IndexOf(mname);
                if (ind < 0)
                {
                    if (mstoreIndex < 0)
                    {
                        // add new record to globalstorage
                        objStorage.modifiedAssetPathList.Add(assetPath);
                        var newStruct = new ftGlobalStorage.AdjustedMesh();
                        newStruct.meshName = new List<string>();
                        newStruct.padding = new List<int>();
                        objStorage.modifiedAssets.Add(newStruct);
                        mstoreIndex = objStorage.modifiedAssets.Count - 1;
                    }

                    var nameList = objStorage.modifiedAssets[mstoreIndex].meshName;
                    var paddingList = objStorage.modifiedAssets[mstoreIndex].padding;
                    var unwrapperList = objStorage.modifiedAssets[mstoreIndex].unwrapper;
                    if (unwrapperList == null)
                    {
                        var s = objStorage.modifiedAssets[mstoreIndex];
                        unwrapperList = s.unwrapper = new List<int>();
                        objStorage.modifiedAssets[mstoreIndex] = s;
                    }
                    while(nameList.Count > unwrapperList.Count) unwrapperList.Add(0); // fix legacy

                    nameList.Add(mname);
                    paddingList.Add(requiredPaddingClamped);
                    unwrapperList.Add((int)ftRenderLightmap.unwrapper);

                    if (!dirtyAssetList.Contains(assetPath)) dirtyAssetList.Add(assetPath);
                    for(int xx=0; xx<ids.Count; xx++) dirtyObjList.Add(ids[xx]);
#if UNITY_2017_1_OR_NEWER
                    objStorage.SyncModifiedAsset(mstoreIndex);
#endif
                }
                else
                {
                    var nameList = objStorage.modifiedAssets[mstoreIndex].meshName;
                    var paddingList = objStorage.modifiedAssets[mstoreIndex].padding;
                    var unwrapperList = objStorage.modifiedAssets[mstoreIndex].unwrapper;
                    if (unwrapperList == null)
                    {
                        var s = objStorage.modifiedAssets[mstoreIndex];
                        unwrapperList = s.unwrapper = new List<int>();
                        objStorage.modifiedAssets[mstoreIndex] = s;
                    }
                    while(nameList.Count > unwrapperList.Count) unwrapperList.Add(0); // fix legacy

                    // modify existing record
                    var oldValue = paddingList[ind];
                    var oldUnwrapperValue = (ftGlobalStorage.Unwrapper)unwrapperList[ind];
                    bool shouldModify = oldValue != requiredPaddingClamped;
                    if (uvPaddingMax)
                    {
                        shouldModify = oldValue < requiredPaddingClamped;
                    }
                    if (oldUnwrapperValue != ftRenderLightmap.unwrapper) shouldModify = true;
                    if (shouldModify)
                    {
                        if (!dirtyAssetList.Contains(assetPath)) dirtyAssetList.Add(assetPath);
                        for(int xx=0; xx<ids.Count; xx++) dirtyObjList.Add(ids[xx]);
                        paddingList[ind] = requiredPaddingClamped;
                        unwrapperList[ind] = (int)ftRenderLightmap.unwrapper;
#if UNITY_2017_1_OR_NEWER
                        objStorage.SyncModifiedAsset(mstoreIndex);
#endif
                    }
                }

                // Backup padding storage to scene
                for(int s=0; s<storages.Length; s++)
                {
                    var str = storages[s];
                    if (str == null) continue;
                    var localIndex = str.modifiedAssetPathList.IndexOf(assetPath);
                    if (localIndex < 0)
                    {
                        str.modifiedAssetPathList.Add(assetPath);
                        str.modifiedAssets.Add(objStorage.modifiedAssets[mstoreIndex]);
                    }
                    else
                    {
                        str.modifiedAssets[localIndex] = objStorage.modifiedAssets[mstoreIndex];
                    }
                }
            }
        }

        EditorUtility.SetDirty(gstorage);
    }

    static bool ValidatePaddingImmutability(AdjustUVPaddingData adata)
    {
        if (validateLightmapStorageImmutability)
        {
            if (adata.dirtyAssetList.Count > 0)
            {
                sceneNeedsToBeRebuilt = true;
                return false;
            }
        }
        return true;
    }

    static bool ValidateScaleOffsetImmutability(ExportSceneData data)
    {
        if (validateLightmapStorageImmutability)
        {
            var holderRect = data.holderRect;
            var objsToWrite = data.objsToWrite;
            var objsToWriteGroup = data.objsToWriteGroup;
            var objsToWriteHolder = data.objsToWriteHolder;
            var storages = data.storages;
            var sceneToID = data.sceneToID;

            var emptyVec4 = new Vector4(1,1,0,0);
            Rect rc = new Rect();
            for(int i=0; i<objsToWrite.Count; i++)
            {
                var obj = objsToWrite[i];
                var lmgroup = objsToWriteGroup[i];
                var holderObj = objsToWriteHolder[i];
                if (holderObj != null)
                {
                    if (!holderRect.TryGetValue(holderObj, out rc))
                    {
                        holderObj = null;
                    }
                }
                var scaleOffset = holderObj == null ? emptyVec4 : new Vector4(rc.width, rc.height, rc.x, rc.y);

                var sceneID = sceneToID[obj.scene];
                var st = storages[sceneID];
                if (st == null)
                {
                    Debug.LogError("ValidateScaleOffsetImmutability: no storage");
                    return false;
                }

                var storedScaleOffset = Vector4.zero;
                if (obj.name == "__ExportTerrain")
                {
                    var tindex = terrainObjectList.IndexOf(obj.transform.parent.gameObject);
                    var terrain = terrainObjectToActual[tindex];
                    int index = st.bakedRenderersTerrain.IndexOf(terrain);
                    /*if (st.bakedIDsTerrain[index] != lmgroup.id)
                    {
                        Debug.LogError("ValidateScaleOffsetImmutability: terrain LMID does not match");
                        return false;
                    }*/
                    if (index < 0 || st.bakedScaleOffsetTerrain.Count <= index) continue;
                    storedScaleOffset = st.bakedScaleOffsetTerrain[index];
                }
                else
                {
                    int index = st.bakedRenderers.IndexOf(obj.GetComponent<Renderer>());
                    /*if (st.bakedIDs[index] != lmgroup.id)
                    {
                        Debug.LogError("ValidateScaleOffsetImmutability: LMID does not match");
                        Debug.LogError(st.bakedIDs[index]+" "+lmgroup.id+" "+lmgroup.name);
                        return false;
                    }*/
                    if (index < 0 || st.bakedScaleOffset.Count <= index) continue;
                    storedScaleOffset = st.bakedScaleOffset[index];
                }
                // approx equality
                if (!(scaleOffset == storedScaleOffset))
                {
                    Debug.LogError("ValidateScaleOffsetImmutability: scale/offset does not match");
                    return false;
                }
            }
        }
        return true;
    }

    static bool ClearUVPadding(ExportSceneData data, AdjustUVPaddingData adata)
    {
        var objsToWrite = data.objsToWrite;
        var dirtyAssetList = adata.dirtyAssetList;
        var dirtyObjList = adata.dirtyObjList;

        for(int i=0; i<objsToWrite.Count; i++)
        {
            var sharedMesh = GetSharedMesh(objsToWrite[i]);
            var assetPath = AssetDatabase.GetAssetPath(sharedMesh);

            int mstoreIndex = gstorage.modifiedAssetPathList.IndexOf(assetPath);
            if (mstoreIndex < 0) continue;

            dirtyObjList.Add(i);
            if (!dirtyAssetList.Contains(assetPath))
            {
                dirtyAssetList.Add(assetPath);
            }
        }

        if (!ValidatePaddingImmutability(adata)) return false;

        for(int i=0; i<dirtyAssetList.Count; i++)
        {
            var assetPath = dirtyAssetList[i];
            int mstoreIndex = gstorage.modifiedAssetPathList.IndexOf(assetPath);
            Debug.Log("Reimport " + assetPath);
            ProgressBarShow("Exporting scene - clearing UV adjustment for " + assetPath + "...", 0);

            gstorage.ClearAssetModifications(mstoreIndex);
        }

        EditorUtility.SetDirty(gstorage);

        return true;
    }

    static bool ReimportModifiedAssets(AdjustUVPaddingData adata)
    {
        var dirtyAssetList = adata.dirtyAssetList;

        for(int i=0; i<dirtyAssetList.Count; i++)
        {
            var assetPath = dirtyAssetList[i];
            Debug.Log("Reimport " + assetPath);
            ProgressBarShow("Exporting scene - adjusting UV padding for " + assetPath + "...", 0);
            //AssetDatabase.ImportAsset(assetPath);
            (AssetImporter.GetAtPath(assetPath) as ModelImporter).SaveAndReimport();

            if (CheckUnwrapError())
            {
                return false;
            }
        }
        return true;
    }

    static void TransformModifiedAssets(ExportSceneData data, AdjustUVPaddingData adata, bool tangentSHLights)
    {
        // Transform modified vertices to world space again
        for(int d=0; d<adata.dirtyObjList.Count; d++)
        {
            int i = adata.dirtyObjList[d];

            // Refresh attributes and indices after reimport
            bool isSkin;
            var m = GetSharedMeshSkinned(data.objsToWrite[i], out isSkin);
            data.objsToWriteVerticesUV[i] = m.uv;
            data.objsToWriteVerticesUV2[i] = m.uv2;
            var inds = new int[m.subMeshCount][];
            for(int n=0; n<inds.Length; n++) inds[n] = m.GetTriangles(n);
            data.objsToWriteIndices[i] = inds;

            TransformVertices(data, tangentSHLights, i); // because vertex count/order could be modified
        }
    }

    static void CalculateHolderUVBounds(ExportSceneData data)
    {
        var objsToWrite = data.objsToWrite;
        var objsToWriteGroup = data.objsToWriteGroup;
        var objsToWriteHolder = data.objsToWriteHolder;
        var objsToWriteVerticesPosW = data.objsToWriteVerticesPosW;
        var objsToWriteVerticesUV = data.objsToWriteVerticesUV;
        var objsToWriteVerticesUV2 = data.objsToWriteVerticesUV2;
        var objsToWriteIndices = data.objsToWriteIndices;
        var objToLodLevel = data.objToLodLevel;
        var holderObjUVBounds = data.holderObjUVBounds;
        var holderObjArea = data.holderObjArea;
        var groupToHolderObjects = data.groupToHolderObjects;

        // Calculate implicit group / atlas packing data
        // UV bounds and worldspace area
        for(int i=0; i<objsToWrite.Count; i++)
        {
            var obj = objsToWrite[i];
            var lmgroup = objsToWriteGroup[i];
            var calculateArea = lmgroup == null ? false : (lmgroup.isImplicit || lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas);
            if (!calculateArea) continue;

            var holderObj = objsToWriteHolder[i];
            var m = GetSharedMesh(obj);
            var mr = obj.GetComponent<Renderer>();

            var vpos = objsToWriteVerticesPosW[i];
            var vuv = objsToWriteVerticesUV2[i];//m.uv2;
            var inds = objsToWriteIndices[i];
            //if (vuv.Length == 0 || obj.GetComponent<BakeryLightMesh>()!=null) vuv = objsToWriteVerticesUV[i];//m.uv; // area lights or objects without UV2 export UV1 instead
            if (vuv.Length == 0 || obj.GetComponent<BakeryLightMesh>()!=null || temporaryAreaLightMeshList.Contains(obj)) vuv = objsToWriteVerticesUV[i];//m.uv; // area lights or objects without UV2 export UV1 instead
            Vector2 uv1 = Vector2.zero;
            Vector2 uv2 = Vector2.zero;
            Vector2 uv3 = Vector2.zero;

            int lodLevel;
            if (!objToLodLevel.TryGetValue(obj, out lodLevel)) lodLevel = -1;

            for(int k=0;k<m.subMeshCount;k++) {

                var indices = inds[k];//m.GetTriangles(k);
                int indexA, indexB, indexC;
                float area = 0;
                //float areaUV = 0;
                Vector4 uvBounds = new Vector4(1,1,0,0); // minx, miny, maxx, maxy

                for(int j=0;j<indices.Length;j+=3)
                {
                    indexA = indices[j];
                    indexB = indices[j + 1];
                    indexC = indices[j + 2];

                    var v1 = vpos[indexA];
                    var v2 = vpos[indexB];
                    var v3 = vpos[indexC];
                    area += Vector3.Cross(v2 - v1, v3 - v1).magnitude;

                    if (vuv.Length > 0)
                    {
                        uv1 = vuv[indexA];
                        uv2 = vuv[indexB];
                        uv3 = vuv[indexC];
                    }

                    /*var uv31 = new Vector3(uv1.x, uv1.y, 0);
                    var uv32 = new Vector3(uv2.x, uv2.y, 0);
                    var uv33 = new Vector3(uv3.x, uv3.y, 0);
                    areaUV += Vector3.Cross(uv32 - uv31, uv33 - uv31).magnitude;*/

                    if (uv1.x < uvBounds.x) uvBounds.x = uv1.x;
                    if (uv1.y < uvBounds.y) uvBounds.y = uv1.y;
                    if (uv1.x > uvBounds.z) uvBounds.z = uv1.x;
                    if (uv1.y > uvBounds.w) uvBounds.w = uv1.y;

                    if (uv2.x < uvBounds.x) uvBounds.x = uv2.x;
                    if (uv2.y < uvBounds.y) uvBounds.y = uv2.y;
                    if (uv2.x > uvBounds.z) uvBounds.z = uv2.x;
                    if (uv2.y > uvBounds.w) uvBounds.w = uv2.y;

                    if (uv3.x < uvBounds.x) uvBounds.x = uv3.x;
                    if (uv3.y < uvBounds.y) uvBounds.y = uv3.y;
                    if (uv3.x > uvBounds.z) uvBounds.z = uv3.x;
                    if (uv3.y > uvBounds.w) uvBounds.w = uv3.y;
                }

                // uv layouts always have empty spaces
                //area /= areaUV;

                var so = new SerializedObject(mr);
                var scaleInLm = so.FindProperty("m_ScaleInLightmap").floatValue;

                area *= scaleInLm;

                if (lmgroup.isImplicit && lodLevel == -1)
                {
                    lmgroup.area += area; // accumulate LMGroup area
                    // only use base scene values, no LODs, to properly initialize autoatlas size
                }
                if (lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
                {
                    // Accumulate per-holder area and UV bounds
                    float existingArea;
                    Vector4 existingBounds;
                    holderObjUVBounds.TryGetValue(holderObj, out existingBounds);
                    if (!holderObjArea.TryGetValue(holderObj, out existingArea))
                    {
                        existingArea = 0;
                        existingBounds = uvBounds;
                        List<GameObject> holderList;
                        if (!groupToHolderObjects.TryGetValue(lmgroup, out holderList))
                        {
                            groupToHolderObjects[lmgroup] = holderList = new List<GameObject>();
                        }
                        holderList.Add(holderObj);
                    }
                    holderObjArea[holderObj] = existingArea + area;

                    existingBounds.x = existingBounds.x < uvBounds.x ? existingBounds.x : uvBounds.x;
                    existingBounds.y = existingBounds.y < uvBounds.y ? existingBounds.y : uvBounds.y;
                    existingBounds.z = existingBounds.z > uvBounds.z ? existingBounds.z : uvBounds.z;
                    existingBounds.w = existingBounds.w > uvBounds.w ? existingBounds.w : uvBounds.w;
                    holderObjUVBounds[holderObj] = existingBounds;
                }
            }
        }
    }

    static int ResolutionFromArea(float area)
    {
        int resolution = (int)(Mathf.Sqrt(area) * texelsPerUnit);
        if (mustBePOT)
        {
            if (atlasCountPriority)
            {
                resolution = Mathf.NextPowerOfTwo(resolution);
            }
            else
            {
                resolution = Mathf.ClosestPowerOfTwo(resolution);
            }
        }
        resolution = Math.Max(resolution, minAutoResolution);
        resolution = Math.Min(resolution, maxAutoResolution);

        return resolution;
    }

    static void CalculateAutoAtlasInitResolution(ExportSceneData data)
    {
        var groupList = data.groupList;

        // Calculate implicit lightmap resolution
        for(int i=0; i<groupList.Count; i++)
        {
            var lmgroup = groupList[i];
            if (lmgroup.isImplicit)
            {
                lmgroup.resolution = ResolutionFromArea(lmgroup.area);
            }
        }
    }

    static void NormalizeHolderArea(BakeryLightmapGroup lmgroup, List<GameObject> holderObjs, ExportSceneData data)
    {
        var holderObjArea = data.holderObjArea;
        var holderObjUVBounds = data.holderObjUVBounds;

        // Divide holders area to get from world space to -> UV space
        float areaMult = 1.0f;
        if (lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
        {
            // ...by maximum lightmap area given texel size (autoAtlas)
            //areaMult = 1.0f / lightmapMaxArea;
            // don't modify
        }
        else
        {
            // ... by maximum holder area (normalize)
            float lmgroupArea = 0;
            for(int i=0; i<holderObjs.Count; i++)
            {
                // space outside of UV bounds shouldn't affect area
                var uvbounds = holderObjUVBounds[holderObjs[i]];
                var width = uvbounds.z - uvbounds.x;
                var height = uvbounds.w - uvbounds.y;
                float uvboundsArea = width * height;

                lmgroupArea += holderObjArea[holderObjs[i]] * uvboundsArea;
            }
            areaMult = 1.0f / lmgroupArea;
        }

        // Perform the division and sum up total UV area
        for(int i=0; i<holderObjs.Count; i++)
        {
            holderObjArea[holderObjs[i]] *= areaMult;
        }
    }

    static void SumHolderAreaPerLODLevel(List<GameObject> holderObjs, ExportSceneData data, PackData pdata)
    {
        var objToLodLevel = data.objToLodLevel;
        var holderObjArea = data.holderObjArea;
        var remainingAreaPerLodLevel = pdata.remainingAreaPerLodLevel;

        for(int i=0; i<holderObjs.Count; i++)
        {
            int lodLevel = -1;
            if (!objToLodLevel.TryGetValue(holderObjs[i], out lodLevel)) lodLevel = -1;

            float lodArea = 0;
            if (!remainingAreaPerLodLevel.TryGetValue(lodLevel, out lodArea)) lodArea = 0;

            remainingAreaPerLodLevel[lodLevel] = lodArea + holderObjArea[holderObjs[i]];
        }
    }

    static int CompareGameObjectsForPacking(GameObject a, GameObject b)
    {
        if (splitByScene)
        {
            if (a.scene.name != b.scene.name) return a.scene.name.CompareTo(b.scene.name);
        }

        if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
        {
            bool ba = a.name != "__ExportTerrainParent";
            bool bb = b.name != "__ExportTerrainParent";
            if (ba != bb) return ba.CompareTo(bb);
        }

        int lodLevelA = -1;
        int lodLevelB = -1;
        if (!cmp_objToLodLevel.TryGetValue(a, out lodLevelA)) lodLevelA = -1;
        if (!cmp_objToLodLevel.TryGetValue(b, out lodLevelB)) lodLevelB = -1;

        if (lodLevelA != lodLevelB) return lodLevelA.CompareTo(lodLevelB);

        float areaA = cmp_holderObjArea[a];
        float areaB = cmp_holderObjArea[b];

        // Workaround for "override resolution"
        // Always pack such rectangles first
        var comp = a.GetComponent<BakeryLightmapGroupSelector>();
        if (comp != null && comp.instanceResolutionOverride) areaA = comp.instanceResolution * 10000;

        comp = b.GetComponent<BakeryLightmapGroupSelector>();
        if (comp != null && comp.instanceResolutionOverride) areaB = comp.instanceResolution * 10000;

        return areaB.CompareTo(areaA);
    }

    static void ApplyAreaToUVBounds(float area, Vector4 uvbounds, out float width, out float height)
    {
        width = height = Mathf.Sqrt(area);
        float uwidth = uvbounds.z - uvbounds.x;
        float uheight = uvbounds.w - uvbounds.y;
        if (uwidth == 0 && uheight == 0)
        {
            width = height = 0;
        }
        else
        {
            float uvratio = uheight / uwidth;
            if (uvratio <= 1.0f)
            {
                width /= uvratio;
                //height *= uvratio;
            }
            else
            {
                height *= uvratio;
                //width /= uvratio;
            }
        }
    }

    static bool Pack(BakeryLightmapGroup lmgroup, List<GameObject> holderObjs, ExportSceneData data, PackData pdata)
    {
        var holderObjArea = data.holderObjArea;
        var holderObjUVBounds = data.holderObjUVBounds;
        var holderRect = data.holderRect;
        var objToLodLevel = data.objToLodLevel;
        var groupList = data.groupList;
        var lmBounds = data.lmBounds;
        var autoAtlasGroups = data.autoAtlasGroups;
        var autoAtlasGroupRootNodes = data.autoAtlasGroupRootNodes;

        var remainingAreaPerLodLevel = pdata.remainingAreaPerLodLevel;

        //Debug.LogError("repack: "+repackScale);
        pdata.repack = false;

        AtlasNode rootNode;

        if (lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas && autoAtlasGroupRootNodes != null && autoAtlasGroupRootNodes.Count > 0)
        {
            rootNode = autoAtlasGroupRootNodes[0];
        }
        else
        {
            rootNode = new AtlasNode();
        }

        rootNode.rc = new Rect(0, 0, 1, 1);
        for(int i=0; i<holderObjs.Count; i++)
        {
            var area = holderObjArea[holderObjs[i]];
            var uvbounds = holderObjUVBounds[holderObjs[i]];

            // Calculate width and height of each holder in atlas UV space
            float width, height;
            var comp = holderObjs[i].GetComponent<BakeryLightmapGroupSelector>();
            if (comp != null && comp.instanceResolutionOverride)
            {
                // Explicit holder size
                pdata.hasResOverrides = true;
                width = height = comp.instanceResolution / (float)lmgroup.resolution;
            }
            else
            {
                // Automatic: width and height = sqrt(area) transformed by UV AABB aspect ratio
                ApplyAreaToUVBounds(area, uvbounds, out width, out height);
            }

            // Clamp to full lightmap size
            float twidth = width;
            float theight = height;
            if (lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
            {
                twidth = (width * texelsPerUnit) / lmgroup.resolution;
                theight = (height * texelsPerUnit) / lmgroup.resolution;

                //if (i==0) Debug.LogError(texelsPerUnit+" "+twidth);
            }
            //float unclampedTwidth = twidth;
            //float unclampedTheight = twidth;
            if (comp != null && comp.instanceResolutionOverride)
            {
            }
            else
            {
                twidth *= pdata.repackScale;
                theight *= pdata.repackScale;
            }
            twidth = twidth > 1 ? 1 : twidth;
            theight = theight > 1 ? 1 : theight;
            twidth = Mathf.Max(twidth, 1.0f / lmgroup.resolution);
            theight = Mathf.Max(theight, 1.0f / lmgroup.resolution);
            var rect = new Rect(0, 0, twidth, theight);

            if (float.IsNaN(twidth) || float.IsNaN(theight))
            {
                ExportSceneError("NaN UVs detected for " + holderObjs[i].name+" "+rect.width+" "+rect.height+" "+width+" "+height+" "+lmgroup.resolution+" "+area+" "+(uvbounds.z - uvbounds.x)+" "+(uvbounds.w - uvbounds.y));
                return false;
            }

            // Try inserting this rect
            // Break autoatlas if lod level changes
            // Optionally break autoatlas if scene changes
            AtlasNode node = null;
            int lodLevel;
            if (!objToLodLevel.TryGetValue(holderObjs[i], out lodLevel)) lodLevel = -1;
            bool splitAtlas = false;
            if (splitByScene)
            {
                if (holderObjs[i].scene.name != lmgroup.sceneName)
                {
                    splitAtlas = true;
                }
            }
            if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
            {
                bool ba = holderObjs[i].name == "__ExportTerrainParent";
                if (ba) lmgroup.containsTerrains = true;

                if (i > 0)
                {
                    bool bb = holderObjs[i-1].name == "__ExportTerrainParent";
                    if (ba != bb)
                    {
                        splitAtlas = true;
                    }
                }
            }
            if (!splitAtlas)
            {
                if (lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
                {
                    if (lodLevel == lmgroup.sceneLodLevel)
                    {
                        node = rootNode.Insert(holderObjs[i], rect);
                    }
                }
                else
                {
                    node = rootNode.Insert(holderObjs[i], rect);
                }
            }

            /*if (node!=null)
            {
                Debug.Log(holderObjs[i].name+" goes straight into "+lmgroup.name);
            }*/

            if (node == null)
            {
                if (lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
                {
                    // Can't fit - try other autoAtlas lightmaps
                    BakeryLightmapGroup newGroup = null;
                    var holder = holderObjs[i];
                    int goodGroup = -1;
                    for(int g=1; g<autoAtlasGroups.Count; g++)
                    {
                        if (splitByScene)
                        {
                            if (autoAtlasGroups[g].sceneName != holderObjs[i].scene.name) continue;
                        }
                        if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                        {
                            bool ba = holderObjs[i].name != "__ExportTerrainParent";
                            bool bb = !autoAtlasGroups[g].containsTerrains;
                            if (ba != bb) continue;
                        }
                        if (autoAtlasGroups[g].sceneLodLevel != lodLevel) continue;
                        twidth = (width * texelsPerUnit) / autoAtlasGroups[g].resolution;
                        theight = (height * texelsPerUnit) / autoAtlasGroups[g].resolution;
                        //unclampedTwidth = twidth;
                        //unclampedTheight = twidth;
                        twidth = twidth > 1 ? 1 : twidth;
                        theight = theight > 1 ? 1 : theight;
                        rect = new Rect(0, 0, twidth, theight);

                        node = autoAtlasGroupRootNodes[g].Insert(holder, rect);
                        if (node != null)
                        {
                            //Debug.Log(holder.name+" fits into "+autoAtlasGroups[g].name);
                            newGroup = autoAtlasGroups[g];
                            goodGroup = g;
                            break;
                        }
                    }

                    // Can't fit - create new lightmap (autoAtlas)
                    if (goodGroup < 0)
                    {
                        newGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();
                        newGroup.name = holder.scene.name + "_LMA" + autoAtlasGroups.Count;
                        newGroup.isImplicit = true;
                        newGroup.sceneLodLevel = lodLevel;
                        if (splitByScene) newGroup.sceneName = holderObjs[i].scene.name;
                        //Debug.Log(holder.name+" creates "+newGroup.name);

                        if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                        {
                            newGroup.containsTerrains = holderObjs[i].name == "__ExportTerrainParent";
                        }

                        newGroup.resolution = (int)(Mathf.Sqrt(remainingAreaPerLodLevel[lodLevel]) * texelsPerUnit);
                        if (mustBePOT)
                        {
                            if (atlasCountPriority)
                            {
                                newGroup.resolution = Mathf.NextPowerOfTwo(newGroup.resolution);
                            }
                            else
                            {
                                newGroup.resolution = Mathf.ClosestPowerOfTwo(newGroup.resolution);
                            }
                        }
                        newGroup.resolution = Math.Max(newGroup.resolution, minAutoResolution);
                        newGroup.resolution = Math.Min(newGroup.resolution, maxAutoResolution);

                        newGroup.bitmask = 1;
                        newGroup.area = 0;
                        newGroup.mode = BakeryLightmapGroup.ftLMGroupMode.PackAtlas;

                        newGroup.id = data.lmid;
                        groupList.Add(newGroup);
                        lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(0,0,0)));
                        data.lmid++;

                        autoAtlasGroups.Add(newGroup);
                        var rootNode2 = new AtlasNode();
                        rootNode2.rc = new Rect(0, 0, 1, 1);
                        autoAtlasGroupRootNodes.Add(rootNode2);

                        twidth = (width * texelsPerUnit) / newGroup.resolution;
                        theight = (height * texelsPerUnit) / newGroup.resolution;
                        //unclampedTwidth = twidth;
                        //unclampedTheight = twidth;
                        twidth = twidth > 1 ? 1 : twidth;
                        theight = theight > 1 ? 1 : theight;

                        rect = new Rect(0, 0, twidth, theight);

                        node = rootNode2.Insert(holder, rect);
                    }

                    // Modify implicit group storage
                    MoveObjectToImplicitGroup(holder, newGroup, data);
                    /*
                    var scn = holder.scene;
                    tempStorage.implicitGroupMap[holder] = newGroup;
                    for(int k=0; k<storages[sceneToID[holder.scene]].implicitGroupedObjects.Count; k++)
                    {
                        if (storages[sceneToID[holder.scene]].implicitGroupedObjects[k] == holder)
                        {
                            storages[sceneToID[holder.scene]].implicitGroups[k] = newGroup;
                            //Debug.LogError("Implicit set: " + k+" "+newGroup.name+" "+holder.name);
                        }
                    }
                    */
                    //lmgroup = newGroup;
                }
                else
                {
                    if (!pdata.repackStage2)
                    {
                        // explicit packed atlas - can't fit - try shrinking the whole atlas
                        pdata.repackTries++;
                        if (pdata.repackTries < atlasMaxTries)
                        {
                            pdata.repack = true;
                            pdata.repackScale *= 0.75f;
                            //Debug.LogError("Can't fit, set " +repackScale);
                            break;
                        }
                    }
                    else
                    {
                        // explicit packed atlas - did fit, now trying to scale up, doesn't work - found optimal fit
                        pdata.repack = true;
                        pdata.repackScale /= atlasScaleUpValue;//*= 0.75f;
                        //Debug.LogError("Final, set " +repackScale);
                        pdata.finalRepack = true;
                        pdata.repackTries = 0;
                        break;
                    }
                }
            }

            if (node == null)
            {
                // No way to fit
                ExportSceneError("Can't fit " + holderObjs[i].name+" "+rect.width+" "+rect.height);
                return false;
            }
            else
            {
                // Generate final rectangle to transform local UV -> atlas UV
                float padding = ((float)atlasPaddingPixels) / lmgroup.resolution;

                var paddedRc = new Rect(node.rc.x + padding,
                                        node.rc.y + padding,
                                        node.rc.width - padding * 2,
                                        node.rc.height - padding * 2);

                paddedRc.x -= uvbounds.x * (paddedRc.width / (uvbounds.z - uvbounds.x));
                paddedRc.y -= uvbounds.y * (paddedRc.height / (uvbounds.w - uvbounds.y));
                paddedRc.width /= uvbounds.z - uvbounds.x;
                paddedRc.height /= uvbounds.w - uvbounds.y;

                holderRect[holderObjs[i]] = paddedRc;
            }

            //float areaReduction = (twidth*theight) / (unclampedTwidth*unclampedTheight);
            remainingAreaPerLodLevel[lodLevel] -= area;// * areaReduction;
        }

        if (!lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
        {
            if (pdata.finalRepack && pdata.repack)
            {
                pdata.continueRepack = true;
                return true;
            }
            if (pdata.finalRepack)
            {
                pdata.continueRepack = false;
                return true;
            }

            if (!pdata.repack && !pdata.repackStage2)
            {
                //if (repackTries > 0) break; // shrinked down just now - don't scale up

                pdata.repackStage2 = true; // scale up now
                pdata.repack = true;
                pdata.repackScale *= atlasScaleUpValue;///= 0.75f;
                pdata.repackTries = 0;
                //Debug.LogError("Scale up, set " +repackScale);
            }
            else if (pdata.repackStage2)
            {
                pdata.repackTries++;
                if (pdata.repackTries == atlasMaxTries)
                {
                    pdata.continueRepack = false;
                    return true;
                }
                pdata.repack = true;
                pdata.repackScale *= atlasScaleUpValue;///= 0.75f;
                //Debug.LogError("Scale up cont, set " +repackScale);
            }
        }
        return true;
    }

    static void MoveObjectToImplicitGroup(GameObject holder, BakeryLightmapGroup newGroup, ExportSceneData data)
    {
        var storages = data.storages;
        var sceneToID = data.sceneToID;

        // Modify implicit group storage
        var scn = holder.scene;
        tempStorage.implicitGroupMap[holder] = newGroup;
        for(int k=0; k<storages[sceneToID[holder.scene]].implicitGroupedObjects.Count; k++)
        {
            if (storages[sceneToID[holder.scene]].implicitGroupedObjects[k] == holder)
            {
                storages[sceneToID[holder.scene]].implicitGroups[k] = newGroup;
                //Debug.LogError("Implicit set: " + k+" "+newGroup.name+" "+holder.name);
            }
        }
    }

    static List<int> GetAtlasBucketRanges(List<GameObject> holderObjs, ExportSceneData data, bool onlyUserSplits)
    {
        var objToLodLevel = data.objToLodLevel;

        var ranges = new List<int>();
        int start = 0;
        int end = 0;
        if (holderObjs.Count > 0)
        {
            var sceneName = holderObjs[0].scene.name;
            int lodLevel;
            if (!objToLodLevel.TryGetValue(holderObjs[0], out lodLevel)) lodLevel = -1;
            bool isTerrain = holderObjs[0].name == "__ExportTerrainParent";

            for(int i=0; i<holderObjs.Count; i++)
            {
                bool splitAtlas = false;

                // Split by scene
                if (splitByScene)
                {
                    var objSceneName = holderObjs[i].scene.name;
                    if (objSceneName != sceneName)
                    {
                        splitAtlas = true;
                        sceneName = objSceneName;
                    }
                }

                if (!onlyUserSplits)
                {
                    // Split by LOD
                    int objLodLevel;
                    if (!objToLodLevel.TryGetValue(holderObjs[i], out objLodLevel)) objLodLevel = -1;
                    if (objLodLevel != lodLevel)
                    {
                        lodLevel = objLodLevel;
                        splitAtlas = true;
                    }

                    // Split by terrain
                    if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                    {
                        bool ba = holderObjs[i].name == "__ExportTerrainParent";
                        if (ba != isTerrain)
                        {
                            isTerrain = ba;
                            splitAtlas = true;
                        }
                    }
                }

                if (splitAtlas)
                {
                    end = i;
                    ranges.Add(start);
                    ranges.Add(end-1);
                    start = end;
                }
            }
        }
        end = holderObjs.Count-1;
        ranges.Add(start);
        ranges.Add(end);
        return ranges;
    }

    static float SumObjectsArea(List<GameObject> holderObjs, int start, int end, ExportSceneData data)
    {
        var holderObjArea = data.holderObjArea;

        float area = 0;
        for(int i=start; i<=end; i++)
        {
            area += holderObjArea[holderObjs[i]];
        }
        return area;
    }

    static BakeryLightmapGroup AllocateAutoAtlas(int count, BakeryLightmapGroup lmgroup, ExportSceneData data, int[] atlasSizes = null)
    {
        var lmBounds = data.lmBounds;
        var groupList = data.groupList;
        var autoAtlasGroups = data.autoAtlasGroups;
        var autoAtlasGroupRootNodes = data.autoAtlasGroupRootNodes;

        BakeryLightmapGroup newGroup = null;

        for(int i=0; i<count; i++)
        {
            // Create additional lightmaps
            newGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();
            newGroup.name = lmgroup.sceneName + "_LMA" + autoAtlasGroups.Count;
            newGroup.isImplicit = true;
            newGroup.sceneLodLevel = lmgroup.sceneLodLevel;
            if (splitByScene) newGroup.sceneName = lmgroup.sceneName;
            newGroup.containsTerrains = lmgroup.containsTerrains;

            newGroup.resolution = atlasSizes != null ? atlasSizes[i] : lmgroup.resolution;

            newGroup.bitmask = 1;
            newGroup.area = 0;
            newGroup.mode = lmgroup.mode;// BakeryLightmapGroup.ftLMGroupMode.PackAtlas;

            newGroup.renderMode = lmgroup.renderMode;
            newGroup.renderDirMode = lmgroup.renderDirMode;
            newGroup.atlasPacker = lmgroup.atlasPacker;
            newGroup.computeSSS = lmgroup.computeSSS;
            newGroup.sssSamples = lmgroup.sssSamples;
            newGroup.sssDensity = lmgroup.sssDensity;
            newGroup.sssColor = lmgroup.sssColor;
            newGroup.fakeShadowBias = lmgroup.fakeShadowBias;
            newGroup.transparentSelfShadow = lmgroup.transparentSelfShadow;
            newGroup.flipNormal = lmgroup.flipNormal;

            newGroup.id = data.lmid;
            groupList.Add(newGroup);
            lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(0,0,0)));
            data.lmid++;

            autoAtlasGroups.Add(newGroup);
            var rootNode2 = new AtlasNode();
            rootNode2.rc = new Rect(0, 0, 1, 1);
            autoAtlasGroupRootNodes.Add(rootNode2);
        }

        return newGroup;
    }

    static bool PackWithXatlas(BakeryLightmapGroup lmgroup, List<GameObject> holderObjs, ExportSceneData data, PackData pdata)
    {
        var holderObjArea = data.holderObjArea;
        var holderObjUVBounds = data.holderObjUVBounds;
        var holderRect = data.holderRect;
        var autoAtlasGroups = data.autoAtlasGroups;
        var objToLodLevel = data.objToLodLevel;
        var objsToWriteHolder = data.objsToWriteHolder;
        var objsToWrite = data.objsToWrite;

        bool warned = false;

        // Split objects into "buckets" by scene, terrains, LODs, etc
        // Objects are already pre-sorted, so we need only ranges
        int bStart = 0;
        int bEnd = holderObjs.Count-1;
        int bucketCount = 2;
        List<int> buckets = null;
        if (lmgroup.isImplicit)
        {
            buckets = GetAtlasBucketRanges(holderObjs, data, postPacking);
            bucketCount = buckets.Count;
        }

        var holderAutoIndex = new int[holderObjs.Count];

        for(int bucket=0; bucket<bucketCount; bucket+=2)
        {
            if (lmgroup.isImplicit)
            {
                bStart = buckets[bucket];
                bEnd = buckets[bucket+1];
            }
            int bSize = bEnd - bStart;
            if (bucket > 0)
            {
                // Start new bucket
                lmgroup = AllocateAutoAtlas(1, lmgroup, data);
            }
            int firstAutoAtlasIndex = autoAtlasGroups.Count - 1;

            if (lmgroup.isImplicit)
            {
                float bucketArea = SumObjectsArea(holderObjs, bStart, bEnd, data);
                lmgroup.resolution = ResolutionFromArea(bucketArea);
            }

            // Fill some LMGroup data
            lmgroup.sceneName = holderObjs[bStart].scene.name;
            int lodLevel;
            if (!objToLodLevel.TryGetValue(holderObjs[bStart], out lodLevel)) lodLevel = -1;
            lmgroup.sceneLodLevel = lodLevel;
            if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
            {
                lmgroup.containsTerrains = holderObjs[bStart].name == "__ExportTerrainParent";
            }

            var atlas = xatlas.xatlasCreateAtlas();

            const int attempts = 4096;
            const int padding = 1;
            const bool allowRotate = false;
            float packTexelsPerUnit = lmgroup.isImplicit ? 1.0f : 0.0f; // multiple atlaseses vs single atlas
            int packResolution = lmgroup.resolution;
            int maxChartSize = 0;//packResolution;
            bool bruteForce = true; // high quality

            int vertCount = 4;
            int indexCount = 6;
            Vector2[] uv = null;
            int[] indices = null;
            if (!holeFilling)
            {
                uv = new Vector2[4];
                indices = new int[6];
                indices[0] = 0;
                indices[1] = 1;
                indices[2] = 2;
                indices[3] = 2;
                indices[4] = 3;
                indices[5] = 0;
            }
            var uvBuffer = new Vector2[4];
            var xrefBuffer = new int[4];
            var indexBuffer = new int[6];

            for(int i=bStart; i<=bEnd; i++)
            {
                if (!warned)
                {
                    var comp = holderObjs[i].GetComponent<BakeryLightmapGroupSelector>();
                    if (comp != null && comp.instanceResolutionOverride)
                    {
                        if (!ExportSceneValidationMessage("When using xatlas as atlas packer, 'Override resolution' option is not supported for LMGroups.\nOption is used on: " + holderObjs[i].name))
                        {
                            xatlas.xatlasClear(atlas);
                            return false;
                        }
                        warned = true;
                    }
                }

                var area = holderObjArea[holderObjs[i]];
                var uvbounds = holderObjUVBounds[holderObjs[i]];

                // Automatic: width and height = sqrt(area) transformed by UV AABB aspect ratio
                float width, height;
                ApplyAreaToUVBounds(area, uvbounds, out width, out height);

                // Clamp to full lightmap size
                float twidth = width;
                float theight = height;
                if (lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas)
                {
                    twidth = (width * texelsPerUnit);// / lmgroup.resolution;
                    theight = (height * texelsPerUnit);// / lmgroup.resolution;
                }

                if (!holeFilling)
                {
                    uv[0] = new Vector2(0,0);
                    uv[1] = new Vector2(twidth,0);
                    uv[2] = new Vector2(twidth,theight);
                    uv[3] = new Vector2(0,theight);
                }
                else
                {
                    List<int> indexList = null;
                    List<Vector2> uvList = null;
                    vertCount = indexCount = 0;
                    int numMeshes = 0;
                    var ubounds = holderObjUVBounds[holderObjs[i]];
                    var holder = holderObjs[i];
                    for(int o=0; o<objsToWriteHolder.Count; o++)
                    {
                        if (objsToWriteHolder[o] != holder) continue;

                        if (numMeshes == 1)
                        {
                            indexList = new List<int>();
                            uvList = new List<Vector2>();
                            for(int j=0; j<indices.Length; j++)
                            {
                                indexList.Add(indices[j]);
                            }
                            for(int j=0; j<uv.Length; j++)
                            {
                                uvList.Add(uv[j]);
                            }
                        }

                        bool isSkin;
                        var mesh = GetSharedMeshSkinned(objsToWrite[o], out isSkin);
                        indices = mesh.triangles;
                        var uv1 = mesh.uv;
                        var uv2 = mesh.uv2;
                        if (uv2 == null || uv2.Length == 0)
                        {
                            uv = uv1;
                        }
                        else
                        {
                            uv = uv2;
                        }
                        for(int t=0; t<uv.Length; t++)
                        {
                            float u = (uv[t].x - ubounds.x) / (ubounds.z - ubounds.x);
                            float v = (uv[t].y - ubounds.y) / (ubounds.w - ubounds.y);
                            u *= twidth;
                            v *= theight;
                            uv[t] = new Vector2(u, v);
                        }

                        if (numMeshes > 0)
                        {
                            for(int j=0; j<indices.Length; j++)
                            {
                                indexList.Add(indices[j] + vertCount);
                            }
                            for(int j=0; j<uv.Length; j++)
                            {
                                uvList.Add(uv[j]);
                            }
                        }

                        vertCount += uv.Length;
                        indexCount += indices.Length;
                        numMeshes++;
                    }
                    if (numMeshes > 1)
                    {
                        uv = uvList.ToArray();
                        indices = indexList.ToArray();
                    }
                }

                var handleUV = GCHandle.Alloc(uv, GCHandleType.Pinned);
                int err = 0;

                try
                {
                    var pointerUV = handleUV.AddrOfPinnedObject();

                    err = xatlas.xatlasAddUVMesh(atlas, vertCount, pointerUV, indexCount, indices, allowRotate);
                }
                finally
                {
                    if (handleUV.IsAllocated) handleUV.Free();
                }

                if (err == 1)
                {
                    Debug.LogError("xatlas::AddMesh: indices are out of range");
                    xatlas.xatlasClear(atlas);
                    return false;
                }
                else if (err == 2)
                {
                    Debug.LogError("xatlas::AddMesh: index count is incorrect");
                    xatlas.xatlasClear(atlas);
                    return false;
                }
                else if (err != 0)
                {
                    Debug.LogError("xatlas::AddMesh: unknown error");
                    xatlas.xatlasClear(atlas);
                    return false;
                }
            }

            //xatlas.xatlasParametrize(atlas);
            xatlas.xatlasPack(atlas, attempts, packTexelsPerUnit, packResolution, maxChartSize, padding, bruteForce);//, allowRotate);

            int atlasCount = xatlas.xatlasGetAtlasCount(atlas);
            var atlasSizes = new int[atlasCount];

            xatlas.xatlasNormalize(atlas, atlasSizes);

            // Create additional lightmaps
            AllocateAutoAtlas(atlasCount-1, lmgroup, data, atlasSizes);

            // Move objects into new atlases
            if (lmgroup.isImplicit)
            {
                for(int i=0; i<=bSize; i++)
                {
                    int atlasIndex = xatlas.xatlasGetAtlasIndex(atlas, i, 0);

                    // Modify implicit group storage
                    var holder = holderObjs[bStart + i];
                    var newGroup = autoAtlasGroups[firstAutoAtlasIndex + atlasIndex];
                    MoveObjectToImplicitGroup(holderObjs[bStart + i], newGroup, data);
                    holderAutoIndex[bStart + i] = firstAutoAtlasIndex + atlasIndex;
                }
            }

            for(int i=0; i<=bSize; i++)
            {
                // Get data from xatlas
                int newVertCount = xatlas.xatlasGetVertexCount(atlas, i);
                uvBuffer = new Vector2[newVertCount];
                xrefBuffer = new int[newVertCount];

                int newIndexCount = xatlas.xatlasGetIndexCount(atlas, i);
                indexBuffer = new int[newIndexCount];

                if (holeFilling)
                {
                   uvBuffer = new Vector2[newVertCount];
                   xrefBuffer = new int[newVertCount];
                   indexBuffer = new int[newIndexCount];
                }

                var handleT = GCHandle.Alloc(uvBuffer, GCHandleType.Pinned);
                var handleX = GCHandle.Alloc(xrefBuffer, GCHandleType.Pinned);
                var handleI = GCHandle.Alloc(indexBuffer, GCHandleType.Pinned);
                try
                {
                    var pointerT = handleT.AddrOfPinnedObject();
                    var pointerX = handleX.AddrOfPinnedObject();
                    var pointerI = handleI.AddrOfPinnedObject();
                    xatlas.xatlasGetData(atlas, i, pointerT, pointerX, pointerI);
                }
                finally
                {
                    if (handleT.IsAllocated) handleT.Free();
                    if (handleX.IsAllocated) handleX.Free();
                    if (handleI.IsAllocated) handleI.Free();
                }

                float minU = float.MaxValue;
                float minV = float.MaxValue;
                float maxU = -float.MaxValue;
                float maxV = -float.MaxValue;
                for(int j=0; j<newVertCount; j++)
                {
                    if (uvBuffer[j].x < minU) minU = uvBuffer[j].x;
                    if (uvBuffer[j].y < minV) minV = uvBuffer[j].y;
                    if (uvBuffer[j].x > maxU) maxU = uvBuffer[j].x;
                    if (uvBuffer[j].y > maxV) maxV = uvBuffer[j].y;
                }

                // Generate final rectangle to transform local UV -> atlas UV
                float upadding = 0;
                var uvbounds = holderObjUVBounds[holderObjs[bStart + i]];
                var paddedRc = new Rect(minU + upadding,
                                        minV + upadding,
                                        (maxU-minU) - upadding * 2,
                                        (maxV-minV) - upadding * 2);

                paddedRc.x -= uvbounds.x * (paddedRc.width / (uvbounds.z - uvbounds.x));
                paddedRc.y -= uvbounds.y * (paddedRc.height / (uvbounds.w - uvbounds.y));
                paddedRc.width /= uvbounds.z - uvbounds.x;
                paddedRc.height /= uvbounds.w - uvbounds.y;

                holderRect[holderObjs[bStart + i]] = paddedRc;
            }

            xatlas.xatlasClear(atlas);
        }

        if (postPacking)
        {
            buckets = GetAtlasBucketRanges(holderObjs, data, false);
            bucketCount = buckets.Count;

            Debug.Log("Bucket count for " + lmgroup.name +": " + (bucketCount/2));

            if (lmgroup.isImplicit)
            {
                // Post-packing for auto-atlased groups
                var autoLMBuckets = new List<int>[autoAtlasGroups.Count];
                for(int bucket=0; bucket<bucketCount; bucket+=2)
                {
                    bStart = buckets[bucket];
                    bEnd = buckets[bucket+1];
                    for(int i=bStart; i<=bEnd; i++)
                    {
                        int autoLM = holderAutoIndex[i];
                        if (autoLMBuckets[autoLM] == null) autoLMBuckets[autoLM] = new List<int>();
                        if (!autoLMBuckets[autoLM].Contains(bucket)) autoLMBuckets[autoLM].Add(bucket);
                    }
                }
                int origGroupCount = autoAtlasGroups.Count;
                for(int i=0; i<origGroupCount; i++)
                {
                    if (autoLMBuckets[i] != null && autoLMBuckets[i].Count > 1)
                    {
                        // Split
                        for(int j=1; j<autoLMBuckets[i].Count; j++)
                        {
                            autoAtlasGroups[i].sceneName = holderObjs[bStart].scene.name;

                            var newGroup = AllocateAutoAtlas(1, autoAtlasGroups[i], data);
                            int bucket = autoLMBuckets[i][j];
                            bStart = buckets[bucket];
                            bEnd = buckets[bucket+1];

                            // Update LMGroup data
                            //newGroup.sceneName = holderObjs[bStart].scene.name;
                            int lodLevel;
                            if (!objToLodLevel.TryGetValue(holderObjs[bStart], out lodLevel)) lodLevel = -1;
                            newGroup.sceneLodLevel = lodLevel;
                            if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                            {
                                newGroup.containsTerrains = holderObjs[bStart].name == "__ExportTerrainParent";
                            }
                            newGroup.parentName = autoAtlasGroups[i].name;
                            autoAtlasGroups[i].parentName = "|";
                            //Debug.LogError(autoAtlasGroups[i].name+" (" +autoAtlasGroups[i].id+") -> "+newGroup.name + " (" + newGroup.id+", "+newGroup.parentID+")");

                            for(int k=bStart; k<=bEnd; k++)
                            {
                                int autoLM = holderAutoIndex[k];
                                if (autoLM == i)
                                {
                                    MoveObjectToImplicitGroup(holderObjs[k], newGroup, data);
                                    holderAutoIndex[k] = -1; // mark as moved
                                }
                            }
                        }
                    }
                }
                for(int i=0; i<origGroupCount; i++)
                {
                    if (autoLMBuckets[i] != null)
                    {
                        for(int j=0; j<holderObjs.Count; j++)
                        {
                            if (holderAutoIndex[j] != i) continue;

                            // Update LMGroup data
                            var newGroup = autoAtlasGroups[i];
                            newGroup.sceneName = holderObjs[j].scene.name;
                            int lodLevel;
                            if (!objToLodLevel.TryGetValue(holderObjs[j], out lodLevel)) lodLevel = -1;
                            newGroup.sceneLodLevel = lodLevel;
                            if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                            {
                                newGroup.containsTerrains = holderObjs[j].name == "__ExportTerrainParent";
                            }

                            break;
                        }
                    }
                }
            }
            else if (bucketCount > 0)
            {
                // Post-packing for explicit groups
                // Single LMGroup -> LMGroup*buckets

                // Setup first bucket
                bStart = buckets[0];
                bEnd = buckets[1];
                int lodLevel;
                if (!objToLodLevel.TryGetValue(holderObjs[bStart], out lodLevel)) lodLevel = -1;
                lmgroup.sceneLodLevel = lodLevel;
                if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                {
                    lmgroup.containsTerrains = holderObjs[bStart].name == "__ExportTerrainParent";
                }
                //Debug.LogError(lmgroup.name+": "+ lmgroup.sceneLodLevel+" because of " + holderObjs[bStart].name);

                // Skip first bucket
                for(int bucket=2; bucket<bucketCount; bucket+=2)
                {
                    bStart = buckets[bucket];
                    bEnd = buckets[bucket+1];

                    var newGroup = AllocateAutoAtlas(1, lmgroup, data);

                    if (!objToLodLevel.TryGetValue(holderObjs[bStart], out lodLevel)) lodLevel = -1;
                    newGroup.sceneLodLevel = lodLevel;
                    if (ftRenderLightmap.giLodMode != ftRenderLightmap.GILODMode.ForceOff && exportTerrainAsHeightmap)
                    {
                        newGroup.containsTerrains = holderObjs[bStart].name == "__ExportTerrainParent";
                    }
                    newGroup.mode = lmgroup.mode;
                    newGroup.parentName = lmgroup.name;
                    lmgroup.parentName = "|";

                    //Debug.LogError(newGroup.name+": "+ newGroup.sceneLodLevel+" because of " + holderObjs[bStart].name);

                    for(int k=bStart; k<=bEnd; k++)
                    {
                        //MoveObjectToImplicitGroup(holderObjs[k], newGroup, data);
                        data.storages[data.sceneToID[holderObjs[k].scene]].implicitGroupedObjects.Add(holderObjs[k]);
                        data.storages[data.sceneToID[holderObjs[k].scene]].implicitGroups.Add(newGroup);
                        tempStorage.implicitGroupMap[holderObjs[k]] = newGroup;
                    }
                }
            }
        }

        return true;
    }

    static void NormalizeAtlas(BakeryLightmapGroup lmgroup, List<GameObject> holderObjs, ExportSceneData data, PackData pdata)
    {
        var holderRect = data.holderRect;

        if (!lmgroup.isImplicit && lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.PackAtlas && !pdata.hasResOverrides)
        {
            float maxx = 0;
            float maxy = 0;
            for(int i=0; i<holderObjs.Count; i++)
            {
                var rect = holderRect[holderObjs[i]];
                if ((rect.x + rect.width) > maxx) maxx = rect.x + rect.width;
                if ((rect.y + rect.height) > maxy) maxy = rect.y + rect.height;
            }
            float maxDimension = maxx > maxy ? maxx : maxy;
            float normalizeScale = 1.0f / maxDimension;
            for(int i=0; i<holderObjs.Count; i++)
            {
                var rect = holderRect[holderObjs[i]];
                holderRect[holderObjs[i]] = new Rect(rect.x * normalizeScale, rect.y * normalizeScale, rect.width * normalizeScale, rect.height * normalizeScale);
            }
        }
    }

    static bool PackAtlases(ExportSceneData data)
    {
        // IN, OUT lmgroup.containsTerrains, OUT holderObjs (sort)
        var groupToHolderObjects = data.groupToHolderObjects;

        // IN
        var objToLodLevel = data.objToLodLevel; // LODs packed to separate atlases
        cmp_objToLodLevel = objToLodLevel;

        // IN/OUT
        var holderObjArea = data.holderObjArea; // performs normalization
        cmp_holderObjArea = holderObjArea;

        // Pack atlases
        // Try to scale all objects to occupy all atlas space
        foreach(var pair in groupToHolderObjects)
        {
            // For every LMGroup with PackAtlas mode
            var lmgroup = pair.Key;
            var holderObjs = pair.Value; // get all objects

            var pdata = new PackData();

            // Normalize by worldspace area and uv area
            // Read/write holderObjArea
            NormalizeHolderArea(lmgroup, holderObjs, data);

            // Sort objects by area and scene LOD level
            // + optionally by scene
            // + split by terrain
            holderObjs.Sort(CompareGameObjectsForPacking);

            var packer = lmgroup.atlasPacker == BakeryLightmapGroup.AtlasPacker.Auto ? atlasPacker : (ftGlobalStorage.AtlasPacker)lmgroup.atlasPacker;
            if (packer == ftGlobalStorage.AtlasPacker.xatlas)
            {
                if (!PackWithXatlas(lmgroup, holderObjs, data, pdata))
                {
                    ExportSceneError("Failed packing atlas");
                    return false;
                }
            }
            else
            {
                // Calculate area sum for every scene LOD level in LMGroup
                // Write remainingAreaPerLodLevel
                SumHolderAreaPerLODLevel(holderObjs, data, pdata);

                // Perform recursive packing
                while(pdata.repack)
                {
                    pdata.continueRepack = true;
                    if (!Pack(lmgroup, holderObjs, data, pdata)) return false;
                    if (!pdata.continueRepack) break;
                }
                // Normalize atlas by largest axis
                NormalizeAtlas(lmgroup, holderObjs, data, pdata);
            }
        }
        cmp_objToLodLevel = null;
        cmp_holderObjArea = null;

        if (!ValidateScaleOffsetImmutability(data))
        {
            sceneNeedsToBeRebuilt = true;
            return false;
        }
        return true;
    }

    static void NormalizeAutoAtlases(ExportSceneData data)
    {
        var autoAtlasGroups = data.autoAtlasGroups;
        var autoAtlasGroupRootNodes = data.autoAtlasGroupRootNodes;
        var holderRect = data.holderRect;

        // Normalize autoatlases
        var stack = new Stack<AtlasNode>();
        for(int g=0; g<autoAtlasGroups.Count; g++)
        {
            var lmgroup = autoAtlasGroups[g];
            var rootNode = autoAtlasGroupRootNodes[g];
            float maxx = 0;
            float maxy = 0;
            rootNode.GetMax(ref maxx, ref maxy);
            float maxDimension = maxx > maxy ? maxx : maxy;
            float normalizeScale = 1.0f / maxDimension;
            stack.Clear();
            stack.Push(rootNode);
            while(stack.Count > 0)
            {
                var node = stack.Pop();
                if (node.obj != null)
                {
                    var rect = holderRect[node.obj];
                    holderRect[node.obj] = new Rect(rect.x * normalizeScale, rect.y * normalizeScale, rect.width * normalizeScale, rect.height * normalizeScale);
                }
                if (node.child0 != null) stack.Push(node.child0);
                if (node.child1 != null) stack.Push(node.child1);
            }
            if (maxDimension < 0.5f)
            {
                lmgroup.resolution /= 2; // shrink the lightmap after normalization if it was too empty
                lmgroup.resolution = Math.Max(lmgroup.resolution, minAutoResolution);
            }
        }
    }

    static void JoinAutoAtlases(ExportSceneData data)
    {
        var autoAtlasGroups = data.autoAtlasGroups;
        var autoAtlasGroupRootNodes = data.autoAtlasGroupRootNodes;
        var groupList = data.groupList;
        var lmBounds = data.lmBounds;
        var holderRect = data.holderRect;
        var objsToWrite = data.objsToWrite;
        var objsToWriteGroup = data.objsToWriteGroup;

        var stack = new Stack<AtlasNode>();

        // Join autoatlases
        var autoAtlasCategories = new List<string>();
        bool joined = false;
        for(int g=0; g<autoAtlasGroups.Count; g++)
        {
            string cat = "/" + autoAtlasGroups[g].sceneLodLevel;
            if (splitByScene) cat = autoAtlasGroups[g].sceneName + cat;
            if (!autoAtlasCategories.Contains(cat)) autoAtlasCategories.Add(cat);
        }
        for(int alod=0; alod<autoAtlasCategories.Count; alod++)
        {
            var cat = autoAtlasCategories[alod];
            var autoAtlasSizes = new List<int>();
            var atlasStack = new Stack<BakeryLightmapGroup>();
            for(int g=0; g<autoAtlasGroups.Count; g++)
            {
                var thisCat = "/" + autoAtlasGroups[g].sceneLodLevel;
                if (splitByScene) thisCat = autoAtlasGroups[g].sceneName + thisCat;

                if (thisCat != cat) continue;
                if (autoAtlasGroups[g].resolution == maxAutoResolution) continue;
                if (!autoAtlasSizes.Contains(autoAtlasGroups[g].resolution)) autoAtlasSizes.Add(autoAtlasGroups[g].resolution);
            }
            autoAtlasSizes.Sort();
            for(int s=0; s<autoAtlasSizes.Count; s++)
            {
                int asize = autoAtlasSizes[s];
                atlasStack.Clear();
                for(int g=0; g<autoAtlasGroups.Count; g++)
                {
                    var thisCat = "/" + autoAtlasGroups[g].sceneLodLevel;
                    if (splitByScene) thisCat = autoAtlasGroups[g].sceneName + thisCat;

                    if (thisCat != cat) continue;
                    if (autoAtlasGroups[g].resolution != asize) continue;
                    atlasStack.Push(autoAtlasGroups[g]);
                    if (atlasStack.Count == 4)
                    {
                        var newGroup = ScriptableObject.CreateInstance<BakeryLightmapGroup>();
                        newGroup.name = autoAtlasGroups[g].name;
                        newGroup.isImplicit = true;
                        newGroup.sceneLodLevel = autoAtlasGroups[g].sceneLodLevel;
                        newGroup.sceneName = autoAtlasGroups[g].sceneName;

                        newGroup.resolution = asize * 2;

                        newGroup.bitmask = autoAtlasGroups[g].bitmask;
                        newGroup.mode = BakeryLightmapGroup.ftLMGroupMode.PackAtlas;

                        newGroup.id = data.lmid;
                        groupList.Add(newGroup);
                        lmBounds.Add(new Bounds(new Vector3(0,0,0), new Vector3(0,0,0)));
                        data.lmid++;

                        autoAtlasGroups.Add(newGroup);
                        var rootNode2 = new AtlasNode();
                        rootNode2.rc = new Rect(0, 0, 1, 1);
                        autoAtlasGroupRootNodes.Add(rootNode2);

                        // Top
                        rootNode2.child0 = new AtlasNode();
                        rootNode2.child0.rc = new Rect(0, 0, 1, 0.5f);

                        // Bottom
                        rootNode2.child1 = new AtlasNode();
                        rootNode2.child1.rc = new Rect(0, 0.5f, 1, 0.5f);

                        for(int gg=0; gg<4; gg++)
                        {
                            var subgroup = atlasStack.Pop();
                            var id = autoAtlasGroups.IndexOf(subgroup);
                            var subgroupRootNode = autoAtlasGroupRootNodes[id];
                            float ox, oy, sx, sy;

                            if (gg == 0)
                            {
                                // Left top
                                rootNode2.child0.child0 = subgroupRootNode;
                                //rootNode2.child0.child0.Transform(0, 0, 0.5f, 0.5f);
                                //offsetScale = rootNode2.child0.child0.rc;
                                ox = 0; oy = 0; sx = 0.5f; sy = 0.5f;
                            }
                            else if (gg == 1)
                            {
                                // Right top
                                rootNode2.child0.child1 = subgroupRootNode;
                                //rootNode2.child0.child1.Transform(0.5f, 0, 0.5f, 0.5f);
                                //offsetScale = rootNode2.child0.child1.rc;
                                ox = 0.5f; oy = 0; sx = 0.5f; sy = 0.5f;
                            }
                            else if (gg == 2)
                            {
                                // Left bottom
                                rootNode2.child1.child0 = subgroupRootNode;
                                //rootNode2.child1.child0.Transform(0, 0.5f, 0.5f, 0.5f);
                                //offsetScale = rootNode2.child1.child0.rc;
                                ox = 0; oy = 0.5f; sx = 0.5f; sy = 0.5f;
                            }
                            else
                            {
                                // Right bottom
                                rootNode2.child1.child1 = subgroupRootNode;
                                //rootNode2.child1.child1.Transform(0.5f, 0.5f, 0.5f, 0.5f);
                                //offsetScale = rootNode2.child1.child1.rc;
                                ox = 0.5f; oy = 0.5f; sx = 0.5f; sy = 0.5f;
                            }

                            autoAtlasGroups.RemoveAt(id);
                            autoAtlasGroupRootNodes.RemoveAt(id);

                            id = groupList.IndexOf(subgroup);
                            groupList.RemoveAt(id);
                            lmBounds.RemoveAt(id);

                            for(int x=id; x<groupList.Count; x++)
                            {
                                groupList[x].id--;
                                data.lmid--;
                            }

                            // Modify implicit group storage
                            joined = true;
                            stack.Clear();
                            stack.Push(subgroupRootNode);
                            while(stack.Count > 0)
                            {
                                var node = stack.Pop();
                                if (node.obj != null)
                                {
                                    var rect = holderRect[node.obj];
                                    holderRect[node.obj] = new Rect(rect.x * sx + ox,
                                                                    rect.y * sy + oy,
                                                                    rect.width * sx,
                                                                    rect.height * sy);

                                    MoveObjectToImplicitGroup(node.obj, newGroup, data);

                                    /*
                                    tempStorage.implicitGroupMap[node.obj] = newGroup;
                                    for(int k=0; k<storages[sceneToID[node.obj.scene]].implicitGroupedObjects.Count; k++)
                                    {
                                        if (storages[sceneToID[node.obj.scene]].implicitGroupedObjects[k] == node.obj)
                                        {
                                            storages[sceneToID[node.obj.scene]].implicitGroups[k] = newGroup;
                                            //Debug.LogError("Implicit set (join): " + k+" "+newGroup.name);
                                        }
                                    }
                                    */
                                }
                                if (node.child0 != null) stack.Push(node.child0);
                                if (node.child1 != null) stack.Push(node.child1);
                            }
                        }
                    }
                }
            }
        }
        if (joined)
        {
            for(int i=0; i<objsToWrite.Count; i++)
            {
                objsToWriteGroup[i] = GetLMGroupFromObject(objsToWrite[i], data);
            }
        }
    }

    static bool ExportSceneValidationMessage(string msg)
    {
        ProgressBarEnd(false);
        if (ftRenderLightmap.verbose)
        {
            if (!EditorUtility.DisplayDialog("Bakery", msg, "Continue anyway", "Cancel"))
            {
                CloseAllFiles();
                userCanceled = true;
                ProgressBarEnd(true);
                return false;
            }
        }
        else
        {
            Debug.LogError(msg);
        }
        ProgressBarInit("Exporting scene - preparing...");
        return true;
    }

    static void ExportSceneError(string phase)
    {
        DebugLogError("Error exporting scene (" + phase + ") - see console for details");
        CloseAllFiles();
        userCanceled = true;
        ProgressBarEnd(true);
    }

    class ExportSceneData
    {
        // Per-scene data
        public ftLightmapsStorage[] storages;
        public int firstNonNullStorage;
        //public ftLightmapsStorage settingsStorage;
        public Dictionary<Scene, int> sceneToID = new Dictionary<Scene, int>();
        public Dictionary<Scene,bool> sceneHasStorage = new Dictionary<Scene,bool>();

        // Object properties
        public Dictionary<GameObject,int> objToLodLevel = new Dictionary<GameObject,int>(); // defines atlas LOD level
        public Dictionary<GameObject,List<int>> objToLodLevelVisible = new Dictionary<GameObject,List<int>>(); // defines LOD levels where this object is visible

        public List<GameObject> objsToWrite = new List<GameObject>();
        public List<bool> objsToWriteLightmapped = new List<bool>();
        public List<BakeryLightmapGroup> objsToWriteGroup = new List<BakeryLightmapGroup>();
        public List<GameObject> objsToWriteHolder = new List<GameObject>();
        public List<Vector4> objsToWriteScaleOffset = new List<Vector4>();
        public List<Vector2[]> objsToWriteUVOverride = new List<Vector2[]>();
        public List<string> objsToWriteNames = new List<string>();
        public List<Vector3[]> objsToWriteVerticesPosW = new List<Vector3[]>();
        public List<Vector3[]> objsToWriteVerticesNormalW = new List<Vector3[]>();
        public List<Vector4[]> objsToWriteVerticesTangentW = new List<Vector4[]>();
        public List<Vector2[]> objsToWriteVerticesUV = new List<Vector2[]>();
        public List<Vector2[]> objsToWriteVerticesUV2 = new List<Vector2[]>();
        public List<int[][]> objsToWriteIndices = new List<int[][]>();

        // Auto-atlasing
        public List<BakeryLightmapGroup> autoAtlasGroups = new List<BakeryLightmapGroup>();
        public List<AtlasNode> autoAtlasGroupRootNodes = new List<AtlasNode>();
        public BakeryLightmapGroup autoVertexGroup;

        // Data to collect for atlas packing
        public Dictionary<GameObject, float> holderObjArea = new Dictionary<GameObject, float>(); // LMGroup holder area, accumulated from all children
        public Dictionary<GameObject, Vector4> holderObjUVBounds = new Dictionary<GameObject, Vector4>(); // LMGroup holder 2D UV AABB
        public Dictionary<BakeryLightmapGroup, List<GameObject>> groupToHolderObjects = new Dictionary<BakeryLightmapGroup, List<GameObject>>(); // LMGroup -> holders map
        public Dictionary<GameObject, Rect> holderRect = new Dictionary<GameObject, Rect>();

        // Per-LMGroup data
        public List<BakeryLightmapGroup> groupList = new List<BakeryLightmapGroup>();
        public List<Bounds> lmBounds = new List<Bounds>(); // list of bounding boxes around LMGroups for testing lights

        // Geometry data
        public List<int>[] indicesOpaqueLOD = null;
        public List<int>[] indicesTransparentLOD = null;

        public int lmid = 0; // LMID counter

        public ExportSceneData(int sceneCount)
        {
            storages = new ftLightmapsStorage[sceneCount];
        }
    }

    class AdjustUVPaddingData
    {
        public List<int> dirtyObjList = new List<int>();
        public List<string> dirtyAssetList = new List<string>();
        public Dictionary<Mesh, List<int>> meshToObjIDs = new Dictionary<Mesh, List<int>>();
        public Dictionary<Mesh, int> meshToPaddingMap = new Dictionary<Mesh, int>();
    }

    class PackData
    {
        public Dictionary<int,float> remainingAreaPerLodLevel = new Dictionary<int,float>();
        public bool repack = true;
        public bool repackStage2 = false;
        public bool finalRepack = false;
        public float repackScale = 1;
        public int repackTries = 0;
        public bool hasResOverrides = false;
        public bool continueRepack = false;
    }

    static public IEnumerator ExportScene(EditorWindow window, bool renderTextures = true, bool atlasOnly = false)
    {
        userCanceled = false;
        ProgressBarInit("Exporting scene - preparing...", window);
        yield return null;

        var bakeryRuntimePath = ftLightmaps.GetRuntimePath();
        gstorage = AssetDatabase.LoadAssetAtPath(bakeryRuntimePath + "ftGlobalStorage.asset", typeof(ftGlobalStorage)) as ftGlobalStorage;

        var time = GetTime();
        var ms = time;
        var startMsU = ms;
        double totalTime = GetTime();
        double vbTimeRead = 0;
        double vbTimeWrite = 0;
        double vbTimeWriteFull = 0;
        double vbTimeWriteT = 0;
        double vbTimeWriteT2 = 0;
        double vbTimeWriteT3 = 0;
        double ibTime = 0;
        var sceneCount = SceneManager.sceneCount;
        var indicesOpaque = new List<int>();
        var indicesTransparent = new List<int>();

        var data = new ExportSceneData(sceneCount);

        bool tangentSHLights = CheckForTangentSHLights();

        // Per-LMGroup data
        var lmAlbedoList = new List<IntPtr>(); // list of albedo texture for UV GBuffer rendering
        var lmAlbedoListTex = new List<Texture>();
        var lmAlphaList = new List<IntPtr>(); // list of alpha textures for alpha buffer generation
        var lmAlphaListTex = new List<Texture>();
        var lmAlphaRefList = new List<float>(); // list of alpha texture refs
        var lmAlphaChannelList = new List<int>(); // list of alpha channels

        // lod-related
        var lmVOffset = new List<int>();
        var lmUVArrays = new List<List<float>>();
        var lmUVArrays2 = new List<float[]>();
        var lmUVArrays3 = new List<float[]>();
        var lmIndexArrays = new List<List<int>>();
        var lmIndexArrays2 = new List<int[]>();
        var lmLocalToGlobalIndices = new List<List<int>>();

        vbtraceTexPosNormalArray = new List<float>();
        vbtraceTexUVArray = new List<float>();

        sceneLodsUsed = 0;

        // Create temp path
        CreateSceneFolder();

        // Init storages
        try
        {
            InitSceneStorage(data);
        }
        catch(Exception e)
        {
            ExportSceneError("Global storage init");
            Debug.LogError("Exception caught: " + e.ToString());
            throw;
        }

        // Create LMGroup for light probes
        if (ftRenderLightmap.lightProbeMode == ftRenderLightmap.LightProbeMode.L1 && renderTextures && !atlasOnly && ftRenderLightmap.hasAnyProbes)
        {
            var c = CreateLightProbeLMGroup(data);
            while(c.MoveNext()) yield return null;
        }

        if (ftRenderLightmap.hasAnyVolumes)
        {
            var c2 = CreateVolumeLMGroup(data);
            while(c2.MoveNext()) yield return null;
        }

        // wip
        var lmBounds = data.lmBounds;
        var storages = data.storages;
        var sceneToID = data.sceneToID;
        var groupList = data.groupList;
        var objToLodLevel = data.objToLodLevel;
        var objToLodLevelVisible = data.objToLodLevelVisible;
        var objsToWrite = data.objsToWrite;
        var objsToWriteGroup = data.objsToWriteGroup;
        var objsToWriteHolder = data.objsToWriteHolder;
        var objsToWriteIndices = data.objsToWriteIndices;
        var objsToWriteNames = data.objsToWriteNames;
        var objsToWriteUVOverride = data.objsToWriteUVOverride;
        var objsToWriteScaleOffset = data.objsToWriteScaleOffset;
        var objsToWriteVerticesUV = data.objsToWriteVerticesUV;
        var objsToWriteVerticesUV2 = data.objsToWriteVerticesUV2;
        var objsToWriteVerticesPosW = data.objsToWriteVerticesPosW;
        var objsToWriteVerticesNormalW = data.objsToWriteVerticesNormalW;
        var objsToWriteVerticesTangentW = data.objsToWriteVerticesTangentW;
        var holderRect = data.holderRect;

        terrainObjectList = new List<GameObject>();
        terrainObjectToActual = new List<Terrain>();
        terrainObjectToHeightMap = new List<Texture>();
        terrainObjectToBounds = new List<float>();
        terrainObjectToBoundsUV = new List<float>();
        terrainObjectToFlags = new List<int>();
        terrainObjectToLMID = new List<int>();
        terrainObjectToHeightMips = new List<List<float[]>>();
        treeObjectList = new List<GameObject>();
        temporaryAreaLightMeshList = new List<GameObject>();
        temporaryAreaLightMeshList2 = new List<BakeryLightMesh>();

        var objects = Resources.FindObjectsOfTypeAll(typeof(GameObject));
        //var objects = UnityEngine.Object.FindObjectsOfTypeAll(typeof(GameObject));

        try
        {
            ms = GetTime();

            //if (!onlyUVdata)
            //{
                time = ms;

                // Get manually created LMGroups
                CollectExplicitLMGroups(data);

                // Object conversion loop / also validate for multiple scene storages
                for(int objNum = 0; objNum < objects.Length; objNum++)
                {
                    GameObject obj = (GameObject)objects[objNum];
                    if (obj == null) continue;
                    if (!CheckForMultipleSceneStorages(obj, data)) yield break;
                    if (ConvertUnityAreaLight(obj)) continue;
                    ConvertTerrain(obj);
                }

                // Regather objects if new were added
                if (terrainObjectList.Count > 0 || treeObjectList.Count > 0 || temporaryAreaLightMeshList.Count > 0)
                {
                    //objects = UnityEngine.Object.FindObjectsOfTypeAll(typeof(GameObject));
                    objects = Resources.FindObjectsOfTypeAll(typeof(GameObject));
                }

                tempStorage.implicitGroupMap = new Dictionary<GameObject, UnityEngine.Object>(); // implicit holder -> LMGroup map. used by GetLMGroupFromObject

                // Find LODGroups -> LODs -> scene-wide LOD distances
                // Map objects to scene-wide LOD levels
                MapObjectsToSceneLODs(data, objects);

                ftModelPostProcessor.Init();

                // Filter objects, convert to property arrays
                if (!FilterObjects(data, objects)) yield break;

                CalculateVertexCountForVertexGroups(data);
                CreateAutoAtlasLMGroups(data, renderTextures, atlasOnly);

                TransformVertices(data, tangentSHLights);

                if (unwrapUVs)
                {
                    var adata = new AdjustUVPaddingData();

                    CalculateUVPadding(data, adata);
                    ResetPaddingStorageData(data);
                    StoreNewUVPadding(data, adata);

                    if (!ValidatePaddingImmutability(adata)) yield break;

                    if (CheckUnwrapError()) yield break;

                    // Reimport assets with adjusted padding
                    if (modifyLightmapStorage)
                    {
                        if (!ReimportModifiedAssets(adata)) yield break;

                        TransformModifiedAssets(data, adata, tangentSHLights);
                    }
                }
                else if (forceDisableUnwrapUVs)
                {
                    var adata = new AdjustUVPaddingData();

                    ResetPaddingStorageData(data);
                    if (!ClearUVPadding(data, adata)) yield break;

                    if (CheckUnwrapError()) yield break;

                    TransformModifiedAssets(data, adata, tangentSHLights);
                }

                CalculateHolderUVBounds(data);
                CalculateAutoAtlasInitResolution(data);
                if (!PackAtlases(data)) yield break;

                if (atlasPacker == ftGlobalStorage.AtlasPacker.Default)
                {
                    NormalizeAutoAtlases(data);
                    JoinAutoAtlases(data);
                }

                InitSceneStorage2(data);

                //TransformVertices(data); // shouldn't be necessary

                // Update objToWriteGroups because of autoAtlas
                if (autoAtlas)
                {
                    for(int i=0; i<objsToWrite.Count; i++)
                    {
                        objsToWriteGroup[i] = GetLMGroupFromObject(objsToWrite[i], data);
                    }
                }

                // Done collecting groups

                if (groupList.Count == 0 && modifyLightmapStorage)
                {
                    DebugLogError("You need to mark some objects static or add Bakery Lightmap Group Selector components on them.");
                    CloseAllFiles();
                    userCanceled = true;
                    ProgressBarEnd(true);
                    yield break;
                }

                if (objsToWrite.Count == 0)
                {
                    DebugLogError("You need to mark some objects static or add Bakery Lightmap Group Selector components on them.");
                    CloseAllFiles();
                    userCanceled = true;
                    ProgressBarEnd(true);
                    yield break;
                }

                if (atlasOnly)
                {
                    atlasOnlyObj = new List<Renderer>();
                    atlasOnlySize = new List<int>();
                    atlasOnlyID = new List<int>();
                    atlasOnlyScaleOffset = new List<Vector4>();
                    var emptyVec4 = new Vector4(1,1,0,0);
                    Rect rc = new Rect();
                    for(int i=0; i<objsToWrite.Count; i++)
                    {
                        var lmgroup = objsToWriteGroup[i];
                        var holderObj = objsToWriteHolder[i];
                        if (holderObj != null)
                        {
                            if (!holderRect.TryGetValue(holderObj, out rc))
                            {
                                holderObj = null;
                            }
                        }
                        var scaleOffset = holderObj == null ? emptyVec4 : new Vector4(rc.width, rc.height, rc.x, rc.y);
                        atlasOnlyObj.Add(objsToWrite[i].GetComponent<Renderer>());
                        atlasOnlyScaleOffset.Add(scaleOffset);
                        atlasOnlySize.Add(lmgroup == null ? 0 : lmgroup.resolution);
                        atlasOnlyID.Add(lmgroup == null ? 0 : lmgroup.id);
                    }
                    yield break;
                }

                // Sort LMGroups so vertex groups are never first (because Unity assumes lightmap compression on LM0)
                for(int i=0; i<groupList.Count; i++)
                {
                    groupList[i].sortingID = i;
                }
                groupList.Sort(delegate(BakeryLightmapGroup a, BakeryLightmapGroup b)
                {
                    int aa = (a.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex) ? -1 : 1;
                    int bb = (b.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex) ? -1 : 1;
                    return bb.CompareTo(aa);
                });
                var lmBounds2 = new List<Bounds>();
                for(int i=0; i<groupList.Count; i++)
                {
                    lmBounds2.Add(lmBounds[groupList[i].sortingID]); // apply same sorting to lmBounds
                    groupList[i].id = i;
                }
                lmBounds = lmBounds2;

                // Check for existing files
                if (overwriteWarning)
                {
                    var checkGroupList = groupList;
                    if (overwriteWarningSelectedOnly)
                    {
                        var selObjs = Selection.objects;
                        checkGroupList = new List<BakeryLightmapGroup>();
                        for(int o=0; o<selObjs.Length; o++)
                        {
                            if (selObjs[o] as GameObject == null) continue;
                            var selGroup = GetLMGroupFromObject(selObjs[o] as GameObject, data);
                            if (selGroup == null) continue;
                            if (!checkGroupList.Contains(selGroup))
                            {
                                checkGroupList.Add(selGroup);
                            }
                        }
                    }
                    var existingFilenames = "";
                    for(int i=0; i<checkGroupList.Count; i++)
                    {
                        var nm = checkGroupList[i].name;
                        var filename = nm + "_final" + overwriteExtensionCheck;
                        var outputPath = ftRenderLightmap.outputPath;
                        if (File.Exists("Assets/" + outputPath + "/" + filename))
                        {
                            existingFilenames += filename + "\n";
                        }
                    }
                    if (existingFilenames.Length > 0 && ftRenderLightmap.verbose)
                    {
                        ProgressBarEnd(false);
                        if (!EditorUtility.DisplayDialog("Lightmap overwrite", "These lightmaps will be overwritten:\n\n" + existingFilenames, "Overwrite", "Cancel"))
                        {
                            CloseAllFiles();
                            userCanceled = true;
                            ProgressBarEnd(true);
                            yield break;
                        }
                        ProgressBarInit("Exporting scene - preparing...", window);
                    }
                }

                ftRenderLightmap.giLodModeEnabled = ftRenderLightmap.giLodMode == ftRenderLightmap.GILODMode.ForceOn;
                ulong approxMem = 0;

                if (groupList.Count > 100 && ftRenderLightmap.verbose)
                {
                    ProgressBarEnd(false);
                    if (!EditorUtility.DisplayDialog("Lightmap count check", groupList.Count + " lightmaps are going to be rendered. Continue?", "Continue", "Cancel"))
                    {
                        CloseAllFiles();
                        userCanceled = true;
                        ProgressBarEnd(true);
                        yield break;
                    }
                    ProgressBarInit("Exporting scene - preparing...", window);
                }

                if (memoryWarning || ftRenderLightmap.giLodMode == ftRenderLightmap.GILODMode.Auto)
                {
                    for(int i=0; i<groupList.Count; i++)
                    {
                        var lmgroup = groupList[i];
                        var res = lmgroup.resolution;
                        ulong lightingSize = (ulong)(res * res * 4 * 2); // RGBA16f
                        approxMem += lightingSize;
                    }
                    var tileSize = ftRenderLightmap.tileSize;
                    approxMem += (ulong)(tileSize * tileSize * 16 * 2); // maximum 2xRGBA32f (for fixPos12)
                }

                if (memoryWarning && ftRenderLightmap.verbose)
                {
                    ProgressBarEnd(false);
                    if (!EditorUtility.DisplayDialog("Lightmap memory check", "Rendering may require more than " + (ulong)((approxMem/1024)/1024) + "MB of video memory. Continue?", "Continue", "Cancel"))
                    {
                        CloseAllFiles();
                        userCanceled = true;
                        ProgressBarEnd(true);
                        yield break;
                    }
                    ProgressBarInit("Exporting scene - preparing...", window);
                }


                if (ftRenderLightmap.giLodMode == ftRenderLightmap.GILODMode.Auto)
                {

                    approxMem /= 1024;
                    approxMem /= 1024;
                    approxMem += 1024; // scene geometry size estimation - completely random

                    if ((int)approxMem > SystemInfo.graphicsMemorySize)
                    {
                        Debug.Log("GI VRAM auto optimization ON: estimated usage " + (int)approxMem + " > " + SystemInfo.graphicsMemorySize);
                        ftRenderLightmap.giLodModeEnabled = true;
                    }
                    else
                    {
                        Debug.Log("GI VRAM auto optimization OFF: estimated usage " + (int)approxMem + " < " + SystemInfo.graphicsMemorySize);
                    }
                }

                // Generate terrain geometry with detail enough for given size for UVGBuffer purposes
                fhmaps = new BinaryWriter(File.Open(scenePath + "/heightmaps.bin", FileMode.Create));
                if (ftRenderLightmap.clientMode) ftClient.serverFileList.Add("heightmaps.bin");
                if (exportTerrainAsHeightmap)
                {
                    for(int i=0; i<objsToWrite.Count; i++)
                    {
                        var obj = objsToWrite[i];
                        if (obj.name != "__ExportTerrain") continue;

                        var holderObj = objsToWriteHolder[i];
                        Rect rc = new Rect();
                        if (holderObj != null)
                        {
                            if (!holderRect.TryGetValue(holderObj, out rc))
                            {
                                holderObj = null;
                            }
                        }
                        if (holderObj == null) continue;

                        var lmgroup = objsToWriteGroup[i];
                        //float terrainPixelWidth = rc.width * lmgroup.resolution;
                        //float terrainPixelHeight = rc.height * lmgroup.resolution;

                        var index = terrainObjectList.IndexOf(obj.transform.parent.gameObject);
                        var terrain = terrainObjectToActual[index];
                        var tdata = terrain.terrainData;
                        //var heightmapResolution = tdata.heightmapResolution;

                        //int closestSize = (int)Mathf.Min(Mathf.NextPowerOfTwo((int)Mathf.Max(terrainPixelWidth, terrainPixelHeight)), heightmapResolution-1);
                        //if (closestSize < 2) continue;
                        //int mipLog2 = (int)(Mathf.Log(closestSize) / Mathf.Log(2.0f));
                        //int maxMipLog2 = (int)(Mathf.Log(heightmapResolution-1) / Mathf.Log(2.0f));
                        //int mip = maxMipLog2 - mipLog2;

                        float scaleX = tdata.size.x;// / (heightmapResolution-1);
                        //float scaleY = tdata.size.y;
                        float scaleZ = tdata.size.z;// / (heightmapResolution-1);
                        float offsetX = obj.transform.position.x;
                        float offsetY = obj.transform.position.y;
                        float offsetZ = obj.transform.position.z;

                        terrainObjectToLMID[index] = lmgroup.id;
                        terrainObjectToBoundsUV[index*4] = rc.x;
                        terrainObjectToBoundsUV[index*4+1] = rc.y;
                        terrainObjectToBoundsUV[index*4+2] = rc.width;
                        terrainObjectToBoundsUV[index*4+3] = rc.height;

                        if (uvgbHeightmap)
                        {
                            var indexArrays = objsToWriteIndices[i] = new int[1][];
                            var indexArray = indexArrays[0] = new int[6];//(closestSize-1)*(closestSize-1)*6];
                            //int indexOffset = 0;
                            //int vertOffset = 0;
                            var uvArray = objsToWriteVerticesUV[i] = objsToWriteVerticesUV2[i] = new Vector2[4];//closestSize*closestSize];

                            var posArray = objsToWriteVerticesPosW[i] = new Vector3[4];
                            var normalArray = objsToWriteVerticesNormalW[i] = new Vector3[4];

                            posArray[0] = new Vector3(offsetX, offsetY, offsetZ);
                            posArray[1] = new Vector3(offsetX + scaleX, offsetY, offsetZ);
                            posArray[2] = new Vector3(offsetX, offsetY, offsetZ + scaleZ);
                            posArray[3] = new Vector3(offsetX + scaleX, offsetY, offsetZ + scaleZ);

                            normalArray[0] = Vector3.up;
                            normalArray[1] = Vector3.up;
                            normalArray[2] = Vector3.up;
                            normalArray[3] = Vector3.up;

                            uvArray[0] = new Vector2(0,0);
                            uvArray[1] = new Vector2(1,0);
                            uvArray[2] = new Vector2(0,1);
                            uvArray[3] = new Vector2(1,1);

                            indexArray[0] = 0;
                            indexArray[1] = 2;
                            indexArray[2] = 3;

                            indexArray[3] = 0;
                            indexArray[4] = 3;
                            indexArray[5] = 1;
                        }
                        else
                        {
                            /*if (mip == 0)
                            {
                                // use existing heightmap
                                var heights = tdata.GetHeights(0, 0, heightmapResolution, heightmapResolution);
                                var posArray = objsToWriteVerticesPosW[i] = new Vector3[heightmapResolution * heightmapResolution];
                                objsToWriteVerticesNormalW[i] = terrainObjectToNormalMip0[index];
                                closestSize = heightmapResolution;
                                scaleX /= closestSize-1;
                                scaleZ /= closestSize-1;
                                for(int y=0; y<closestSize; y++)
                                {
                                    for(int x=0; x<closestSize; x++)
                                    {
                                        float px = x * scaleX + offsetX;
                                        float pz = y * scaleZ + offsetZ;
                                        posArray[y * closestSize + x] = new Vector3(px, heights[y, x] * scaleY + offsetY, pz);
                                    }
                                }
                            }
                            else
                            {
                                // use mip
                                var heights = terrainObjectToHeightMips[index][mip - 1];
                                var posArray = objsToWriteVerticesPosW[i] = new Vector3[closestSize * closestSize];
                                objsToWriteVerticesNormalW[i] = terrainObjectToNormalMips[index][mip - 1];
                                scaleX /= closestSize-1;
                                scaleZ /= closestSize-1;
                                for(int y=0; y<closestSize; y++)
                                {
                                    for(int x=0; x<closestSize; x++)
                                    {
                                        float px = x * scaleX + offsetX;
                                        float pz = y * scaleZ + offsetZ;
                                        posArray[y * closestSize + x] = new Vector3(px, heights[y * closestSize + x] * scaleY + offsetY, pz);
                                    }
                                }
                            }
                            var indexArrays = objsToWriteIndices[i] = new int[1][];
                            var indexArray = indexArrays[0] = new int[(closestSize-1)*(closestSize-1)*6];
                            int indexOffset = 0;
                            int vertOffset = 0;
                            var uvArray = objsToWriteVerticesUV[i] = objsToWriteVerticesUV2[i] = new Vector2[closestSize*closestSize];
                            for(int y=0; y<closestSize; y++)
                            {
                                for(int x=0; x<closestSize; x++)
                                {
                                    uvArray[y * closestSize + x] = new Vector2(x / (float)(closestSize-1), y / (float)(closestSize-1));

                                    if (x < closestSize-1 && y < closestSize-1)
                                    {
                                        indexArray[indexOffset] = vertOffset;
                                        indexArray[indexOffset + 1] = vertOffset + closestSize;
                                        indexArray[indexOffset + 2] = vertOffset + closestSize + 1;

                                        indexArray[indexOffset + 3] = vertOffset;
                                        indexArray[indexOffset + 4] = vertOffset + closestSize + 1;
                                        indexArray[indexOffset + 5] = vertOffset + 1;

                                        indexOffset += 6;
                                    }
                                    vertOffset++;
                                }
                            }*/
                        }
                    }

                    // Export heightmap metadata
                    if (terrainObjectToActual.Count > 0)
                    {
                        terrainObjectToHeightMapPtr = new IntPtr[terrainObjectToHeightMap.Count];
                        for(int i=0; i<terrainObjectToHeightMap.Count; i++)
                        {
                            fhmaps.Write(terrainObjectToLMID[i]);
                            for(int fl=0; fl<6; fl++) fhmaps.Write(terrainObjectToBounds[i*6+fl]);
                            for(int fl=0; fl<4; fl++) fhmaps.Write(terrainObjectToBoundsUV[i*4+fl]);
                            fhmaps.Write(terrainObjectToFlags[i]);
                        }
                    }
                }

                // Write mark last written scene
                File.WriteAllText(scenePath + "/lastscene.txt", ftRenderLightmap.GenerateLightingDataAssetName());

                // Write lightmap definitions
                var flms = new BinaryWriter(File.Open(scenePath + "/lms.bin", FileMode.Create));
                var flmlod = new BinaryWriter(File.Open(scenePath + "/lmlod.bin", FileMode.Create));
                var flmuvgb = new BinaryWriter(File.Open(scenePath + "/lmuvgb.bin", FileMode.Create));

                if (ftRenderLightmap.clientMode)
                {
                    ftClient.serverFileList.Add("lms.bin");
                    ftClient.serverFileList.Add("lmlod.bin");
                    ftClient.serverFileList.Add("lmuvgb.bin");
                }

                // Init global UVGBuffer flags
                int uvgbGlobalFlags = 0;
                if (exportShaderColors)
                {
                    if (ftRenderLightmap.renderDirMode == ftRenderLightmap.RenderDirMode.BakedNormalMaps)
                    {
                        uvgbGlobalFlags = UVGBFLAG_FACENORMAL | UVGBFLAG_POS | UVGBFLAG_SMOOTHPOS;
                    }
                    else if (ftRenderLightmap.renderDirMode == ftRenderLightmap.RenderDirMode.RNM ||
                            (ftRenderLightmap.renderDirMode == ftRenderLightmap.RenderDirMode.SH && tangentSHLights))
                    {
                        uvgbGlobalFlags = UVGBFLAG_NORMAL | UVGBFLAG_FACENORMAL | UVGBFLAG_POS | UVGBFLAG_SMOOTHPOS | UVGBFLAG_TANGENT;
                    }
                    else
                    {
                        uvgbGlobalFlags = UVGBFLAG_NORMAL | UVGBFLAG_FACENORMAL | UVGBFLAG_POS | UVGBFLAG_SMOOTHPOS;
                    }
                }
                else
                {
                    uvgbGlobalFlags = UVGBFLAG_NORMAL | UVGBFLAG_FACENORMAL | UVGBFLAG_ALBEDO | UVGBFLAG_EMISSIVE | UVGBFLAG_POS | UVGBFLAG_SMOOTHPOS;
                }
                if (terrainObjectToActual.Count > 0) uvgbGlobalFlags |= UVGBFLAG_TERRAIN;
                SetUVGBFlags(uvgbGlobalFlags);

                for(int i=0; i<groupList.Count; i++)
                {
                    var lmgroup = groupList[i];
                    flms.Write(lmgroup.name);

                    flmlod.Write(lmgroup.sceneLodLevel);

                    int uvgbflags = 0;

                    if (lmgroup.containsTerrains && exportTerrainAsHeightmap)
                        uvgbflags = uvgbGlobalFlags | (UVGBFLAG_NORMAL | UVGBFLAG_TERRAIN);

                    if (lmgroup.renderDirMode == BakeryLightmapGroup.RenderDirMode.BakedNormalMaps)
                        uvgbflags = UVGBFLAG_FACENORMAL | UVGBFLAG_POS | UVGBFLAG_SMOOTHPOS;

                    if (lmgroup.renderDirMode == BakeryLightmapGroup.RenderDirMode.RNM ||
                        (lmgroup.renderDirMode == BakeryLightmapGroup.RenderDirMode.SH && tangentSHLights))
                        uvgbflags = UVGBFLAG_NORMAL | UVGBFLAG_FACENORMAL | UVGBFLAG_POS | UVGBFLAG_SMOOTHPOS | UVGBFLAG_TANGENT;

                    if (lmgroup.probes) uvgbflags = UVGBFLAG_RESERVED;

                    flmuvgb.Write(uvgbflags);

                    if (ftRenderLightmap.clientMode)
                    {
                        if (uvgbflags == 0) uvgbflags = uvgbGlobalFlags;

                        ftClient.serverFileList.Add("uvpos_" + lmgroup.name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                        ftClient.serverFileList.Add("uvnormal_" + lmgroup.name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                        ftClient.serverFileList.Add("uvalbedo_" + lmgroup.name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                        if (lmgroup.mode != BakeryLightmapGroup.ftLMGroupMode.Vertex)
                        {
                            if ((uvgbflags & UVGBFLAG_SMOOTHPOS) != 0) ftClient.serverFileList.Add("uvsmoothpos_" + lmgroup.name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                        }
                        if ((uvgbflags & UVGBFLAG_FACENORMAL) != 0) ftClient.serverFileList.Add("uvfacenormal_" + lmgroup.name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                        if ((uvgbflags & UVGBFLAG_TANGENT) != 0) ftClient.serverFileList.Add("uvtangent_" + lmgroup.name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                    }

                    if (lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex)
                    {
                        int atlasTexSize = (int)Mathf.Ceil(Mathf.Sqrt((float)lmgroup.totalVertexCount));
                        if (atlasTexSize > 8192) Debug.LogWarning("Warning: vertex lightmap group " + lmgroup.name + " uses resolution of " + atlasTexSize);
                        atlasTexSize = (int)Mathf.Ceil(atlasTexSize / (float)ftRenderLightmap.tileSize) * ftRenderLightmap.tileSize;
                        flms.Write(-atlasTexSize);
                    }
                    else
                    {
                        flms.Write(lmgroup.resolution);
                    }
                    //Debug.LogError(lmgroup.name+": " + lmgroup.resolution);
                }
                flms.Close();
                flmlod.Close();
                flmuvgb.Close();

                voffset = ioffset = soffset = 0; // vertex/index/surface write

                // Per-surface alpha texture IDs
                var alphaIDs = new List<ushort>();

                int albedoCounter = 0;
                var albedoMap = new Dictionary<IntPtr, int>(); // albedo ptr -> ID map

                int alphaCounter = 0;
                var alphaMap = new Dictionary<IntPtr, List<int>>(); // alpha ptr -> ID map

                var dummyTexList = new List<Texture>(); // list of single-color 1px textures
                var dummyPixelArray = new Color[1];

                if (ftRenderLightmap.checkOverlaps)
                {
                    var quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
                    var plane = GameObject.CreatePrimitive(PrimitiveType.Plane);
                    var qmesh = quad.GetComponent<MeshFilter>().sharedMesh;
                    var pmesh = plane.GetComponent<MeshFilter>().sharedMesh;
                    DestroyImmediate(quad);
                    DestroyImmediate(plane);
                    bool canCheck = ftModelPostProcessor.InitOverlapCheck();
                    if (!canCheck)
                    {
                        DebugLogError("Can't load ftOverlapTest.shader");
                        CloseAllFiles();
                        userCanceled = true;
                        ProgressBarEnd(true);
                        yield break;
                    }
                    for(int g=0; g<groupList.Count; g++)
                    {
                        var lmgroup = groupList[g];
                        if (lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex) continue;
                        for(int i=0; i<objsToWrite.Count; i++)
                        {
                            if (objsToWriteGroup[i] != lmgroup) continue;
                            var obj = objsToWrite[i];

                            var mesh = GetSharedMesh(obj);
                            if (mesh == qmesh || mesh == pmesh) continue;

                            var uv = objsToWriteVerticesUV[i];//mesh.uv;
                            var uv2 = objsToWriteVerticesUV2[i];//mesh.uv2;
                            var usedUVs = uv2.Length == 0 ? uv : uv2;
                            bool validUVs = true;
                            for(int v=0; v<usedUVs.Length; v++)
                            {
                                if (usedUVs[v].x < -0.0001f || usedUVs[v].x > 1.0001f || usedUVs[v].y < -0.0001f || usedUVs[v].y > 1.0001f)
                                {
                                    validUVs = false;
                                    break;
                                }
                            }
                            if (!validUVs && ftRenderLightmap.verbose)
                            {
                                string objPath = obj.name;
                                var prt = obj.transform.parent;
                                while(prt != null)
                                {
                                    objPath = prt.name + "\\" + objPath;
                                    prt = prt.parent;
                                }
                                ftRenderLightmap.simpleProgressBarEnd();
                                if (!EditorUtility.DisplayDialog("Incorrect UVs", "Object " + objPath + " UVs are out of 0-1 bounds", "Continue", "Stop"))
                                {
                                    CloseAllFiles();
                                    userCanceled = true;
                                    ProgressBarEnd(true);
                                    yield break;
                                }
                                ProgressBarInit("Exporting scene - preparing...", window);
                            }

                            int overlap = ftModelPostProcessor.DoOverlapCheck(obj, false);
                            if (overlap != 0 && ftRenderLightmap.verbose)
                            {
                                //storage.debugRT = ftModelPostProcessor.rt;
                                string objPath = obj.name;
                                var prt = obj.transform.parent;
                                while(prt != null)
                                {
                                    objPath = prt.name + "\\" + objPath;
                                    prt = prt.parent;
                                }
                                if (overlap < 0)
                                {
                                    ftRenderLightmap.simpleProgressBarEnd();
                                    if (!EditorUtility.DisplayDialog("Incorrect UVs", "Object " + objPath + " has no UV2", "Continue", "Stop"))
                                    {
                                        CloseAllFiles();
                                        userCanceled = true;
                                        ProgressBarEnd(true);
                                        yield break;
                                    }
                                    ProgressBarInit("Exporting scene - preparing...", window);
                                }
                                else
                                {
                                  ftRenderLightmap.simpleProgressBarEnd();
                                    if (!EditorUtility.DisplayDialog("Incorrect UVs", "Object " + objPath + " has overlapping UVs", "Continue", "Stop"))
                                    {
                                        CloseAllFiles();
                                        userCanceled = true;
                                        ProgressBarEnd(true);
                                        yield break;
                                    }
                                    ProgressBarInit("Exporting scene - preparing...", window);
                                }
                            }
                        }
                    }
                    ftModelPostProcessor.EndOverlapCheck();
                }

                // Prepare progressbar
                int progressNumObjects = 0;
                foreach(GameObject obj in objects)
                {
                    if (obj == null) continue;
                    if (!obj.activeInHierarchy) continue;
                    progressNumObjects++;
                }

                // Open files to write
                fscene = new BinaryWriter(File.Open(scenePath + "/objects.bin", FileMode.Create));
                fmesh = new BinaryWriter(File.Open(scenePath + "/mesh.bin", FileMode.Create));
                flmid = new BinaryWriter(File.Open(scenePath + "/lmid.bin", FileMode.Create));
                fseamfix = new BinaryWriter(File.Open(scenePath + "/seamfix.bin", FileMode.Create));
                fsurf = new BinaryWriter(File.Open(scenePath + "/surf.bin", FileMode.Create));
                fmatid = new BinaryWriter(File.Open(scenePath + "/matid.bin", FileMode.Create));
                fmatide = new BinaryWriter(File.Open(scenePath + "/emissiveid.bin", FileMode.Create));
                fmatideb = new BinaryWriter(File.Open(scenePath + "/emissivemul.bin", FileMode.Create));
                fmatidh = new BinaryWriter(File.Open(scenePath + "/heightmapid.bin", FileMode.Create));
                falphaid = new BinaryWriter(File.Open(scenePath + "/alphaid.bin", FileMode.Create));

                fvbfull =     new BufferedBinaryWriterFloat( new BinaryWriter(File.Open(scenePath + "/vbfull.bin", FileMode.Create)) );
                fvbtrace =    new BufferedBinaryWriterFloat( new BinaryWriter(File.Open(scenePath + "/vbtrace.bin", FileMode.Create)) );
                fvbtraceTex = new BufferedBinaryWriterFloat( new BinaryWriter(File.Open(scenePath + "/vbtraceTex.bin", FileMode.Create)) );
                fvbtraceUV0 = new BufferedBinaryWriterFloat( new BinaryWriter(File.Open(scenePath + "/vbtraceUV0.bin", FileMode.Create)) );

                fib =         new BufferedBinaryWriterInt( new BinaryWriter(File.Open(scenePath + "/ib.bin", FileMode.Create)) );

                fib32 = new BinaryWriter(File.Open(scenePath + "/ib32.bin", FileMode.Create));
                fib32lod = new BinaryWriter[sceneLodsUsed];
                for(int i=0; i<sceneLodsUsed; i++)
                {
                    fib32lod[i] = new BinaryWriter(File.Open(scenePath + "/ib32_lod" + i + ".bin", FileMode.Create));
                    if (ftRenderLightmap.clientMode) ftClient.serverFileList.Add("ib32_lod" + i + ".bin");
                }
                falphaidlod = new BinaryWriter[sceneLodsUsed];
                for(int i=0; i<sceneLodsUsed; i++)
                {
                    falphaidlod[i] = new BinaryWriter(File.Open(scenePath + "/alphaid_lod" + i + ".bin", FileMode.Create));
                    if (ftRenderLightmap.clientMode) ftClient.serverFileList.Add("alphaid2_lod" + i + ".bin"); // alphaid2, not alphaid
                }

                if (ftRenderLightmap.clientMode)
                {
                    ftClient.serverFileList.Add("objects.bin");
                    ftClient.serverFileList.Add("mesh.bin");
                    ftClient.serverFileList.Add("lmid.bin");
                    ftClient.serverFileList.Add("seamfix.bin");
                    ftClient.serverFileList.Add("surf.bin");
                    ftClient.serverFileList.Add("matid.bin");
                    ftClient.serverFileList.Add("emissiveid.bin");
                    ftClient.serverFileList.Add("emissivemul.bin");
                    ftClient.serverFileList.Add("heightmapid.bin");
                    ftClient.serverFileList.Add("alphaid2.bin"); // alphaid2, not alphaid
                        ftClient.serverFileList.Add("alphabuffer.bin");
                    ftClient.serverFileList.Add("vbfull.bin");
                    ftClient.serverFileList.Add("vbtrace.bin");
                    ftClient.serverFileList.Add("vbtraceTex.bin");
                    ftClient.serverFileList.Add("vbtraceUV0.bin");
                    ftClient.serverFileList.Add("ib.bin");
                    ftClient.serverFileList.Add("ib32.bin");
                }

                // Export heightmap metadata
                //fhmaps.Write(terrainObjectToActual.Count);
                if (terrainObjectToActual.Count > 0)
                {
                    //terrainObjectToHeightMapPtr = new IntPtr[terrainObjectToHeightMap.Count];
                    /*for(int i=0; i<terrainObjectToHeightMap.Count; i++)
                    {
                        for(int fl=0; fl<6; fl++) fhmaps.Write(terrainObjectToBounds[i*6+fl]);
                    }*/
                }

                // Export some scene data
                // - LMIDs
                // - mesh definitions
                // - surface definitions
                // - albedo IDs
                // - alpha IDs
                // - update LMGroup bounds
                // - export index buffer
                // - generate tracing index buffer

                areaLightCounter = -2;
                //var defaultTexST = new Vector4(1,1,0,0);
                //var objsToWriteTexST = new List<Vector4>();

                for(int i=0; i<objsToWrite.Count; i++)
                {
                    var obj = objsToWrite[i];
                    var lmgroup = objsToWriteGroup[i];
                    var holderObj = objsToWriteHolder[i];

                    if (obj == null)
                    {
                        // wtf
                        DebugLogError("Object " + objsToWriteNames[i] + " was destroyed mid-export");
                        CloseAllFiles();
                        userCanceled = true;
                        ProgressBarEnd(true);
                        yield break;
                    }

                    var mr = obj.GetComponent<Renderer>();
                    var m = GetSharedMesh(mr);

                    var inds = objsToWriteIndices[i];

                    // Write LMID, mesh and surface definition
                    int id = exportLMID(flmid, obj, lmgroup);
                    exportMesh(fmesh, m);
                    exportSurfs(fsurf, inds, inds.Length);// m);

                    int lodLevel;
                    if (!objToLodLevel.TryGetValue(obj, out lodLevel)) lodLevel = -1;

                    bool isTerrain = (exportTerrainAsHeightmap && obj.name == "__ExportTerrain");

                    // Write albedo IDs, collect alpha IDs, update LMGroup bounds
                    if (id >= 0) {
                        for(int k=0; k<m.subMeshCount; k++) {
                            // Get mesh albedos
                            int texID = -1;
                            Material mat = null;
                            Texture tex = null;
                            //var texST = defaultTexST;
                            if (k < mr.sharedMaterials.Length) {
                                mat = mr.sharedMaterials[k];
                                if (mat != null)
                                {
                                    if (mat.HasProperty("_MainTex"))
                                    {
                                        tex = mat.mainTexture;
                                        //if (mat.HasProperty("_MainTex_ST"))
                                        //{
                                          //  texST = mat.GetVector("_MainTex_ST");
                                        //}
                                    }
                                    else if (mat.HasProperty("_BaseColorMap"))
                                    {
                                        // HDRP
                                        tex = mat.GetTexture("_BaseColorMap");
                                    }
                                    else if (mat.HasProperty("_BaseMap"))
                                    {
                                        // URP
                                        tex = mat.GetTexture("_BaseMap");
                                    }
                                }
                            }
                            IntPtr texPtr = (IntPtr)0;
                            Texture texWrite = null;
                            if (tex != null)
                            {
                                texPtr = tex.GetNativeTexturePtr();
                                texWrite = tex;
                                if (texPtr == (IntPtr)0)
                                {
                                    if ((tex as RenderTexture) != null)
                                    {
                                        Debug.LogError("RenderTexture " + tex.name + " cannot be used as albedo (GetNativeTexturePtr returned null)");
                                        tex = null;
                                        texWrite = null;
                                    }
                                    else
                                    {
                                        Debug.LogError("Texture " + tex.name + " cannot be used as albedo (GetNativeTexturePtr returned null)");
                                        tex = null;
                                        texWrite = null;
                                    }
                                }
                            }
                            if (tex == null)
                            {
                                // Create dummy 1px texture
                                var dummyTex = new Texture2D(1,1);
                                dummyPixelArray[0] = (mat == null || !mat.HasProperty("_Color")) ? Color.white : mat.color;
                                dummyTex.SetPixels(dummyPixelArray);
                                dummyTex.Apply();
                                texWrite = dummyTex;
                                dummyTexList.Add(dummyTex);
                                texPtr = dummyTex.GetNativeTexturePtr();
                                if (texPtr == (IntPtr)0)
                                {
                                    Debug.LogError("Failed to call GetNativeTexturePtr() on newly created texture");
                                    texWrite = null;
                                }
                            }
                            if (!albedoMap.TryGetValue(texPtr, out texID))
                            {
                                lmAlbedoList.Add(texPtr);
                                lmAlbedoListTex.Add(texWrite);
                                albedoMap[texPtr] = albedoCounter;
                                texID = albedoCounter;
                                albedoCounter++;
                            }

                            // Write albedo ID
                            fmatid.Write((ushort)texID);

                            // Get mesh alphas
                            ushort alphaID = 0xFFFF;
                            int alphaChannel = 3; // A
                            if (mat != null && mat.HasProperty("_TransparencyLM")) // will override _MainTex.a if present
                            {
                                var tex2 = mat.GetTexture("_TransparencyLM");
                                if (tex2 != null)
                                {
                                    tex = tex2;
                                    texPtr = tex.GetNativeTexturePtr();
                                    alphaChannel = 0; // R
                                }
                            }
                            if (tex != null) {
                                var matTag = mat.GetTag("RenderType", true);
                                bool isCutout = matTag == "TransparentCutout";
                                if (isCutout || matTag == "Transparent" || matTag == "TreeLeaf") {

                                    float alphaRef = 0.5f;
                                    if (mat != null && mat.HasProperty("_Cutoff"))
                                    {
                                        alphaRef = mat.GetFloat("_Cutoff");
                                    }
                                    float opacity = 1.0f;
                                    if (!isCutout && mat.HasProperty("_Color"))
                                    {
                                        opacity = mat.color.a;
                                    }
                                    // let constant alpha affect cutout theshold for alphablend materials
                                    alphaRef = 1.0f - (1.0f - alphaRef) * opacity;
                                    if (alphaRef > 1) alphaRef = 1;

                                    // allow same map instances with different threshold
                                    List<int> texIDs;
                                    if (!alphaMap.TryGetValue(texPtr, out texIDs))
                                    {
                                        alphaMap[texPtr] = texIDs = new List<int>();

                                        lmAlphaList.Add(texPtr);
                                        lmAlphaListTex.Add(tex);
                                        lmAlphaRefList.Add(alphaRef);
                                        lmAlphaChannelList.Add(alphaChannel);

                                        texIDs.Add(alphaCounter);
                                        texID = alphaCounter;
                                        alphaCounter++;
                                        //Debug.Log("Alpha " + texID+": " + tex.name+" "+alphaRef);
                                        alphaID = (ushort)texID;
                                    }
                                    else
                                    {
                                        int matchingInstance = -1;
                                        for(int instance=0; instance<texIDs.Count; instance++)
                                        {
                                            texID = texIDs[instance];
                                            if (Mathf.Abs(lmAlphaRefList[texID] - alphaRef) <= alphaInstanceThreshold)
                                            {
                                                if (lmAlphaChannelList[texID] == alphaChannel)
                                                {
                                                    matchingInstance = instance;
                                                    alphaID = (ushort)texID;
                                                    break;
                                                }
                                            }
                                        }
                                        if (matchingInstance < 0)
                                        {
                                            lmAlphaList.Add(texPtr);
                                            lmAlphaListTex.Add(tex);
                                            lmAlphaRefList.Add(alphaRef);
                                            lmAlphaChannelList.Add(alphaChannel);

                                            texIDs.Add(alphaCounter);
                                            texID = alphaCounter;
                                            alphaCounter++;
                                            //Debug.Log("Alpha " + texID+": " + tex.name+" "+alphaRef);
                                            alphaID = (ushort)texID;
                                        }
                                    }
                                }
                            }
                            alphaIDs.Add(alphaID);

                            // Get mesh emissives
                            if (exportShaderColors)
                            {
                                for(int s=0; s<sceneCount; s++)
                                {
                                    if (storages[s] == null) continue;
                                    while(storages[s].hasEmissive.Count <= id) storages[s].hasEmissive.Add(true);
                                    storages[s].hasEmissive[id] = true;
                                }
                            }

                            texID = -1;
                            tex = null;
                            if (mat!=null && mat.shaderKeywords.Contains("_EMISSION"))
                            {
                                if (mat.HasProperty("_EmissionMap")) tex = mat.GetTexture("_EmissionMap");
                                if (tex != null)
                                {
                                    texPtr = tex.GetNativeTexturePtr();
                                    if (texPtr == (IntPtr)0)
                                    {
                                        if ((tex as RenderTexture) != null)
                                        {
                                            Debug.LogError("RenderTexture " + tex.name + " cannot be used as emission (GetNativeTexturePtr returned null)");
                                            tex = null;
                                        }
                                        else
                                        {
                                            Debug.LogError("Texture " + tex.name + " cannot be used as emission (GetNativeTexturePtr returned null)");
                                            tex = null;
                                        }
                                        //DebugLogError("Null emission tex ptr");
                                    }
                                }
                                if (tex == null && mat.HasProperty("_EmissionColor"))
                                {
                                    // Create dummy 1px texture
                                    var dummyTex = new Texture2D(1,1);
                                    dummyPixelArray[0] = mat.GetColor("_EmissionColor");
                                    dummyTex.SetPixels(dummyPixelArray);
                                    dummyTex.Apply();
                                    tex = dummyTex;
                                    dummyTexList.Add(dummyTex);
                                    texPtr = dummyTex.GetNativeTexturePtr();
                                    if (texPtr == (IntPtr)0)
                                    {
                                        Debug.LogError("Failed to call GetNativeTexturePtr() on newly created texture");
                                        texWrite = null;
                                        //DebugLogError("Null dummy tex ptr");
                                    }
                                }
                                if (!albedoMap.TryGetValue(texPtr, out texID))
                                {
                                    lmAlbedoList.Add(texPtr);
                                    lmAlbedoListTex.Add(tex);
                                    albedoMap[texPtr] = albedoCounter;
                                    texID = albedoCounter;
                                    albedoCounter++;
                                }
                                for(int s=0; s<sceneCount; s++)
                                {
                                    if (storages[s] == null) continue;
                                    while(storages[s].hasEmissive.Count <= id) storages[s].hasEmissive.Add(false);
                                    storages[s].hasEmissive[id] = true;
                                }

                                fmatide.Write((ushort)texID);
                                fmatideb.Write(mat.HasProperty("_EmissionColor") ? mat.GetColor("_EmissionColor").maxColorComponent : 1);
                            }
                            else
                            {
                                fmatide.Write((ushort)0xFFFF);
                                fmatideb.Write(0.0f);
                            }

                            if (isTerrain && uvgbHeightmap)
                            {
                                var hindex = terrainObjectList.IndexOf(obj.transform.parent.gameObject);
                                //var htex = terrainObjectToHeightMap[hindex];
                                //texPtr = htex.GetNativeTexturePtr();

                                //heightmapList.Add(texPtr);
                                //heightmapListTex.Add(htex);
                                //heightmapListBounds.Add();

                                //texID = heightmapCounter;
                                //heightmapCounter++;

                                fmatidh.Write((ushort)hindex);//texID);
                            }
                            else
                            {
                                fmatidh.Write((ushort)0xFFFF);
                            }
                        }

                        // Update LMGroup bounds
                        if (modifyLightmapStorage)
                        {
                            if (lmBounds[id].size == Vector3.zero) {
                                lmBounds[id] = mr.bounds;
                            } else {
                                var b = lmBounds[id];
                                b.Encapsulate(mr.bounds);
                                lmBounds[id] = b;
                            }
                        }

                    } else {
                        // Write empty albedo/alpha IDs for non-lightmapped
                        for(int k=0; k<m.subMeshCount; k++) {
                            fmatid.Write((ushort)0);
                            alphaIDs.Add(0xFFFF);
                            fmatide.Write((ushort)0xFFFF);
                            fmatideb.Write(0.0f);
                            fmatidh.Write((ushort)0xFFFF);
                        }
                    }

                    int currentVoffset = voffset;
                    voffset += objsToWriteVerticesPosW[i].Length;// m.vertexCount;

                    // Check if mesh is flipped
                    bool isFlipped = Mathf.Sign(obj.transform.lossyScale.x*obj.transform.lossyScale.y*obj.transform.lossyScale.z) < 0;
                    if (lmgroup != null && lmgroup.flipNormal) isFlipped = !isFlipped;

                    while(lmIndexArrays.Count <= id)
                    {
                        lmIndexArrays.Add(new List<int>());
                        lmLocalToGlobalIndices.Add(new List<int>());
                        lmVOffset.Add(0);
                    }

                    var mmr = obj.GetComponent<Renderer>();
                    var castsShadows = mmr.shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.Off;
                    if (exportTerrainAsHeightmap && obj.name == "__ExportTerrain") castsShadows = false; // prevent exporting placeholder quads to ftrace

                    time = GetTime();
                    for(int k=0;k<m.subMeshCount;k++) {
                        // Export regular index buffer
                        //var indexCount = exportIB(fib, m, k, isFlipped, false, 0, null, 0);
                        var indexCount = exportIB(fib, inds[k], isFlipped, false, 0, null, 0);

                        bool submeshCastsShadows = castsShadows;
                        if (submeshCastsShadows)
                        {
                            var mats = mmr.sharedMaterials;
                            if (mats.Length > k)
                            {
                                if (mats[k] != null)
                                {
                                    var matTag = mats[k].GetTag("RenderType", true);
                                    if (matTag == "Transparent" || matTag == "TreeLeaf")
                                    {
                                        if (mats[k].HasProperty("_Color"))
                                        {
                                            if (mats[k].color.a < 0.5f) submeshCastsShadows = false;
                                        }
                                    }
                                }
                            }
                        }

                        // Generate tracing index buffer, write alpha IDs per triangle
                        if (submeshCastsShadows)
                        {
                            var alphaID = alphaIDs[(alphaIDs.Count - m.subMeshCount) + k];

                            if (lodLevel < 0)
                            {
                                // Export persistent IB
                                var indicesOpaqueArray = indicesOpaque;
                                var indicesTransparentArray = indicesTransparent;
                                var falphaidFile = falphaid;
                                exportIB32(indicesOpaqueArray, indicesTransparentArray, id>=0 ? lmIndexArrays[id] : null,
                                             inds[k], isFlipped, currentVoffset, id>=0 ? lmVOffset[id] : 0, falphaidFile, alphaID);
                            }
                            else
                            {
                                // Export LOD IBs
                                var visList = objToLodLevelVisible[obj];
                                for(int vlod=0; vlod<visList.Count; vlod++)
                                {
                                    int lod = visList[vlod];
                                    var indicesOpaqueArray = data.indicesOpaqueLOD[lod];
                                    var indicesTransparentArray = data.indicesTransparentLOD[lod];
                                    var falphaidFile = falphaidlod[lod];
                                    exportIB32(indicesOpaqueArray, indicesTransparentArray, id>=0 ? lmIndexArrays[id] : null,
                                                 inds[k], isFlipped, currentVoffset, id>=0 ? lmVOffset[id] : 0, falphaidFile, alphaID);
                                }
                            }
                        }
                        ioffset += indexCount;
                    }
                    ibTime += GetTime() - time;

                    if (id >= 0)
                    {
                        var vcount = objsToWriteVerticesPosW[i].Length;//m.vertexCount;
                        var remapArray = lmLocalToGlobalIndices[id];
                        var addition = lmVOffset[id];
                        for(int k=0; k<vcount; k++)
                        {
                            remapArray.Add(k + currentVoffset);
                        }
                        lmVOffset[id] += vcount;
                    }
                }

        }
        catch(Exception e)
        {
            DebugLogError("Error exporting scene - see console for details");
            CloseAllFiles();
            userCanceled = true;
            ProgressBarEnd(true);
            Debug.LogError("Exception caught: " + e.ToString());
            throw;
        }

        ProgressBarShow("Exporting scene - finishing objects...", 0.5f);
        if (userCanceled)
        {
            CloseAllFiles();
            ProgressBarEnd(true);
            yield break;
        }
        yield return null;

        try
        {
                // Write vertex buffers and update storage
                Rect rc = new Rect();
                var emptyVec4 = new Vector4(1,1,0,0);
                for(int i=0; i<objsToWrite.Count; i++)
                {
                    var obj = objsToWrite[i];
                    var m = GetSharedMesh(obj);
                    var lmgroup = objsToWriteGroup[i];

                    var id = lmgroup == null ? -1 : objsToWriteGroup[i].id;

                    BakeryLightMesh areaLight = obj.GetComponent<BakeryLightMesh>();
                    if (areaLight == null)
                    {
                        var areaIndex = temporaryAreaLightMeshList.IndexOf(obj);
                        if (areaIndex >= 0) areaLight = temporaryAreaLightMeshList2[areaIndex];
                    }
                    //var areaLight =
                    if (areaLight != null) id = areaLight.lmid;

                    var vertexBake = lmgroup != null ? (lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex) : false;
                    //var castsShadows = obj.GetComponent<Renderer>().shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.Off;

                    var holderObj = objsToWriteHolder[i];
                    if (holderObj != null)
                    {
                        if (!holderRect.TryGetValue(holderObj, out rc))
                        {
                            holderObj = null;
                        }
                    }

                    time = GetTime();
                    //var vertices = m.vertices;
                    //var normals = m.normals;
                    //var tangents = m.tangents;
                    var uv = objsToWriteVerticesUV[i];//m.uv;
                    var uv2 = objsToWriteVerticesUV2[i];//m.uv2;
                    if (uv2.Length == 0 && !vertexBake) uv2 = uv;//m.uv;
                    vbTimeRead += GetTime() - time;

                    var inds = objsToWriteIndices[i];

                        var time2 = GetTime();
                        time = time2;

                    // Transform UVs
                    var tformedPos = objsToWriteVerticesPosW[i];// new Vector3[vertices.Length];
                    var tformedNormals = objsToWriteVerticesNormalW[i];// new Vector3[normals.Length];
                    Vector4[] tformedTangents = null;
                    if (NeedsTangents(lmgroup, tangentSHLights))
                    {
                        tformedTangents = objsToWriteVerticesTangentW[i];
                    }
                    Vector2[] tformedUV2;
                    if (areaLight == null && !vertexBake)
                    {
                        tformedUV2 = holderObj == null ? uv2 : new Vector2[tformedPos.Length];
                        for(int t=0; t<tformedPos.Length; t++)
                        {
                            if (holderObj != null)
                            {
                                tformedUV2[t].x = uv2[t].x * rc.width + rc.x;
                                tformedUV2[t].y = uv2[t].y * rc.height + rc.y;
                            }
                        }
                        objsToWriteUVOverride.Add(null);
                    }
                    else if (vertexBake)
                    {
                        tformedUV2 = GenerateVertexBakeUVs(lmgroup.vertexCounter, tformedPos.Length, lmgroup.totalVertexCount);
                        lmgroup.vertexCounter += tformedPos.Length;
                        objsToWriteUVOverride.Add(tformedUV2);
                    }
                    else
                    {
                        tformedUV2 = uv;
                        objsToWriteUVOverride.Add(null);
                    }

                    if (id >= 0)
                    {
                        while(lmUVArrays.Count <= id)
                        {
                            lmUVArrays.Add(new List<float>());
                        }
                        var lmUVArray = lmUVArrays[id];
                        for(int k=0; k<tformedUV2.Length; k++)
                        {
                            lmUVArray.Add(tformedUV2[k].x);
                            lmUVArray.Add(tformedUV2[k].y);
                        }
                    }

                    exportVBFull(fvbfull, tformedPos, tformedNormals, tformedTangents, uv, tformedUV2);
                        vbTimeWriteFull += GetTime() - time;
                        time = GetTime();
                    //if (castsShadows)
                    //{
                        exportVBTrace(fvbtrace, m, tformedPos, tformedNormals);
                            vbTimeWriteT += GetTime() - time;
                            time = GetTime();
                        exportVBTraceTexAttribs(vbtraceTexPosNormalArray, vbtraceTexUVArray, tformedPos, tformedNormals, tformedUV2, id, vertexBake, obj);
                            vbTimeWriteT2 += GetTime() - time;
                            time = GetTime();
                        exportVBTraceUV0(fvbtraceUV0, uv, tformedPos.Length);
                            vbTimeWriteT3 += GetTime() - time;
                            time = GetTime();
                    //}
                    voffset += tformedPos.Length;
                    vbTimeWrite += GetTime() - time2;


                    // update storage
                    // also write seamfix.bin
                    var sceneID = sceneToID[obj.scene];
                    if (obj.name == "__ExportTerrain")
                    {
                        fseamfix.Write(false);
                        var index = terrainObjectList.IndexOf(obj.transform.parent.gameObject);
                        var terrain = terrainObjectToActual[index];
                        var scaleOffset = holderObj == null ? emptyVec4 : new Vector4(rc.width, rc.height, rc.x, rc.y);
                        if (!storages[sceneID].bakedRenderersTerrain.Contains(terrain))
                        {
                            if (modifyLightmapStorage)
                            {
                                storages[sceneID].bakedRenderersTerrain.Add(terrain);
                                storages[sceneID].bakedIDsTerrain.Add(CorrectLMGroupID(id, lmgroup, groupList));
                                storages[sceneID].bakedScaleOffsetTerrain.Add(scaleOffset);
                            }
                        }
                        objsToWriteScaleOffset.Add(scaleOffset);
                    }
                    else
                    {
                        fseamfix.Write(true);
                        var scaleOffset = holderObj == null ? emptyVec4 : new Vector4(rc.width, rc.height, rc.x, rc.y);
                        if (modifyLightmapStorage)
                        {
                            bool vertexImplicit = false;
                            if (vertexBake)
                            {
                                if (lmgroup.isImplicit) vertexImplicit = true;
                            }
                            if (!vertexImplicit)
                            {
                                storages[sceneID].bakedRenderers.Add(obj.GetComponent<Renderer>());
                                storages[sceneID].bakedIDs.Add(CorrectLMGroupID(id, lmgroup, groupList));
                                storages[sceneID].bakedScaleOffset.Add(scaleOffset);
                                storages[sceneID].bakedVertexOffset.Add(vertexBake ? (lmgroup.vertexCounter - tformedPos.Length) : -1);
                                storages[sceneID].bakedVertexColorMesh.Add(null);
                            }
                        }
                        objsToWriteScaleOffset.Add(scaleOffset);
                    }
                }

                // Generate LOD UVs
                if (ftRenderLightmap.giLodModeEnabled)
                {
                    for(int s=0; s<sceneCount; s++)
                    {
                        if (storages[s] == null) continue;
                        storages[s].lmGroupMinLOD = new int[groupList.Count];
                        storages[s].lmGroupLODMatrix = new int[groupList.Count * groupList.Count];
                    }
                    for(int i=0; i<groupList.Count; i++)
                    {
                        var lmgroup = groupList[i];
                        if (lmgroup.resolution < 128)
                        {
                            lmUVArrays2.Add(null);
                            lmIndexArrays2.Add(null);
                            lmUVArrays3.Add(null);
                            continue;
                        }
                        if (lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex || lmgroup.containsTerrains)
                        {
                            lmUVArrays2.Add(null);
                            lmIndexArrays2.Add(null);
                            lmUVArrays3.Add(null);
                            if (lmgroup.containsTerrains)
                            {
                                int minLodResolutionTerrain = 128;
                                for(int s=0; s<sceneCount; s++)
                                {
                                    if (storages[s] == null) continue;
                                    int minLOD = (int)(Mathf.Log(lmgroup.resolution, 2.0f) - Mathf.Log(minLodResolutionTerrain, 2.0f)) - 1;
                                    if (minLOD < 0) minLOD = 0;
                                    storages[s].lmGroupMinLOD[lmgroup.id] = minLOD;
                                }
                            }
                            continue;
                        }
                        int id = lmgroup.id;
                        lmUVArrays2.Add(lmUVArrays[i].ToArray());
                        lmIndexArrays2.Add(lmIndexArrays[i].ToArray());

                        lmUVArrays3.Add(lmUVArrays[i].ToArray());
                        int uvIslands = uvrLoad(lmUVArrays2[i], lmUVArrays2[i].Length/2, lmIndexArrays2[i], lmIndexArrays2[i].Length);
                        if (uvIslands <= 0)
                        {
                            Debug.LogError("Can't generate LOD UVs for  " + lmgroup.name+" "+lmUVArrays2[i].Length+" "+lmIndexArrays2[i].Length+" "+lmgroup.containsTerrains);
                            uvrUnload();
                            continue;
                        }
                        int minLodResolution = Mathf.NextPowerOfTwo((int)Mathf.Ceil(Mathf.Sqrt((float)uvIslands)));
                        minLodResolution = minLodResolution << 1;
                        if (minLodResolution > lmgroup.resolution)
                        {
                            Debug.LogWarning("Not generating LOD UVs for " + lmgroup.name + ", because there are too many UV islands");
                            uvrUnload();
                            continue;
                        }
                        Debug.Log("Min LOD resolution for " + lmgroup.name + " is " + minLodResolution);
                        for(int s=0; s<sceneCount; s++)
                        {
                            if (storages[s] == null) continue;
                            int minLOD = (int)(Mathf.Log(lmgroup.resolution, 2.0f) - Mathf.Log(minLodResolution, 2.0f)) - 1;
                            if (minLOD < 0) minLOD = 0;
                            storages[s].lmGroupMinLOD[lmgroup.id] = minLOD;
                        }

                        int uvrErrCode = uvrRepack(0, minLodResolution);
                        if (uvrErrCode == -1)
                        {
                            Debug.LogError("Can't repack LOD UVs for " + lmgroup.name);
                            uvrUnload();
                            continue;
                        }
                        Debug.Log("Tries left: " + uvrErrCode);
                        uvrUnload();
                        var numLocalVerts = lmUVArrays2[i].Length / 2;
                        for(int k=0; k<numLocalVerts; k++)
                        {
                            float u = lmUVArrays2[i][k * 2];
                            u = Mathf.Clamp(u, 0, 0.99999f);
                            u += id * 10;
                            if (i >= 0 && (int)u > id*10)
                            {
                                Debug.LogError("Float overflow (GI LOD)");
                            }
                            lmUVArrays2[i][k * 2] = u;
                        }
                    }
                }

                // Write vbTraceTex
                int numTraceVerts = vbtraceTexUVArray.Count/2;
                for(int i=0; i<numTraceVerts; i++)
                {
                    fvbtraceTex.Write(vbtraceTexPosNormalArray[i * 6]);
                    fvbtraceTex.Write(vbtraceTexPosNormalArray[i * 6 + 1]);
                    fvbtraceTex.Write(vbtraceTexPosNormalArray[i * 6 + 2]);
                    fvbtraceTex.Write(vbtraceTexPosNormalArray[i * 6 + 3]);
                    fvbtraceTex.Write(vbtraceTexPosNormalArray[i * 6 + 4]);
                    fvbtraceTex.Write(vbtraceTexPosNormalArray[i * 6 + 5]);

                    fvbtraceTex.Write(vbtraceTexUVArray[i * 2]);
                    fvbtraceTex.Write(vbtraceTexUVArray[i * 2 + 1]);
                }

                // Generate LOD UV buffer
                if (ftRenderLightmap.giLodModeEnabled)
                {
                    var uvBuffOffsets = new int[lmUVArrays3.Count];
                    var uvBuffLengths = new int[lmUVArrays3.Count];
                    int uvBuffSize = 0;
                    for(int i=0; i< lmUVArrays3.Count; i++)
                    {
                        if (lmUVArrays3[i] == null) continue;
                        uvBuffOffsets[i] = uvBuffSize;
                        uvBuffLengths[i] = lmUVArrays3[i].Length;
                        uvBuffSize += lmUVArrays3[i].Length;
                    }
                    var uvSrcBuff = new float[uvBuffSize];
                    var uvDestBuff = new float[uvBuffSize];
                    for(int i=0; i< lmUVArrays3.Count; i++)
                    {
                        if (lmUVArrays3[i] == null) continue;
                        var arr = lmUVArrays3[i];
                        var arr2 = lmUVArrays2[i];
                        var offset = uvBuffOffsets[i];
                        for(int j=0; j<arr.Length; j++)
                        {
                            uvSrcBuff[j + offset] = arr[j];
                            uvDestBuff[j + offset] = arr2[j];
                        }
                    }
                    var lmrIndicesOffsets = new int[lmIndexArrays2.Count];
                    var lmrIndicesLengths = new int[lmIndexArrays2.Count];
                    int lmrIndicesSize = 0;
                    for(int i=0; i< lmIndexArrays2.Count; i++)
                    {
                        if (lmIndexArrays2[i] == null) continue;
                        lmrIndicesOffsets[i] = lmrIndicesSize;
                        lmrIndicesLengths[i] = lmIndexArrays2[i].Length;
                        lmrIndicesSize += lmIndexArrays2[i].Length;
                    }
                    var lmrIndicesBuff = new int[lmrIndicesSize];
                    for(int i=0; i< lmIndexArrays2.Count; i++)
                    {
                        if (lmIndexArrays2[i] == null) continue;
                        var arr = lmIndexArrays2[i];
                        var offset = lmrIndicesOffsets[i];
                        for(int j=0; j<arr.Length; j++)
                        {
                            lmrIndicesBuff[j + offset] = arr[j];
                        }
                    }

                    for(int s=0; s<sceneCount; s++)
                    {
                        if (storages[s] == null) continue;
                        storages[s].uvBuffOffsets = uvBuffOffsets;
                        storages[s].uvBuffLengths = uvBuffLengths;
                        storages[s].uvSrcBuff = uvSrcBuff;
                        storages[s].uvDestBuff = uvDestBuff;
                        storages[s].lmrIndicesOffsets = lmrIndicesOffsets;
                        storages[s].lmrIndicesLengths = lmrIndicesLengths;
                        storages[s].lmrIndicesBuff = lmrIndicesBuff;

                    }
                    vbtraceTexUVArrayLOD = new float[vbtraceTexUVArray.Count];
                    for(int i=0; i<groupList.Count; i++)
                    {
                        var lmgroup = groupList[i];
                        if (lmgroup.resolution < 128) continue;
                        if (lmgroup.mode == BakeryLightmapGroup.ftLMGroupMode.Vertex || lmgroup.containsTerrains) continue;
                        var remapArray = lmLocalToGlobalIndices[i];
                        var uvArray = lmUVArrays2[i];
                        for(int j=0; j<remapArray.Count; j++)
                        {
                            vbtraceTexUVArrayLOD[remapArray[j]*2] = uvArray[j*2];
                            vbtraceTexUVArrayLOD[remapArray[j]*2+1] = uvArray[j*2+1];
                        }
                    }
                }

                // Write tracing index buffer
                fib32.Write(indicesOpaque.Count); // firstAlphaTriangle
                for(int i=0; i<indicesOpaque.Count; i++) fib32.Write(indicesOpaque[i]); // opaque triangles
                for(int i=0; i<indicesTransparent.Count; i++) fib32.Write(indicesTransparent[i]); // alpha triangles

                // Write scene LOD tracing index buffers
                for(int lod=0; lod<sceneLodsUsed; lod++)
                {
                    var indicesOpaqueArray = data.indicesOpaqueLOD[lod];
                    var indicesTransparentArray = data.indicesTransparentLOD[lod];
                    fib32lod[lod].Write(indicesOpaqueArray.Count);
                    for(int i=0; i<indicesOpaqueArray.Count; i++) fib32lod[lod].Write(indicesOpaqueArray[i]); // opaque triangles
                    for(int i=0; i<indicesTransparentArray.Count; i++) fib32lod[lod].Write(indicesTransparentArray[i]); // alpha triangles
                }

                Debug.Log("Wrote binaries in " + ((GetTime() - totalTime)/1000.0) + "s");
                Debug.Log("VB read time " + (vbTimeRead/1000.0) + "s");
                Debug.Log("VB write time " + (vbTimeWrite/1000.0) + "s");
                Debug.Log("VB write time (full) " + (vbTimeWriteFull/1000.0) + "s");
                Debug.Log("VB write time (trace) " + (vbTimeWriteT/1000.0) + "s");
                Debug.Log("VB write time (trace tex) " + (vbTimeWriteT2/1000.0) + "s");
                Debug.Log("VB write time (UV0) " + (vbTimeWriteT3/1000.0) + "s");
                Debug.Log("IB time " + (ibTime/1000.0) + "s");


                fscene.Write(objsToWrite.Count);
                int meshID = 0;
                foreach(var obj in objsToWrite) {
                    fscene.Write(meshID);
                    meshID++;
                }
                foreach(var obj in objsToWrite) {
                    fscene.Write(obj.name);
                }

                fscene.Close();
                fmesh.Close();
                flmid.Close();
                fseamfix.Close();
                fsurf.Close();
                fmatid.Close();
                fmatide.Close();
                fmatideb.Close();
                fmatidh.Close();
                fvbfull.Close();
                fvbtrace.Close();
                fvbtraceTex.Close();
                fvbtraceUV0.Close();
                fib.Close();
                fib32.Close();
                falphaid.Close();
                fhmaps.Close();

                if (fib32lod != null)
                {
                    for(int i=0; i<fib32lod.Length; i++) fib32lod[i].Close();
                }
                if (falphaidlod != null)
                {
                    for(int i=0; i<falphaidlod.Length; i++) falphaidlod[i].Close();
                }

                if (modifyLightmapStorage)
                {
                    for(int s=0; s<sceneCount; s++)
                    {
                        if (storages[s] == null) continue;
                        storages[s].bounds = lmBounds;
                    }
                }
            //}

            startMsU = GetTime();
        }
        catch(Exception e)
        {
            DebugLogError("Error exporting scene - see console for details");
            CloseAllFiles();
            userCanceled = true;
            ProgressBarEnd(true);
            Debug.LogError("Exception caught: " + e.ToString());
            throw;
        }

        if (exportShaderColors && renderTextures)
        {
            yield return null;
            ProgressBarShow("Exporting scene - shaded surface colors...", 0.55f);
            for(int g=0; g<groupList.Count; g++)
            {
                var str = storages[data.firstNonNullStorage];
                if (str == null) Debug.LogError("storages[data.firstNonNullStorage] == null");
                if (str.hasEmissive == null) Debug.LogError("storages[data.firstNonNullStorage].hasEmissive == null");
                if (groupList[g] == null) Debug.LogError("group is null");

                var hasEmissive = str.hasEmissive.Count > groupList[g].id && str.hasEmissive[groupList[g].id];

                bool vertexBake = groupList[g].mode == BakeryLightmapGroup.ftLMGroupMode.Vertex;

                int res = groupList[g].resolution;
                if (vertexBake)
                {
                    if (groupList[g].totalVertexCount == 0)
                    {
                        DebugLogError("Vertex lightmap group " + groupList[g].name + " has 0 static vertices. Make sure objects inside the group don't all have Scale In Lightmap == 0.");
                        CloseAllFiles();
                        userCanceled = true;
                        ProgressBarEnd(true);
                        yield break;
                    }
                    int atlasTexSize = (int)Mathf.Ceil(Mathf.Sqrt((float)groupList[g].totalVertexCount));
                    atlasTexSize = (int)Mathf.Ceil(atlasTexSize / (float)ftRenderLightmap.tileSize) * ftRenderLightmap.tileSize;
                    res = atlasTexSize;
                }

                var bakeWithNormalMaps = (groupList[g].renderDirMode == BakeryLightmapGroup.RenderDirMode.BakedNormalMaps) ?
                    true : (ftRenderLightmap.renderDirMode == ftRenderLightmap.RenderDirMode.BakedNormalMaps);

                if (groupList[g].probes) bakeWithNormalMaps = false;

                ftUVGBufferGen.StartUVGBuffer(res, hasEmissive, bakeWithNormalMaps);
                for(int i=0; i<objsToWrite.Count; i++)
                {
                    var obj = objsToWrite[i];
                    var lmgroup = objsToWriteGroup[i];
                    if (lmgroup == null) continue;
                    if (lmgroup.id != groupList[g].id) continue;
                    if (obj.GetComponent<BakeryLightMesh>()) continue;
                    var bakedMesh = GetSharedMeshBaked(obj);
                    ftUVGBufferGen.RenderUVGBuffer(bakedMesh,
                        obj.GetComponent<Renderer>(),
                        objsToWriteScaleOffset[i],
                        obj.transform.localToWorldMatrix,
                        vertexBake,
                        objsToWriteUVOverride[i],
                        bakeWithNormalMaps && !exportTerrainAsHeightmap && obj.name == "__ExportTerrain");
                }
                ftUVGBufferGen.EndUVGBuffer();

                var albedo = ftUVGBufferGen.texAlbedo;
                var emissive = ftUVGBufferGen.texEmissive;
                var normal = ftUVGBufferGen.texNormal;
                if (hasEmissive)
                {
                    //albedo = ftUVGBufferGen.GetAlbedoWithoutEmissive(ftUVGBufferGen.texAlbedo, ftUVGBufferGen.texEmissive);
                    //if ((unityVersionMajor == 2017 && unityVersionMinor < 2) || unityVersionMajor < 2017)
                    //{
#if UNITY_2017_2_OR_NEWER
#else
                        // Unity before 2017.2: emissive packed to RGBM
                        // Unity after 2017.2: linear emissive
                        emissive = ftUVGBufferGen.DecodeFromRGBM(emissive);
#endif
                    //}
                    if (ftRenderLightmap.hackEmissiveBoost != 1.0f)
                    {
                        ftUVGBufferGen.Multiply(emissive, ftRenderLightmap.hackEmissiveBoost);
                    }
                    if (!vertexBake) ftUVGBufferGen.Dilate(emissive);
                }
                if (!vertexBake) ftUVGBufferGen.Dilate(albedo);

                SaveGBufferMap(albedo.GetNativeTexturePtr(),
                    scenePath + "/uvalbedo_" + groupList[g].name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"),
                    ftRenderLightmap.compressedGBuffer);
                GL.IssuePluginEvent(5);
                //if (g==2) storage.debugTex = emissive;
                yield return null;

                if (hasEmissive)
                {
                    SaveGBufferMap(emissive.GetNativeTexturePtr(),
                        scenePath + "/uvemissive_" + groupList[g].name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"),
                        ftRenderLightmap.compressedGBuffer);
                    GL.IssuePluginEvent(5);
                    yield return null;
                    if (ftRenderLightmap.clientMode) ftClient.serverFileList.Add("uvemissive_" + groupList[g].name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"));
                }

                if (bakeWithNormalMaps)
                {
                    SaveGBufferMap(normal.GetNativeTexturePtr(),
                        scenePath + "/uvnormal_" + groupList[g].name + (ftRenderLightmap.compressedGBuffer ? ".lz4" : ".dds"),
                        ftRenderLightmap.compressedGBuffer);
                    GL.IssuePluginEvent(5);
                    yield return null;
                }
            }
        }

        ProgressBarShow(exportShaderColors ? "Exporting scene - alpha buffer..." : "Exporting scene - UV GBuffer and alpha buffer...", 0.55f);
        if (userCanceled)
        {
            CloseAllFiles();
            userCanceled = true;
            ProgressBarEnd(true);
            yield break;
        }
        yield return null;

        InitShaders();
        LoadScene(scenePath);

        // Force load textures to VRAM
        var forceRt = new RenderTexture(1, 1, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
        var forceTex = new Texture2D(1, 1, TextureFormat.ARGB32, false, false);
        if (!exportShaderColors)
        {
            for(int i=0; i<lmAlbedoListTex.Count; i++)
            {
                Graphics.Blit(lmAlbedoListTex[i] as Texture2D, forceRt);
                Graphics.SetRenderTarget(forceRt);
                forceTex.ReadPixels(new Rect(0,0,1, 1), 0, 0, true);
                forceTex.Apply();
                lmAlbedoList[i] = lmAlbedoListTex[i].GetNativeTexturePtr();
            }
        }
        for(int i=0; i<lmAlphaListTex.Count; i++)
        {
            Graphics.Blit(lmAlphaListTex[i] as Texture2D, forceRt);
            Graphics.SetRenderTarget(forceRt);
            forceTex.ReadPixels(new Rect(0,0,1, 1), 0, 0, true);
            forceTex.Apply();
            lmAlphaList[i] = lmAlphaListTex[i].GetNativeTexturePtr();
        }

        if (exportShaderColors)
        {
            if (terrainObjectToActual.Count > 0)
            {
                //for(int i=0; i<heightmapListTex.Count; i++)
                terrainObjectToHeightMapPtr = new IntPtr[terrainObjectToHeightMap.Count];
                for(int i=0; i<terrainObjectToHeightMap.Count; i++)
                {
                    Graphics.Blit(terrainObjectToHeightMap[i] as Texture2D, forceRt);
                    Graphics.SetRenderTarget(forceRt);
                    forceTex.ReadPixels(new Rect(0,0,1, 1), 0, 0, true);
                    forceTex.Apply();
                    terrainObjectToHeightMapPtr[i] = terrainObjectToHeightMap[i].GetNativeTexturePtr();
                }
                SetAlbedos(terrainObjectToHeightMap.Count, terrainObjectToHeightMapPtr);
                int cerr = CopyAlbedos();
                if (cerr != 0)
                {
                    DebugLogError("Failed to copy textures (" + cerr + ")");
                    CloseAllFiles();
                    userCanceled = true;
                    ProgressBarEnd(true);
                    yield break;
                }
            }
            else
            {
                SetAlbedos(0, null);
            }
        }
        else
        {
            SetAlbedos(lmAlbedoList.Count, lmAlbedoList.ToArray());
        }

        SetAlphas(lmAlphaList.Count, lmAlphaList.ToArray(), lmAlphaRefList.ToArray(), lmAlphaChannelList.ToArray(), sceneLodsUsed, flipAlpha);

        GL.IssuePluginEvent(6); // render alpha buffer
        int uerr = 0;
        while(uerr == 0)
        {
            uerr = GetABGErrorCode();
            yield return null;
        }

        /*yield return new WaitForEndOfFrame();
        int uerr = ftGenerateAlphaBuffer();*/
        if (uerr != 0 && uerr != 99999)
        {
            DebugLogError("ftGenerateAlphaBuffer error: " + uerr);
            CloseAllFiles();
            userCanceled = true;
            ProgressBarEnd(true);
            yield break;
        }

        if (!renderTextures)
        {
            ProgressBarEnd(true);//false);
            yield break;
        }

        //ProgressBarShow("Exporting scene - UV GBuffer...", 0.8f);
        //if (userCanceled) yield break;
        //yield return null;

        //GL.IssuePluginEvent(1); // render UV GBuffer
        //yield return new WaitForEndOfFrame();

        SetFixPos(false);//true); // do it manually
        SetCompression(ftRenderLightmap.compressedGBuffer);

        if (!exportShaderColors)
        {
            uerr = ftRenderUVGBuffer();
            if (uerr != 0)
            {
                DebugLogError("ftRenderUVGBuffer error: " + uerr);
                CloseAllFiles();
                userCanceled = true;
                ProgressBarEnd(true);
                yield break;
            }
        }

        ms = GetTime();
        Debug.Log("UVGB/fixPos/alpha time: " + ((ms - startMsU) / 1000.0) + " seconds");

        ProgressBarEnd(true);

        Debug.Log("Scene export finished");
    }

    int countChildrenFlat(Transform tform)
    {
        int count = 1;
        foreach(Transform t in tform)
        {
            count += countChildrenFlat(t);
        }
        return count;
    }
}

#endif