AssetViewer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using SFB;
  5. using TriLibCore.General;
  6. using TriLibCore.Extensions;
  7. using TriLibCore.Utils;
  8. using UnityEngine;
  9. using UnityEngine.EventSystems;
  10. using UnityEngine.UI;
  11. namespace TriLibCore.Samples
  12. {
  13. /// <summary>Represents a TriLib sample which allows the user to load models and HDR skyboxes from the local file-system.</summary>
  14. public class AssetViewer : AssetViewerBase
  15. {
  16. /// <summary>
  17. /// Maximum camera distance ratio based on model bounds.
  18. /// </summary>
  19. private const float MaxCameraDistanceRatio = 3f;
  20. /// <summary>
  21. /// Camera distance ratio based on model bounds.
  22. /// </summary>
  23. protected const float CameraDistanceRatio = 2f;
  24. /// <summary>
  25. /// minimum camera distance.
  26. /// </summary>
  27. protected const float MinCameraDistance = 0.01f;
  28. /// <summary>
  29. /// Skybox scale based on model bounds.
  30. /// </summary>
  31. protected const float SkyboxScale = 100f;
  32. /// <summary>
  33. /// Skybox game object.
  34. /// </summary>
  35. [SerializeField]
  36. protected GameObject Skybox;
  37. /// <summary>
  38. /// Skybox game object renderer.
  39. /// </summary>
  40. [SerializeField]
  41. private Renderer _skyboxRenderer;
  42. /// <summary>
  43. /// Directional light.
  44. /// </summary>
  45. [SerializeField]
  46. private Light _light;
  47. /// <summary>
  48. /// Skybox material preset to create the final skybox material.
  49. /// </summary>
  50. [SerializeField]
  51. private Material _skyboxMaterialPreset;
  52. /// <summary>
  53. /// Main reflection probe.
  54. /// </summary>
  55. [SerializeField]
  56. private ReflectionProbe _reflectionProbe;
  57. /// <summary>
  58. /// Skybox exposure slider.
  59. /// </summary>
  60. [SerializeField]
  61. private Slider _skyboxExposureSlider;
  62. /// <summary>
  63. /// Current camera distance.
  64. /// </summary>
  65. protected float CameraDistance = 1f;
  66. /// <summary>
  67. /// Current camera pivot position.
  68. /// </summary>
  69. protected Vector3 CameraPivot;
  70. /// <summary>
  71. /// Current directional light angle.
  72. /// </summary>
  73. private Vector2 _lightAngle = new Vector2(0f, -45f);
  74. /// <summary>
  75. /// Input multiplier based on loaded model bounds.
  76. /// </summary>
  77. protected float InputMultiplier = 1f;
  78. /// <summary>
  79. /// Skybox instantiated material.
  80. /// </summary>
  81. private Material _skyboxMaterial;
  82. /// <summary>
  83. /// Texture loaded for skybox.
  84. /// </summary>
  85. private Texture2D _skyboxTexture;
  86. /// <summary>
  87. /// List of loaded animations.
  88. /// </summary>
  89. private List<AnimationClip> _animations;
  90. /// <summary>
  91. /// Created animation component for the loaded model.
  92. /// </summary>
  93. private Animation _animation;
  94. /// <summary>Gets the playing Animation State.</summary>
  95. private AnimationState CurrentAnimationState
  96. {
  97. get
  98. {
  99. if (_animation != null)
  100. {
  101. return _animation[PlaybackAnimation.options[PlaybackAnimation.value].text];
  102. }
  103. return null;
  104. }
  105. }
  106. /// <summary>Is there any animation playing?</summary>
  107. private bool AnimationIsPlaying => _animation != null && _animation.isPlaying;
  108. /// <summary>Shows the file picker for loading a model from the local file-system.</summary>
  109. public void LoadModelFromFile()
  110. {
  111. base.LoadModelFromFile();
  112. }
  113. /// <summary>Shows the file picker for loading a skybox from the local file-system.</summary>
  114. public void LoadSkyboxFromFile()
  115. {
  116. SetLoading(false);
  117. var title = "Select a skybox image";
  118. var extensions = new ExtensionFilter[]
  119. {
  120. new ExtensionFilter("Radiance HDR Image (hdr)", "hdr")
  121. };
  122. StandaloneFileBrowser.OpenFilePanelAsync(title, null, extensions, true, OnSkyboxStreamSelected);
  123. }
  124. /// <summary>
  125. /// Removes the skybox texture.
  126. /// </summary>
  127. public void ClearSkybox()
  128. {
  129. if (_skyboxMaterial == null)
  130. {
  131. _skyboxMaterial = Instantiate(_skyboxMaterialPreset);
  132. }
  133. _skyboxMaterial.mainTexture = null;
  134. _skyboxExposureSlider.value = 1f;
  135. OnSkyboxExposureChanged(1f);
  136. }
  137. public void ResetModelScale()
  138. {
  139. if (RootGameObject != null)
  140. {
  141. RootGameObject.transform.localScale = Vector3.one;
  142. }
  143. }
  144. /// <summary>
  145. /// Plays the selected animation.
  146. /// </summary>
  147. public override void PlayAnimation()
  148. {
  149. if (_animation == null)
  150. {
  151. return;
  152. }
  153. _animation.Play(PlaybackAnimation.options[PlaybackAnimation.value].text);
  154. }
  155. /// <summary>
  156. /// Stop playing the selected animation.
  157. /// </summary>
  158. public override void StopAnimation()
  159. {
  160. if (_animation == null)
  161. {
  162. return;
  163. }
  164. PlaybackSlider.value = 0f;
  165. _animation.Stop();
  166. SampleAnimationAt(0f);
  167. }
  168. /// <summary>Switches to the animation selected on the Dropdown.</summary>
  169. /// <param name="index">The selected Animation index.</param>
  170. public override void PlaybackAnimationChanged(int index)
  171. {
  172. StopAnimation();
  173. }
  174. /// <summary>Event triggered when the Animation slider value has been changed by the user.</summary>
  175. /// <param name="value">The Animation playback normalized position.</param>
  176. public override void PlaybackSliderChanged(float value)
  177. {
  178. if (!AnimationIsPlaying)
  179. {
  180. var animationState = CurrentAnimationState;
  181. if (animationState != null)
  182. {
  183. SampleAnimationAt(value);
  184. }
  185. }
  186. }
  187. /// <summary>Samples the Animation at the given normalized time.</summary>
  188. /// <param name="value">The Animation normalized time.</param>
  189. private void SampleAnimationAt(float value)
  190. {
  191. if (_animation == null || RootGameObject == null)
  192. {
  193. return;
  194. }
  195. var animationClip = _animation.GetClip(PlaybackAnimation.options[PlaybackAnimation.value].text);
  196. animationClip.SampleAnimation(RootGameObject, animationClip.length * value);
  197. }
  198. /// <summary>
  199. /// Event triggered when the user selects the skybox on the selection dialog.
  200. /// </summary>
  201. /// <param name="files">Selected files.</param>
  202. private void OnSkyboxStreamSelected(IList<ItemWithStream> files)
  203. {
  204. if (files != null && files.Count > 0 && files[0].HasData)
  205. {
  206. #if (UNITY_WSA || UNITY_ANDROID) && !UNITY_EDITOR
  207. Dispatcher.InvokeAsync(new ContextualizedAction<Stream>(LoadSkybox, files[0].OpenStream()));
  208. #else
  209. LoadSkybox(files[0].OpenStream());
  210. #endif
  211. } else
  212. {
  213. #if (UNITY_WSA || UNITY_ANDROID) && !UNITY_EDITOR
  214. Dispatcher.InvokeAsync(new ContextualizedAction(ClearSkybox));
  215. #else
  216. ClearSkybox();
  217. #endif
  218. }
  219. }
  220. /// <summary>Loads the skybox from the given Stream.</summary>
  221. /// <param name="stream">The Stream containing the HDR Image data.</param>
  222. /// <returns>Coroutine IEnumerator.</returns>
  223. private IEnumerator DoLoadSkybox(Stream stream)
  224. {
  225. //Double frame waiting hack
  226. yield return new WaitForEndOfFrame();
  227. yield return new WaitForEndOfFrame();
  228. if (_skyboxTexture != null)
  229. {
  230. Destroy(_skyboxTexture);
  231. }
  232. ClearSkybox();
  233. _skyboxTexture = HDRLoader.HDRLoader.Load(stream, out var gamma, out var exposure);
  234. _skyboxMaterial.mainTexture = _skyboxTexture;
  235. _skyboxExposureSlider.value = 1f;
  236. OnSkyboxExposureChanged(exposure);
  237. stream.Close();
  238. SetLoading(false);
  239. }
  240. /// <summary>Starts the Coroutine to load the skybox from the given Sstream.</summary>
  241. /// <param name="stream">The Stream containing the HDR Image data.</param>
  242. private void LoadSkybox(Stream stream)
  243. {
  244. SetLoading(true);
  245. StartCoroutine(DoLoadSkybox(stream));
  246. }
  247. /// <summary>Event triggered when the skybox exposure Slider has changed.</summary>
  248. /// <param name="exposure">The new exposure value.</param>
  249. public void OnSkyboxExposureChanged(float exposure)
  250. {
  251. _skyboxMaterial.SetFloat("_Exposure", exposure);
  252. _skyboxRenderer.material = _skyboxMaterial;
  253. RenderSettings.skybox = _skyboxMaterial;
  254. DynamicGI.UpdateEnvironment();
  255. _reflectionProbe.RenderProbe();
  256. }
  257. /// <summary>Initializes the base-class and clears the skybox Texture.</summary>
  258. protected override void Start()
  259. {
  260. base.Start();
  261. AssetLoaderOptions = AssetLoader.CreateDefaultLoaderOptions();
  262. ClearSkybox();
  263. }
  264. /// <summary>Handles the input.</summary>
  265. private void Update()
  266. {
  267. ProcessInput();
  268. UpdateHUD();
  269. }
  270. /// <summary>Handles the input and moves the Camera accordingly.</summary>
  271. protected virtual void ProcessInput()
  272. {
  273. ProcessInputInternal(Camera.main.transform);
  274. }
  275. /// <summary>
  276. /// Handles the input using the given Camera.
  277. /// </summary>
  278. /// <param name="cameraTransform">The Camera to process input movements.</param>
  279. private void ProcessInputInternal(Transform cameraTransform)
  280. {
  281. if (!EventSystem.current.IsPointerOverGameObject())
  282. {
  283. if (Input.GetMouseButton(0))
  284. {
  285. if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt))
  286. {
  287. _lightAngle.x = Mathf.Repeat(_lightAngle.x + Input.GetAxis("Mouse X"), 360f);
  288. _lightAngle.y = Mathf.Clamp(_lightAngle.y + Input.GetAxis("Mouse Y"), -MaxPitch, MaxPitch);
  289. }
  290. else
  291. {
  292. UpdateCamera();
  293. }
  294. }
  295. if (Input.GetMouseButton(2))
  296. {
  297. CameraPivot -= cameraTransform.up * Input.GetAxis("Mouse Y") * InputMultiplier + cameraTransform.right * Input.GetAxis("Mouse X") * InputMultiplier;
  298. }
  299. CameraDistance = Mathf.Min(CameraDistance - Input.mouseScrollDelta.y * InputMultiplier, InputMultiplier * (1f / InputMultiplierRatio) * MaxCameraDistanceRatio);
  300. if (CameraDistance < 0f)
  301. {
  302. CameraPivot += cameraTransform.forward * -CameraDistance;
  303. CameraDistance = 0f;
  304. }
  305. Skybox.transform.position = CameraPivot;
  306. cameraTransform.position = CameraPivot + Quaternion.AngleAxis(CameraAngle.x, Vector3.up) * Quaternion.AngleAxis(CameraAngle.y, Vector3.right) * new Vector3(0f, 0f, Mathf.Max(MinCameraDistance, CameraDistance));
  307. cameraTransform.LookAt(CameraPivot);
  308. _light.transform.position = CameraPivot + Quaternion.AngleAxis(_lightAngle.x, Vector3.up) * Quaternion.AngleAxis(_lightAngle.y, Vector3.right) * Vector3.forward;
  309. _light.transform.LookAt(CameraPivot);
  310. }
  311. }
  312. /// <summary>Updates the HUD information.</summary>
  313. private void UpdateHUD()
  314. {
  315. var animationState = CurrentAnimationState;
  316. var time = animationState == null ? 0f : PlaybackSlider.value * animationState.length % animationState.length;
  317. var seconds = time % 60f;
  318. var milliseconds = time * 100f % 100f;
  319. PlaybackTime.text = $"{seconds:00}:{milliseconds:00}";
  320. var normalizedTime = animationState == null ? 0f : animationState.normalizedTime % 1f;
  321. if (AnimationIsPlaying)
  322. {
  323. PlaybackSlider.value = float.IsNaN(normalizedTime) ? 0f : normalizedTime;
  324. }
  325. var animationIsPlaying = AnimationIsPlaying;
  326. if (_animation != null)
  327. {
  328. Play.gameObject.SetActive(!animationIsPlaying);
  329. Stop.gameObject.SetActive(animationIsPlaying);
  330. }
  331. }
  332. /// <summary>Event triggered when the user selects a file or cancels the Model selection dialog.</summary>
  333. /// <param name="hasFiles">If any file has been selected, this value is <c>true</c>, otherwise it is <c>false</c>.</param>
  334. protected override void OnBeginLoadModel(bool hasFiles)
  335. {
  336. base.OnBeginLoadModel(hasFiles);
  337. if (hasFiles)
  338. {
  339. _animations = null;
  340. }
  341. }
  342. /// <summary>Event triggered when the Model Meshes and hierarchy are loaded.</summary>
  343. /// <param name="assetLoaderContext">The Asset Loader Context reference. Asset Loader Context contains the information used during the Model loading process, which is available to almost every Model processing method</param>
  344. protected override void OnLoad(AssetLoaderContext assetLoaderContext)
  345. {
  346. base.OnLoad(assetLoaderContext);
  347. ResetModelScale();
  348. if (assetLoaderContext.RootGameObject != null)
  349. {
  350. PlaybackAnimation.options.Clear();
  351. if (assetLoaderContext.Options.AnimationType == AnimationType.Legacy)
  352. {
  353. _animation = assetLoaderContext.RootGameObject.GetComponent<Animation>();
  354. if (_animation != null)
  355. {
  356. _animations = _animation.GetAllAnimationClips();
  357. if (_animations.Count > 0)
  358. {
  359. foreach (var animationClip in _animations)
  360. {
  361. PlaybackAnimation.options.Add(new Dropdown.OptionData(animationClip.name));
  362. }
  363. PlaybackAnimation.captionText.text = _animations[0].name;
  364. }
  365. else
  366. {
  367. _animation = null;
  368. }
  369. }
  370. if (_animation == null)
  371. {
  372. PlaybackAnimation.captionText.text = null;
  373. }
  374. }
  375. PlaybackAnimation.value = 0;
  376. StopAnimation();
  377. RootGameObject = assetLoaderContext.RootGameObject;
  378. }
  379. ModelTransformChanged();
  380. }
  381. /// <summary>
  382. /// Changes the camera placement when the Model has changed.
  383. /// </summary>
  384. protected virtual void ModelTransformChanged()
  385. {
  386. if (RootGameObject != null)
  387. {
  388. var bounds = RootGameObject.CalculateBounds();
  389. Camera.main.FitToBounds(bounds, CameraDistanceRatio);
  390. CameraDistance = Camera.main.transform.position.magnitude;
  391. CameraPivot = bounds.center;
  392. Skybox.transform.localScale = bounds.size.magnitude * SkyboxScale * Vector3.one;
  393. InputMultiplier = bounds.size.magnitude * InputMultiplierRatio;
  394. CameraAngle = Vector2.zero;
  395. }
  396. }
  397. /// <summary>
  398. /// Event is triggered when any error occurs.
  399. /// </summary>
  400. /// <param name="contextualizedError">The Contextualized Error that has occurred.</param>
  401. protected override void OnError(IContextualizedError contextualizedError)
  402. {
  403. base.OnError(contextualizedError);
  404. StopAnimation();
  405. }
  406. }
  407. }