Files
aibis-dream/Assets/Shapes/Scripts/Editor/Components/ShapeRendererEditor.cs
T
2025-05-07 15:07:44 +08:00

294 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Reflection;
using Object = UnityEngine.Object;
// Shapes © Freya Holmér - https://twitter.com/FreyaHolmer/
// Website & Documentation - https://acegikmo.com/shapes/
namespace Shapes {
//[CustomEditor( typeof( ShapeRenderer ) )]
[CanEditMultipleObjects]
public class ShapeRendererEditor : Editor {
static bool showDepth = false;
static bool showStencil = false;
// ShapeRenderer
protected SerializedProperty propColor;
SerializedProperty propZTest = null;
SerializedProperty propZOffsetFactor = null;
SerializedProperty propZOffsetUnits = null;
SerializedProperty propColorMask = null;
SerializedProperty propStencilComp = null;
SerializedProperty propStencilOpPass = null;
SerializedProperty propStencilRefID = null;
SerializedProperty propStencilReadMask = null;
SerializedProperty propStencilWriteMask = null;
SerializedProperty propBlendMode = null;
SerializedProperty propScaleMode = null;
SerializedProperty propDetailLevel = null;
SerializedProperty propRenderQueue = null;
// MeshRenderer
SerializedObject soRnd;
SerializedProperty propSortingOrder;
SerializedProperty propSortingLayer;
static GUIContent blendModeGuiContent = new GUIContent(
"Blend Mode",
"[Opaque] does not support partial transparency, " +
"but will write to the depth buffer and sort correctly. " +
"For best results, use MSAA in your project to avoid aliasing " +
"(note that it may still be aliased in the scene view)\n" +
"\n" +
"[Transparent] supports partial transparency, " +
"but may not sort properly in some cases.\n" +
"\n" +
"[Additive] is good for glowing/brightening effects against dark backgrounds\n" +
"\n" +
"[Multiplicative] is good for tinting/darkening effects against bright backgrounds"
);
static GUIContent scaleModeGuiContent = new GUIContent(
"Scale Mode",
"[Uniform] mode means thickness will also scale with the transform, regardless of thickness space settings\n\n" +
"[Coordinate] mode means thickness values will remain the same even when scaling"
);
static GUIContent renderQueueGuiContent = new GUIContent(
"Render Queue",
"The render queue of this object. Default is -1, which means it will use the queue from the shader.\n\n" +
"[Opaque] = 2450 (AlphaTest)\n" +
"[All Other Blend Modes] = 3000 (Transparent)"
);
static GUIContent zTestGuiContent = new GUIContent(
"Depth Test",
"How this shape should render depending on the contents of the depth buffer. Note: anything other than [Less Equal] will not use GPU instancing\n\n" +
"[Less Equal] means it will not render when behind something (default)\n\n" +
"[Always] will completely ignore the depth buffer, drawing on top of everything else"
);
static GUIContent zOffsetFactorGuiContent = new GUIContent(
"Depth Offset Factor",
"Depth buffer offset factor, taking the slope into account (default is 0)\n\n" +
"Practically, this is mostly used to be able to have two things on the same plane, but have one still render on top of the other without Z-fighting/flickering.\n" +
"Setting this to, say, -1, will make this render on top of things in the same plane, setting this to 1 will make it render behind things on the same plane"
);
static GUIContent zOffsetUnitsGuiContent = new GUIContent(
"Depth Offset Units",
"Depth buffer offset, not taking the slope into account (default is 0)\n\n" +
"I've never found much use of this one, seems like a bad version of Z offset factor? It's mostly here for completeness I guess"
);
static GUIContent colorMaskGuiContent = new GUIContent(
"Color Mask",
"The color channels to render to (default is RGBA)\n\n" +
"This is useful when you want to specifically exclude writing to alpha, or, when you want it to not render color at all, and only render to stencil"
);
static GUIContent stencilCompGuiContent = new GUIContent( "Compare", "Stencil compare function" );
static GUIContent stencilOpPassGuiContent = new GUIContent( "Pass", "Stencil Op Pass" );
static GUIContent stencilIDGuiContent = new GUIContent( "Ref", "Stencil reference ID" );
static GUIContent stencilReadMaskGuiContent = new GUIContent( "Read Mask", "Bitmask for reading stencil values" );
static GUIContent stencilWriteMaskGuiContent = new GUIContent( "Write Mask", "Bitmask for writing stencil values" );
public virtual void OnEnable() {
soRnd = new SerializedObject( targets.Select( t => ( (Component)t ).GetComponent<MeshRenderer>() as Object ).ToArray() );
propSortingOrder = soRnd.FindProperty( "m_SortingOrder" );
propSortingLayer = soRnd.FindProperty( "m_SortingLayerID" );
// will assign all null properties, even in derived types
FindAllProperties();
// hide mesh filter/renderer components
foreach( ShapeRenderer shape in targets.Cast<ShapeRenderer>() )
shape.HideMeshFilterRenderer();
}
void FindAllProperties() {
IEnumerable<FieldInfo> GetFields( Type type ) {
return type.GetFields( BindingFlags.Instance | BindingFlags.NonPublic )
.Where( x => x.FieldType == typeof(SerializedProperty) && x.Name.StartsWith( "m_" ) == false && x.GetValue( this ) == null );
}
IEnumerable<FieldInfo> fieldsBase = GetFields( GetType().BaseType );
IEnumerable<FieldInfo> fieldsInherited = GetFields( GetType() );
foreach( FieldInfo field in fieldsBase.Concat( fieldsInherited ) ) {
string fieldName = char.ToLowerInvariant( field.Name[4] ) + field.Name.Substring( 5 );
field.SetValue( this, serializedObject.FindProperty( fieldName ) );
if( field.GetValue( this ) == null )
Debug.LogError( $"Failed to load {target.GetType()} property: {field.Name} !=> {fieldName}" );
}
}
bool updateMeshFromEditorChange = false;
protected void BeginProperties( bool showColor = true, bool canEditDetailLevel = true ) {
soRnd.Update();
using( new ShapesUI.GroupScope() ) {
updateMeshFromEditorChange = false;
ShapesUI.SortedEnumPopup<ShapesBlendMode>( blendModeGuiContent, propBlendMode );
if( ( target as ShapeRenderer ).HasScaleModes )
EditorGUILayout.PropertyField( propScaleMode, scaleModeGuiContent );
// sorting/depth stuff
using( new EditorGUI.IndentLevelScope( 1 ) ) {
if( showDepth = EditorGUILayout.Foldout( showDepth, "Sorting & Depth" ) ) {
using( ShapesUI.TempLabelWidth( 140 ) ) {
EditorGUILayout.PropertyField( propRenderQueue, renderQueueGuiContent );
ShapesUI.RenderSortingLayerField( propSortingLayer );
EditorGUILayout.PropertyField( propSortingOrder );
EditorGUILayout.PropertyField( propZTest, zTestGuiContent );
EditorGUILayout.PropertyField( propZOffsetFactor, zOffsetFactorGuiContent );
EditorGUILayout.PropertyField( propZOffsetUnits, zOffsetUnitsGuiContent );
}
}
}
// stencil
using( new EditorGUI.IndentLevelScope( 1 ) ) {
if( showStencil = EditorGUILayout.Foldout( showStencil, "Masking" ) ) {
DrawColorMaskButtons();
EditorGUILayout.PropertyField( propStencilComp, stencilCompGuiContent );
EditorGUILayout.PropertyField( propStencilOpPass, stencilOpPassGuiContent );
EditorGUILayout.PropertyField( propStencilRefID, stencilIDGuiContent );
EditorGUILayout.PropertyField( propStencilReadMask, stencilReadMaskGuiContent );
EditorGUILayout.PropertyField( propStencilWriteMask, stencilWriteMaskGuiContent );
}
}
// warning box about instancing
int uniqueCount = 0;
int instancedCount = 0;
foreach( ShapeRenderer obj in targets.Cast<ShapeRenderer>() ) {
if( obj.IsUsingUniqueMaterials )
uniqueCount++;
else
instancedCount++;
}
if( uniqueCount > 0 ) {
string infix;
if( instancedCount == 0 )
infix = uniqueCount == 1 ? "this object is" : "these objects are";
else // mixed selection
infix = "some of these objects are";
string label = $"Note: {infix} not GPU instanced due to custom depth/mask settings";
GUIStyle wrapLabel = new GUIStyle( EditorStyles.miniLabel );
wrapLabel.wordWrap = true;
using( ShapesUI.Horizontal ) {
GUIContent icon = EditorGUIUtility.IconContent( "console.warnicon.sml" );
GUILayout.Label( icon );
GUILayout.TextArea( label, wrapLabel );
if( GUILayout.Button( "Reset", EditorStyles.miniButton ) ) {
propZTest.enumValueIndex = (int)ShapeRenderer.DEFAULT_ZTEST;
propZOffsetFactor.floatValue = ShapeRenderer.DEFAULT_ZOFS_FACTOR;
propZOffsetUnits.intValue = ShapeRenderer.DEFAULT_ZOFS_UNITS;
propColorMask.intValue = (int)ShapeRenderer.DEFAULT_COLOR_MASK;
propStencilComp.enumValueIndex = (int)ShapeRenderer.DEFAULT_STENCIL_COMP;
propStencilOpPass.enumValueIndex = (int)ShapeRenderer.DEFAULT_STENCIL_OP;
propStencilRefID.intValue = ShapeRenderer.DEFAULT_STENCIL_REF_ID;
propStencilReadMask.intValue = ShapeRenderer.DEFAULT_STENCIL_MASK;
propStencilWriteMask.intValue = ShapeRenderer.DEFAULT_STENCIL_MASK;
propRenderQueue.intValue = ShapeRenderer.DEFAULT_RENDER_QUEUE_AUTO;
}
}
}
}
if( ( target as ShapeRenderer ).HasDetailLevels ) {
using( new EditorGUI.DisabledScope( canEditDetailLevel == false ) ) {
if( canEditDetailLevel ) {
using( var chChk = new EditorGUI.ChangeCheckScope() ) {
EditorGUILayout.PropertyField( propDetailLevel );
if( chChk.changed )
updateMeshFromEditorChange = true;
}
} else {
EditorGUILayout.TextField( propDetailLevel.displayName, "∞", GUI.skin.label );
}
}
}
if( showColor )
PropertyFieldColor();
}
protected bool EndProperties() {
bool propertiesDidChange = soRnd.ApplyModifiedProperties() | serializedObject.ApplyModifiedProperties();
if( updateMeshFromEditorChange ) {
foreach( ShapeRenderer shape in targets.Cast<ShapeRenderer>() )
shape.UpdateMesh();
updateMeshFromEditorChange = false;
}
return propertiesDidChange;
}
protected void PropertyFieldColor() => EditorGUILayout.PropertyField( propColor );
protected void PropertyFieldColor( string s ) => EditorGUILayout.PropertyField( propColor, new GUIContent( s ) );
protected void PropertyFieldColor( GUIContent content ) => EditorGUILayout.PropertyField( propColor, content );
const string colorCh = "RGBA";
const float COLOR_BTN_SAT = 0.5f;
static Color[] channelColors = new Color[] {
Color.HSVToRGB( 0, COLOR_BTN_SAT, 1 ),
Color.HSVToRGB( 1f / 3f, COLOR_BTN_SAT, 1 ),
Color.HSVToRGB( 2f / 3f, COLOR_BTN_SAT, 1 ),
Color.HSVToRGB( 0, 0, 1 ),
};
static Color channelUnsetColor = new Color( 0.5f, 0.5f, 0.5f, 1f );
void DrawColorMaskButtons() {
int value = propColorMask.intValue;
void DoButton( int i ) {
int flagValue = 1 << ( 3 - i ); // enum is in ABGR order
bool prevBit = ( value & flagValue ) > 0;
GUI.color = prevBit ? channelColors[i] : channelUnsetColor;
bool newBit = GUILayout.Toggle( prevBit, colorCh[i].ToString(), ShapesUI.GetMiniButtonStyle( i, 4 ), GUILayout.Width( 28 ) );
GUI.color = Color.white;
if( prevBit != newBit ) {
int sign = prevBit == false ? 1 : -1;
propColorMask.intValue += sign * flagValue;
}
}
GUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel( colorMaskGuiContent );
for( int i = 0; i < 4; i++ )
DoButton( i );
GUILayout.EndHorizontal();
}
public bool HasFrameBounds() => true;
public Bounds OnGetFrameBounds() {
if( serializedObject.isEditingMultipleObjects ) {
// this only works for multiselecting shapes of the same type
// todo: might be able to make a solution using Editor.CreateEditor shenanigans
Bounds bounds = ( (ShapeRenderer)serializedObject.targetObjects[0] ).GetWorldBounds();
for( int i = 1; i < serializedObject.targetObjects.Length; i++ )
bounds.Encapsulate( ( (ShapeRenderer)serializedObject.targetObjects[i] ).GetWorldBounds() );
return bounds;
} else {
return ( (ShapeRenderer)target ).GetWorldBounds();
}
}
}
}