// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Microsoft.MixedReality.Toolkit.Rendering;
using System.Collections.Generic;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities
{
///
/// An abstract primitive component to animate and visualize a clipping primitive that can be
/// used to drive per pixel based clipping.
///
[ExecuteAlways]
[HelpURL("https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/Rendering/ClippingPrimitive.html")]
public abstract class ClippingPrimitive : MonoBehaviour, IMaterialInstanceOwner
{
[Tooltip("The renderer(s) that should be affected by the primitive.")]
[SerializeField]
protected List renderers = new List();
public enum Side
{
Inside = 1,
Outside = -1
}
[Tooltip("Which side of the primitive to clip pixels against.")]
[SerializeField]
protected Side clippingSide = Side.Inside;
///
/// The renderer(s) that should be affected by the primitive.
///
public Side ClippingSide
{
get => clippingSide;
set => clippingSide = value;
}
[SerializeField]
[Tooltip("Toggles whether the primitive will use the Camera OnPreRender event")]
private bool useOnPreRender;
///
/// Toggles whether the primitive will use the Camera OnPreRender event.
///
///
/// This is especially helpful if you're trying to clip dynamically created objects that may be added to the scene after LateUpdate such as OnWillRender
///
public bool UseOnPreRender
{
get => useOnPreRender;
set
{
if (cameraMethods == null)
{
cameraMethods = EnsureComponent(Camera.main.gameObject);
}
if (value)
{
cameraMethods.OnCameraPreRender += OnCameraPreRender;
}
else
{
cameraMethods.OnCameraPreRender -= OnCameraPreRender;
}
useOnPreRender = value;
}
}
protected abstract string Keyword { get; }
protected abstract string ClippingSideProperty { get; }
protected MaterialPropertyBlock materialPropertyBlock;
private int clippingSideID;
private CameraEventRouter cameraMethods;
///
/// Adds a renderer to the list of objects this clipping primitive clips.
///
///
public void AddRenderer(Renderer _renderer)
{
if (_renderer != null)
{
if (!renderers.Contains(_renderer))
{
renderers.Add(_renderer);
}
var matInstance = EnsureComponent(_renderer.gameObject);
ToggleClippingFeature(matInstance.AcquireMaterials(this), gameObject.activeInHierarchy);
}
}
///
/// Removes a renderer to the list of objects this clipping primitive clips.
///
public void RemoveRenderer(Renderer _renderer)
{
renderers.Remove(_renderer);
if (_renderer != null)
{
var materialInstance = _renderer.GetComponent();
if (materialInstance != null)
{
// There is no need to acquire new instances if ones do not already exist since we are
// in the process of removing.
ToggleClippingFeature(materialInstance.AcquireMaterials(this, false), false);
materialInstance.ReleaseMaterial(this);
}
}
}
///
/// Removes all renderers in the list of objects this clipping primitive clips.
///
public void ClearRenderers()
{
if (renderers != null)
{
// Remove from end of list to avoid re-allocation of array
for (int i = renderers.Count - 1; i >= 0; i--)
{
RemoveRenderer(renderers[0]);
}
}
}
///
/// Returns a copy of the current list of renderers.
///
/// The current list of renderers.
public IEnumerable GetRenderersCopy()
{
return new List(renderers);
}
#region MonoBehaviour Implementation
protected void OnEnable()
{
Initialize();
UpdateRenderers();
ToggleClippingFeature(true);
if (useOnPreRender)
{
cameraMethods = EnsureComponent(Camera.main.gameObject);
cameraMethods.OnCameraPreRender += OnCameraPreRender;
}
}
protected void OnDisable()
{
UpdateRenderers();
ToggleClippingFeature(false);
if (cameraMethods != null)
{
cameraMethods.OnCameraPreRender -= OnCameraPreRender;
}
}
#if UNITY_EDITOR
// We need this class to be updated once per frame even when in edit mode. Ideally this would
// occur after all other objects are updated in LateUpdate(), but because the ExecuteInEditMode
// attribute only invokes Update() we handle edit mode updating in Update() and runtime updating
// in LateUpdate().
protected void Update()
{
if (Application.isPlaying)
{
return;
}
Initialize();
UpdateRenderers();
}
#endif
protected void LateUpdate()
{
// Deferring the LateUpdate() call to OnCameraPreRender()
if (!useOnPreRender)
{
UpdateRenderers();
}
}
protected void OnCameraPreRender(CameraEventRouter router)
{
// Only subscribed to via UseOnPreRender property setter
UpdateRenderers();
}
protected void OnDestroy()
{
ClearRenderers();
}
#endregion MonoBehaviour Implementation
#region IMaterialInstanceOwner Implementation
///
public void OnMaterialChanged(MaterialInstance materialInstance)
{
if (materialInstance != null)
{
ToggleClippingFeature(materialInstance.AcquireMaterials(this), gameObject.activeInHierarchy);
}
UpdateRenderers();
}
#endregion IMaterialInstanceOwner Implementation
protected virtual void Initialize()
{
materialPropertyBlock = new MaterialPropertyBlock();
clippingSideID = Shader.PropertyToID(ClippingSideProperty);
}
protected virtual void UpdateRenderers()
{
if (renderers == null)
{
return;
}
for (var i = 0; i < renderers.Count; ++i)
{
var _renderer = renderers[i];
if (_renderer == null)
{
continue;
}
_renderer.GetPropertyBlock(materialPropertyBlock);
materialPropertyBlock.SetFloat(clippingSideID, (float)clippingSide);
UpdateShaderProperties(materialPropertyBlock);
_renderer.SetPropertyBlock(materialPropertyBlock);
}
}
protected abstract void UpdateShaderProperties(MaterialPropertyBlock materialPropertyBlock);
protected void ToggleClippingFeature(bool keywordOn)
{
if (renderers != null)
{
for (var i = 0; i < renderers.Count; ++i)
{
var _renderer = renderers[i];
if (_renderer != null)
{
var materialInstance = EnsureComponent(_renderer.gameObject);
ToggleClippingFeature(materialInstance.AcquireMaterials(this), keywordOn);
}
}
}
}
protected void ToggleClippingFeature(Material[] materials, bool keywordOn)
{
if (materials != null)
{
foreach (var material in materials)
{
ToggleClippingFeature(material, keywordOn);
}
}
}
protected void ToggleClippingFeature(Material material, bool keywordOn)
{
if (material != null)
{
if (keywordOn)
{
material.EnableKeyword(Keyword);
}
else
{
material.DisableKeyword(Keyword);
}
}
}
///
/// Ensure that a component of type exists on the game object.
/// If it doesn't exist, creates it.
///
/// A component on the game object for which a component of type should exist.
/// The component that was retrieved or created.
private static T EnsureComponent(GameObject go) where T : Component
{
var foundComponent = go.GetComponent();
return foundComponent == null ? go.AddComponent() : foundComponent;
}
}
}