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 }