BabylonExporter.Texture.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using Autodesk.Max;
  5. using BabylonExport.Entities;
  6. using System.Drawing;
  7. namespace Max2Babylon
  8. {
  9. partial class BabylonExporter
  10. {
  11. // -------------------------------
  12. // --- "public" export methods ---
  13. // -------------------------------
  14. private BabylonTexture ExportTexture(IStdMat2 stdMat, int index, out BabylonFresnelParameters fresnelParameters, BabylonScene babylonScene, bool allowCube = false, bool forceAlpha = false)
  15. {
  16. fresnelParameters = null;
  17. if (!stdMat.MapEnabled(index))
  18. {
  19. return null;
  20. }
  21. var texMap = stdMat.GetSubTexmap(index);
  22. if (texMap == null)
  23. {
  24. RaiseWarning("Texture channel " + index + " activated but no texture found.", 2);
  25. return null;
  26. }
  27. texMap = _exportFresnelParameters(texMap, out fresnelParameters);
  28. var amount = stdMat.GetTexmapAmt(index, 0);
  29. return _exportTexture(texMap, amount, babylonScene, allowCube, forceAlpha);
  30. }
  31. private BabylonTexture ExportPBRTexture(IIGameMaterial materialNode, int index, BabylonScene babylonScene, float amount = 1.0f)
  32. {
  33. var texMap = _getTexMap(materialNode, index);
  34. if (texMap != null)
  35. {
  36. return _exportTexture(texMap, amount, babylonScene);
  37. }
  38. return null;
  39. }
  40. private BabylonTexture ExportBaseColorAlphaTexture(IIGameMaterial materialNode, BabylonScene babylonScene, string materialName)
  41. {
  42. ITexmap baseColorTexMap = _getTexMap(materialNode, 1);
  43. ITexmap alphaTexMap = _getTexMap(materialNode, 9); // Transparency weight map
  44. // --- Babylon texture ---
  45. var baseColorTexture = _getBitmapTex(baseColorTexMap);
  46. var alphaTexture = _getBitmapTex(alphaTexMap);
  47. // Use one as a reference for UVs parameters
  48. var texture = baseColorTexture != null ? baseColorTexture : alphaTexture;
  49. if (texture == null)
  50. {
  51. return null;
  52. }
  53. var hasAlpha = alphaTexMap != null || (baseColorTexture != null && baseColorTexture.AlphaSource == 0); // Alpha source is 'Image Alpha'
  54. var babylonTexture = new BabylonTexture
  55. {
  56. name = materialName + "_baseColor" + (hasAlpha ? ".png" : ".jpg") // TODO - unsafe name, may conflict with another texture name
  57. };
  58. // Level
  59. babylonTexture.level = 1.0f;
  60. // Alpha
  61. babylonTexture.hasAlpha = hasAlpha;
  62. babylonTexture.getAlphaFromRGB = false;
  63. // UVs
  64. var uvGen = _exportUV(texture, babylonTexture);
  65. // Is cube
  66. _exportIsCube(texture, babylonTexture, false);
  67. // --- Merge baseColor and alpha maps ---
  68. // Load bitmaps
  69. var baseColorBitmap = _loadTexture(baseColorTexMap);
  70. var alphaBitmap = _loadTexture(alphaTexMap);
  71. // Retreive dimensions
  72. int width = 0;
  73. int height = 0;
  74. var haveSameDimensions = _getMinimalBitmapDimensions(out width, out height, baseColorBitmap, alphaBitmap);
  75. if (!haveSameDimensions)
  76. {
  77. RaiseWarning("Base color and transparency color maps should have same dimensions", 2);
  78. }
  79. var getAlphaFromRGB = false;
  80. if (alphaTexture != null)
  81. {
  82. getAlphaFromRGB = (alphaTexture.AlphaSource == 2) || (alphaTexture.AlphaSource == 3); // 'RGB intensity' or 'None (Opaque)'
  83. }
  84. // Create baseColor+alpha map
  85. Bitmap baseColorAlphaBitmap = new Bitmap(width, height);
  86. for (int x = 0; x < width; x++)
  87. {
  88. for (int y = 0; y < height; y++)
  89. {
  90. var baseColor = baseColorBitmap != null ? baseColorBitmap.GetPixel(x, y) : Color.FromArgb(255, 255, 255);
  91. Color baseColorAlpha;
  92. if (babylonTexture.hasAlpha)
  93. {
  94. if (alphaBitmap != null)
  95. {
  96. // Retreive alpha from alpha texture
  97. var alphaColor = alphaBitmap.GetPixel(x, y);
  98. var alpha = getAlphaFromRGB ? alphaColor.R : alphaColor.A;
  99. baseColorAlpha = Color.FromArgb(alpha, baseColor);
  100. }
  101. else
  102. {
  103. // Use all channels from base color
  104. baseColorAlpha = baseColor;
  105. }
  106. }
  107. else
  108. {
  109. // Only use RGB channels from base color
  110. baseColorAlpha = Color.FromArgb(baseColor.R, baseColor.G, baseColor.B);
  111. }
  112. baseColorAlphaBitmap.SetPixel(x, y, baseColorAlpha);
  113. }
  114. }
  115. // Write bitmap
  116. var absolutePath = Path.Combine(babylonScene.OutputPath, babylonTexture.name);
  117. RaiseMessage($"Texture | write image '{babylonTexture.name}'", 2);
  118. baseColorAlphaBitmap.Save(absolutePath);
  119. return babylonTexture;
  120. }
  121. private BabylonTexture ExportMetallicRoughnessTexture(IIGameMaterial materialNode, float metallic, float roughness, BabylonScene babylonScene, string materialName)
  122. {
  123. ITexmap metallicTexMap = _getTexMap(materialNode, 5);
  124. ITexmap roughnessTexMap = _getTexMap(materialNode, 4);
  125. // --- Babylon texture ---
  126. var metallicTexture = _getBitmapTex(metallicTexMap);
  127. var roughnessTexture = _getBitmapTex(roughnessTexMap);
  128. // Use one as a reference for UVs parameters
  129. var texture = metallicTexture != null ? metallicTexture : roughnessTexture;
  130. if (texture == null)
  131. {
  132. return null;
  133. }
  134. var babylonTexture = new BabylonTexture
  135. {
  136. name = materialName + "_metallicRoughness" + ".jpg" // TODO - unsafe name, may conflict with another texture name
  137. };
  138. // Level
  139. babylonTexture.level = 1.0f;
  140. // No alpha
  141. babylonTexture.hasAlpha = false;
  142. babylonTexture.getAlphaFromRGB = false;
  143. // UVs
  144. var uvGen = _exportUV(texture, babylonTexture);
  145. // Is cube
  146. _exportIsCube(texture, babylonTexture, false);
  147. // --- Merge metallic and roughness maps ---
  148. // Load bitmaps
  149. var metallicBitmap = _loadTexture(metallicTexMap);
  150. var roughnessBitmap = _loadTexture(roughnessTexMap);
  151. // Retreive dimensions
  152. int width = 0;
  153. int height = 0;
  154. var haveSameDimensions = _getMinimalBitmapDimensions(out width, out height, metallicBitmap, roughnessBitmap);
  155. if (!haveSameDimensions)
  156. {
  157. RaiseWarning("Metallic and roughness maps should have same dimensions", 2);
  158. }
  159. // Create metallic+roughness map
  160. Bitmap metallicRoughnessBitmap = new Bitmap(width, height);
  161. for (int x = 0; x < width; x++)
  162. {
  163. for (int y = 0; y < height; y++)
  164. {
  165. var _metallic = metallicBitmap != null ? metallicBitmap.GetPixel(x, y).R :
  166. metallic * 255.0f;
  167. var _roughness = roughnessBitmap != null ? roughnessBitmap.GetPixel(x, y).R :
  168. roughness * 255.0f;
  169. // The metalness values are sampled from the B channel.
  170. // The roughness values are sampled from the G channel.
  171. // These values are linear. If other channels are present (R or A), they are ignored for metallic-roughness calculations.
  172. Color colorMetallicRoughness = Color.FromArgb(
  173. 0,
  174. (int)_roughness,
  175. (int)_metallic
  176. );
  177. metallicRoughnessBitmap.SetPixel(x, y, colorMetallicRoughness);
  178. }
  179. }
  180. // Write bitmap
  181. var absolutePath = Path.Combine(babylonScene.OutputPath, babylonTexture.name);
  182. RaiseMessage($"Texture | write image '{babylonTexture.name}'", 2);
  183. metallicRoughnessBitmap.Save(absolutePath);
  184. return babylonTexture;
  185. }
  186. // -------------------------
  187. // -- Export sub methods ---
  188. // -------------------------
  189. private BabylonTexture _exportTexture(ITexmap texMap, float amount, BabylonScene babylonScene, bool allowCube = false, bool forceAlpha = false)
  190. {
  191. if (texMap.GetParamBlock(0) == null || texMap.GetParamBlock(0).Owner == null)
  192. {
  193. return null;
  194. }
  195. var texture = texMap.GetParamBlock(0).Owner as IBitmapTex;
  196. if (texture == null)
  197. {
  198. return null;
  199. }
  200. var babylonTexture = new BabylonTexture
  201. {
  202. name = Path.GetFileName(texture.MapName)
  203. };
  204. // Level
  205. babylonTexture.level = amount;
  206. // Alpha
  207. if (forceAlpha)
  208. {
  209. babylonTexture.hasAlpha = true;
  210. babylonTexture.getAlphaFromRGB = (texture.AlphaSource == 2) || (texture.AlphaSource == 3); // 'RGB intensity' or 'None (Opaque)'
  211. }
  212. else
  213. {
  214. babylonTexture.hasAlpha = (texture.AlphaSource != 3); // Not 'None (Opaque)'
  215. babylonTexture.getAlphaFromRGB = (texture.AlphaSource == 2); // 'RGB intensity'
  216. }
  217. // UVs
  218. var uvGen = _exportUV(texture, babylonTexture);
  219. // Animations
  220. var animations = new List<BabylonAnimation>();
  221. ExportFloatAnimation("uOffset", animations, key => new[] { uvGen.GetUOffs(key) });
  222. ExportFloatAnimation("vOffset", animations, key => new[] { -uvGen.GetVOffs(key) });
  223. ExportFloatAnimation("uScale", animations, key => new[] { uvGen.GetUScl(key) });
  224. ExportFloatAnimation("vScale", animations, key => new[] { uvGen.GetVScl(key) });
  225. ExportFloatAnimation("uAng", animations, key => new[] { uvGen.GetUAng(key) });
  226. ExportFloatAnimation("vAng", animations, key => new[] { uvGen.GetVAng(key) });
  227. ExportFloatAnimation("wAng", animations, key => new[] { uvGen.GetWAng(key) });
  228. babylonTexture.animations = animations.ToArray();
  229. // Is cube
  230. _exportIsCube(texture, babylonTexture, allowCube);
  231. // Copy texture to output
  232. var absolutePath = texture.Map.FullFilePath;
  233. try
  234. {
  235. if (File.Exists(absolutePath))
  236. {
  237. if (CopyTexturesToOutput)
  238. {
  239. File.Copy(absolutePath, Path.Combine(babylonScene.OutputPath, babylonTexture.name), true);
  240. }
  241. }
  242. }
  243. catch
  244. {
  245. // silently fails
  246. }
  247. return babylonTexture;
  248. }
  249. private ITexmap _exportFresnelParameters(ITexmap texMap, out BabylonFresnelParameters fresnelParameters)
  250. {
  251. fresnelParameters = null;
  252. // Fallout
  253. if (texMap.ClassName == "Falloff") // This is the only way I found to detect it. This is crappy but it works
  254. {
  255. RaiseMessage("fresnelParameters", 2);
  256. fresnelParameters = new BabylonFresnelParameters();
  257. var paramBlock = texMap.GetParamBlock(0);
  258. var color1 = paramBlock.GetColor(0, 0, 0);
  259. var color2 = paramBlock.GetColor(4, 0, 0);
  260. fresnelParameters.isEnabled = true;
  261. fresnelParameters.leftColor = color2.ToArray();
  262. fresnelParameters.rightColor = color1.ToArray();
  263. if (paramBlock.GetInt(8, 0, 0) == 2)
  264. {
  265. fresnelParameters.power = paramBlock.GetFloat(12, 0, 0);
  266. }
  267. else
  268. {
  269. fresnelParameters.power = 1;
  270. }
  271. var texMap1 = paramBlock.GetTexmap(2, 0, 0);
  272. var texMap1On = paramBlock.GetInt(3, 0, 0);
  273. var texMap2 = paramBlock.GetTexmap(6, 0, 0);
  274. var texMap2On = paramBlock.GetInt(7, 0, 0);
  275. if (texMap1 != null && texMap1On != 0)
  276. {
  277. texMap = texMap1;
  278. fresnelParameters.rightColor = new float[] { 1, 1, 1 };
  279. if (texMap2 != null && texMap2On != 0)
  280. {
  281. RaiseWarning(string.Format("You cannot specify two textures for falloff. Only one is supported"), 2);
  282. }
  283. }
  284. else if (texMap2 != null && texMap2On != 0)
  285. {
  286. fresnelParameters.leftColor = new float[] { 1, 1, 1 };
  287. texMap = texMap2;
  288. }
  289. else
  290. {
  291. return null;
  292. }
  293. }
  294. return texMap;
  295. }
  296. private IStdUVGen _exportUV(IBitmapTex texture, BabylonTexture babylonTexture)
  297. {
  298. var uvGen = texture.UVGen;
  299. switch (uvGen.GetCoordMapping(0))
  300. {
  301. case 1: //MAP_SPHERICAL
  302. babylonTexture.coordinatesMode = 1;
  303. break;
  304. case 2: //MAP_PLANAR
  305. babylonTexture.coordinatesMode = 2;
  306. break;
  307. default:
  308. babylonTexture.coordinatesMode = 0;
  309. break;
  310. }
  311. babylonTexture.coordinatesIndex = uvGen.MapChannel - 1;
  312. if (uvGen.MapChannel > 2)
  313. {
  314. RaiseWarning(string.Format("Unsupported map channel, Only channel 1 and 2 are supported."), 2);
  315. }
  316. babylonTexture.uOffset = uvGen.GetUOffs(0);
  317. babylonTexture.vOffset = uvGen.GetVOffs(0);
  318. babylonTexture.uScale = uvGen.GetUScl(0);
  319. babylonTexture.vScale = uvGen.GetVScl(0);
  320. if (Path.GetExtension(texture.MapName).ToLower() == ".dds")
  321. {
  322. babylonTexture.vScale *= -1; // Need to invert Y-axis for DDS texture
  323. }
  324. babylonTexture.uAng = uvGen.GetUAng(0);
  325. babylonTexture.vAng = uvGen.GetVAng(0);
  326. babylonTexture.wAng = uvGen.GetWAng(0);
  327. babylonTexture.wrapU = BabylonTexture.AddressMode.CLAMP_ADDRESSMODE; // CLAMP
  328. if ((uvGen.TextureTiling & 1) != 0) // WRAP
  329. {
  330. babylonTexture.wrapU = BabylonTexture.AddressMode.WRAP_ADDRESSMODE;
  331. }
  332. else if ((uvGen.TextureTiling & 4) != 0) // MIRROR
  333. {
  334. babylonTexture.wrapU = BabylonTexture.AddressMode.MIRROR_ADDRESSMODE;
  335. }
  336. babylonTexture.wrapV = BabylonTexture.AddressMode.CLAMP_ADDRESSMODE; // CLAMP
  337. if ((uvGen.TextureTiling & 2) != 0) // WRAP
  338. {
  339. babylonTexture.wrapV = BabylonTexture.AddressMode.WRAP_ADDRESSMODE;
  340. }
  341. else if ((uvGen.TextureTiling & 8) != 0) // MIRROR
  342. {
  343. babylonTexture.wrapV = BabylonTexture.AddressMode.MIRROR_ADDRESSMODE;
  344. }
  345. return uvGen;
  346. }
  347. private void _exportIsCube(IBitmapTex texture, BabylonTexture babylonTexture, bool allowCube)
  348. {
  349. var absolutePath = texture.Map.FullFilePath;
  350. try
  351. {
  352. if (File.Exists(absolutePath))
  353. {
  354. babylonTexture.isCube = _isTextureCube(absolutePath);
  355. }
  356. else
  357. {
  358. RaiseWarning(string.Format("Texture {0} not found.", babylonTexture.name), 2);
  359. }
  360. }
  361. catch
  362. {
  363. // silently fails
  364. }
  365. if (babylonTexture.isCube && !allowCube)
  366. {
  367. RaiseWarning(string.Format("Cube texture are only supported for reflection channel"), 2);
  368. }
  369. }
  370. private bool _isTextureCube(string filepath)
  371. {
  372. try
  373. {
  374. if (Path.GetExtension(filepath).ToLower() != ".dds")
  375. {
  376. return false;
  377. }
  378. var data = File.ReadAllBytes(filepath);
  379. var intArray = new int[data.Length / 4];
  380. Buffer.BlockCopy(data, 0, intArray, 0, intArray.Length * 4);
  381. int width = intArray[4];
  382. int height = intArray[3];
  383. int mipmapsCount = intArray[7];
  384. if ((width >> (mipmapsCount - 1)) > 1)
  385. {
  386. var expected = 1;
  387. var currentSize = Math.Max(width, height);
  388. while (currentSize > 1)
  389. {
  390. currentSize = currentSize >> 1;
  391. expected++;
  392. }
  393. RaiseWarning(string.Format("Mipmaps chain is not complete: {0} maps instead of {1} (based on texture max size: {2})", mipmapsCount, expected, width), 2);
  394. RaiseWarning(string.Format("You must generate a complete mipmaps chain for .dds)"), 2);
  395. RaiseWarning(string.Format("Mipmaps will be disabled for this texture. If you want automatic texture generation you cannot use a .dds)"), 2);
  396. }
  397. bool isCube = (intArray[28] & 0x200) == 0x200;
  398. return isCube;
  399. }
  400. catch
  401. {
  402. return false;
  403. }
  404. }
  405. // -------------------------
  406. // --------- Utils ---------
  407. // -------------------------
  408. private IBitmapTex _getBitmapTex(ITexmap texMap)
  409. {
  410. if (texMap == null || texMap.GetParamBlock(0) == null || texMap.GetParamBlock(0).Owner == null)
  411. {
  412. return null;
  413. }
  414. return texMap.GetParamBlock(0).Owner as IBitmapTex;
  415. }
  416. private ITexmap _getTexMap(IIGameMaterial materialNode, int index)
  417. {
  418. ITexmap texMap = null;
  419. if (materialNode.MaxMaterial.SubTexmapOn(index) == 1)
  420. {
  421. texMap = materialNode.MaxMaterial.GetSubTexmap(index);
  422. // No warning displayed because by default, physical material in 3ds Max have all maps on
  423. // Would be tedious for the user to uncheck all unused maps
  424. //if (texMap == null)
  425. //{
  426. // RaiseWarning("Texture channel " + index + " activated but no texture found.", 2);
  427. //}
  428. }
  429. return texMap;
  430. }
  431. private bool _getMinimalBitmapDimensions(out int width, out int height, params Bitmap[] bitmaps)
  432. {
  433. var haveSameDimensions = true;
  434. var bitmapsNoNull = ((new List<Bitmap>(bitmaps)).FindAll(bitmap => bitmap != null)).ToArray();
  435. if (bitmapsNoNull.Length > 0)
  436. {
  437. // Init with first element
  438. width = bitmapsNoNull[0].Width;
  439. height = bitmapsNoNull[0].Height;
  440. // Update with others
  441. for (int i = 1; i < bitmapsNoNull.Length; i++)
  442. {
  443. var bitmap = bitmapsNoNull[i];
  444. if (width != bitmap.Width || height != bitmap.Height)
  445. {
  446. haveSameDimensions = false;
  447. }
  448. width = Math.Min(width, bitmap.Width);
  449. height = Math.Min(height, bitmap.Height);
  450. }
  451. }
  452. else
  453. {
  454. width = 0;
  455. height = 0;
  456. }
  457. return haveSameDimensions;
  458. }
  459. private Bitmap LoadTexture(string absolutePath)
  460. {
  461. if (File.Exists(absolutePath))
  462. {
  463. return new Bitmap(absolutePath);
  464. }
  465. else
  466. {
  467. RaiseWarning(string.Format("Texture {0} not found.", Path.GetFileName(absolutePath), 2));
  468. return null;
  469. }
  470. }
  471. private Bitmap _loadTexture(ITexmap texMap)
  472. {
  473. if (texMap == null || texMap.GetParamBlock(0) == null || texMap.GetParamBlock(0).Owner == null)
  474. {
  475. return null;
  476. }
  477. var texture = texMap.GetParamBlock(0).Owner as IBitmapTex;
  478. if (texture == null)
  479. {
  480. return null;
  481. }
  482. return LoadTexture(texture.Map.FullFilePath);
  483. }
  484. }
  485. }