// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Microsoft.MixedReality.GraphicsTools
{
///
/// An abstract primitive component to animate and visualize a clipping primitive that can be
/// used to drive per pixel based clipping.
///
[ExecuteAlways]
public abstract class ClippingPrimitive : MonoBehaviour, IMaterialInstanceOwner
{
[Header("Renders to Clip")]
[SerializeField, Tooltip("Toggles whether clipping will apply to shared materials or material instances (default) on renderers within the renderers list. This cannot be altered when renderers are already specified.")]
private bool applyToSharedMaterial = false;
///
/// Toggles whether clipping will apply to shared materials or material instances (default) on renderers within the renderers list. This cannot be altered when renderers are already specified.
///
///
/// Applying to shared materials will allow for GPU instancing to batch calls between Renderers that interact with the same clipping primitives.
///
public bool ApplyToSharedMaterial
{
get => applyToSharedMaterial;
set
{
if (value != applyToSharedMaterial)
{
if (renderers.Count > 0)
{
throw new InvalidOperationException("Cannot change material applied to after renderers have been added.");
}
applyToSharedMaterial = value;
}
}
}
[Tooltip("The renderer(s) that should be affected by the primitive. Renderers with materials in the materials list do not need to be added to this list.")]
[SerializeField]
protected List renderers = new List();
[Header("Materials to Clip")]
[Tooltip("The materials(s) that should be affected by the primitive. Materials on renderers within the renderers list do not need to be added to this list.")]
[SerializeField]
protected List materials = new List();
///
/// Should clipping occur inside or outside of the clipping shape?
///
public enum Side
{
Inside = 1,
Outside = -1
}
[Header("Clip Settings")]
[Tooltip("Which side of the primitive to clip pixels against.")]
[SerializeField]
protected Side clippingSide = Side.Inside;
///
/// Which side of the primitive to clip pixels against.
///
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 = Camera.main.gameObject.EnsureComponent();
}
if (useOnPreRender != value)
{
if (value)
{
cameraMethods.OnCameraPreRender += OnCameraPreRender;
}
else if (!value)
{
cameraMethods.OnCameraPreRender -= OnCameraPreRender;
}
useOnPreRender = value;
}
}
}
private int clippingSideID;
private CameraEventRouter cameraMethods;
private bool isDirty;
///
/// Keeping track of any field, property or transformation changes to optimize material property block setting.
///
public bool IsDirty
{
get => isDirty;
set => isDirty = value;
}
///
/// The property block in use for material adjustments.
///
protected MaterialPropertyBlock materialPropertyBlock;
///
/// Returns the shader keyword name used to toggle this clipping feature on/off.
///
protected abstract string Keyword { get; }
///
/// Returns the shader property name used to control the clipping side.
///
protected abstract string ClippingSideProperty { get; }
///
/// 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);
}
ToggleClippingFeature(AcquireMaterials(_renderer), gameObject.activeInHierarchy);
IsDirty = true;
}
}
///
/// Removes a renderer from the list of objects this clipping primitive clips.
///
public void RemoveRenderer(Renderer _renderer)
{
int index = renderers.IndexOf(_renderer);
if (index >= 0)
{
RemoveRenderer(index);
}
}
///
/// Resets the state of the renderer to before being added to the primitive.
///
public void ResetRenderer(Renderer _renderer, bool autoDestroyMaterial = true)
{
if (_renderer != null)
{
// There is no need to acquire new instances if ones do not already exist since we are
// in the process of removing.
ToggleClippingFeature(AcquireMaterials(_renderer, instance: false), false);
var materialInstance = _renderer.GetComponent();
if (materialInstance != null)
{
materialInstance.ReleaseMaterial(this, autoDestroyMaterial);
}
// Reset the material property block.
materialPropertyBlock.Clear();
_renderer.SetPropertyBlock(materialPropertyBlock);
}
}
private void RemoveRenderer(int index, bool autoDestroyMaterial = true)
{
Renderer _renderer = renderers[index];
int lastIndex = renderers.Count - 1;
if (index != lastIndex)
{
renderers[index] = renderers[lastIndex];
}
renderers.RemoveAt(lastIndex);
ResetRenderer(_renderer, autoDestroyMaterial);
}
///
/// Removes all renderers in the list of objects this clipping primitive clips.
///
public void ClearRenderers(bool autoDestroyMaterial = true)
{
if (renderers != null)
{
while (renderers.Count != 0)
{
RemoveRenderer(renderers.Count - 1, autoDestroyMaterial);
}
}
}
///
/// Adds a material to the list of objects this clipping primitive clips.
///
public void AddMaterial(Material material)
{
if (material != null)
{
if (!materials.Contains(material))
{
materials.Add(material);
}
ToggleClippingFeature(material, gameObject.activeInHierarchy);
IsDirty = true;
}
}
///
/// Removes a material from the list of objects this clipping primitive clips.
///
public void RemoveMaterial(Material material)
{
int index = materials.IndexOf(material);
if (index >= 0)
{
RemoveMaterial(index);
}
}
///
/// Resets the state of the material to before being added to the primitive.
///
public void ResetMaterial(Material material)
{
if (material != null)
{
ToggleClippingFeature(material, false);
}
}
private void RemoveMaterial(int index)
{
Material material = materials[index];
int lastIndex = materials.Count - 1;
if (index != lastIndex)
{
materials[index] = materials[lastIndex];
}
materials.RemoveAt(lastIndex);
ResetMaterial(material);
}
///
/// Removes all materials in the list of objects this clipping primitive clips.
///
public void ClearMaterials()
{
if (materials != null)
{
while (materials.Count != 0)
{
RemoveMaterial(materials.Count - 1);
}
}
}
///
/// Returns a copy of the current list of renderers.
///
public IEnumerable GetRenderersCopy()
{
return new List(renderers);
}
///
/// Returns a copy of the current list of materials.
///
public IEnumerable GetMaterialsCopy()
{
return new List(materials);
}
#region MonoBehaviour Implementation
///
/// Turns clipping on.
///
protected void OnEnable()
{
Initialize();
UpdateRenderState();
#if UNITY_EDITOR
if (!Application.isPlaying)
{
UnityEditor.EditorApplication.update += EditorUpdate;
}
#endif
ToggleClippingFeature(true);
if (useOnPreRender)
{
cameraMethods = Camera.main.gameObject.EnsureComponent();
cameraMethods.OnCameraPreRender += OnCameraPreRender;
}
}
///
/// Turns clipping off.
///
protected void OnDisable()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.update -= EditorUpdate;
#endif
UpdateRenderState();
ToggleClippingFeature(false);
if (cameraMethods != null)
{
UseOnPreRender = false;
}
}
#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 here and runtime updating
// in LateUpdate().
protected void EditorUpdate()
{
Initialize();
UpdateRenderState();
}
#endif
///
/// Updates all renderers with latest clipping state.
///
protected void LateUpdate()
{
// Deferring the LateUpdate() call to OnCameraPreRender()
if (!useOnPreRender)
{
UpdateRenderState();
}
}
///
/// Updates all renderers with latest clipping state.
///
protected void OnCameraPreRender(CameraEventRouter router)
{
// Only subscribed to via UseOnPreRender property setter
UpdateRenderState();
}
///
/// Removes all renderers from the clipping primitive.
///
protected void OnDestroy()
{
ClearRenderers();
ClearMaterials();
}
#endregion MonoBehaviour Implementation
#region IMaterialInstanceOwner Implementation
///
public void OnMaterialChanged(MaterialInstance materialInstance)
{
if (materialInstance != null)
{
ToggleClippingFeature(materialInstance.AcquireMaterials(this), gameObject.activeInHierarchy);
}
UpdateRenderState();
}
#endregion IMaterialInstanceOwner Implementation
///
/// Caches commonly used state.
///
protected virtual void Initialize()
{
materialPropertyBlock = new MaterialPropertyBlock();
clippingSideID = Shader.PropertyToID(ClippingSideProperty);
}
///
/// Updates the render state of all renderers and materials.
///
protected virtual void UpdateRenderState()
{
CheckTransformChange();
if (!IsDirty)
{
return;
}
BeginUpdateShaderProperties();
if (renderers != null)
{
for (int i = renderers.Count - 1; i >= 0; --i)
{
var _renderer = renderers[i];
if (_renderer == null)
{
if (!Application.isEditor)
{
RemoveRenderer(i);
}
continue;
}
_renderer.GetPropertyBlock(materialPropertyBlock);
materialPropertyBlock.SetFloat(clippingSideID, (float)clippingSide);
UpdateShaderProperties(materialPropertyBlock);
_renderer.SetPropertyBlock(materialPropertyBlock);
}
}
if (materials != null)
{
for (int i = materials.Count - 1; i >= 0; --i)
{
var material = materials[i];
if (material == null)
{
if (!Application.isEditor)
{
RemoveMaterial(i);
}
continue;
}
material.SetFloat(clippingSideID, (float)clippingSide);
UpdateShaderProperties(material);
}
}
EndUpdateShaderProperties();
IsDirty = false;
}
///
/// Method called when before each renderer is updated.
///
protected virtual void BeginUpdateShaderProperties() { }
///
/// Method called for each renderer.
///
protected abstract void UpdateShaderProperties(MaterialPropertyBlock materialPropertyBlock);
///
/// Method called for each material.
///
protected abstract void UpdateShaderProperties(Material material);
///
/// Method called when after each renderer is updated.
///
protected virtual void EndUpdateShaderProperties() { }
///
/// Enables or disables clipping for all renderers.
///
protected void ToggleClippingFeature(bool keywordOn)
{
if (renderers != null)
{
for (var i = 0; i < renderers.Count; ++i)
{
var _renderer = renderers[i];
if (_renderer != null)
{
ToggleClippingFeature(AcquireMaterials(_renderer), keywordOn);
}
}
}
if (materials != null)
{
for (var i = 0; i < materials.Count; ++i)
{
var material = materials[i];
if (material != null)
{
ToggleClippingFeature(material, keywordOn);
}
}
}
}
///
/// Enables or disables clipping for a list of materials.
///
protected void ToggleClippingFeature(Material[] materialsToToggle, bool keywordOn)
{
if (materialsToToggle != null)
{
foreach (var material in materialsToToggle)
{
ToggleClippingFeature(material, keywordOn);
}
}
}
///
/// Enables or disables clipping on a material.
///
protected void ToggleClippingFeature(Material materialToToggle, bool keywordOn)
{
if (materialToToggle != null)
{
if (keywordOn)
{
materialToToggle.EnableKeyword(Keyword);
}
else
{
materialToToggle.DisableKeyword(Keyword);
}
}
}
private Material[] AcquireMaterials(Renderer renderer, bool instance = true)
{
if (applyToSharedMaterial)
{
return renderer.sharedMaterials;
}
else
{
return renderer.EnsureComponent().AcquireMaterials(this, instance);
}
}
private void CheckTransformChange()
{
if (transform.hasChanged)
{
IsDirty = true;
transform.hasChanged = false;
}
}
}
}