using System.Linq; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Rendering; // Shapes © Freya Holmér - https://twitter.com/FreyaHolmer/ // Website & Documentation - https://acegikmo.com/shapes/ namespace Shapes { /// The base type of all shape components [DisallowMultipleComponent] public abstract class ShapeRenderer : MonoBehaviour { bool initializedComponents = false; MeshRenderer rnd; MeshFilter mf; int meshOwnerID; MaterialPropertyBlock mpb; MaterialPropertyBlock Mpb => mpb ?? ( mpb = new MaterialPropertyBlock() ); // hecking, gosh, I want the C#8 ??= operator Material[] instancedMaterials = null; // used when pass tags are anything but the default (eg ZTest != Less Equal, or scale offset is set, or a weird blend mode) internal bool IsUsingUniqueMaterials => IsInstanced == false; /// Used to mark this mesh as changed. Only required when manually editing the point lists of polygons and polylines, otherwise you shouldn't ever need to use this [System.NonSerialized] public bool meshOutOfDate = true; /// The current mesh in use by this shape. Note: Do not directly modify this mesh, it may not update properly or your changes will be overwritten or you'll modify assets. It's here for reading purposes only public Mesh Mesh { get => mf.sharedMesh; private set => mf.sharedMesh = value; } /// The sorting layer ID of this renderer public int SortingLayerID { get => MakeSureComponentExists( ref rnd, out _ ).sortingLayerID; set => MakeSureComponentExists( ref rnd, out _ ).sortingLayerID = value; } /// The sorting order of this renderer public int SortingOrder { get => MakeSureComponentExists( ref rnd, out _ ).sortingOrder; set => MakeSureComponentExists( ref rnd, out _ ).sortingOrder = value; } /// Gets the name of the current sorting layer of this renderer public string SortingLayerName => SortingLayer.IDToName( SortingLayerID ); // Properties [SerializeField] ShapesBlendMode blendMode = ShapesBlendMode.Transparent; /// What blending mode to use public ShapesBlendMode BlendMode { get => blendMode; set { blendMode = value; UpdateMaterial(); } } [SerializeField] ScaleMode scaleMode = ScaleMode.Uniform; /// Sets how this shape should behave when scaled public ScaleMode ScaleMode { get => scaleMode; set => SetIntNow( ShapesMaterialUtils.propScaleMode, (int)( scaleMode = value ) ); } [SerializeField] [ShapesColorField( true )] private protected Color color = Color.white; /// The color of this shape. The alpha channel is used for opacity/intensity in all blend modes public virtual Color Color { get => color; set => SetColorNow( ShapesMaterialUtils.propColor, color = value ); } [SerializeField] private protected DetailLevel detailLevel = DetailLevel.Medium; /// What detail level to use for 3D primitives (3D Lines/Sphere/Torus/Cone) public virtual DetailLevel DetailLevel { get => detailLevel; set { detailLevel = value; UpdateMesh( force: true ); } } #region instancing breaking pass tags // if and only if all are set to their default values bool IsInstanced => UsingDefaultZTests && UsingDefaultMasking && UsingDefaultRenderQueue; // render queue offset bool UsingDefaultRenderQueue => renderQueue == DEFAULT_RENDER_QUEUE_AUTO; public int RenderQueue { get => renderQueue; set { renderQueue = value; if( IsUsingUniqueMaterials ) { UpdateMaterial(); foreach( Material instancedMaterial in instancedMaterials ) instancedMaterial.renderQueue = renderQueue; } } } [SerializeField] int renderQueue = DEFAULT_RENDER_QUEUE_AUTO; /// The default render queue, which is auto-set based on blend mode public const int DEFAULT_RENDER_QUEUE_AUTO = -1; // Z testing properties /// The default Z test (depth test) value of LessEqual public const CompareFunction DEFAULT_ZTEST = CompareFunction.LessEqual; /// The default Z offset factor of 0 public const float DEFAULT_ZOFS_FACTOR = 0f; /// The default Z offset units of 0 public const int DEFAULT_ZOFS_UNITS = 0; /// The default ColorMask of 15 (RGBA) public const ColorWriteMask DEFAULT_COLOR_MASK = ColorWriteMask.All; bool UsingDefaultZTests => zTest == DEFAULT_ZTEST && zOffsetFactor == DEFAULT_ZOFS_FACTOR && zOffsetUnits == DEFAULT_ZOFS_UNITS; [SerializeField] CompareFunction zTest = DEFAULT_ZTEST; /// public CompareFunction ZTest { get => zTest; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propZTest, (int)( zTest = value ) ); } [SerializeField] float zOffsetFactor = DEFAULT_ZOFS_FACTOR; /// public float ZOffsetFactor { get => zOffsetFactor; set => SetFloatOnAllInstancedMaterials( ShapesMaterialUtils.propZOffsetFactor, zOffsetFactor = value ); } [SerializeField] int zOffsetUnits = DEFAULT_ZOFS_UNITS; /// public int ZOffsetUnits { get => zOffsetUnits; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propZOffsetUnits, zOffsetUnits = value ); } [SerializeField] ColorWriteMask colorMask = DEFAULT_COLOR_MASK; /// public ColorWriteMask ColorMask { get => colorMask; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propColorMask, (int)( colorMask = value ) ); } // stencil /// The default stencil compare function of CompareFunction.Always public const CompareFunction DEFAULT_STENCIL_COMP = CompareFunction.Always; /// The default stencil operation of StencilOp.Keep public const StencilOp DEFAULT_STENCIL_OP = StencilOp.Keep; /// The default stencil ref id 0 public const byte DEFAULT_STENCIL_REF_ID = 0; /// The default stencil mask of 255 public const byte DEFAULT_STENCIL_MASK = 255; // read & write bool UsingDefaultMasking => stencilComp == DEFAULT_STENCIL_COMP && stencilOpPass == DEFAULT_STENCIL_OP && stencilRefID == DEFAULT_STENCIL_REF_ID && stencilReadMask == DEFAULT_STENCIL_MASK && stencilWriteMask == DEFAULT_STENCIL_MASK && colorMask == DEFAULT_COLOR_MASK; [SerializeField] CompareFunction stencilComp = DEFAULT_STENCIL_COMP; /// public CompareFunction StencilComp { get => stencilComp; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilComp, (int)( stencilComp = value ) ); } [SerializeField] StencilOp stencilOpPass = DEFAULT_STENCIL_OP; /// public StencilOp StencilOpPass { get => stencilOpPass; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilOpPass, (int)( stencilOpPass = value ) ); } [SerializeField] byte stencilRefID = DEFAULT_STENCIL_REF_ID; /// public byte StencilRefID { get => stencilRefID; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilID, stencilRefID = value ); } [SerializeField] byte stencilReadMask = DEFAULT_STENCIL_MASK; /// public byte StencilReadMask { get => stencilReadMask; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilReadMask, stencilReadMask = value ); } [SerializeField] byte stencilWriteMask = DEFAULT_STENCIL_MASK; /// public byte StencilWriteMask { get => stencilWriteMask; set => SetIntOnAllInstancedMaterials( ShapesMaterialUtils.propStencilWriteMask, stencilWriteMask = value ); } #endregion #if UNITY_EDITOR public virtual void OnValidate() { // OnValidate can get called before awake in editor, so make sure the required things are initialized if( rnd == null ) rnd = GetComponent(); // Needed for ApplyProperties if( mf == null ) mf = GetComponent(); // Needed for UpdateMesh ShapeClampRanges(); UpdateAllMaterialProperties(); ApplyProperties(); if( MeshUpdateMode == MeshUpdateMode.SelfGenerated ) meshOutOfDate = true; // we can't update the mesh in OnValidate because of https://issuetracker.unity3d.com/issues/warning-appears-when-changing-meshfilter-dot-sharedmesh-during-onvalidate // UpdateMesh( force:true ); gosh I wish I could do this it would solve so many problems but Unity has some WEIRD quirks here } internal void HideMeshFilterRenderer() { VerifyComponents(); const HideFlags flags = HideFlags.HideInInspector; // Hide mesh renderer and filter rnd.hideFlags = flags; mf.hideFlags = flags; } #endif T MakeSureComponentExists( ref T field, out bool created ) where T : Component { if( field == null ) { field = GetComponent(); if( field == null ) { field = gameObject.AddComponent(); created = true; } field.hideFlags = HideFlags.HideInInspector; } created = false; return field; } void VerifyComponents() { if( initializedComponents == false ) { initializedComponents = true; MakeSureComponentExists( ref mf, out _ ); MakeSureComponentExists( ref rnd, out bool createdRnd ); } // these have to be turned off aggressively, otherwise batching can break if( rnd.receiveShadows ) rnd.receiveShadows = false; if( rnd.shadowCastingMode != ShadowCastingMode.Off ) rnd.shadowCastingMode = ShadowCastingMode.Off; if( rnd.lightProbeUsage != LightProbeUsage.Off ) rnd.lightProbeUsage = LightProbeUsage.Off; if( rnd.reflectionProbeUsage != ReflectionProbeUsage.Off ) rnd.reflectionProbeUsage = ReflectionProbeUsage.Off; } public virtual void Awake() { VerifyComponents(); UpdateMaterial(); UpdateMesh(); UpdateAllMaterialProperties(); } bool HasGeneratedOrCopyOfMesh => MeshUpdateMode == MeshUpdateMode.SelfGenerated || MeshUpdateMode == MeshUpdateMode.UseAssetCopy; public virtual void OnEnable() { UpdateMesh(); rnd.enabled = true; #if UNITY_EDITOR if( HasGeneratedOrCopyOfMesh ) UnityEditor.Undo.undoRedoPerformed += UpdateMeshOnUndoRedo; #endif if( UseCamOnPreCull ) SubscribeCamPreCull(); } void OnDisable() { if( rnd != null ) rnd.enabled = false; #if UNITY_EDITOR if( HasGeneratedOrCopyOfMesh ) UnityEditor.Undo.undoRedoPerformed -= UpdateMeshOnUndoRedo; #endif if( UseCamOnPreCull ) UnsubscribeCamPreCull(); } // These are used for polyline and polygon to detect mesh changes and lazily update before rendering void OnPreCamCullWithCam( Camera cam ) => CamOnPreCull(); #if UNITY_2019_1_OR_NEWER void OnPreCamCullWithCam( ScriptableRenderContext ctx, Camera cam ) => CamOnPreCull(); #endif void SubscribeCamPreCull() { if( UnityInfo.UsingSRP ) { #if UNITY_2019_1_OR_NEWER RenderPipelineManager.beginCameraRendering += OnPreCamCullWithCam; #else UnityEngine.Experimental.Rendering.RenderPipeline.beginCameraRendering += OnPreCamCullWithCam; #endif } else Camera.onPreCull += OnPreCamCullWithCam; } void UnsubscribeCamPreCull() { if( UnityInfo.UsingSRP ) { #if UNITY_2019_1_OR_NEWER RenderPipelineManager.beginCameraRendering -= OnPreCamCullWithCam; #else UnityEngine.Experimental.Rendering.RenderPipeline.beginCameraRendering -= OnPreCamCullWithCam; #endif } else Camera.onPreCull -= OnPreCamCullWithCam; } #if UNITY_EDITOR void UpdateMeshOnUndoRedo() => UpdateMesh( true ); #endif void Reset() { UpdateAllMaterialProperties(); UpdateMesh( true ); } void OnDestroy() { if( HasGeneratedOrCopyOfMesh && Mesh != null ) DestroyImmediate( Mesh ); this.TryDestroyInOnDestroy( rnd ); this.TryDestroyInOnDestroy( mf ); TryDestroyInstancedMaterials( inOnDestroy: true ); } private protected abstract Bounds GetBounds_Internal(); private protected abstract void SetAllMaterialProperties(); private protected virtual void ShapeClampRanges() => _ = 0; private protected abstract Material[] GetMaterials(); private protected virtual void GenerateMesh() => _ = 0; private protected virtual Mesh GetInitialMeshAsset() => ShapesMeshUtils.QuadMesh[HasDetailLevels ? (int)DetailLevel.Medium : 0]; private protected virtual MeshUpdateMode MeshUpdateMode => MeshUpdateMode.UseAsset; internal virtual bool HasScaleModes => true; internal virtual bool HasDetailLevels => true; private protected virtual bool UseCamOnPreCull => false; internal virtual void CamOnPreCull() => _ = 0; void UpdateMeshBounds() => Mesh.bounds = GetBounds_Internal(); void TryDestroyInstancedMaterials( bool inOnDestroy = false ) { if( instancedMaterials != null ) { for( int i = 0; i < instancedMaterials.Length; i++ ) { if( instancedMaterials[i] != null ) { if( inOnDestroy ) this.TryDestroyInOnDestroy( instancedMaterials[i] ); else instancedMaterials[i].DestroyBranched(); } } } } void MakeSureMaterialInstancesAreGood( Material[] sourceMats ) { Material InstantiateMaterial( int index ) => new Material( sourceMats[index] ) { name = sourceMats[index].name + " (instance)" }; void PopulateAll() { instancedMaterials = new Material[sourceMats.Length]; for( int i = 0; i < sourceMats.Length; i++ ) instancedMaterials[i] = InstantiateMaterial( i ); } if( instancedMaterials == null ) { // no instanced materials exist, create all PopulateAll(); } else { if( instancedMaterials.Length != sourceMats.Length ) { // length mismatch, regenerate all TryDestroyInstancedMaterials(); PopulateAll(); } else { // same length! make sure they are all matching for( int i = 0; i < sourceMats.Length; i++ ) { if( instancedMaterials[i] == null ) { // if null, create instance instancedMaterials[i] = InstantiateMaterial( i ); } else { // if not null, then make sure the shader is matching if( instancedMaterials[i].shader != sourceMats[i].shader ) { // mismatch, destroy instance and assign new one instancedMaterials[i].DestroyBranched(); instancedMaterials[i] = InstantiateMaterial( i ); } else { // they do use the same shader, but, make sure they also use the same keywords just to be safe instancedMaterials[i].shaderKeywords = sourceMats[i].shaderKeywords; } } } } } } private protected void UpdateMaterial() { Material[] targetMats = GetMaterials(); // this means we have unique material properties for this shape, so, instantiate them // but! only when they're in the scene if( IsUsingUniqueMaterials ) { MakeSureMaterialInstancesAreGood( targetMats ); targetMats = instancedMaterials; } VerifyComponents(); #if UNITY_EDITOR if( EditorApplication.isPlaying == false ) UpdateMaterialsEditorMode( targetMats ); else #endif rnd.sharedMaterials = targetMats; } #if UNITY_EDITOR void UpdateMaterialsEditorMode( Material[] targetMats ) { bool needsUpdate = false; if( rnd.sharedMaterials.Length != targetMats.Length ) { needsUpdate = true; } else { for( int i = 0; i < targetMats.Length; i++ ) { if( rnd.sharedMaterials[i] != targetMats[i] ) { needsUpdate = true; break; } } } if( needsUpdate ) { Undo.RecordObject( rnd, "" ); rnd.sharedMaterials = targetMats; } } #endif /// Updates the mesh this object is using. /// If this mesh generates procedural meshes, such as the polygon or polyline, it will regenerate it if force is set to true /// public void UpdateMesh( bool force = false ) { MeshUpdateMode mode = MeshUpdateMode; // if we're using a mesh asset, we only assign if it's null or mismatching if( mode == MeshUpdateMode.UseAsset && ( Mesh == null || Mesh != GetInitialMeshAsset() ) ) { Mesh = GetInitialMeshAsset(); return; } // the next two modes are copy-sensitive, meaning that if we duplicate this object, // we also have to duplicate the mesh and update which mesh the duplicate is pointing to int id = gameObject.GetInstanceID(); bool createMesh = Mesh == null || meshOwnerID != id; // create new mesh if( createMesh ) { meshOwnerID = id; if( mode == MeshUpdateMode.UseAssetCopy ) { Mesh = Instantiate( GetInitialMeshAsset() ); Mesh.hideFlags = HideFlags.HideAndDontSave; Mesh.MarkDynamic(); } else if( mode == MeshUpdateMode.SelfGenerated ) { Mesh = new Mesh() { hideFlags = HideFlags.HideAndDontSave }; Mesh.MarkDynamic(); GenerateMesh(); } } else if( force && mode == MeshUpdateMode.SelfGenerated ) { GenerateMesh(); // update existing mesh } } /// Returns the local space bounds /// Note: This does not take into account screen space sizing, so it will only behave as you expect when using meters only public Bounds GetBounds() => GetBounds_Internal(); /// Returns the world space bounds of the local space bounds /// Note: This does not take into account screen space sizing, so it will only behave as you expect when using meters only public Bounds GetWorldBounds() { Bounds localBounds = GetBounds_Internal(); Vector3 min = Vector3.one * float.MaxValue; Vector3 max = Vector3.one * float.MinValue; Transform tf = transform; for( int x = -1; x <= 1; x += 2 ) for( int y = -1; y <= 1; y += 2 ) for( int z = -1; z <= 1; z += 2 ) { Vector3 wPt = tf.TransformPoint( localBounds.center + Vector3.Scale( localBounds.extents, new Vector3( x, y, z ) ) ); min = Vector3.Min( min, wPt ); max = Vector3.Max( max, wPt ); } return new Bounds( ( max + min ) / 2f, max - min ); } void OnDidApplyAnimationProperties() => UpdateAllMaterialProperties(); // so this is not great but it works don't judge void SetIntOnAllInstancedMaterials( int property, int value ) { if( IsUsingUniqueMaterials ) { UpdateMaterial(); foreach( Material instancedMaterial in instancedMaterials ) instancedMaterial.SetInt_Shapes( property, value ); } } void SetFloatOnAllInstancedMaterials( int property, float value ) { if( IsUsingUniqueMaterials ) { UpdateMaterial(); foreach( Material instancedMaterial in instancedMaterials ) instancedMaterial.SetFloat( property, value ); } } internal void UpdateAllMaterialProperties() { if( gameObject.scene.IsValid() == false ) return; // not in a scene :c UpdateMaterial(); if( IsUsingUniqueMaterials ) // I wish we could material property block these ;-; foreach( Material instancedMaterial in instancedMaterials ) { instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propZTest, (int)zTest ); instancedMaterial.SetFloat( ShapesMaterialUtils.propZOffsetFactor, zOffsetFactor ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propZOffsetUnits, zOffsetUnits ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propColorMask, (int)colorMask ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilComp, (int)stencilComp ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilOpPass, (int)stencilOpPass ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilID, stencilRefID ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilReadMask, stencilReadMask ); instancedMaterial.SetInt_Shapes( ShapesMaterialUtils.propStencilWriteMask, stencilWriteMask ); instancedMaterial.renderQueue = renderQueue; } SetColor( ShapesMaterialUtils.propColor, color ); if( HasScaleModes ) SetInt( ShapesMaterialUtils.propScaleMode, (int)scaleMode ); SetAllMaterialProperties(); ApplyProperties(); } private protected void ApplyProperties() { VerifyComponents(); // make sure components exists. rnd can be uninitialized if you modify an object that has never had awake called rnd.SetPropertyBlock( Mpb ); if( MeshUpdateMode == MeshUpdateMode.UseAssetCopy ) UpdateMeshBounds(); } private protected void SetAllDashValues( DashStyle style, bool dashed, bool matchSpacingToSize, float thickness, bool setType, bool now ) { float netDashSize = style.GetNetAbsoluteSize( dashed, thickness ); if( dashed ) { SetFloat( ShapesMaterialUtils.propDashSpacing, GetNetDashSpacing( style, true, matchSpacingToSize, thickness ) ); SetFloat( ShapesMaterialUtils.propDashOffset, style.offset ); SetInt( ShapesMaterialUtils.propDashSpace, (int)style.space ); SetInt( ShapesMaterialUtils.propDashSnap, (int)style.snap ); if( setType ) { SetInt( ShapesMaterialUtils.propDashType, (int)style.type ); if( style.type.HasModifier() ) SetFloat( ShapesMaterialUtils.propDashShapeModifier, style.shapeModifier ); } } if( now ) SetFloatNow( ShapesMaterialUtils.propDashSize, netDashSize ); else SetFloat( ShapesMaterialUtils.propDashSize, netDashSize ); } private protected float GetNetDashSpacing( DashStyle style, bool dashed, bool matchSpacingToSize, float thickness ) { if( matchSpacingToSize && style.space == DashSpace.FixedCount ) return 0.5f; return matchSpacingToSize ? style.GetNetAbsoluteSize( dashed, thickness ) : style.GetNetAbsoluteSpacing( dashed, thickness ); } private protected void SetColor( int prop, Color value ) { if( ShapeGroup.shapeGroupsInScene > 0 ) { // if color tint groups exist, see if we have any ShapeGroup[] groups = GetComponentsInParent(); if( groups != null ) foreach( ShapeGroup shapeGroup in groups.Where( g => g.IsEnabled ) ) value *= shapeGroup.Color; } Mpb.SetColor( prop, value ); } private protected void SetFloat( int prop, float value ) => Mpb.SetFloat( prop, value ); private protected void SetInt( int prop, int value ) => Mpb.SetInt_Shapes( prop, value ); private protected void SetVector3( int prop, Vector3 value ) => Mpb.SetVector( prop, value ); private protected void SetVector4( int prop, Vector4 value ) => Mpb.SetVector( prop, value ); private protected void SetColorNow( int prop, Color value ) { SetColor( prop, value ); ApplyProperties(); } private protected void SetFloatNow( int prop, float value ) { Mpb.SetFloat( prop, value ); ApplyProperties(); } private protected void SetIntNow( int prop, int value ) { Mpb.SetInt_Shapes( prop, value ); ApplyProperties(); } private protected void SetVector3Now( int prop, Vector3 value ) { Mpb.SetVector( prop, value ); ApplyProperties(); } private protected void SetVector4Now( int prop, Vector4 value ) { Mpb.SetVector( prop, value ); ApplyProperties(); } } }