using System.Collections; using System.Collections.Generic; using System.IO; using SFB; using TriLibCore.General; using TriLibCore.Extensions; using TriLibCore.Utils; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace TriLibCore.Samples { /// Represents a TriLib sample which allows the user to load models and HDR skyboxes from the local file-system. public class AssetViewer : AssetViewerBase { /// /// Maximum camera distance ratio based on model bounds. /// private const float MaxCameraDistanceRatio = 3f; /// /// Camera distance ratio based on model bounds. /// protected const float CameraDistanceRatio = 2f; /// /// minimum camera distance. /// protected const float MinCameraDistance = 0.01f; /// /// Skybox scale based on model bounds. /// protected const float SkyboxScale = 100f; /// /// Skybox game object. /// [SerializeField] protected GameObject Skybox; /// /// Skybox game object renderer. /// [SerializeField] private Renderer _skyboxRenderer; /// /// Directional light. /// [SerializeField] private Light _light; /// /// Skybox material preset to create the final skybox material. /// [SerializeField] private Material _skyboxMaterialPreset; /// /// Main reflection probe. /// [SerializeField] private ReflectionProbe _reflectionProbe; /// /// Skybox exposure slider. /// [SerializeField] private Slider _skyboxExposureSlider; /// /// Current camera distance. /// protected float CameraDistance = 1f; /// /// Current camera pivot position. /// protected Vector3 CameraPivot; /// /// Current directional light angle. /// private Vector2 _lightAngle = new Vector2(0f, -45f); /// /// Input multiplier based on loaded model bounds. /// protected float InputMultiplier = 1f; /// /// Skybox instantiated material. /// private Material _skyboxMaterial; /// /// Texture loaded for skybox. /// private Texture2D _skyboxTexture; /// /// List of loaded animations. /// private List _animations; /// /// Created animation component for the loaded model. /// private Animation _animation; /// Gets the playing Animation State. private AnimationState CurrentAnimationState { get { if (_animation != null) { return _animation[PlaybackAnimation.options[PlaybackAnimation.value].text]; } return null; } } /// Is there any animation playing? private bool AnimationIsPlaying => _animation != null && _animation.isPlaying; /// Shows the file picker for loading a model from the local file-system. public void LoadModelFromFile() { base.LoadModelFromFile(); } /// Shows the file picker for loading a skybox from the local file-system. public void LoadSkyboxFromFile() { SetLoading(false); var title = "Select a skybox image"; var extensions = new ExtensionFilter[] { new ExtensionFilter("Radiance HDR Image (hdr)", "hdr") }; StandaloneFileBrowser.OpenFilePanelAsync(title, null, extensions, true, OnSkyboxStreamSelected); } /// /// Removes the skybox texture. /// public void ClearSkybox() { if (_skyboxMaterial == null) { _skyboxMaterial = Instantiate(_skyboxMaterialPreset); } _skyboxMaterial.mainTexture = null; _skyboxExposureSlider.value = 1f; OnSkyboxExposureChanged(1f); } public void ResetModelScale() { if (RootGameObject != null) { RootGameObject.transform.localScale = Vector3.one; } } /// /// Plays the selected animation. /// public override void PlayAnimation() { if (_animation == null) { return; } _animation.Play(PlaybackAnimation.options[PlaybackAnimation.value].text); } /// /// Stop playing the selected animation. /// public override void StopAnimation() { if (_animation == null) { return; } PlaybackSlider.value = 0f; _animation.Stop(); SampleAnimationAt(0f); } /// Switches to the animation selected on the Dropdown. /// The selected Animation index. public override void PlaybackAnimationChanged(int index) { StopAnimation(); } /// Event triggered when the Animation slider value has been changed by the user. /// The Animation playback normalized position. public override void PlaybackSliderChanged(float value) { if (!AnimationIsPlaying) { var animationState = CurrentAnimationState; if (animationState != null) { SampleAnimationAt(value); } } } /// Samples the Animation at the given normalized time. /// The Animation normalized time. private void SampleAnimationAt(float value) { if (_animation == null || RootGameObject == null) { return; } var animationClip = _animation.GetClip(PlaybackAnimation.options[PlaybackAnimation.value].text); animationClip.SampleAnimation(RootGameObject, animationClip.length * value); } /// /// Event triggered when the user selects the skybox on the selection dialog. /// /// Selected files. private void OnSkyboxStreamSelected(IList files) { if (files != null && files.Count > 0 && files[0].HasData) { #if (UNITY_WSA || UNITY_ANDROID) && !UNITY_EDITOR Dispatcher.InvokeAsync(new ContextualizedAction(LoadSkybox, files[0].OpenStream())); #else LoadSkybox(files[0].OpenStream()); #endif } else { #if (UNITY_WSA || UNITY_ANDROID) && !UNITY_EDITOR Dispatcher.InvokeAsync(new ContextualizedAction(ClearSkybox)); #else ClearSkybox(); #endif } } /// Loads the skybox from the given Stream. /// The Stream containing the HDR Image data. /// Coroutine IEnumerator. private IEnumerator DoLoadSkybox(Stream stream) { //Double frame waiting hack yield return new WaitForEndOfFrame(); yield return new WaitForEndOfFrame(); if (_skyboxTexture != null) { Destroy(_skyboxTexture); } ClearSkybox(); _skyboxTexture = HDRLoader.HDRLoader.Load(stream, out var gamma, out var exposure); _skyboxMaterial.mainTexture = _skyboxTexture; _skyboxExposureSlider.value = 1f; OnSkyboxExposureChanged(exposure); stream.Close(); SetLoading(false); } /// Starts the Coroutine to load the skybox from the given Sstream. /// The Stream containing the HDR Image data. private void LoadSkybox(Stream stream) { SetLoading(true); StartCoroutine(DoLoadSkybox(stream)); } /// Event triggered when the skybox exposure Slider has changed. /// The new exposure value. public void OnSkyboxExposureChanged(float exposure) { _skyboxMaterial.SetFloat("_Exposure", exposure); _skyboxRenderer.material = _skyboxMaterial; RenderSettings.skybox = _skyboxMaterial; DynamicGI.UpdateEnvironment(); _reflectionProbe.RenderProbe(); } /// Initializes the base-class and clears the skybox Texture. protected override void Start() { base.Start(); AssetLoaderOptions = AssetLoader.CreateDefaultLoaderOptions(); ClearSkybox(); } /// Handles the input. private void Update() { ProcessInput(); UpdateHUD(); } /// Handles the input and moves the Camera accordingly. protected virtual void ProcessInput() { ProcessInputInternal(Camera.main.transform); } /// /// Handles the input using the given Camera. /// /// The Camera to process input movements. private void ProcessInputInternal(Transform cameraTransform) { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButton(0)) { if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) { _lightAngle.x = Mathf.Repeat(_lightAngle.x + Input.GetAxis("Mouse X"), 360f); _lightAngle.y = Mathf.Clamp(_lightAngle.y + Input.GetAxis("Mouse Y"), -MaxPitch, MaxPitch); } else { UpdateCamera(); } } if (Input.GetMouseButton(2)) { CameraPivot -= cameraTransform.up * Input.GetAxis("Mouse Y") * InputMultiplier + cameraTransform.right * Input.GetAxis("Mouse X") * InputMultiplier; } CameraDistance = Mathf.Min(CameraDistance - Input.mouseScrollDelta.y * InputMultiplier, InputMultiplier * (1f / InputMultiplierRatio) * MaxCameraDistanceRatio); if (CameraDistance < 0f) { CameraPivot += cameraTransform.forward * -CameraDistance; CameraDistance = 0f; } Skybox.transform.position = CameraPivot; cameraTransform.position = CameraPivot + Quaternion.AngleAxis(CameraAngle.x, Vector3.up) * Quaternion.AngleAxis(CameraAngle.y, Vector3.right) * new Vector3(0f, 0f, Mathf.Max(MinCameraDistance, CameraDistance)); cameraTransform.LookAt(CameraPivot); _light.transform.position = CameraPivot + Quaternion.AngleAxis(_lightAngle.x, Vector3.up) * Quaternion.AngleAxis(_lightAngle.y, Vector3.right) * Vector3.forward; _light.transform.LookAt(CameraPivot); } } /// Updates the HUD information. private void UpdateHUD() { var animationState = CurrentAnimationState; var time = animationState == null ? 0f : PlaybackSlider.value * animationState.length % animationState.length; var seconds = time % 60f; var milliseconds = time * 100f % 100f; PlaybackTime.text = $"{seconds:00}:{milliseconds:00}"; var normalizedTime = animationState == null ? 0f : animationState.normalizedTime % 1f; if (AnimationIsPlaying) { PlaybackSlider.value = float.IsNaN(normalizedTime) ? 0f : normalizedTime; } var animationIsPlaying = AnimationIsPlaying; if (_animation != null) { Play.gameObject.SetActive(!animationIsPlaying); Stop.gameObject.SetActive(animationIsPlaying); } } /// Event triggered when the user selects a file or cancels the Model selection dialog. /// If any file has been selected, this value is true, otherwise it is false. protected override void OnBeginLoadModel(bool hasFiles) { base.OnBeginLoadModel(hasFiles); if (hasFiles) { _animations = null; } } /// Event triggered when the Model Meshes and hierarchy are loaded. /// 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 protected override void OnLoad(AssetLoaderContext assetLoaderContext) { base.OnLoad(assetLoaderContext); ResetModelScale(); if (assetLoaderContext.RootGameObject != null) { PlaybackAnimation.options.Clear(); if (assetLoaderContext.Options.AnimationType == AnimationType.Legacy) { _animation = assetLoaderContext.RootGameObject.GetComponent(); if (_animation != null) { _animations = _animation.GetAllAnimationClips(); if (_animations.Count > 0) { foreach (var animationClip in _animations) { PlaybackAnimation.options.Add(new Dropdown.OptionData(animationClip.name)); } PlaybackAnimation.captionText.text = _animations[0].name; } else { _animation = null; } } if (_animation == null) { PlaybackAnimation.captionText.text = null; } } PlaybackAnimation.value = 0; StopAnimation(); RootGameObject = assetLoaderContext.RootGameObject; } ModelTransformChanged(); } /// /// Changes the camera placement when the Model has changed. /// protected virtual void ModelTransformChanged() { if (RootGameObject != null) { var bounds = RootGameObject.CalculateBounds(); Camera.main.FitToBounds(bounds, CameraDistanceRatio); CameraDistance = Camera.main.transform.position.magnitude; CameraPivot = bounds.center; Skybox.transform.localScale = bounds.size.magnitude * SkyboxScale * Vector3.one; InputMultiplier = bounds.size.magnitude * InputMultiplierRatio; CameraAngle = Vector2.zero; } } /// /// Event is triggered when any error occurs. /// /// The Contextualized Error that has occurred. protected override void OnError(IContextualizedError contextualizedError) { base.OnError(contextualizedError); StopAnimation(); } } }