MB2_TextureBakeResults.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. using UnityEngine;
  2. using System;
  3. using System.Text;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using DigitalOpus.MB.Core;
  7. using UnityEngine.Serialization;
  8. /// <summary>
  9. /// Used internally during the material baking process
  10. /// </summary>
  11. [Serializable]
  12. public class MB_AtlasesAndRects{
  13. public Texture2D[] atlases;
  14. [NonSerialized]
  15. public List<MB_MaterialAndUVRect> mat2rect_map;
  16. public string[] texPropertyNames;
  17. }
  18. [System.Serializable]
  19. public class MB_MultiMaterial{
  20. public Material combinedMaterial;
  21. public bool considerMeshUVs;
  22. public List<Material> sourceMaterials = new List<Material>();
  23. }
  24. [System.Serializable]
  25. public class MB_MaterialAndUVRect
  26. {
  27. /// <summary>
  28. /// The source material that was baked into the atlas.
  29. /// </summary>
  30. public Material material;
  31. /// <summary>
  32. /// The rectangle in the atlas where the texture (including all tiling) was copied to.
  33. /// </summary>
  34. public Rect atlasRect;
  35. /// <summary>
  36. /// For debugging. The name of the first srcObj that uses this MaterialAndUVRect.
  37. /// </summary>
  38. public string srcObjName;
  39. public bool allPropsUseSameTiling = true;
  40. /// <summary>
  41. /// Only valid if allPropsUseSameTiling = true. Else should be 0,0,0,0
  42. /// The material tiling on the source material
  43. /// </summary>
  44. [FormerlySerializedAs("sourceMaterialTiling")]
  45. public Rect allPropsUseSameTiling_sourceMaterialTiling;
  46. /// <summary>
  47. /// Only valid if allPropsUseSameTiling = true. Else should be 0,0,0,0
  48. /// The encapsulating sampling rect that was used to sample for the atlas. Note that the case
  49. /// of dont-considerMeshUVs is the same as do-considerMeshUVs where the uv rect is 0,0,1,1
  50. /// </summary>
  51. [FormerlySerializedAs("samplingEncapsulatinRect")]
  52. public Rect allPropsUseSameTiling_samplingEncapsulatinRect;
  53. /// <summary>
  54. /// Only valid if allPropsUseSameTiling = false.
  55. /// The UVrect of the source mesh that was baked. We are using a trick here.
  56. /// Instead of storing the material tiling for each
  57. /// texture property here, we instead bake all those tilings into the atlases and here we pretend
  58. /// that all those tilings were 0,0,1,1. Then all we need is to store is the
  59. /// srcUVsamplingRect
  60. /// </summary>
  61. public Rect propsUseDifferntTiling_srcUVsamplingRect;
  62. /// <summary>
  63. /// The tilling type for this rectangle in the atlas.
  64. /// </summary>
  65. public MB_TextureTilingTreatment tilingTreatment = MB_TextureTilingTreatment.unknown;
  66. /// <param name="mat">The Material</param>
  67. /// <param name="destRect">The rect in the atlas this material maps to</param>
  68. /// <param name="allPropsUseSameTiling">If true then use sourceMaterialTiling and samplingEncapsulatingRect.
  69. /// if false then use srcUVsamplingRect. None used values should be 0,0,0,0.</param>
  70. /// <param name="sourceMaterialTiling">allPropsUseSameTiling_sourceMaterialTiling</param>
  71. /// <param name="samplingEncapsulatingRect">allPropsUseSameTiling_samplingEncapsulatinRect</param>
  72. /// <param name="srcUVsamplingRect">propsUseDifferntTiling_srcUVsamplingRect</param>
  73. public MB_MaterialAndUVRect(Material mat,
  74. Rect destRect,
  75. bool allPropsUseSameTiling,
  76. Rect sourceMaterialTiling,
  77. Rect samplingEncapsulatingRect,
  78. Rect srcUVsamplingRect,
  79. MB_TextureTilingTreatment treatment,
  80. string objName)
  81. {
  82. if (allPropsUseSameTiling)
  83. {
  84. Debug.Assert(srcUVsamplingRect == new Rect(0, 0, 0, 0));
  85. }
  86. if (!allPropsUseSameTiling) {
  87. Debug.Assert(samplingEncapsulatingRect == new Rect(0, 0, 0, 0));
  88. Debug.Assert(sourceMaterialTiling == new Rect(0, 0, 0, 0));
  89. }
  90. material = mat;
  91. atlasRect = destRect;
  92. tilingTreatment = treatment;
  93. this.allPropsUseSameTiling = allPropsUseSameTiling;
  94. allPropsUseSameTiling_sourceMaterialTiling = sourceMaterialTiling;
  95. allPropsUseSameTiling_samplingEncapsulatinRect = samplingEncapsulatingRect;
  96. propsUseDifferntTiling_srcUVsamplingRect = srcUVsamplingRect;
  97. srcObjName = objName;
  98. }
  99. public override int GetHashCode()
  100. {
  101. return material.GetInstanceID() ^ allPropsUseSameTiling_samplingEncapsulatinRect.GetHashCode() ^ propsUseDifferntTiling_srcUVsamplingRect.GetHashCode();
  102. }
  103. public override bool Equals(object obj)
  104. {
  105. if (!(obj is MB_MaterialAndUVRect)) return false;
  106. MB_MaterialAndUVRect b = (MB_MaterialAndUVRect)obj;
  107. return material == b.material &&
  108. allPropsUseSameTiling_samplingEncapsulatinRect == b.allPropsUseSameTiling_samplingEncapsulatinRect &&
  109. allPropsUseSameTiling_sourceMaterialTiling == b.allPropsUseSameTiling_sourceMaterialTiling &&
  110. allPropsUseSameTiling == b.allPropsUseSameTiling &&
  111. propsUseDifferntTiling_srcUVsamplingRect == b.propsUseDifferntTiling_srcUVsamplingRect;
  112. }
  113. public Rect GetEncapsulatingRect()
  114. {
  115. if (allPropsUseSameTiling)
  116. {
  117. return allPropsUseSameTiling_samplingEncapsulatinRect;
  118. }
  119. else
  120. {
  121. return propsUseDifferntTiling_srcUVsamplingRect;
  122. }
  123. }
  124. public Rect GetMaterialTilingRect()
  125. {
  126. if (allPropsUseSameTiling)
  127. {
  128. return allPropsUseSameTiling_sourceMaterialTiling;
  129. }
  130. else
  131. {
  132. return new Rect(0, 0, 1, 1);
  133. }
  134. }
  135. }
  136. /// <summary>
  137. /// This class stores the results from an MB2_TextureBaker when materials are combined into atlases. It stores
  138. /// a list of materials and the corresponding UV rectangles in the atlases. It also stores the configuration
  139. /// options that were used to generate the combined material.
  140. ///
  141. /// It can be saved as an asset in the project so that textures can be baked in one scene and used in another.
  142. /// </summary>
  143. public class MB2_TextureBakeResults : ScriptableObject {
  144. public static int VERSION { get{ return 3252; }}
  145. public int version;
  146. public MB_MaterialAndUVRect[] materialsAndUVRects;
  147. public MB_MultiMaterial[] resultMaterials;
  148. public bool doMultiMaterial;
  149. public MB2_TextureBakeResults()
  150. {
  151. version = VERSION;
  152. }
  153. private void OnEnable()
  154. {
  155. // backward compatibility copy depricated fixOutOfBounds values to resultMaterials
  156. if (version < 3251)
  157. {
  158. for (int i = 0; i < materialsAndUVRects.Length; i++)
  159. {
  160. materialsAndUVRects[i].allPropsUseSameTiling = true;
  161. }
  162. }
  163. version = VERSION;
  164. }
  165. /// <summary>
  166. /// Creates for materials on renderer.
  167. /// </summary>
  168. /// <returns>Generates an MB2_TextureBakeResult that can be used if all objects to be combined use the same material.
  169. /// Returns a MB2_TextureBakeResults that will map all materials used by renderer r to
  170. /// the rectangle 0,0..1,1 in the atlas.</returns>
  171. /// <param name="r">The red component.</param>
  172. public static MB2_TextureBakeResults CreateForMaterialsOnRenderer(GameObject[] gos, List<Material> matsOnTargetRenderer)
  173. {
  174. HashSet<Material> fullMaterialList = new HashSet<Material>(matsOnTargetRenderer);
  175. for (int i = 0; i < gos.Length; i++)
  176. {
  177. if (gos[i] == null)
  178. {
  179. Debug.LogError(string.Format("Game object {0} in list of objects to add was null", i));
  180. return null;
  181. }
  182. Material[] oMats = MB_Utility.GetGOMaterials(gos[i]);
  183. if (oMats.Length == 0)
  184. {
  185. Debug.LogError(string.Format("Game object {0} in list of objects to add no renderer", i));
  186. return null;
  187. }
  188. for (int j = 0; j < oMats.Length; j++)
  189. {
  190. if (!fullMaterialList.Contains(oMats[j])) { fullMaterialList.Add(oMats[j]); }
  191. }
  192. }
  193. Material[] rms = new Material[fullMaterialList.Count];
  194. fullMaterialList.CopyTo(rms);
  195. MB2_TextureBakeResults tbr = (MB2_TextureBakeResults) ScriptableObject.CreateInstance( typeof(MB2_TextureBakeResults) );
  196. List<MB_MaterialAndUVRect> mss = new List<MB_MaterialAndUVRect>();
  197. for (int i = 0; i < rms.Length; i++)
  198. {
  199. if (rms[i] != null)
  200. {
  201. MB_MaterialAndUVRect matAndUVRect = new MB_MaterialAndUVRect(rms[i], new Rect(0f, 0f, 1f, 1f), true, new Rect(0f,0f,1f,1f), new Rect(0f,0f,1f,1f), new Rect(0,0,0,0), MB_TextureTilingTreatment.none, "");
  202. if (!mss.Contains(matAndUVRect))
  203. {
  204. mss.Add(matAndUVRect);
  205. }
  206. }
  207. }
  208. tbr.resultMaterials = new MB_MultiMaterial[mss.Count];
  209. for (int i = 0; i < mss.Count; i++){
  210. tbr.resultMaterials[i] = new MB_MultiMaterial();
  211. List<Material> sourceMats = new List<Material>();
  212. sourceMats.Add (mss[i].material);
  213. tbr.resultMaterials[i].sourceMaterials = sourceMats;
  214. tbr.resultMaterials[i].combinedMaterial = mss[i].material;
  215. tbr.resultMaterials[i].considerMeshUVs = false;
  216. }
  217. if (rms.Length == 1)
  218. {
  219. tbr.doMultiMaterial = false;
  220. } else
  221. {
  222. tbr.doMultiMaterial = true;
  223. }
  224. tbr.materialsAndUVRects = mss.ToArray();
  225. return tbr;
  226. }
  227. public bool DoAnyResultMatsUseConsiderMeshUVs()
  228. {
  229. if (resultMaterials == null) return false;
  230. for (int i = 0; i < resultMaterials.Length; i++)
  231. {
  232. if (resultMaterials[i].considerMeshUVs) return true;
  233. }
  234. return false;
  235. }
  236. public bool ContainsMaterial(Material m)
  237. {
  238. for (int i = 0; i < materialsAndUVRects.Length; i++)
  239. {
  240. if (materialsAndUVRects[i].material == m){
  241. return true;
  242. }
  243. }
  244. return false;
  245. }
  246. public string GetDescription(){
  247. StringBuilder sb = new StringBuilder();
  248. sb.Append("Shaders:\n");
  249. HashSet<Shader> shaders = new HashSet<Shader>();
  250. if (materialsAndUVRects != null){
  251. for (int i = 0; i < materialsAndUVRects.Length; i++){
  252. if (materialsAndUVRects[i].material != null)
  253. {
  254. shaders.Add(materialsAndUVRects[i].material.shader);
  255. }
  256. }
  257. }
  258. foreach(Shader m in shaders){
  259. sb.Append(" ").Append(m.name).AppendLine();
  260. }
  261. sb.Append("Materials:\n");
  262. if (materialsAndUVRects != null){
  263. for (int i = 0; i < materialsAndUVRects.Length; i++){
  264. if (materialsAndUVRects[i].material != null)
  265. {
  266. sb.Append(" ").Append(materialsAndUVRects[i].material.name).AppendLine();
  267. }
  268. }
  269. }
  270. return sb.ToString();
  271. }
  272. public class Material2AtlasRectangleMapper
  273. {
  274. MB2_TextureBakeResults tbr;
  275. int[] numTimesMatAppearsInAtlas;
  276. MB_MaterialAndUVRect[] matsAndSrcUVRect;
  277. public Material2AtlasRectangleMapper(MB2_TextureBakeResults res)
  278. {
  279. tbr = res;
  280. matsAndSrcUVRect = res.materialsAndUVRects;
  281. //count the number of times a material appears in the atlas. used for fast lookup
  282. numTimesMatAppearsInAtlas = new int[matsAndSrcUVRect.Length];
  283. for (int i = 0; i < matsAndSrcUVRect.Length; i++)
  284. {
  285. if (numTimesMatAppearsInAtlas[i] > 1)
  286. {
  287. continue;
  288. }
  289. int count = 1;
  290. for (int j = i + 1; j < matsAndSrcUVRect.Length; j++)
  291. {
  292. if (matsAndSrcUVRect[i].material == matsAndSrcUVRect[j].material)
  293. {
  294. count++;
  295. }
  296. }
  297. numTimesMatAppearsInAtlas[i] = count;
  298. if (count > 1)
  299. {
  300. //allMatsAreUnique = false;
  301. for (int j = i + 1; j < matsAndSrcUVRect.Length; j++)
  302. {
  303. if (matsAndSrcUVRect[i].material == matsAndSrcUVRect[j].material)
  304. {
  305. numTimesMatAppearsInAtlas[j] = count;
  306. }
  307. }
  308. }
  309. }
  310. }
  311. /// <summary>
  312. /// A material can appear more than once in an atlas if using fixOutOfBoundsUVs.
  313. /// in this case you need to use the UV rect of the mesh to find the correct rectangle.
  314. /// If the all properties on the mat use the same tiling then
  315. /// encapsulatingRect can be larger and will include baked UV and material tiling
  316. /// If mat uses different tiling for different maps then encapsulatingRect is the uvs of
  317. /// source mesh used to bake atlas and sourceMaterialTilingOut is 0,0,1,1. This works because
  318. /// material tiling was baked into the atlas.
  319. /// </summary>
  320. public bool TryMapMaterialToUVRect(Material mat, Mesh m, int submeshIdx, int idxInResultMats, MB3_MeshCombinerSingle.MeshChannelsCache meshChannelCache, Dictionary<int, MB_Utility.MeshAnalysisResult[]> meshAnalysisCache,
  321. out MB_TextureTilingTreatment tilingTreatment,
  322. out Rect rectInAtlas,
  323. out Rect encapsulatingRectOut,
  324. out Rect sourceMaterialTilingOut,
  325. ref String errorMsg,
  326. MB2_LogLevel logLevel)
  327. {
  328. if (tbr.version < VERSION)
  329. {
  330. UpgradeToCurrentVersion(tbr);
  331. }
  332. tilingTreatment = MB_TextureTilingTreatment.unknown;
  333. if (tbr.materialsAndUVRects.Length == 0)
  334. {
  335. errorMsg = "The 'Texture Bake Result' needs to be re-baked to be compatible with this version of Mesh Baker. Please re-bake using the MB3_TextureBaker.";
  336. rectInAtlas = new Rect();
  337. encapsulatingRectOut = new Rect();
  338. sourceMaterialTilingOut = new Rect();
  339. return false;
  340. }
  341. if (mat == null)
  342. {
  343. rectInAtlas = new Rect();
  344. encapsulatingRectOut = new Rect();
  345. sourceMaterialTilingOut = new Rect();
  346. errorMsg = String.Format("Mesh {0} Had no material on submesh {1} cannot map to a material in the atlas", m.name, submeshIdx);
  347. return false;
  348. }
  349. if (submeshIdx >= m.subMeshCount)
  350. {
  351. errorMsg = "Submesh index is greater than the number of submeshes";
  352. rectInAtlas = new Rect();
  353. encapsulatingRectOut = new Rect();
  354. sourceMaterialTilingOut = new Rect();
  355. return false;
  356. }
  357. //find the first index of this material
  358. int idx = -1;
  359. for (int i = 0; i < matsAndSrcUVRect.Length; i++)
  360. {
  361. if (mat == matsAndSrcUVRect[i].material)
  362. {
  363. idx = i;
  364. break;
  365. }
  366. }
  367. // if couldn't find material
  368. if (idx == -1)
  369. {
  370. rectInAtlas = new Rect();
  371. encapsulatingRectOut = new Rect();
  372. sourceMaterialTilingOut = new Rect();
  373. errorMsg = String.Format("Material {0} could not be found in the Texture Bake Result", mat.name);
  374. return false;
  375. }
  376. if (!tbr.resultMaterials[idxInResultMats].considerMeshUVs)
  377. {
  378. if (numTimesMatAppearsInAtlas[idx] != 1)
  379. {
  380. Debug.LogError("There is a problem with this TextureBakeResults. FixOutOfBoundsUVs is false and a material appears more than once.");
  381. }
  382. MB_MaterialAndUVRect mr = matsAndSrcUVRect[idx];
  383. rectInAtlas = mr.atlasRect;
  384. tilingTreatment = mr.tilingTreatment;
  385. encapsulatingRectOut = mr.GetEncapsulatingRect();
  386. sourceMaterialTilingOut = mr.GetMaterialTilingRect();
  387. return true;
  388. }
  389. else
  390. {
  391. //todo what if no UVs
  392. //Find UV rect in source mesh
  393. MB_Utility.MeshAnalysisResult[] mar;
  394. if (!meshAnalysisCache.TryGetValue(m.GetInstanceID(), out mar))
  395. {
  396. mar = new MB_Utility.MeshAnalysisResult[m.subMeshCount];
  397. for (int j = 0; j < m.subMeshCount; j++)
  398. {
  399. Vector2[] uvss = meshChannelCache.GetUv0Raw(m);
  400. MB_Utility.hasOutOfBoundsUVs(uvss, m, ref mar[j], j);
  401. }
  402. meshAnalysisCache.Add(m.GetInstanceID(), mar);
  403. }
  404. //this could be a mesh that was not used in the texture baking that has huge UV tiling too big for the rect that was baked
  405. //find a record that has an atlas uvRect capable of containing this
  406. bool found = false;
  407. Rect encapsulatingRect = new Rect(0,0,0,0);
  408. Rect sourceMaterialTiling = new Rect(0,0,0,0);
  409. if (logLevel >= MB2_LogLevel.trace)
  410. {
  411. Debug.Log(String.Format("Trying to find a rectangle in atlas capable of holding tiled sampling rect for mesh {0} using material {1} meshUVrect={2}", m, mat, mar[submeshIdx].uvRect.ToString("f5")));
  412. }
  413. for (int i = idx; i < matsAndSrcUVRect.Length; i++)
  414. {
  415. MB_MaterialAndUVRect matAndUVrect = matsAndSrcUVRect[i];
  416. if (matAndUVrect.material == mat)
  417. {
  418. if (matAndUVrect.allPropsUseSameTiling)
  419. {
  420. encapsulatingRect = matAndUVrect.allPropsUseSameTiling_samplingEncapsulatinRect;
  421. sourceMaterialTiling = matAndUVrect.allPropsUseSameTiling_sourceMaterialTiling;
  422. }
  423. else
  424. {
  425. encapsulatingRect = matAndUVrect.propsUseDifferntTiling_srcUVsamplingRect;
  426. sourceMaterialTiling = new Rect(0, 0, 1, 1);
  427. }
  428. if (IsMeshAndMaterialRectEnclosedByAtlasRect(
  429. matAndUVrect.tilingTreatment,
  430. mar[submeshIdx].uvRect,
  431. sourceMaterialTiling,
  432. encapsulatingRect,
  433. logLevel))
  434. {
  435. if (logLevel >= MB2_LogLevel.trace)
  436. {
  437. Debug.Log("Found rect in atlas capable of containing tiled sampling rect for mesh " + m + " at idx=" + i);
  438. }
  439. idx = i;
  440. found = true;
  441. break;
  442. }
  443. }
  444. }
  445. if (found)
  446. {
  447. MB_MaterialAndUVRect mr = matsAndSrcUVRect[idx];
  448. rectInAtlas = mr.atlasRect;
  449. tilingTreatment = mr.tilingTreatment;
  450. encapsulatingRectOut = mr.GetEncapsulatingRect();
  451. sourceMaterialTilingOut = mr.GetMaterialTilingRect();
  452. return true;
  453. }
  454. else
  455. {
  456. rectInAtlas = new Rect();
  457. encapsulatingRectOut = new Rect();
  458. sourceMaterialTilingOut = new Rect();
  459. errorMsg = String.Format("Could not find a tiled rectangle in the atlas capable of containing the uv and material tiling on mesh {0} for material {1}. Was this mesh included when atlases were baked?", m.name, mat);
  460. return false;
  461. }
  462. }
  463. }
  464. private void UpgradeToCurrentVersion(MB2_TextureBakeResults tbr)
  465. {
  466. if (tbr.version < 3252)
  467. {
  468. for (int i = 0; i < tbr.materialsAndUVRects.Length; i++)
  469. {
  470. tbr.materialsAndUVRects[i].allPropsUseSameTiling = true;
  471. }
  472. }
  473. }
  474. }
  475. public static bool IsMeshAndMaterialRectEnclosedByAtlasRect(MB_TextureTilingTreatment tilingTreatment, Rect uvR, Rect sourceMaterialTiling, Rect samplingEncapsulatinRect, MB2_LogLevel logLevel)
  476. {
  477. Rect potentialRect = new Rect();
  478. // test to see if this would fit in what was baked in the atlas
  479. potentialRect = MB3_UVTransformUtility.CombineTransforms(ref uvR, ref sourceMaterialTiling);
  480. if (logLevel >= MB2_LogLevel.trace)
  481. {
  482. if (logLevel >= MB2_LogLevel.trace) Debug.Log("IsMeshAndMaterialRectEnclosedByAtlasRect Rect in atlas uvR=" + uvR.ToString("f5") + " sourceMaterialTiling=" + sourceMaterialTiling.ToString("f5") + "Potential Rect (must fit in encapsulating) " + potentialRect.ToString("f5") + " encapsulating=" + samplingEncapsulatinRect.ToString("f5") + " tilingTreatment=" + tilingTreatment);
  483. }
  484. if (tilingTreatment == MB_TextureTilingTreatment.edgeToEdgeX)
  485. {
  486. if (MB3_UVTransformUtility.LineSegmentContainsShifted(samplingEncapsulatinRect.y, samplingEncapsulatinRect.height, potentialRect.y, potentialRect.height))
  487. {
  488. return true;
  489. }
  490. }
  491. else if (tilingTreatment == MB_TextureTilingTreatment.edgeToEdgeY)
  492. {
  493. if (MB3_UVTransformUtility.LineSegmentContainsShifted(samplingEncapsulatinRect.x, samplingEncapsulatinRect.width, potentialRect.x, potentialRect.width))
  494. {
  495. return true;
  496. }
  497. }
  498. else if (tilingTreatment == MB_TextureTilingTreatment.edgeToEdgeXY)
  499. {
  500. //only one rect in atlas and is edge to edge in both X and Y directions.
  501. return true;
  502. }
  503. else
  504. {
  505. if (MB3_UVTransformUtility.RectContainsShifted(ref samplingEncapsulatinRect, ref potentialRect))
  506. {
  507. return true;
  508. }
  509. }
  510. return false;
  511. }
  512. }