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