CnC_Remastered_Collection

Command and Conquer: Red Alert
Log | Files | Refs | README | LICENSE

TextureManager.cs (22333B)


      1 //
      2 // Copyright 2020 Electronic Arts Inc.
      3 //
      4 // The Command & Conquer Map Editor and corresponding source code is free 
      5 // software: you can redistribute it and/or modify it under the terms of 
      6 // the GNU General Public License as published by the Free Software Foundation, 
      7 // either version 3 of the License, or (at your option) any later version.
      8 
      9 // The Command & Conquer Map Editor and corresponding source code is distributed 
     10 // in the hope that it will be useful, but with permitted additional restrictions 
     11 // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT 
     12 // distributed with this program. You should have received a copy of the 
     13 // GNU General Public License along with permitted additional restrictions 
     14 // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
     15 using Newtonsoft.Json.Linq;
     16 using Pfim;
     17 using System;
     18 using System.Collections.Generic;
     19 using System.Drawing;
     20 using System.Drawing.Imaging;
     21 using System.IO;
     22 using System.IO.Compression;
     23 using System.Linq;
     24 using System.Runtime.InteropServices;
     25 using TGASharpLib;
     26 
     27 namespace MobiusEditor.Utility
     28 {
     29     public class TextureManager
     30     {
     31 #if false
     32         private class ImageData
     33         {
     34             public TGA TGA;
     35             public JObject Metadata;
     36         }
     37 #endif
     38 
     39         private readonly MegafileManager megafileManager;
     40 
     41         private Dictionary<string, Bitmap> cachedTextures = new Dictionary<string, Bitmap>();
     42         private Dictionary<(string, TeamColor), (Bitmap, Rectangle)> teamColorTextures = new Dictionary<(string, TeamColor), (Bitmap, Rectangle)>();
     43 
     44         public TextureManager(MegafileManager megafileManager)
     45         {
     46             this.megafileManager = megafileManager;
     47         }
     48 
     49         public void Reset()
     50         {
     51             cachedTextures.Clear();
     52             teamColorTextures.Clear();
     53         }
     54 
     55         public (Bitmap, Rectangle) GetTexture(string filename, TeamColor teamColor)
     56         {
     57             if (teamColorTextures.TryGetValue((filename, teamColor), out (Bitmap bitmap, Rectangle opaqueBounds) result))
     58             {
     59                 return result;
     60             }
     61 
     62             if (!cachedTextures.TryGetValue(filename, out result.bitmap))
     63             {
     64                 if (Path.GetExtension(filename).ToLower() == ".tga")
     65                 {
     66                     TGA tga = null;
     67                     JObject metadata = null;
     68 
     69                     // First attempt to find the texture in an archive
     70                     var name = Path.GetFileNameWithoutExtension(filename);
     71                     var archiveDir = Path.GetDirectoryName(filename);
     72                     var archivePath = archiveDir + ".ZIP";
     73                     using (var fileStream = megafileManager.Open(archivePath))
     74                     {
     75                         if (fileStream != null)
     76                         {
     77                             using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Read))
     78                             {
     79                                 foreach (var entry in archive.Entries)
     80                                 {
     81                                     if (name == Path.GetFileNameWithoutExtension(entry.Name))
     82                                     {
     83                                         if ((tga == null) && (Path.GetExtension(entry.Name).ToLower() == ".tga"))
     84                                         {
     85                                             using (var stream = entry.Open())
     86                                             using (var memStream = new MemoryStream())
     87                                             {
     88                                                 stream.CopyTo(memStream);
     89                                                 tga = new TGA(memStream);
     90                                             }
     91                                         }
     92                                         else if ((metadata == null) && (Path.GetExtension(entry.Name).ToLower() == ".meta"))
     93                                         {
     94                                             using (var stream = entry.Open())
     95                                             using (var reader = new StreamReader(stream))
     96                                             {
     97                                                 metadata = JObject.Parse(reader.ReadToEnd());
     98                                             }
     99                                         }
    100 
    101                                         if ((tga != null) && (metadata != null))
    102                                         {
    103                                             break;
    104                                         }
    105                                     }
    106                                 }
    107                             }
    108                         }
    109                     }
    110 
    111                     // Next attempt to load a standalone file
    112                     if (tga == null)
    113                     {
    114                         using (var fileStream = megafileManager.Open(filename))
    115                         {
    116                             if (fileStream != null)
    117                             {
    118                                 tga = new TGA(fileStream);
    119                             }
    120                         }
    121                     }
    122 
    123                     if (tga != null)
    124                     {
    125                         var bitmap = tga.ToBitmap(true);
    126                         if (metadata != null)
    127                         {
    128                             var size = new Size(metadata["size"][0].ToObject<int>(), metadata["size"][1].ToObject<int>());
    129                             var crop = Rectangle.FromLTRB(
    130                                 metadata["crop"][0].ToObject<int>(),
    131                                 metadata["crop"][1].ToObject<int>(),
    132                                 metadata["crop"][2].ToObject<int>(),
    133                                 metadata["crop"][3].ToObject<int>()
    134                             );
    135 
    136                             var uncroppedBitmap = new Bitmap(size.Width, size.Height, bitmap.PixelFormat);
    137                             using (var g = Graphics.FromImage(uncroppedBitmap))
    138                             {
    139                                 g.DrawImage(bitmap, crop, new Rectangle(Point.Empty, bitmap.Size), GraphicsUnit.Pixel);
    140                             }
    141                             cachedTextures[filename] = uncroppedBitmap;
    142                         }
    143                         else
    144                         {
    145                             cachedTextures[filename] = bitmap;
    146                         }
    147                     }
    148 
    149 #if false
    150                     // Attempt to load parent directory as archive
    151                     var archiveDir = Path.GetDirectoryName(filename);
    152                     var archivePath = archiveDir + ".ZIP";
    153                     using (var fileStream = megafileManager.Open(archivePath))
    154                     {
    155                         if (fileStream != null)
    156                         {
    157                             using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Read))
    158                             {
    159                                 var images = new Dictionary<string, ImageData>();
    160 
    161                                 foreach (var entry in archive.Entries)
    162                                 {
    163                                     var name = Path.GetFileNameWithoutExtension(entry.Name);
    164                                     if (!images.TryGetValue(name, out ImageData imageData))
    165                                     {
    166                                         imageData = images[name] = new ImageData { TGA = null, Metadata = null };
    167                                     }
    168 
    169                                     if ((imageData.TGA == null) && (Path.GetExtension(entry.Name).ToLower() == ".tga"))
    170                                     {
    171                                         using (var stream = entry.Open())
    172                                         using (var memStream = new MemoryStream())
    173                                         {
    174                                             stream.CopyTo(memStream);
    175                                             imageData.TGA = new TGA(memStream);
    176                                         }
    177                                     }
    178                                     else if ((imageData.Metadata == null) && (Path.GetExtension(entry.Name).ToLower() == ".meta"))
    179                                     {
    180                                         using (var stream = entry.Open())
    181                                         using (var reader = new StreamReader(stream))
    182                                         {
    183                                             imageData.Metadata = JObject.Parse(reader.ReadToEnd());
    184                                         }
    185                                     }
    186 
    187                                     if ((imageData.TGA != null) && (imageData.Metadata != null))
    188                                     {
    189                                         var bitmap = imageData.TGA.ToBitmap(true);
    190                                         var size = new Size(imageData.Metadata["size"][0].ToObject<int>(), imageData.Metadata["size"][1].ToObject<int>());
    191                                         var crop = Rectangle.FromLTRB(
    192                                             imageData.Metadata["crop"][0].ToObject<int>(),
    193                                             imageData.Metadata["crop"][1].ToObject<int>(),
    194                                             imageData.Metadata["crop"][2].ToObject<int>(),
    195                                             imageData.Metadata["crop"][3].ToObject<int>()
    196                                         );
    197 
    198                                         var uncroppedBitmap = new Bitmap(size.Width, size.Height, bitmap.PixelFormat);
    199                                         using (var g = Graphics.FromImage(uncroppedBitmap))
    200                                         {
    201                                             g.DrawImage(bitmap, crop, new Rectangle(Point.Empty, bitmap.Size), GraphicsUnit.Pixel);
    202                                         }
    203                                         cachedTextures[Path.Combine(archiveDir, name) + ".tga"] = uncroppedBitmap;
    204 
    205                                         images.Remove(name);
    206                                     }
    207                                 }
    208 
    209                                 foreach (var item in images.Where(x => x.Value.TGA != null))
    210                                 {
    211                                     cachedTextures[Path.Combine(archiveDir, item.Key) + ".tga"] = item.Value.TGA.ToBitmap(true);
    212                                 }
    213                             }
    214                         }
    215                     }
    216 #endif
    217                 }
    218 
    219                 if (!cachedTextures.TryGetValue(filename, out result.bitmap))
    220                 {
    221                     // Try loading as a DDS
    222                     var ddsFilename = Path.ChangeExtension(filename, ".DDS");
    223                     using (var fileStream = megafileManager.Open(ddsFilename))
    224                     {
    225                         if (fileStream != null)
    226                         {
    227                             var bytes = new byte[fileStream.Length];
    228                             fileStream.Read(bytes, 0, bytes.Length);
    229 
    230                             using (var image = Dds.Create(bytes, new PfimConfig()))
    231                             {
    232                                 PixelFormat format;
    233                                 switch (image.Format)
    234                                 {
    235                                     case Pfim.ImageFormat.Rgb24:
    236                                         format = PixelFormat.Format24bppRgb;
    237                                         break;
    238 
    239                                     case Pfim.ImageFormat.Rgba32:
    240                                         format = PixelFormat.Format32bppArgb;
    241                                         break;
    242 
    243                                     case Pfim.ImageFormat.R5g5b5:
    244                                         format = PixelFormat.Format16bppRgb555;
    245                                         break;
    246 
    247                                     case Pfim.ImageFormat.R5g6b5:
    248                                         format = PixelFormat.Format16bppRgb565;
    249                                         break;
    250 
    251                                     case Pfim.ImageFormat.R5g5b5a1:
    252                                         format = PixelFormat.Format16bppArgb1555;
    253                                         break;
    254 
    255                                     case Pfim.ImageFormat.Rgb8:
    256                                         format = PixelFormat.Format8bppIndexed;
    257                                         break;
    258 
    259                                     default:
    260                                         format = PixelFormat.DontCare;
    261                                         break;
    262                                 }
    263 
    264                                 var bitmap = new Bitmap(image.Width, image.Height, format);
    265                                 var bitmapData = bitmap.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat);
    266                                 Marshal.Copy(image.Data, 0, bitmapData.Scan0, image.Stride * image.Height);
    267                                 bitmap.UnlockBits(bitmapData);
    268                                 cachedTextures[filename] = bitmap;
    269                             }
    270                         }
    271                     }
    272                 }
    273             }
    274 
    275             if (!cachedTextures.TryGetValue(filename, out result.bitmap))
    276             {
    277                 return result;
    278             }
    279 
    280             result.bitmap = new Bitmap(result.bitmap);
    281             result.opaqueBounds = new Rectangle(0, 0, result.bitmap.Width, result.bitmap.Height);
    282             if (teamColor != null)
    283             {
    284                 float frac(float x) => x - (int)x;
    285                 float lerp(float x, float y, float t) => (x * (1.0f - t)) + (y * t);
    286                 float saturate(float x) => Math.Max(0.0f, Math.Min(1.0f, x));
    287 
    288                 BitmapData data = null;
    289                 try
    290                 {
    291                     data = result.bitmap.LockBits(new Rectangle(0, 0, result.bitmap.Width, result.bitmap.Height), ImageLockMode.ReadWrite, result.bitmap.PixelFormat);
    292                     var bpp = Image.GetPixelFormatSize(data.PixelFormat) / 8;
    293                     var bytes = new byte[data.Stride * data.Height];
    294                     Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
    295 
    296                     result.opaqueBounds = CalculateOpaqueBounds(bytes, data.Width, data.Height, bpp, data.Stride);
    297 
    298                     for (int j = 0; j < bytes.Length; j += bpp)
    299                     {
    300                         var pixel = Color.FromArgb(bytes[j + 2], bytes[j + 1], bytes[j + 0]);
    301                         (float r, float g, float b) = (pixel.R.ToLinear(), pixel.G.ToLinear(), pixel.B.ToLinear());
    302 
    303                         (float x, float y, float z, float w) K = (0.0f, -1.0f / 3.0f, 2.0f / 3.0f, -1.0f);
    304                         (float x, float y, float z, float w) p = (g >= b) ? (g, b, K.x, K.y) : (b, g, K.w, K.z);
    305                         (float x, float y, float z, float w) q = (r >= p.x) ? (r, p.y, p.z, p.x) : (p.x, p.y, p.w, r);
    306                         (float d, float e) = (q.x - Math.Min(q.w, q.y), 1e-10f);
    307                         (float hue, float saturation, float value) = (Math.Abs(q.z + (q.w - q.y) / (6.0f * d + e)), d / (q.x + e), q.x);
    308 
    309                         var lowerHue = teamColor.LowerBounds.GetHue() / 360.0f;
    310                         var upperHue = teamColor.UpperBounds.GetHue() / 360.0f;
    311                         if ((hue >= lowerHue) && (upperHue >= hue))
    312                         {
    313                             hue = (hue / (upperHue - lowerHue)) * ((upperHue + teamColor.Fudge) - (lowerHue - teamColor.Fudge));
    314                             hue += teamColor.HSVShift.X;
    315                             saturation += teamColor.HSVShift.Y;
    316                             value += teamColor.HSVShift.Z;
    317 
    318                             (float x, float y, float z, float w) L = (1.0f, 2.0f / 3.0f, 1.0f / 3.0f, 3.0f);
    319                             (float x, float y, float z) m = (
    320                                 Math.Abs(frac(hue + L.x) * 6.0f - L.w),
    321                                 Math.Abs(frac(hue + L.y) * 6.0f - L.w),
    322                                 Math.Abs(frac(hue + L.z) * 6.0f - L.w)
    323                             );
    324 
    325                             r = value * lerp(L.x, saturate(m.x - L.x), saturation);
    326                             g = value * lerp(L.x, saturate(m.y - L.x), saturation);
    327                             b = value * lerp(L.x, saturate(m.z - L.x), saturation);
    328 
    329                             (float x, float y, float z) n = (
    330                                 Math.Min(1.0f, Math.Max(0.0f, r - teamColor.InputLevels.X) / (teamColor.InputLevels.Z - teamColor.InputLevels.X)),
    331                                 Math.Min(1.0f, Math.Max(0.0f, g - teamColor.InputLevels.X) / (teamColor.InputLevels.Z - teamColor.InputLevels.X)),
    332                                 Math.Min(1.0f, Math.Max(0.0f, b - teamColor.InputLevels.X) / (teamColor.InputLevels.Z - teamColor.InputLevels.X))
    333                             );
    334                             n.x = (float)Math.Pow(n.x, teamColor.InputLevels.Y);
    335                             n.y = (float)Math.Pow(n.y, teamColor.InputLevels.Y);
    336                             n.z = (float)Math.Pow(n.z, teamColor.InputLevels.Y);
    337 
    338                             r = lerp(teamColor.OutputLevels.X, teamColor.OutputLevels.Y, n.x);
    339                             g = lerp(teamColor.OutputLevels.X, teamColor.OutputLevels.Y, n.y);
    340                             b = lerp(teamColor.OutputLevels.X, teamColor.OutputLevels.Y, n.z);
    341                         }
    342 
    343                         (float x, float y, float z) n2 = (
    344                             Math.Min(1.0f, Math.Max(0.0f, r - teamColor.OverallInputLevels.X) / (teamColor.OverallInputLevels.Z - teamColor.OverallInputLevels.X)),
    345                             Math.Min(1.0f, Math.Max(0.0f, g - teamColor.OverallInputLevels.X) / (teamColor.OverallInputLevels.Z - teamColor.OverallInputLevels.X)),
    346                             Math.Min(1.0f, Math.Max(0.0f, b - teamColor.OverallInputLevels.X) / (teamColor.OverallInputLevels.Z - teamColor.OverallInputLevels.X))
    347                         );
    348                         n2.x = (float)Math.Pow(n2.x, teamColor.OverallInputLevels.Y);
    349                         n2.y = (float)Math.Pow(n2.y, teamColor.OverallInputLevels.Y);
    350                         n2.z = (float)Math.Pow(n2.z, teamColor.OverallInputLevels.Y);
    351 
    352                         r = lerp(teamColor.OverallOutputLevels.X, teamColor.OverallOutputLevels.Y, n2.x);
    353                         g = lerp(teamColor.OverallOutputLevels.X, teamColor.OverallOutputLevels.Y, n2.y);
    354                         b = lerp(teamColor.OverallOutputLevels.X, teamColor.OverallOutputLevels.Y, n2.z);
    355 
    356                         bytes[j + 2] = (byte)(r.ToSRGB() * 255.0f);
    357                         bytes[j + 1] = (byte)(g.ToSRGB() * 255.0f);
    358                         bytes[j + 0] = (byte)(b.ToSRGB() * 255.0f);
    359                     }
    360 
    361                     Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
    362                 }
    363                 finally
    364                 {
    365                     if (data != null)
    366                     {
    367                         result.bitmap.UnlockBits(data);
    368                     }
    369                 }
    370             }
    371             else
    372             {
    373                 result.opaqueBounds = CalculateOpaqueBounds(result.bitmap);
    374             }
    375 
    376             teamColorTextures[(filename, teamColor)] = result;
    377             return result;
    378         }
    379 
    380         private static Rectangle CalculateOpaqueBounds(byte[] data, int width, int height, int bpp, int stride)
    381         {
    382             bool isTransparentRow(int y)
    383             {
    384                 var start = y * stride;
    385                 for (var i = bpp - 1; i < stride; i += bpp)
    386                 {
    387                     if (data[start + i] != 0)
    388                     {
    389                         return false;
    390                     }
    391                 }
    392                 return true;
    393             }
    394 
    395             var opaqueBounds = new Rectangle(0, 0, width, height);
    396             for (int y = 0; y < height; ++y)
    397             {
    398                 if (!isTransparentRow(y))
    399                 {
    400                     opaqueBounds.Offset(0, y);
    401                     break;
    402                 }
    403             }
    404             for (int y = height; y > 0; --y)
    405             {
    406                 if (!isTransparentRow(y - 1))
    407                 {
    408                     opaqueBounds.Height = y - opaqueBounds.Top;
    409                     break;
    410                 }
    411             }
    412 
    413             bool isTransparentColumn(int x)
    414             {
    415                 var start = (x * bpp) + (bpp - 1);
    416                 for (var y = opaqueBounds.Top; y < opaqueBounds.Bottom; ++y)
    417                 {
    418                     if (data[start + (y * stride)] != 0)
    419                     {
    420                         return false;
    421                     }
    422                 }
    423                 return true;
    424             }
    425 
    426             for (int x = 0; x < width; ++x)
    427             {
    428                 if (!isTransparentColumn(x))
    429                 {
    430                     opaqueBounds.Offset(x, 0);
    431                     break;
    432                 }
    433             }
    434             for (int x = width; x > 0; --x)
    435             {
    436                 if (!isTransparentColumn(x - 1))
    437                 {
    438                     opaqueBounds.Width = x - opaqueBounds.Left;
    439                     break;
    440                 }
    441             }
    442 
    443             return opaqueBounds;
    444         }
    445 
    446         private static Rectangle CalculateOpaqueBounds(Bitmap bitmap)
    447         {
    448             BitmapData data = null;
    449             try
    450             {
    451                 data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat);
    452                 var bpp = Image.GetPixelFormatSize(data.PixelFormat) / 8;
    453                 var bytes = new byte[data.Stride * data.Height];
    454                 Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
    455                 return CalculateOpaqueBounds(bytes, data.Width, data.Height, bpp, data.Stride);
    456             }
    457             finally
    458             {
    459                 if (data != null)
    460                 {
    461                     bitmap.UnlockBits(data);
    462                 }
    463             }
    464         }
    465     }
    466 }