private TextureFormat ParseTextureFormat(SupportedAlphaTextureFormats format)
        {
            switch (format)
            {
            default:
            case SupportedAlphaTextureFormats.RGBA32:
                return(TextureFormat.RGBA32);

            case SupportedAlphaTextureFormats.ARGB32:
                return(TextureFormat.ARGB32);

            case SupportedAlphaTextureFormats.ARGB444:
                return(TextureFormat.ARGB4444);

            case SupportedAlphaTextureFormats.RGBA444:
                return(TextureFormat.RGBA4444);
            }
        }
Пример #2
0
        /// <summary>
        /// TEMP: Creates super-atlas populated with all archives in array.
        /// Currently does not support animated textures, normal map, or emission map.
        /// TODO: Integrate this feature fully with material system.
        /// </summary>
        /// <param name="archives">Archive array.</param>
        /// <param name="borderSize">Number of pixels border to add around image.</param>
        /// <param name="dilate">Blend texture into surrounding empty pixels. Requires border.</param>
        /// <param name="maxAtlasSize">Maximum atlas size.</param>
        /// <param name="alphaTextureFormat">Alpha TextureFormat.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <returns>TextureAtlasBuilder.</returns>
        public TextureAtlasBuilder CreateTextureAtlasBuilder(
            int[] archives,
            int borderSize   = 0,
            bool dilate      = false,
            int maxAtlasSize = 2048,
            SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32,
            SupportedNonAlphaTextureFormats nonAlphaFormat  = SupportedNonAlphaTextureFormats.RGB24)
        {
            // Iterate archives
            TextureFile         textureFile = new TextureFile();
            TextureAtlasBuilder builder     = new TextureAtlasBuilder();
            GetTextureSettings  settings    = TextureReader.CreateTextureSettings(0, 0, 0, 0, borderSize, dilate, maxAtlasSize);

            settings.stayReadable = true;
            for (int i = 0; i < archives.Length; i++)
            {
                // Load texture file
                settings.archive = archives[i];
                textureFile.Load(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true);

                // Add all records for this archive - single frame only
                for (int record = 0; record < textureFile.RecordCount; record++)
                {
                    settings.record = record;
                    GetTextureResults results = GetTexture2D(settings, alphaTextureFormat, nonAlphaFormat);
                    DFSize            size    = textureFile.GetSize(record);
                    DFSize            scale   = textureFile.GetScale(record);
                    builder.AddTextureItem(
                        results.albedoMap,
                        settings.archive,
                        settings.record,
                        0, 1,
                        new Vector2(size.Width, size.Height),
                        new Vector2(scale.Width, scale.Height));
                }
            }

            // Apply the builder
            builder.Rebuild(borderSize);

            return(builder);
        }
Пример #3
0
        /// <summary>
        /// Gets terrain normal map texture array containing each terrain tile in a seperate array slice.
        /// </summary>
        /// <param name="archive">Archive index.</param>
        /// <param name="stayReadable">Texture should stay readable.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <returns>Texture2DArray or null</returns>
        public Texture2DArray GetTerrainNormalMapTextureArray(
            int archive,
            bool stayReadable = false,
            SupportedAlphaTextureFormats alphaFormat = SupportedAlphaTextureFormats.RGBA32)
        {
            // Load texture file and check count matches terrain tiles
            TextureFile textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(archive)), FileUsage.UseMemory, true);
            int         numSlices   = 0;

            if (textureFile.RecordCount == 56)
            {
                numSlices = textureFile.RecordCount;
            }
            else
            {
                return(null);
            }

            Texture2DArray textureArray;
            int            width;
            int            height;

            // try to import first replacement texture for tile archive to determine width and height of replacement texture set (must be the same for all replacement textures for Texture2DArray)
            if (TextureReplacement.CustomNormalExist(archive, 0, 0))
            {
                Texture2D normalMap = TextureReplacement.LoadCustomNormal(archive, 0, 0);
                width  = normalMap.width;
                height = normalMap.height;
            }
            else
            {
                return(null);
            }

            textureArray = new Texture2DArray(width, height, numSlices, TextureFormat.ARGB32, MipMaps);

            // Rollout tiles into texture array
            for (int record = 0; record < textureFile.RecordCount; record++)
            {
                Texture2D normalMap;
                // Import custom texture(s)
                if (TextureReplacement.CustomNormalExist(archive, record, 0))
                {
                    normalMap = TextureReplacement.LoadCustomNormal(archive, record, 0);
                }
                else // if current texture does not exist
                {
                    Debug.LogErrorFormat("Terrain: imported archive {0} does not contain normal for record {1}.", archive, record);
                    return(null);
                }

                // enforce that all custom normal map textures have the same dimension (requirement of Texture2DArray)
                if ((normalMap.width != width) || (normalMap.height != height))
                {
                    Debug.LogErrorFormat("Terrain: failed to inject normal maps for archive {0}, incorrect size at record {1}.", archive, record);
                    return(null);
                }

                // Insert into texture array
                textureArray.SetPixels32(normalMap.GetPixels32(), record, 0);
            }
            textureArray.Apply(true, !stayReadable);

            // Change settings for these textures
            textureArray.wrapMode   = TextureWrapMode.Clamp;
            textureArray.anisoLevel = 8;

            return(textureArray);
        }
Пример #4
0
        /// <summary>
        /// Gets Texture2D atlas from Daggerfall texture archive.
        /// Every record and frame in the archive will be added to atlas.
        /// An array of rects will be returned with sub-texture rect for each record index and frame.
        /// Currently supports one archive per atlas. Super-atlas packing (multiple archives) is in the works.
        /// </summary>
        /// <param name="settings">Get texture settings.</param>
        /// <param name="alphaTextureFormat">Alpha TextureFormat.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <returns>GetTextureResults.</returns>
        public GetTextureResults GetTexture2DAtlas(
            GetTextureSettings settings,
            SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32,
            SupportedNonAlphaTextureFormats nonAlphaFormat  = SupportedNonAlphaTextureFormats.RGB24)
        {
            GetTextureResults results = new GetTextureResults();

            // Individual textures must remain readable to pack into atlas
            bool stayReadable = settings.stayReadable;

            settings.stayReadable = true;

            // Assign texture file
            TextureFile textureFile;

            if (settings.textureFile == null)
            {
                textureFile          = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true);
                settings.textureFile = textureFile;
            }
            else
            {
                textureFile = settings.textureFile;
            }

            // Create lists
            results.atlasSizes       = new List <Vector2>(textureFile.RecordCount);
            results.atlasScales      = new List <Vector2>(textureFile.RecordCount);
            results.atlasOffsets     = new List <Vector2>(textureFile.RecordCount);
            results.atlasFrameCounts = new List <int>(textureFile.RecordCount);

            // Read every texture in archive
            bool               hasNormalMaps    = false;
            bool               hasEmissionMaps  = false;
            bool               hasAnimation     = false;
            bool               allowImport      = (settings.atlasMaxSize == 4096);
            List <Texture2D>   albedoTextures   = new List <Texture2D>();
            List <Texture2D>   normalTextures   = new List <Texture2D>();
            List <Texture2D>   emissionTextures = new List <Texture2D>();
            List <RecordIndex> indices          = new List <RecordIndex>();

            for (int record = 0; record < textureFile.RecordCount; record++)
            {
                // Get record index and frame count
                settings.record = record;
                int frames = textureFile.GetFrameCount(record);
                if (frames > 1)
                {
                    hasAnimation = true;
                }

                // Get record information
                DFSize      size   = textureFile.GetSize(record);
                DFSize      scale  = textureFile.GetScale(record);
                DFPosition  offset = textureFile.GetOffset(record);
                RecordIndex ri     = new RecordIndex()
                {
                    startIndex = albedoTextures.Count,
                    frameCount = frames,
                    width      = size.Width,
                    height     = size.Height,
                };
                indices.Add(ri);
                for (int frame = 0; frame < frames; frame++)
                {
                    settings.frame = frame;
                    GetTextureResults nextTextureResults = GetTexture2D(settings, alphaTextureFormat, nonAlphaFormat, allowImport);

                    albedoTextures.Add(nextTextureResults.albedoMap);
                    if (nextTextureResults.normalMap != null)
                    {
                        normalTextures.Add(nextTextureResults.normalMap);
                        hasNormalMaps = true;
                    }
                    if (nextTextureResults.emissionMap != null)
                    {
                        emissionTextures.Add(nextTextureResults.emissionMap);
                        hasEmissionMaps = true;
                    }
                }

                results.atlasSizes.Add(new Vector2(size.Width, size.Height));
                results.atlasScales.Add(new Vector2(scale.Width, scale.Height));
                results.atlasOffsets.Add(new Vector2(offset.X, offset.Y));
                results.atlasFrameCounts.Add(frames);
                results.textureFile = textureFile;
            }

            // Pack albedo textures into atlas and get our rects
            Texture2D atlasAlbedoMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, ParseTextureFormat(alphaTextureFormat), MipMaps);

            Rect[] rects = atlasAlbedoMap.PackTextures(albedoTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable);

            // Pack normal textures into atlas
            Texture2D atlasNormalMap = null;

            if (hasNormalMaps)
            {
                // Normals must be ARGB32
                atlasNormalMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, TextureFormat.ARGB32, MipMaps);
                atlasNormalMap.PackTextures(normalTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable);
            }

            // Pack emission textures into atlas
            // TODO: Change this as packing not consistent
            Texture2D atlasEmissionMap = null;

            if (hasEmissionMaps)
            {
                // Repacking to ensure correct mix of lit and unlit
                atlasEmissionMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, ParseTextureFormat(alphaTextureFormat), MipMaps);
                atlasEmissionMap.PackTextures(emissionTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable);
            }

            // Add to results
            if (results.atlasRects == null)
            {
                results.atlasRects = new List <Rect>(rects.Length);
            }
            if (results.atlasIndices == null)
            {
                results.atlasIndices = new List <RecordIndex>(indices.Count);
            }
            results.atlasRects.AddRange(rects);
            results.atlasIndices.AddRange(indices);

            // Shrink UV rect to compensate for internal border
            float ru          = 1f / atlasAlbedoMap.width;
            float rv          = 1f / atlasAlbedoMap.height;
            int   finalBorder = settings.borderSize + settings.atlasShrinkUVs;

            for (int i = 0; i < results.atlasRects.Count; i++)
            {
                Rect rct = results.atlasRects[i];
                rct.xMin += finalBorder * ru;
                rct.xMax -= finalBorder * ru;
                rct.yMin += finalBorder * rv;
                rct.yMax -= finalBorder * rv;
                results.atlasRects[i] = rct;
            }

            // Store results
            results.albedoMap       = atlasAlbedoMap;
            results.normalMap       = atlasNormalMap;
            results.emissionMap     = atlasEmissionMap;
            results.isAtlasAnimated = hasAnimation;
            results.isEmissive      = hasEmissionMaps;

            return(results);
        }
Пример #5
0
        /// <summary>
        /// Gets Unity textures from Daggerfall texture with all options.
        /// Returns all supported texture maps for Standard shader in one call.
        /// </summary>
        /// <param name="settings">Get texture settings.</param>
        /// <param name="alphaTextureFormat">Alpha TextureFormat.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <param name="allowImport">Import texture from disk if present.</param>
        /// <returns>GetTextureResults.</returns>
        public GetTextureResults GetTexture2D(
            GetTextureSettings settings,
            SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32,
            SupportedNonAlphaTextureFormats nonAlphaFormat  = SupportedNonAlphaTextureFormats.RGB24,
            bool allowImport = true)
        {
            GetTextureResults results = new GetTextureResults();

            // Check if window or auto-emissive
            bool isWindow   = ClimateSwaps.IsExteriorWindow(settings.archive, settings.record);
            bool isEmissive = (settings.autoEmission) ? IsEmissive(settings.archive, settings.record) : false;

            // Override readable flag when user has set preference in material reader
            if (DaggerfallUnity.Instance.MaterialReader.ReadableTextures)
            {
                settings.stayReadable = true;
            }

            // Assign texture file
            TextureFile textureFile;

            if (settings.textureFile == null)
            {
                textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true);
            }
            else
            {
                textureFile = settings.textureFile;
            }

            // Get starting DFBitmap and albedo Color32 array
            DFSize   sz;
            DFBitmap srcBitmap = textureFile.GetDFBitmap(settings.record, settings.frame);

            Color32[] albedoColors = textureFile.GetColor32(srcBitmap, settings.alphaIndex, settings.borderSize, out sz);

            // Sharpen source image
            if (settings.sharpen)
            {
                albedoColors = ImageProcessing.Sharpen(ref albedoColors, sz.Width, sz.Height);
            }

            // Dilate edges
            if (settings.borderSize > 0 && settings.dilate && !settings.copyToOppositeBorder)
            {
                ImageProcessing.DilateColors(ref albedoColors, sz);
            }

            // Copy to opposite border
            if (settings.borderSize > 0 && settings.copyToOppositeBorder)
            {
                ImageProcessing.WrapBorder(ref albedoColors, sz, settings.borderSize);
            }

            // Set albedo texture
            Texture2D albedoMap = null;

            if (allowImport && TextureReplacement.CustomTextureExist(settings.archive, settings.record, settings.frame))
            {
                // Import albedo texture
                albedoMap = TextureReplacement.LoadCustomTexture(settings.archive, settings.record, settings.frame);
            }
            else
            {
                // Create albedo texture
                if (settings.alphaIndex < 0)
                {
                    albedoMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(nonAlphaFormat), MipMaps);
                }
                else
                {
                    albedoMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps);
                }
                albedoMap.SetPixels32(albedoColors);
                albedoMap.Apply(true, !settings.stayReadable);
            }

            // Set normal texture
            Texture2D normalMap = null;

            if (allowImport && TextureReplacement.CustomNormalExist(settings.archive, settings.record, settings.frame))
            {
                // Always import normal if present on disk
                normalMap = TextureReplacement.LoadCustomNormal(settings.archive, settings.record, settings.frame);
            }
            else if (settings.createNormalMap && textureFile.SolidType == TextureFile.SolidTypes.None)
            {
                // Create normal texture - must be ARGB32
                // Normal maps are bypassed for solid-colour textures
                Color32[] normalColors;
                normalColors = ImageProcessing.GetBumpMap(ref albedoColors, sz.Width, sz.Height);
                normalColors = ImageProcessing.ConvertBumpToNormals(ref normalColors, sz.Width, sz.Height, settings.normalStrength);
                normalMap    = new Texture2D(sz.Width, sz.Height, TextureFormat.ARGB32, MipMaps);
                normalMap.SetPixels32(normalColors);
                normalMap.Apply(true, !settings.stayReadable);
            }

            // Import emission map or create basic emissive texture
            Texture2D emissionMap    = null;
            bool      resultEmissive = false;

            if (allowImport && TextureReplacement.CustomEmissionExist(settings.archive, settings.record, settings.frame))
            {
                // Always import emission if present on disk
                emissionMap    = TextureReplacement.LoadCustomEmission(settings.archive, settings.record, settings.frame);
                resultEmissive = true;
            }
            else
            {
                if (settings.createEmissionMap || (settings.autoEmission && isEmissive) && !isWindow)
                {
                    // Just reuse albedo map for basic colour emission
                    emissionMap    = albedoMap;
                    resultEmissive = true;
                }

                // Windows need special handling as only glass parts are emissive
                if ((settings.createEmissionMap || settings.autoEmissionForWindows) && isWindow)
                {
                    // Create custom emission texture for glass area of windows
                    Color32[] emissionColors = textureFile.GetWindowColors32(srcBitmap);
                    emissionMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps);
                    emissionMap.SetPixels32(emissionColors);
                    emissionMap.Apply(true, !settings.stayReadable);
                    resultEmissive = true;
                }

                // Lights need special handling as this archive contains a mix of emissive and non-emissive flats
                // This can cause problems with atlas packing due to mismatch between albedo and emissive texture counts
                if ((settings.createEmissionMap || settings.autoEmission) && settings.archive == LightsTextureArchive)
                {
                    // For the unlit flats we create a null-emissive black texture
                    if (!isEmissive)
                    {
                        Color32[] emissionColors = new Color32[sz.Width * sz.Height];
                        emissionMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps);
                        emissionMap.SetPixels32(emissionColors);
                        emissionMap.Apply(true, !settings.stayReadable);
                        resultEmissive = true;
                    }
                }
            }

            // Shrink UV rect to compensate for internal border
            float ru = 1f / sz.Width;
            float rv = 1f / sz.Height;

            results.singleRect = new Rect(
                settings.borderSize * ru,
                settings.borderSize * rv,
                (sz.Width - settings.borderSize * 2) * ru,
                (sz.Height - settings.borderSize * 2) * rv);

            // Store results
            results.albedoMap   = albedoMap;
            results.normalMap   = normalMap;
            results.emissionMap = emissionMap;
            results.isWindow    = isWindow;
            results.isEmissive  = resultEmissive;
            results.textureFile = textureFile;

            return(results);
        }
 private TextureFormat ParseTextureFormat(SupportedAlphaTextureFormats format)
 {
     switch (format)
     {
         default:
         case SupportedAlphaTextureFormats.RGBA32:
             return TextureFormat.RGBA32;
         case SupportedAlphaTextureFormats.ARGB32:
             return TextureFormat.ARGB32;
         case SupportedAlphaTextureFormats.ARGB444:
             return TextureFormat.ARGB4444;
         case SupportedAlphaTextureFormats.RGBA444:
             return TextureFormat.RGBA4444;
     }
 }
        /// <summary>
        /// Gets Texture2D atlas from Daggerfall texture archive.
        /// Every record and frame in the archive will be added to atlas.
        /// An array of rects will be returned with sub-texture rect for each record index and frame.
        /// Currently supports one archive per atlas. Super-atlas packing (multiple archives) is in the works.
        /// </summary>
        /// <param name="settings">Get texture settings.</param>
        /// <param name="alphaTextureFormat">Alpha TextureFormat.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <returns>GetTextureResults.</returns>
        public GetTextureResults GetTexture2DAtlas(
            GetTextureSettings settings,
            SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32,
            SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24)
        {
            GetTextureResults results = new GetTextureResults();

            // Individual textures must remain readable to pack into atlas
            bool stayReadable = settings.stayReadable;
            settings.stayReadable = true;

            // Assign texture file
            TextureFile textureFile;
            if (settings.textureFile == null)
            {
                textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true);
                settings.textureFile = textureFile;
            }
            else
                textureFile = settings.textureFile;

            // Create lists
            results.atlasSizes = new List<Vector2>(textureFile.RecordCount);
            results.atlasScales = new List<Vector2>(textureFile.RecordCount);
            results.atlasOffsets = new List<Vector2>(textureFile.RecordCount);
            results.atlasFrameCounts = new List<int>(textureFile.RecordCount);

            // Read every texture in archive
            bool hasNormalMaps = false;
            bool hasEmissionMaps = false;
            bool hasAnimation = false;
            List<Texture2D> albedoTextures = new List<Texture2D>();
            List<Texture2D> normalTextures = new List<Texture2D>();
            List<Texture2D> emissionTextures = new List<Texture2D>();
            List<RecordIndex> indices = new List<RecordIndex>();
            for (int record = 0; record < textureFile.RecordCount; record++)
            {
                // Get record index and frame count
                settings.record = record;
                int frames = textureFile.GetFrameCount(record);
                if (frames > 1)
                    hasAnimation = true;

                // Get record information
                DFSize size = textureFile.GetSize(record);
                DFSize scale = textureFile.GetScale(record);
                DFPosition offset = textureFile.GetOffset(record);
                RecordIndex ri = new RecordIndex()
                {
                    startIndex = albedoTextures.Count,
                    frameCount = frames,
                    width = size.Width,
                    height = size.Height,
                };
                indices.Add(ri);
                for (int frame = 0; frame < frames; frame++)
                {
                    settings.frame = frame;
                    GetTextureResults nextTextureResults = GetTexture2D(settings, alphaTextureFormat, nonAlphaFormat);
                    albedoTextures.Add(nextTextureResults.albedoMap);
                    if (nextTextureResults.normalMap != null)
                    {
                        normalTextures.Add(nextTextureResults.normalMap);
                        hasNormalMaps = true;
                    }
                    if (nextTextureResults.emissionMap != null)
                    {
                        emissionTextures.Add(nextTextureResults.emissionMap);
                        hasEmissionMaps = true;
                    }
                }

                results.atlasSizes.Add(new Vector2(size.Width, size.Height));
                results.atlasScales.Add(new Vector2(scale.Width, scale.Height));
                results.atlasOffsets.Add(new Vector2(offset.X, offset.Y));
                results.atlasFrameCounts.Add(frames);
                results.textureFile = textureFile;
            }

            // Pack albedo textures into atlas and get our rects
            Texture2D atlasAlbedoMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, ParseTextureFormat(alphaTextureFormat), MipMaps);
            Rect[] rects = atlasAlbedoMap.PackTextures(albedoTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable);

            // Pack normal textures into atlas
            Texture2D atlasNormalMap = null;
            if (hasNormalMaps)
            {
                // Normals must be ARGB32
                atlasNormalMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, TextureFormat.ARGB32, MipMaps);
                atlasNormalMap.PackTextures(normalTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable);
            }

            // Pack emission textures into atlas
            // TODO: Change this as packing not consistent
            Texture2D atlasEmissionMap = null;
            if (hasEmissionMaps)
            {
                // Repacking to ensure correct mix of lit and unlit
                atlasEmissionMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, ParseTextureFormat(alphaTextureFormat), MipMaps);
                atlasEmissionMap.PackTextures(emissionTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable);
            }

            // Add to results
            if (results.atlasRects == null) results.atlasRects = new List<Rect>(rects.Length);
            if (results.atlasIndices == null) results.atlasIndices = new List<RecordIndex>(indices.Count);
            results.atlasRects.AddRange(rects);
            results.atlasIndices.AddRange(indices);

            // Shrink UV rect to compensate for internal border
            float ru = 1f / atlasAlbedoMap.width;
            float rv = 1f / atlasAlbedoMap.height;
            int finalBorder = settings.borderSize + settings.atlasShrinkUVs;
            for (int i = 0; i < results.atlasRects.Count; i++)
            {
                Rect rct = results.atlasRects[i];
                rct.xMin += finalBorder * ru;
                rct.xMax -= finalBorder * ru;
                rct.yMin += finalBorder * rv;
                rct.yMax -= finalBorder * rv;
                results.atlasRects[i] = rct;
            }

            // Store results
            results.albedoMap = atlasAlbedoMap;
            results.normalMap = atlasNormalMap;
            results.emissionMap = atlasEmissionMap;
            results.isAtlasAnimated = hasAnimation;
            results.isEmissive = hasEmissionMaps;

            return results;
        }
        /// <summary>
        /// Gets Unity textures from Daggerfall texture with all options.
        /// Returns all supported texture maps for Standard shader in one call.
        /// </summary>
        /// <param name="settings">Get texture settings.</param>
        /// <param name="alphaTextureFormat">Alpha TextureFormat.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <returns>GetTextureResults.</returns>
        public GetTextureResults GetTexture2D(
            GetTextureSettings settings,
            SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32,
            SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24)
        {
            GetTextureResults results = new GetTextureResults();

            // Check if window or auto-emissive
            bool isWindow = ClimateSwaps.IsExteriorWindow(settings.archive, settings.record);
            bool isEmissive = (settings.autoEmission) ? IsEmissive(settings.archive, settings.record) : false;

            // Assign texture file
            TextureFile textureFile;
            if (settings.textureFile == null)
                textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true);
            else
                textureFile = settings.textureFile;

            // Get starting DFBitmap and albedo Color32 array
            DFSize sz;
            DFBitmap srcBitmap = textureFile.GetDFBitmap(settings.record, settings.frame);
            Color32[] albedoColors = textureFile.GetColor32(srcBitmap, settings.alphaIndex, settings.borderSize, out sz);

            // Sharpen source image
            if (settings.sharpen)
                albedoColors = ImageProcessing.Sharpen(ref albedoColors, sz.Width, sz.Height);

            // Dilate edges
            if (settings.borderSize > 0 && settings.dilate && !settings.copyToOppositeBorder)
                ImageProcessing.DilateColors(ref albedoColors, sz);

            // Copy to opposite border
            if (settings.borderSize > 0 && settings.copyToOppositeBorder)
                ImageProcessing.WrapBorder(ref albedoColors, sz, settings.borderSize);

            // Create albedo texture
            Texture2D albedoMap = null;
            if (settings.alphaIndex < 0)
                albedoMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(nonAlphaFormat), MipMaps);
            else
                albedoMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps);
            albedoMap.SetPixels32(albedoColors);
            albedoMap.Apply(true, !settings.stayReadable);

            // Create normal texture - must be ARGB32
            // Normal maps are bypassed for solid-colour textures
            Texture2D normalMap = null;
            if (settings.createNormalMap && textureFile.SolidType == TextureFile.SolidTypes.None)
            {
                Color32[] normalColors;
                normalColors = ImageProcessing.GetBumpMap(ref albedoColors, sz.Width, sz.Height);
                normalColors = ImageProcessing.ConvertBumpToNormals(ref normalColors, sz.Width, sz.Height, settings.normalStrength);
                normalMap = new Texture2D(sz.Width, sz.Height, TextureFormat.ARGB32, MipMaps);
                normalMap.SetPixels32(normalColors);
                normalMap.Apply(true, !settings.stayReadable);
            }

            // Create basic emissive texture
            Texture2D emissionMap = null;
            bool resultEmissive = false;
            if (settings.createEmissionMap || (settings.autoEmission && isEmissive) && !isWindow)
            {
                // Just reuse albedo map for basic colour emission
                emissionMap = albedoMap;
                resultEmissive = true;
            }

            // Windows need special handling as only glass parts are emissive
            if ((settings.createEmissionMap || settings.autoEmissionForWindows) && isWindow)
            {
                // Create custom emission texture for glass area of windows
                Color32[] emissionColors = textureFile.GetWindowColors32(srcBitmap);
                emissionMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps);
                emissionMap.SetPixels32(emissionColors);
                emissionMap.Apply(true, !settings.stayReadable);
                resultEmissive = true;
            }

            // Lights need special handling as this archive contains a mix of emissive and non-emissive flats
            // This can cause problems with atlas packing due to mismatch between albedo and emissive texture counts
            if ((settings.createEmissionMap || settings.autoEmission) && settings.archive == LightsTextureArchive)
            {
                // For the unlit flats we create a null-emissive black texture
                if (!isEmissive)
                {
                    Color32[] emissionColors = new Color32[sz.Width * sz.Height];
                    emissionMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps);
                    emissionMap.SetPixels32(emissionColors);
                    emissionMap.Apply(true, !settings.stayReadable);
                    resultEmissive = true;
                }
            }

            // Shrink UV rect to compensate for internal border
            float ru = 1f / sz.Width;
            float rv = 1f / sz.Height;
            results.singleRect = new Rect(
                settings.borderSize * ru,
                settings.borderSize * rv,
                (sz.Width - settings.borderSize * 2) * ru,
                (sz.Height - settings.borderSize * 2) * rv);

            // Store results
            results.albedoMap = albedoMap;
            results.normalMap = normalMap;
            results.emissionMap = emissionMap;
            results.isWindow = isWindow;
            results.isEmissive = resultEmissive;
            results.textureFile = textureFile;

            return results;
        }
        /// <summary>
        /// TEMP: Creates super-atlas populated with all archives in array.
        /// Currently does not support animated textures, normal map, or emission map.
        /// TODO: Integrate this feature fully with material system.
        /// </summary>
        /// <param name="archives">Archive array.</param>
        /// <param name="borderSize">Number of pixels border to add around image.</param>
        /// <param name="dilate">Blend texture into surrounding empty pixels. Requires border.</param>
        /// <param name="maxAtlasSize">Maximum atlas size.</param>
        /// <param name="alphaTextureFormat">Alpha TextureFormat.</param>
        /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param>
        /// <returns>TextureAtlasBuilder.</returns>
        public TextureAtlasBuilder CreateTextureAtlasBuilder(
            int[] archives,
            int borderSize = 0,
            bool dilate = false,
            int maxAtlasSize = 2048,
            SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32,
            SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24)
        {
            // Iterate archives
            TextureFile textureFile = new TextureFile();
            TextureAtlasBuilder builder = new TextureAtlasBuilder();
            GetTextureSettings settings = TextureReader.CreateTextureSettings(0, 0, 0, 0, borderSize, dilate, maxAtlasSize);
            settings.stayReadable = true;
            for (int i = 0; i < archives.Length; i++)
            {
                // Load texture file
                settings.archive = archives[i];
                textureFile.Load(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true);

                // Add all records for this archive - single frame only
                for (int record = 0; record < textureFile.RecordCount; record++)
                {
                    settings.record = record;
                    GetTextureResults results = GetTexture2D(settings, alphaTextureFormat, nonAlphaFormat);
                    DFSize size = textureFile.GetSize(record);
                    DFSize scale = textureFile.GetScale(record);
                    builder.AddTextureItem(
                        results.albedoMap,
                        settings.archive,
                        settings.record,
                        0, 1,
                        new Vector2(size.Width, size.Height),
                        new Vector2(scale.Width, scale.Height));
                }
            }

            // Apply the builder
            builder.Rebuild(borderSize);

            return builder;
        }