#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;
}
}
}
}
}
}