#pragma warning disable 649 #pragma warning disable 108 #pragma warning disable 618 using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using TriLibCore.SFB; using TriLibCore.Extensions; using TriLibCore.General; using TriLibCore.Utils; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Rendering; 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; /// /// Scene CanvasScaler. /// [SerializeField] protected CanvasScaler CanvasScaler; /// /// Camera selection Dropdown. /// [SerializeField] private Dropdown _camerasDropdown; /// /// Camera loading Toggle. /// [SerializeField] private Toggle _loadCamerasToggle; /// /// Lights loading Toggle. /// [SerializeField] private Toggle _loadLightsToggle; /// /// Point Clouds loading Toggle. /// [SerializeField] private Toggle _loadPointClouds; /// /// 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; /// /// Loading time indicator. /// [SerializeField] private Text _loadingTimeText; /// /// Memory usage Text. /// [SerializeField] private Text _memoryUsageText; /// /// Error panel. /// [SerializeField] private GameObject _errorPanel; /// /// Error panel inner text. /// [SerializeField] private Text _errorPanelText; /// /// Main scene Camera. /// [SerializeField] private Camera _mainCamera; /// /// Debug options dropdown; /// [SerializeField] private Dropdown _debugOptionsDropdown; /// /// Current camera distance. /// protected float CameraDistance = 1f; /// /// Current camera pivot position. /// protected Vector3 CameraPivot; /// /// 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; /// /// Loaded model cameras. /// private IList _cameras; /// /// Stop Watch used to track the model loading time. /// private Stopwatch _stopwatch; /// /// Current directional light angle. /// private Vector2 _lightAngle = new Vector2(0f, -45f); /// /// Reference to the ShowSkeleton behavior added to the loaded game object. /// private ShowSkeleton _showSkeleton; /// /// Use Coroutines Toggle. /// [SerializeField] private Toggle _useCoroutinesToggle; /// /// Reference to the shader used to display normals. /// private Shader _showNormalsShader; /// /// Reference to the shader used to display metallic. /// private Shader _showMetallicShader; /// /// Reference to the shader used to display smooth. /// private Shader _showSmoothShader; /// /// Reference to the shader used to display albedo. /// private Shader _showAlbedoShader; /// /// Reference to the shader used to display emission. /// private Shader _showEmissionShader; /// /// Reference to the shader used to display occlusion. /// private Shader _showOcclusionShader; /// 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() { AssetLoaderOptions.ImportCameras = _loadCamerasToggle.isOn; AssetLoaderOptions.ImportLights = _loadLightsToggle.isOn; AssetLoaderOptions.LoadPointClouds = _loadPointClouds.isOn; base.LoadModelFromFile(); } /// /// Shows the URL selector for loading a model from network. /// public void LoadModelFromURLWithDialogValues() { AssetLoaderOptions.ImportCameras = _loadCamerasToggle.isOn; AssetLoaderOptions.ImportLights = _loadLightsToggle.isOn; AssetLoaderOptions.LoadPointClouds = _loadPointClouds.isOn; base.LoadModelFromURLWithDialogValues(); } /// 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); } /// /// Resets the model scale when loading a new model. /// 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, PlayMode.StopAll); } /// /// 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(); } /// Switches to the camera selected on the Dropdown. /// The selected Camera index. public void CameraChanged(int index) { for (var i = 0; i < _cameras.Count; i++) { var camera = _cameras[i]; camera.enabled = false; } if (index == 0) { _mainCamera.enabled = true; } else { _cameras[index - 1].enabled = true; } } /// 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) { Utils.Dispatcher.InvokeAsyncUnchecked(LoadSkybox, files[0].OpenStream()); } else { Utils.Dispatcher.InvokeAsync(ClearSkybox); } } /// /// Event triggered when the user ticks the Use Coroutine toggle. /// /// Is the toggle on? public void OnUseCoroutinesToggleChanged(bool isOn) { AssetLoaderOptions.UseCoroutines = isOn; } /// /// Event triggered when the user changes the debug options dropdown value. /// /// The dropdown value. public void OnDebugOptionsDropdownChanged(int value) { switch (value) { default: if (_showSkeleton != null) { _showSkeleton.enabled = value == 1; } _mainCamera.ResetReplacementShader(); _mainCamera.renderingPath = RenderingPath.UsePlayerSettings; break; case 2: _mainCamera.SetReplacementShader(_showAlbedoShader, null); _mainCamera.renderingPath = RenderingPath.Forward; break; case 3: _mainCamera.SetReplacementShader(_showEmissionShader, null); _mainCamera.renderingPath = RenderingPath.Forward; break; case 4: _mainCamera.SetReplacementShader(_showOcclusionShader, null); _mainCamera.renderingPath = RenderingPath.Forward; break; case 5: _mainCamera.SetReplacementShader(_showNormalsShader, null); _mainCamera.renderingPath = RenderingPath.Forward; break; case 6: _mainCamera.SetReplacementShader(_showMetallicShader, null); _mainCamera.renderingPath = RenderingPath.Forward; break; case 7: _mainCamera.SetReplacementShader(_showSmoothShader, null); _mainCamera.renderingPath = RenderingPath.Forward; break; } } /// 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 Stream. /// 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(); if (SystemInfo.deviceType == UnityEngine.DeviceType.Handheld) { CanvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; } AssetLoaderOptions = AssetLoader.CreateDefaultLoaderOptions(); AssetLoaderOptions.Timeout = 180; AssetLoaderOptions.ShowLoadingWarnings = true; AssetLoaderOptions.AlphaMaterialMode = AlphaMaterialMode.Cutout; AssetLoaderOptions.UseCoroutines = _useCoroutinesToggle.isOn; AssetLoaderOptions.UseUnityNativeTextureLoader = true; _showNormalsShader = Shader.Find("Hidden/ShowNormals"); _showMetallicShader = Shader.Find("Hidden/ShowMetallic"); _showSmoothShader = Shader.Find("Hidden/ShowSmooth"); _showAlbedoShader = Shader.Find("Hidden/ShowAlbedo"); _showOcclusionShader = Shader.Find("Hidden/ShowOcclusion"); _showEmissionShader = Shader.Find("Hidden/ShowEmission"); ClearSkybox(); InvokeRepeating("ShowMemoryUsage", 0f, 1f); } /// /// Updates the memory usage text. /// private void ShowMemoryUsage() { #if TRILIB_SHOW_MEMORY_USAGE var memory = RuntimeProcessUtils.GetProcessMemory(); PeakMemory = Math.Max(memory, PeakMemory); _memoryUsageText.text = $"{ProcessUtils.SizeSuffix(memory)} Peak: {ProcessUtils.SizeSuffix(PeakMemory)}"; #else var memory = System.GC.GetTotalMemory(false); PeakMemory = Math.Max(memory, PeakMemory); _memoryUsageText.text = $"{ProcessUtils.SizeSuffix(memory)} Peak: {ProcessUtils.SizeSuffix(PeakMemory)}"; #endif } /// Handles the input. private void Update() { ProcessInput(); UpdateHUD(); } /// Handles the input and moves the Camera accordingly. protected virtual void ProcessInput() { if (!_mainCamera.enabled) { return; } ProcessInputInternal(_mainCamera.transform); } /// /// Handles the input using the given Camera. /// /// The Camera to process input movements. private void ProcessInputInternal(Transform cameraTransform) { if (!EventSystem.current.IsPointerOverGameObject()) { if (GetMouseButton(0)) { if (GetKey(KeyCode.LeftAlt) || GetKey(KeyCode.RightAlt)) { _lightAngle.x = Mathf.Repeat(_lightAngle.x + GetAxis("Mouse X"), 360f); _lightAngle.y = Mathf.Clamp(_lightAngle.y + GetAxis("Mouse Y"), -MaxPitch, MaxPitch); } else { UpdateCamera(); } } if (GetMouseButton(2)) { CameraPivot -= cameraTransform.up * GetAxis("Mouse Y") * InputMultiplier + cameraTransform.right * GetAxis("Mouse X") * InputMultiplier; } CameraDistance = Mathf.Min(CameraDistance - GetMouseScrollDelta().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); } else { Play.gameObject.SetActive(true); Stop.gameObject.SetActive(false); PlaybackSlider.value = 0f; } } /// 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) { if (Application.GetStackTraceLogType(LogType.Exception) != StackTraceLogType.None || Application.GetStackTraceLogType(LogType.Error) != StackTraceLogType.None) { _errorPanel.SetActive(false); } _animations = null; _loadingTimeText.text = null; _stopwatch = new Stopwatch(); _stopwatch.Start(); } } /// Event triggered when the Model Meshes and hierarchy are loaded. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. protected override void OnLoad(AssetLoaderContext assetLoaderContext) { base.OnLoad(assetLoaderContext); ResetModelScale(); _camerasDropdown.options.Clear(); PlaybackAnimation.options.Clear(); _cameras = null; _animation = null; _mainCamera.enabled = true; if (assetLoaderContext.RootGameObject != null) { if (assetLoaderContext.Options.ImportCameras) { _cameras = assetLoaderContext.RootGameObject.GetComponentsInChildren(); if (_cameras.Count > 0) { _camerasDropdown.gameObject.SetActive(true); _camerasDropdown.options.Add(new Dropdown.OptionData("User Camera")); for (var i = 0; i < _cameras.Count; i++) { var camera = _cameras[i]; camera.enabled = false; _camerasDropdown.options.Add(new Dropdown.OptionData(camera.name)); } _camerasDropdown.captionText.text = _cameras[0].name; } else { _cameras = null; } } _animation = assetLoaderContext.RootGameObject.GetComponent(); if (_animation != null) { _animations = _animation.GetAllAnimationClips(); if (_animations.Count > 0) { PlaybackAnimation.interactable = true; for (var i = 0; i < _animations.Count; i++) { var animationClip = _animations[i]; PlaybackAnimation.options.Add(new Dropdown.OptionData(animationClip.name)); } PlaybackAnimation.captionText.text = _animations[0].name; } else { _animation = null; } } _camerasDropdown.value = 0; PlaybackAnimation.value = 0; StopAnimation(); RootGameObject = assetLoaderContext.RootGameObject; } if (_cameras == null) { _camerasDropdown.gameObject.SetActive(false); } if (_animation == null) { PlaybackAnimation.interactable = false; PlaybackAnimation.captionText.text = "No Animations"; } ModelTransformChanged(); } /// /// Fits the camera into a custom given bounds. /// /// The bounds to fit the camera to. public void SetCustomBounds(Bounds bounds) { _mainCamera.FitToBounds(bounds, CameraDistanceRatio); CameraDistance = _mainCamera.transform.position.magnitude; CameraPivot = bounds.center; Skybox.transform.localScale = bounds.size.magnitude * SkyboxScale * Vector3.one; InputMultiplier = bounds.size.magnitude * InputMultiplierRatio; CameraAngle = Vector2.zero; } /// /// Changes the camera placement when the Model has changed. /// protected virtual void ModelTransformChanged() { if (RootGameObject != null && _mainCamera.enabled) { var bounds = RootGameObject.CalculateBounds(); _mainCamera.FitToBounds(bounds, CameraDistanceRatio); // Uncomment this code to scale up small objects //if (bounds.size.magnitude < 1f) //{ // var increase = 1f / bounds.size.magnitude; // RootGameObject.transform.localScale *= increase; // bounds = RootGameObject.CalculateBounds(); //} CameraDistance = _mainCamera.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) { if (Application.GetStackTraceLogType(LogType.Exception) != StackTraceLogType.None || Application.GetStackTraceLogType(LogType.Error) != StackTraceLogType.None) { _errorPanelText.text = contextualizedError.ToString(); _errorPanel.SetActive(true); } base.OnError(contextualizedError); StopAnimation(); } /// Event is triggered when the Model (including Textures and Materials) has been fully loaded. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. protected override void OnMaterialsLoad(AssetLoaderContext assetLoaderContext) { base.OnMaterialsLoad(assetLoaderContext); _stopwatch.Stop(); _loadingTimeText.text = $"Loaded in: {_stopwatch.Elapsed.Minutes:00}:{_stopwatch.Elapsed.Seconds:00}"; ModelTransformChanged(); if (assetLoaderContext.RootGameObject != null) { _showSkeleton = assetLoaderContext.RootGameObject.AddComponent(); _showSkeleton.Setup(assetLoaderContext, this); assetLoaderContext.Allocations.Add(_showSkeleton); if (assetLoaderContext.Options.LoadPointClouds && assetLoaderContext.RootGameObject != null) { HandlePointClouds(assetLoaderContext); } } OnDebugOptionsDropdownChanged(_debugOptionsDropdown.value); } /// Handles Point Clouds rendering. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private void HandlePointClouds(AssetLoaderContext assetLoaderContext) { Material material = null; foreach (var gameObject in assetLoaderContext.GameObjects.Values) { if (gameObject.TryGetComponent(out var meshFilter)) { var points = meshFilter.sharedMesh.vertices; var colors = meshFilter.sharedMesh.colors32; if (colors == null || colors.Length != points.Length) { colors = new Color32[points.Length]; for (var i = 0; i < colors.Length; i++) { colors[i] = Color.white; } } if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11 && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D12) { var mesh = new Mesh(); mesh.indexFormat = IndexFormat.UInt32; mesh.SetVertices(points); mesh.SetColors(colors); mesh.SetIndices( Enumerable.Range(0, points.Length).ToArray(), MeshTopology.Points, 0 ); mesh.UploadMeshData(!assetLoaderContext.Options.ReadEnabled); meshFilter.sharedMesh = mesh; var meshRenderer = gameObject.AddComponent(); var materials = new Material[meshFilter.sharedMesh.subMeshCount]; if (material == null) { material = new Material(Shader.Find("Hidden/PointCloud_GL")); } for (var i = 0; i < materials.Length; i++) { materials[i] = material; } meshRenderer.materials = materials; } else { var pointCloudRenderer = assetLoaderContext.RootGameObject.AddComponent(); var data = ScriptableObject.CreateInstance(); data.Initialize(points, colors); pointCloudRenderer.sourceData = data; pointCloudRenderer.destroyData = true; pointCloudRenderer.pointSize = 0.01f; } } } } } }