TextureManager.cs 22 KB

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