EditorAnimInfo.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Text;
  6. using bs;
  7. namespace bs.Editor
  8. {
  9. /** @addtogroup AnimationEditor
  10. * @{
  11. */
  12. /// <summary>
  13. /// A set of animation curves for a field of a certain type.
  14. /// </summary>
  15. internal struct FieldAnimCurves
  16. {
  17. public SerializableProperty.FieldType type;
  18. public EdCurveDrawInfo[] curveInfos;
  19. public bool isPropertyCurve;
  20. }
  21. /// <summary>
  22. /// Stores tangent modes for an 3D vector animation curve (one mode for each keyframe).
  23. /// </summary>
  24. [SerializeObject]
  25. internal class EditorVector3CurveTangents
  26. {
  27. public string name;
  28. public TangentMode[] tangentsX;
  29. public TangentMode[] tangentsY;
  30. public TangentMode[] tangentsZ;
  31. }
  32. /// <summary>
  33. /// Stores tangent modes for an float animation curve (one mode for each keyframe).
  34. /// </summary>
  35. [SerializeObject]
  36. internal class EditorFloatCurveTangents
  37. {
  38. public string name;
  39. public TangentMode[] tangents;
  40. }
  41. /// <summary>
  42. /// Stores tangent information for all curves in an animation clip.
  43. /// </summary>
  44. [SerializeObject]
  45. internal class EditorAnimClipTangents
  46. {
  47. public EditorVector3CurveTangents[] positionCurves;
  48. public EditorVector3CurveTangents[] rotationCurves;
  49. public EditorVector3CurveTangents[] scaleCurves;
  50. public EditorFloatCurveTangents[] floatCurves;
  51. }
  52. /// <summary>
  53. /// Contains editor-only data for a specific <see cref="AnimationClip"/>.
  54. /// </summary>
  55. [SerializeObject]
  56. internal class EditorAnimClipData
  57. {
  58. public NamedVector3Curve[] eulerCurves;
  59. public EditorAnimClipTangents tangents;
  60. }
  61. /// <summary>
  62. /// Stores animation clip data for clips that are currently being edited.
  63. /// </summary>
  64. internal class EditorAnimClipInfo
  65. {
  66. public AnimationClip clip;
  67. public bool isImported;
  68. public int sampleRate;
  69. public Dictionary<string, FieldAnimCurves> curves = new Dictionary<string, FieldAnimCurves>();
  70. public AnimationEvent[] events = new AnimationEvent[0];
  71. /// <summary>
  72. /// Loads curve and event information from the provided clip, and creates a new instance of this object containing
  73. /// the required data for editing the source clip in the animation editor.
  74. /// </summary>
  75. /// <param name="clip">Clip to load.</param>
  76. /// <returns>Editor specific editable information about an animation clip.</returns>
  77. public static EditorAnimClipInfo Create(AnimationClip clip)
  78. {
  79. EditorAnimClipInfo clipInfo = new EditorAnimClipInfo();
  80. clipInfo.clip = clip;
  81. clipInfo.isImported = IsClipImported(clip);
  82. clipInfo.sampleRate = (int)clip.SampleRate;
  83. AnimationCurves clipCurves = clip.Curves;
  84. EditorAnimClipData editorClipData = null;
  85. EditorAnimClipData lGetAnimClipData(ResourceMeta meta)
  86. {
  87. object editorData = meta.EditorData;
  88. EditorAnimClipData output = editorData as EditorAnimClipData;
  89. if (output == null)
  90. {
  91. // Old editor data stores tangents only
  92. if (editorData is EditorAnimClipTangents tangents)
  93. {
  94. output = new EditorAnimClipData();
  95. output.tangents = tangents;
  96. }
  97. }
  98. return output;
  99. }
  100. string resourcePath = ProjectLibrary.GetPath(clip);
  101. if (!string.IsNullOrEmpty(resourcePath))
  102. {
  103. LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
  104. string clipName = PathEx.GetTail(resourcePath);
  105. if (entry != null && entry.Type == LibraryEntryType.File)
  106. {
  107. FileEntry fileEntry = (FileEntry)entry;
  108. ResourceMeta[] metas = fileEntry.ResourceMetas;
  109. if (clipInfo.isImported)
  110. {
  111. for (int i = 0; i < metas.Length; i++)
  112. {
  113. if (clipName == metas[i].SubresourceName)
  114. {
  115. editorClipData = lGetAnimClipData(metas[i]);
  116. break;
  117. }
  118. }
  119. }
  120. else
  121. {
  122. if (metas.Length > 0)
  123. editorClipData = lGetAnimClipData(metas[0]);
  124. }
  125. }
  126. }
  127. if (editorClipData == null)
  128. {
  129. editorClipData = new EditorAnimClipData();
  130. editorClipData.tangents = new EditorAnimClipTangents();
  131. }
  132. int globalCurveIdx = 0;
  133. void lLoadVector3Curve(NamedVector3Curve[] curves, EditorVector3CurveTangents[] tangents, string subPath)
  134. {
  135. foreach (var curveEntry in curves)
  136. {
  137. TangentMode[] tangentsX = null;
  138. TangentMode[] tangentsY = null;
  139. TangentMode[] tangentsZ = null;
  140. if (tangents != null)
  141. {
  142. foreach (var tangentEntry in tangents)
  143. {
  144. if (tangentEntry.name == curveEntry.name)
  145. {
  146. tangentsX = tangentEntry.tangentsX;
  147. tangentsY = tangentEntry.tangentsY;
  148. tangentsZ = tangentEntry.tangentsZ;
  149. break;
  150. }
  151. }
  152. }
  153. // Convert compound curve to three per-component curves
  154. AnimationCurve[] componentCurves = AnimationUtility.SplitCurve3D(curveEntry.curve);
  155. FieldAnimCurves fieldCurves = new FieldAnimCurves();
  156. fieldCurves.type = SerializableProperty.FieldType.Vector3;
  157. fieldCurves.curveInfos = new EdCurveDrawInfo[3];
  158. fieldCurves.isPropertyCurve = !clipInfo.isImported;
  159. fieldCurves.curveInfos[0] = new EdCurveDrawInfo();
  160. fieldCurves.curveInfos[0].curve = new EdAnimationCurve(componentCurves[0], tangentsX);
  161. fieldCurves.curveInfos[0].color = GetUniqueColor(globalCurveIdx++);
  162. fieldCurves.curveInfos[1] = new EdCurveDrawInfo();
  163. fieldCurves.curveInfos[1].curve = new EdAnimationCurve(componentCurves[1], tangentsY);
  164. fieldCurves.curveInfos[1].color = GetUniqueColor(globalCurveIdx++);
  165. fieldCurves.curveInfos[2] = new EdCurveDrawInfo();
  166. fieldCurves.curveInfos[2].curve = new EdAnimationCurve(componentCurves[2], tangentsZ);
  167. fieldCurves.curveInfos[2].color = GetUniqueColor(globalCurveIdx++);
  168. string curvePath = curveEntry.name.TrimEnd('/') + subPath;
  169. clipInfo.curves[curvePath] = fieldCurves;
  170. }
  171. };
  172. NamedQuaternionCurve[] rotationCurves = clipCurves.Rotation;
  173. if (editorClipData.eulerCurves == null || editorClipData.eulerCurves.Length != rotationCurves.Length)
  174. {
  175. // Convert rotation from quaternion to euler if we don't have original euler animation data stored.
  176. editorClipData.eulerCurves = new NamedVector3Curve[rotationCurves.Length];
  177. for (int i = 0; i < rotationCurves.Length; i++)
  178. {
  179. editorClipData.eulerCurves[i] = new NamedVector3Curve();
  180. editorClipData.eulerCurves[i].name = rotationCurves[i].name;
  181. editorClipData.eulerCurves[i].flags = rotationCurves[i].flags;
  182. editorClipData.eulerCurves[i].curve = AnimationUtility.QuaternionToEulerCurve(rotationCurves[i].curve);
  183. }
  184. }
  185. lLoadVector3Curve(clipCurves.Position, editorClipData.tangents.positionCurves, "/Position");
  186. lLoadVector3Curve(editorClipData.eulerCurves, editorClipData.tangents.rotationCurves, "/Rotation");
  187. lLoadVector3Curve(clipCurves.Scale, editorClipData.tangents.scaleCurves, "/Scale");
  188. // Find which individual float curves belong to the same field
  189. Dictionary<string, Tuple<int, int, bool>[]> floatCurveMapping = new Dictionary<string, Tuple<int, int, bool>[]>();
  190. {
  191. int curveIdx = 0;
  192. foreach (var curveEntry in clipCurves.Generic)
  193. {
  194. string path = curveEntry.name;
  195. string pathNoSuffix = null;
  196. string pathSuffix;
  197. if (path.Length >= 2)
  198. {
  199. pathSuffix = path.Substring(path.Length - 2, 2);
  200. pathNoSuffix = path.Substring(0, path.Length - 2);
  201. }
  202. else
  203. pathSuffix = "";
  204. int tangentIdx = -1;
  205. int currentTangentIdx = 0;
  206. foreach (var tangentEntry in editorClipData.tangents.floatCurves)
  207. {
  208. if (tangentEntry.name == curveEntry.name)
  209. {
  210. tangentIdx = currentTangentIdx;
  211. break;
  212. }
  213. currentTangentIdx++;
  214. }
  215. Animation.PropertySuffixInfo suffixInfo;
  216. if (Animation.PropertySuffixInfos.TryGetValue(pathSuffix, out suffixInfo))
  217. {
  218. Tuple<int, int, bool>[] curveInfo;
  219. if (!floatCurveMapping.TryGetValue(pathNoSuffix, out curveInfo))
  220. curveInfo = new Tuple<int, int, bool>[4];
  221. curveInfo[suffixInfo.elementIdx] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector);
  222. floatCurveMapping[pathNoSuffix] = curveInfo;
  223. }
  224. else
  225. {
  226. Tuple<int, int, bool>[] curveInfo = new Tuple<int, int, bool>[4];
  227. curveInfo[0] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector);
  228. floatCurveMapping[path] = curveInfo;
  229. }
  230. curveIdx++;
  231. }
  232. }
  233. foreach (var KVP in floatCurveMapping)
  234. {
  235. int numCurves = 0;
  236. for (int i = 0; i < 4; i++)
  237. {
  238. if (KVP.Value[i] == null)
  239. continue;
  240. numCurves++;
  241. }
  242. if (numCurves == 0)
  243. continue; // Invalid curve
  244. FieldAnimCurves fieldCurves = new FieldAnimCurves();
  245. // Deduce type (note that all single value types are assumed to be float even if their source type is int or bool)
  246. if (numCurves == 1)
  247. fieldCurves.type = SerializableProperty.FieldType.Float;
  248. else if (numCurves == 2)
  249. fieldCurves.type = SerializableProperty.FieldType.Vector2;
  250. else if (numCurves == 3)
  251. fieldCurves.type = SerializableProperty.FieldType.Vector3;
  252. else // 4 curves
  253. {
  254. bool isVector = KVP.Value[0].Item3;
  255. if (isVector)
  256. fieldCurves.type = SerializableProperty.FieldType.Vector4;
  257. else
  258. fieldCurves.type = SerializableProperty.FieldType.Color;
  259. }
  260. bool isMorphCurve = false;
  261. string curvePath = KVP.Key;
  262. fieldCurves.curveInfos = new EdCurveDrawInfo[numCurves];
  263. for (int i = 0; i < numCurves; i++)
  264. {
  265. int curveIdx = KVP.Value[i].Item1;
  266. int tangentIdx = KVP.Value[i].Item2;
  267. TangentMode[] tangents = null;
  268. if (tangentIdx != -1)
  269. tangents = editorClipData.tangents.floatCurves[tangentIdx].tangents;
  270. fieldCurves.curveInfos[i] = new EdCurveDrawInfo();
  271. fieldCurves.curveInfos[i].curve = new EdAnimationCurve(clipCurves.Generic[curveIdx].curve, tangents);
  272. fieldCurves.curveInfos[i].color = GetUniqueColor(globalCurveIdx++);
  273. if (clipCurves.Generic[curveIdx].flags.HasFlag(AnimationCurveFlags.MorphFrame))
  274. {
  275. curvePath = "MorphShapes/Frames/" + KVP.Key;
  276. isMorphCurve = true;
  277. }
  278. else if (clipCurves.Generic[curveIdx].flags.HasFlag(AnimationCurveFlags.MorphWeight))
  279. {
  280. curvePath = "MorphShapes/Weight/" + KVP.Key;
  281. isMorphCurve = true;
  282. }
  283. }
  284. fieldCurves.isPropertyCurve = !clipInfo.isImported && !isMorphCurve;
  285. clipInfo.curves[curvePath] = fieldCurves;
  286. }
  287. // Add events
  288. clipInfo.events = clip.Events;
  289. return clipInfo;
  290. }
  291. /// <summary>
  292. /// Checks is the specified animation clip is imported from an external file, or created within the editor.
  293. /// </summary>
  294. /// <param name="clip">Clip to check.</param>
  295. /// <returns>True if the clip is imported from an external file (e.g. FBX file), or false if the clip is a native
  296. /// resource created within the editor.</returns>
  297. public static bool IsClipImported(AnimationClip clip)
  298. {
  299. string resourcePath = ProjectLibrary.GetPath(clip);
  300. return ProjectLibrary.IsSubresource(resourcePath);
  301. }
  302. /// <summary>
  303. /// Checks does a curve with the specified path represent a curve affecting a morph shape.
  304. /// </summary>
  305. /// <param name="path">Path of the curve to check.</param>
  306. /// <returns>True if morph shape frame or weight animation, false otherwise.</returns>
  307. public static bool IsMorphShapeCurve(string path)
  308. {
  309. if (string.IsNullOrEmpty(path))
  310. return false;
  311. string trimmedPath = path.Trim('/');
  312. string[] entries = trimmedPath.Split('/');
  313. if (entries.Length < 3)
  314. return false;
  315. if (entries[entries.Length - 3] != "MorphShapes")
  316. return false;
  317. return entries[entries.Length - 2] == "Weight" || entries[entries.Length - 2] == "Frames";
  318. }
  319. /// <summary>
  320. /// Applies any changes made to the animation curves or events to the actual animation clip. Only works for
  321. /// non-imported animation clips.
  322. /// </summary>
  323. /// <param name="editorData">Additional animation clip data for use in editor.</param>
  324. public void Apply(out EditorAnimClipData editorData)
  325. {
  326. if (isImported || clip == null)
  327. {
  328. editorData = null;
  329. return;
  330. }
  331. List<NamedVector3Curve> positionCurves = new List<NamedVector3Curve>();
  332. List<NamedQuaternionCurve> rotationCurves = new List<NamedQuaternionCurve>();
  333. List<NamedVector3Curve> scaleCurves = new List<NamedVector3Curve>();
  334. List<NamedFloatCurve> floatCurves = new List<NamedFloatCurve>();
  335. List<EditorVector3CurveTangents> positionTangents = new List<EditorVector3CurveTangents>();
  336. List<EditorVector3CurveTangents> rotationTangents = new List<EditorVector3CurveTangents>();
  337. List<EditorVector3CurveTangents> scaleTangents = new List<EditorVector3CurveTangents>();
  338. List<EditorFloatCurveTangents> floatTangents = new List<EditorFloatCurveTangents>();
  339. List<NamedVector3Curve> eulerRotationCurves = new List<NamedVector3Curve>();
  340. foreach (var kvp in curves)
  341. {
  342. string[] pathEntries = kvp.Key.Split('/');
  343. if (pathEntries.Length == 0)
  344. continue;
  345. string lastEntry = pathEntries[pathEntries.Length - 1];
  346. if (lastEntry == "Position" || lastEntry == "Rotation" || lastEntry == "Scale")
  347. {
  348. StringBuilder sb = new StringBuilder();
  349. for (int i = 0; i < pathEntries.Length - 2; i++)
  350. sb.Append(pathEntries[i] + "/");
  351. if (pathEntries.Length > 1)
  352. sb.Append(pathEntries[pathEntries.Length - 2]);
  353. string curvePath = sb.ToString();
  354. NamedVector3Curve curve = new NamedVector3Curve();
  355. curve.name = curvePath;
  356. curve.curve = AnimationUtility.CombineCurve3D(new[]
  357. {
  358. new AnimationCurve(kvp.Value.curveInfos[0].curve.KeyFrames),
  359. new AnimationCurve(kvp.Value.curveInfos[1].curve.KeyFrames),
  360. new AnimationCurve(kvp.Value.curveInfos[2].curve.KeyFrames)
  361. });
  362. EditorVector3CurveTangents curveTangents = new EditorVector3CurveTangents();
  363. curveTangents.name = curvePath;
  364. curveTangents.tangentsX = kvp.Value.curveInfos[0].curve.TangentModes;
  365. curveTangents.tangentsY = kvp.Value.curveInfos[1].curve.TangentModes;
  366. curveTangents.tangentsZ = kvp.Value.curveInfos[2].curve.TangentModes;
  367. if (lastEntry == "Position")
  368. {
  369. positionCurves.Add(curve);
  370. positionTangents.Add(curveTangents);
  371. }
  372. else if (lastEntry == "Rotation")
  373. {
  374. NamedQuaternionCurve quatCurve = new NamedQuaternionCurve();
  375. quatCurve.name = curve.name;
  376. quatCurve.curve = AnimationUtility.EulerToQuaternionCurve(curve.curve);
  377. rotationCurves.Add(quatCurve);
  378. eulerRotationCurves.Add(curve);
  379. rotationTangents.Add(curveTangents);
  380. }
  381. else if (lastEntry == "Scale")
  382. {
  383. scaleCurves.Add(curve);
  384. scaleTangents.Add(curveTangents);
  385. }
  386. }
  387. else
  388. {
  389. Action<int, string, string, AnimationCurveFlags> addCurve = (idx, path, subPath, flags) =>
  390. {
  391. string fullPath = path + subPath;
  392. NamedFloatCurve curve = new NamedFloatCurve(fullPath,
  393. new AnimationCurve(kvp.Value.curveInfos[idx].curve.KeyFrames));
  394. curve.flags = flags;
  395. EditorFloatCurveTangents curveTangents = new EditorFloatCurveTangents();
  396. curveTangents.name = fullPath;
  397. curveTangents.tangents = kvp.Value.curveInfos[idx].curve.TangentModes;
  398. floatCurves.Add(curve);
  399. floatTangents.Add(curveTangents);
  400. };
  401. switch (kvp.Value.type)
  402. {
  403. case SerializableProperty.FieldType.Vector2:
  404. addCurve(0, kvp.Key, ".x", 0);
  405. addCurve(1, kvp.Key, ".y", 0);
  406. break;
  407. case SerializableProperty.FieldType.Vector3:
  408. addCurve(0, kvp.Key, ".x", 0);
  409. addCurve(1, kvp.Key, ".y", 0);
  410. addCurve(2, kvp.Key, ".z", 0);
  411. break;
  412. case SerializableProperty.FieldType.Vector4:
  413. addCurve(0, kvp.Key, ".x", 0);
  414. addCurve(1, kvp.Key, ".y", 0);
  415. addCurve(2, kvp.Key, ".z", 0);
  416. addCurve(3, kvp.Key, ".w", 0);
  417. break;
  418. case SerializableProperty.FieldType.Color:
  419. addCurve(0, kvp.Key, ".r", 0);
  420. addCurve(1, kvp.Key, ".g", 0);
  421. addCurve(2, kvp.Key, ".b", 0);
  422. addCurve(3, kvp.Key, ".a", 0);
  423. break;
  424. case SerializableProperty.FieldType.Bool:
  425. case SerializableProperty.FieldType.Int:
  426. case SerializableProperty.FieldType.Float:
  427. {
  428. AnimationCurveFlags flags = 0;
  429. string path = kvp.Key;
  430. if (IsMorphShapeCurve(kvp.Key))
  431. {
  432. string trimmedPath = path.Trim('/');
  433. string[] entries = trimmedPath.Split('/');
  434. bool isWeight = entries[entries.Length - 2] == "Weight";
  435. if (isWeight)
  436. flags = AnimationCurveFlags.MorphWeight;
  437. else
  438. flags = AnimationCurveFlags.MorphFrame;
  439. path = entries[entries.Length - 1];
  440. }
  441. addCurve(0, path, "", flags);
  442. }
  443. break;
  444. }
  445. }
  446. }
  447. AnimationCurves newClipCurves = new AnimationCurves();
  448. newClipCurves.Position = positionCurves.ToArray();
  449. newClipCurves.Rotation = rotationCurves.ToArray();
  450. newClipCurves.Scale = scaleCurves.ToArray();
  451. newClipCurves.Generic = floatCurves.ToArray();
  452. clip.Curves = newClipCurves;
  453. clip.Events = events;
  454. clip.SampleRate = sampleRate;
  455. editorData = new EditorAnimClipData();
  456. editorData.tangents = new EditorAnimClipTangents();
  457. editorData.tangents.positionCurves = positionTangents.ToArray();
  458. editorData.tangents.rotationCurves = rotationTangents.ToArray();
  459. editorData.tangents.scaleCurves = scaleTangents.ToArray();
  460. editorData.tangents.floatCurves = floatTangents.ToArray();
  461. editorData.eulerCurves = eulerRotationCurves.ToArray();
  462. }
  463. /// <summary>
  464. /// Saves the animation curves and events stored in this object, into the associated animation clip resource.
  465. /// Relevant animation clip resource must already be created and exist in the project library.
  466. /// </summary>
  467. public void SaveToClip()
  468. {
  469. if (!isImported)
  470. {
  471. EditorAnimClipData editorAnimClipData;
  472. Apply(out editorAnimClipData);
  473. string resourcePath = ProjectLibrary.GetPath(clip);
  474. ProjectLibrary.Save(clip);
  475. ProjectLibrary.SetEditorData(resourcePath, editorAnimClipData);
  476. }
  477. else
  478. {
  479. string resourcePath = ProjectLibrary.GetPath(clip);
  480. LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
  481. if (entry != null && entry.Type == LibraryEntryType.File)
  482. {
  483. FileEntry fileEntry = (FileEntry)entry;
  484. MeshImportOptions meshImportOptions = (MeshImportOptions)fileEntry.Options;
  485. string clipName = PathEx.GetTail(resourcePath);
  486. List<ImportedAnimationEvents> newEvents = new List<ImportedAnimationEvents>();
  487. newEvents.AddRange(meshImportOptions.AnimationEvents);
  488. bool isExisting = false;
  489. for (int i = 0; i < newEvents.Count; i++)
  490. {
  491. if (newEvents[i].Name == clipName)
  492. {
  493. newEvents[i].Events = events;
  494. isExisting = true;
  495. break;
  496. }
  497. }
  498. if (!isExisting)
  499. {
  500. ImportedAnimationEvents newEntry = new ImportedAnimationEvents();
  501. newEntry.Name = clipName;
  502. newEntry.Events = events;
  503. newEvents.Add(newEntry);
  504. }
  505. meshImportOptions.AnimationEvents = newEvents.ToArray();
  506. ProjectLibrary.Reimport(resourcePath, meshImportOptions, true);
  507. }
  508. }
  509. }
  510. /// <summary>
  511. /// Generates a unique color based on the provided index.
  512. /// </summary>
  513. /// <param name="idx">Index to use for generating a color. Should be less than 30 in order to guarantee reasonably
  514. /// different colors.</param>
  515. /// <returns>Unique color.</returns>
  516. public static Color GetUniqueColor(int idx)
  517. {
  518. const int COLOR_SPACING = 359 / 15;
  519. float hue = ((idx * COLOR_SPACING) % 359) / 359.0f;
  520. return Color.HSV2RGB(new Color(hue, 175.0f / 255.0f, 175.0f / 255.0f));
  521. }
  522. }
  523. /** @} */
  524. }