Files
ToriaAssets/Imports/Beautify addon/Editor/EdgeFusionEditor.cs
T

489 lines
21 KiB
C#
Raw Normal View History

2026-08-13 16:49:22 +02:00
using System;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
namespace EdgeFusion {
[CustomEditor(typeof(global::EdgeFusion.EdgeFusion))]
public class EdgeFusionEditor : VolumeComponentEditor {
SerializedDataParameter blendLayers, renderingLayerFilter, blendWithOthers, specialGroupLayers, doubleSidedLayers, intensity, radius, distanceCompensation, enableIntraObjectFusion, normalThreshold, concavityTest, intraObjectFusionPerObject;
SerializedDataParameter sampleCount, jitter, maxBlendDistance, binarySearchSteps, maxScreenRadius;
SerializedDataParameter doubleSidedRenderingLayerFilter, specialGroupRenderingLayerFilter;
SerializedDataParameter earlyExitHits, antiFlicker;
SerializedDataParameter debugMode, depthDebugMultiplier, shadowProtection, noiseIntensity, noiseScale, noiseContrast;
SerializedDataParameter compareMode, compareSameSide, comparePanning, compareLineAngle, compareLineWidth, compareLineColor;
SerializedDataParameter msaaEdgeFixPower;
SerializedProperty idExclusionPairs;
static readonly QualityPreset[] k_Presets = (QualityPreset[])Enum.GetValues(typeof(QualityPreset));
static readonly int[] k_PresetValues = k_Presets
.Select(p => (int)p)
.ToArray();
static PropertyInfo pipelineMsaaProperty;
static Type cachedPipelineType;
string[] presetNames;
bool renderFeaturePresent;
Terrain[] sceneTerrainsCache;
bool hasEdgeFusionObjectInScene;
const string k_AdditionalGroupsFoldoutKey = "EdgeFusion.AdditionalGroupsFoldout";
bool showAdditionalGroups;
bool msaaCameraActive;
public override void OnEnable () {
try {
presetNames = k_Presets
.Select(p => ObjectNames.NicifyVariableName(p.ToString()))
.ToArray();
var o = new PropertyFetcher<global::EdgeFusion.EdgeFusion>(serializedObject);
blendLayers = Unpack(o.Find(x => x.blendLayers));
renderingLayerFilter = Unpack(o.Find(x => x.renderingLayerFilter));
blendWithOthers = Unpack(o.Find(x => x.blendWithOthers));
specialGroupLayers = Unpack(o.Find(x => x.specialGroupLayers));
doubleSidedLayers = Unpack(o.Find(x => x.doubleSidedLayers));
doubleSidedRenderingLayerFilter = Unpack(o.Find(x => x.doubleSidedRenderingLayerFilter));
specialGroupRenderingLayerFilter = Unpack(o.Find(x => x.specialGroupRenderingLayerFilter));
intensity = Unpack(o.Find(x => x.intensity));
radius = Unpack(o.Find(x => x.radius));
distanceCompensation = Unpack(o.Find(x => x.distanceCompensation));
enableIntraObjectFusion = Unpack(o.Find(x => x.enableIntraObjectFusion));
intraObjectFusionPerObject = Unpack(o.Find(x => x.intraObjectFusionPerObject));
normalThreshold = Unpack(o.Find(x => x.normalThreshold));
concavityTest = Unpack(o.Find(x => x.concavityTest));
sampleCount = Unpack(o.Find(x => x.sampleCount));
jitter = Unpack(o.Find(x => x.jitter));
maxBlendDistance = Unpack(o.Find(x => x.maxBlendDistance));
binarySearchSteps = Unpack(o.Find(x => x.binarySearchSteps));
earlyExitHits = Unpack(o.Find(x => x.earlyExitHits));
antiFlicker = Unpack(o.Find(x => x.antiFlicker));
debugMode = Unpack(o.Find(x => x.debugMode));
depthDebugMultiplier = Unpack(o.Find(x => x.depthDebugMultiplier));
msaaEdgeFixPower = Unpack(o.Find(x => x.msaaEdgeFixPower));
shadowProtection = Unpack(o.Find(x => x.shadowProtection));
maxScreenRadius = Unpack(o.Find(x => x.maxScreenRadius));
noiseIntensity = Unpack(o.Find(x => x.noiseIntensity));
noiseScale = Unpack(o.Find(x => x.noiseScale));
noiseContrast = Unpack(o.Find(x => x.noiseContrast));
compareMode = Unpack(o.Find(x => x.compareMode));
compareSameSide = Unpack(o.Find(x => x.compareSameSide));
comparePanning = Unpack(o.Find(x => x.comparePanning));
compareLineAngle = Unpack(o.Find(x => x.compareLineAngle));
compareLineWidth = Unpack(o.Find(x => x.compareLineWidth));
compareLineColor = Unpack(o.Find(x => x.compareLineColor));
idExclusionPairs = serializedObject.FindProperty("idExclusionPairs");
renderFeaturePresent = IsEdgeFusionRenderFeaturePresent();
// PORT NOTE (Unity 2022.3): FindObjectsByType does not exist before 2023.1; use the classic API.
sceneTerrainsCache = UnityEngine.Object.FindObjectsOfType<Terrain>();
showAdditionalGroups = EditorPrefs.GetBool(k_AdditionalGroupsFoldoutKey, false);
msaaCameraActive = CameraUsesMsaa();
hasEdgeFusionObjectInScene = SceneHasEdgeFusionObject();
}
catch {
}
}
public override void OnInspectorGUI () {
int blendLayersValue = blendLayers.overrideState.boolValue ? blendLayers.value.intValue : -1;
int doubleSidedLayersValue = doubleSidedLayers.overrideState.boolValue ? doubleSidedLayers.value.intValue : 0;
int specialGroupLayersValue = specialGroupLayers.overrideState.boolValue ? specialGroupLayers.value.intValue : 0;
uint renderingLayerFilterValue = renderingLayerFilter.overrideState.boolValue ? (uint)renderingLayerFilter.value.intValue : uint.MaxValue;
serializedObject.Update();
if (!renderFeaturePresent) {
EditorGUILayout.Space();
EditorGUILayout.HelpBox("Edge Fusion Render Feature is not added to the current URP Renderer. The effect will not be visible.", MessageType.Error);
if (GUILayout.Button("Select Current URP Renderer", GUILayout.Height(20))) {
SelectCurrentURPRenderer();
}
EditorGUILayout.Space();
}
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Online Resources & Support")) {
ContactUsWindow.ShowScreen();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Object Selection", EditorStyles.boldLabel);
PropertyField(blendLayers);
PropertyField(renderingLayerFilter, new GUIContent("Rendering Layer Filter"));
// Only show "Blend With Others" if something is excluded (otherwise it has no effect)
bool somethingExcluded = blendLayersValue != -1 || renderingLayerFilterValue != uint.MaxValue;
if (somethingExcluded) {
PropertyField(blendWithOthers, new GUIContent("Blend With Others", "When enabled, visible geometry not selected by any layer setting will also blend with marked objects."));
}
EditorGUILayout.Space(4);
using (var foldoutScope = new EditorGUI.ChangeCheckScope()) {
showAdditionalGroups = EditorGUILayout.Foldout(showAdditionalGroups, "Additional Object Groups", true);
if (foldoutScope.changed) {
EditorPrefs.SetBool(k_AdditionalGroupsFoldoutKey, showAdditionalGroups);
}
}
if (showAdditionalGroups) {
EditorGUI.indentLevel++;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Double-Sided Objects", EditorStyles.miniLabel);
PropertyField(doubleSidedLayers, new GUIContent("Layers"));
PropertyField(doubleSidedRenderingLayerFilter, new GUIContent("Rendering Layer Filter"));
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Special Group (vertex displacement, animated vegetation)", EditorStyles.miniLabel);
PropertyField(specialGroupLayers, new GUIContent("Layers"));
PropertyField(specialGroupRenderingLayerFilter, new GUIContent("Rendering Layer Filter"));
EditorGUILayout.EndVertical();
EditorGUI.indentLevel--;
if (specialGroupLayers.overrideState.boolValue && specialGroupLayersValue == -1) {
EditorGUILayout.HelpBox("Special Group selection should only include objects with custom vertex shaders.", MessageType.Warning);
}
}
if ((blendLayersValue & doubleSidedLayersValue) != 0) {
EditorGUILayout.HelpBox("Layers included in Double-Sided Layers are automatically excluded from Blend Layers.", MessageType.Info);
}
if (((blendLayersValue & specialGroupLayersValue) != 0) || ((doubleSidedLayersValue & specialGroupLayersValue) != 0)) {
EditorGUILayout.HelpBox("Layers included in Special Group Layers are automatically excluded from Blend Layers and Double-Sided Layers.", MessageType.Info);
}
int effectiveBlendForTerrain = (blendLayersValue & ~doubleSidedLayersValue) & ~specialGroupLayersValue;
CheckTerrainBlendLayers(effectiveBlendForTerrain, renderingLayerFilterValue);
EditorGUILayout.Space();
EditorGUILayout.LabelField("ID Exclusions", EditorStyles.boldLabel);
DrawIdExclusionPairs(blendLayersValue, renderingLayerFilterValue);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Blending", EditorStyles.boldLabel);
PropertyField(intensity);
PropertyField(radius);
PropertyField(distanceCompensation, new GUIContent("Distance Compensation", "Scales the effective radius proportionally with distance from the camera. Keeps a small radius up close while increasing it for distant objects."));
PropertyField(maxScreenRadius);
PropertyField(enableIntraObjectFusion);
if (enableIntraObjectFusion.value.boolValue) {
EditorGUI.indentLevel++;
PropertyField(normalThreshold);
PropertyField(concavityTest);
PropertyField(intraObjectFusionPerObject, new GUIContent("Per Object", "When enabled, intra-object fusion only runs on renderers that have an Edge Fusion Object component with the effect allowed; fill-pass normals are skipped."));
if (intraObjectFusionPerObject.value.boolValue && !hasEdgeFusionObjectInScene) {
EditorGUILayout.HelpBox("Add 'Edge Fusion Object' component to the gameobjects you want to control intra-object fusion.", MessageType.Warning);
}
EditorGUI.indentLevel--;
}
PropertyField(shadowProtection);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Noise", EditorStyles.boldLabel);
PropertyField(noiseIntensity);
if (noiseIntensity.value.floatValue > 0) {
EditorGUI.indentLevel++;
PropertyField(noiseScale, new GUIContent("Scale"));
PropertyField(noiseContrast, new GUIContent("Contrast"));
EditorGUI.indentLevel--;
}
PropertyField(jitter);
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel);
if (GUILayout.Button("Tips", GUILayout.Width(50), GUILayout.Height(18))) {
Application.OpenURL("https://kronnect.com/guides/edge-fusion-performance-tips/");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
PropertyField(sampleCount);
int currentQuality = sampleCount.value.intValue;
int selectedIndex = GetClosestPresetIndex(currentQuality);
using (var cc = new EditorGUI.ChangeCheckScope()) {
EditorGUI.BeginDisabledGroup(!sampleCount.overrideState.boolValue);
int newIndex = EditorGUILayout.Popup(selectedIndex, presetNames ?? Array.Empty<string>(), GUILayout.Width(80f));
EditorGUI.EndDisabledGroup();
if (cc.changed && newIndex >= 0 && newIndex < k_PresetValues.Length) {
var selectedPreset = (QualityPreset)k_PresetValues[newIndex];
sampleCount.value.intValue = k_PresetValues[newIndex];
binarySearchSteps.value.intValue = GetBinarySearchStepsForPreset(selectedPreset);
binarySearchSteps.overrideState.boolValue = true;
earlyExitHits.value.intValue = GetEarlyExitHitsForPreset(selectedPreset);
earlyExitHits.overrideState.boolValue = true;
}
}
EditorGUILayout.EndHorizontal();
PropertyField(binarySearchSteps);
PropertyField(earlyExitHits, new GUIContent("Early Exit Hits", "Stops searching edge position after n nearest edge hits"));
PropertyField(antiFlicker, new GUIContent("Anti-Flicker", "Stabilises the blending when two edge candidates are near-tie in distance. Averages them with a soft-argmin so edgeUV moves continuously instead of snapping between winners. Helps in any scene with sub-pixel motion between frames (VR, slow camera motion, animated objects, FPS mouse look, camera shake). Disable to match the classic pick-one behaviour."));
PropertyField(maxBlendDistance);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Debug", EditorStyles.boldLabel);
PropertyField(debugMode);
if (debugMode.overrideState.boolValue && debugMode.value.intValue == (int)DebugMode.Depth) {
EditorGUI.indentLevel++;
PropertyField(depthDebugMultiplier, new GUIContent("Depth Multiplier"));
EditorGUI.indentLevel--;
}
if (debugMode.overrideState.boolValue && debugMode.value.intValue == (int)DebugMode.SpecialGroup && (!specialGroupLayers.overrideState.boolValue || specialGroupLayersValue == 0)) {
EditorGUILayout.HelpBox("Special Group is not executing because Special Group Layers is set to Nothing.", MessageType.Warning);
}
PropertyField(compareMode, new GUIContent("Compare Mode"));
if (compareMode.value.boolValue) {
EditorGUI.indentLevel++;
PropertyField(compareSameSide, new GUIContent("Same Side"));
if (compareSameSide.value.boolValue) {
PropertyField(comparePanning, new GUIContent("Panning"));
}
else {
PropertyField(compareLineAngle, new GUIContent("Line Angle"));
}
PropertyField(compareLineWidth, new GUIContent("Line Width"));
PropertyField(compareLineColor, new GUIContent("Line Color"));
EditorGUI.indentLevel--;
}
if (msaaCameraActive) {
PropertyField(msaaEdgeFixPower, new GUIContent("MSAA Edge Fix Amount", "When using MSAA in forward pass and certain blending layers configuration, you can use this option to reduce MSAA edge artifacts due to multi-sampling."));
}
serializedObject.ApplyModifiedProperties();
}
void CheckTerrainBlendLayers (int blendLayerMask, uint renderingLayerMaskValue) {
if (sceneTerrainsCache == null || sceneTerrainsCache.Length == 0) return;
bool hasTerrainWithDrawInstanced = false;
bool hasTerrainWithoutDrawInstanced = false;
uint rlMask = renderingLayerMaskValue;
foreach (var terrain in sceneTerrainsCache) {
if (terrain == null) continue;
bool selectedByBlendLayers = IsLayerInMask(terrain.gameObject.layer, blendLayerMask);
bool selectedByRenderingLayers = (rlMask == uint.MaxValue) || ((terrain.renderingLayerMask & rlMask) != 0);
bool consideredForBlend = selectedByBlendLayers && selectedByRenderingLayers;
if (!consideredForBlend) continue;
if (terrain.drawInstanced) {
hasTerrainWithDrawInstanced = true;
}
else {
hasTerrainWithoutDrawInstanced = true;
}
}
if (hasTerrainWithDrawInstanced) {
EditorGUILayout.HelpBox(
"Terrain has Draw Instanced option enabled which is not compatible with Edge Fusion. However, you can exclude it from blending layers or rendering layers as a workaround which is also faster.",
MessageType.Error
);
}
else if (hasTerrainWithoutDrawInstanced) {
EditorGUILayout.HelpBox(
"For best performance, you can exclude terrain objects from blending (via Blend Layers or Rendering Layers).",
MessageType.Warning
);
}
}
static bool IsLayerInMask (int layer, int layerMask) {
return ((layerMask & (1 << layer)) != 0);
}
static readonly string[] exclusionIdNames = BuildExclusionIdNames();
bool SceneHasEdgeFusionObject () {
// PORT NOTE (Unity 2022.3): FindObjectsByType does not exist before 2023.1; use the classic API.
return UnityEngine.Object.FindObjectsOfType<EdgeFusionObject>().Length > 0;
}
static string[] BuildExclusionIdNames () {
var names = new string[32];
for (int i = 0; i < 30; i++) {
names[i] = $"ID {i + 1}";
}
names[30] = "ID 31 (Terrain)";
names[31] = "ID 32 (Special Group)";
return names;
}
void DrawIdExclusionPairs (int blendLayersValue, uint renderingLayerFilterValue) {
EditorGUILayout.BeginHorizontal();
GUILayout.Space(15f);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Exclusion Pairs", EditorStyles.miniLabel);
bool usesTerrain = false;
for (int i = 0; i < idExclusionPairs.arraySize; i++) {
var element = idExclusionPairs.GetArrayElementAtIndex(i);
var enabledProp = element.FindPropertyRelative("enabled");
var idA = element.FindPropertyRelative("idA");
var idB = element.FindPropertyRelative("idB");
if (enabledProp.boolValue && (idA.intValue == 31 || idB.intValue == 31)) usesTerrain = true;
EditorGUILayout.BeginHorizontal();
enabledProp.boolValue = EditorGUILayout.Toggle(enabledProp.boolValue, GUILayout.Width(16));
EditorGUI.BeginDisabledGroup(!enabledProp.boolValue);
idA.intValue = EditorGUILayout.Popup(idA.intValue - 1, exclusionIdNames) + 1;
EditorGUILayout.LabelField("↔", GUILayout.Width(20));
idB.intValue = EditorGUILayout.Popup(idB.intValue - 1, exclusionIdNames) + 1;
EditorGUI.EndDisabledGroup();
if (GUILayout.Button("×", GUILayout.Width(20))) {
idExclusionPairs.DeleteArrayElementAtIndex(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
if (GUILayout.Button("+ Add Exclusion Pair")) {
idExclusionPairs.InsertArrayElementAtIndex(idExclusionPairs.arraySize);
var newElement = idExclusionPairs.GetArrayElementAtIndex(idExclusionPairs.arraySize - 1);
newElement.FindPropertyRelative("enabled").boolValue = true;
newElement.FindPropertyRelative("idA").intValue = 1;
newElement.FindPropertyRelative("idB").intValue = 31;
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
bool terrainIncludedInBlend = blendLayersValue == -1 && renderingLayerFilterValue == uint.MaxValue;
if (usesTerrain && terrainIncludedInBlend) {
EditorGUILayout.HelpBox(
"You are excluding Terrain (ID 31) but Blend Layers and Rendering Layer Filter include everything. " +
"For terrain to receive ID 31, it must be excluded from Blend Layers or Rendering Layer Filter so it goes through the fill pass.",
MessageType.Warning
);
}
if (idExclusionPairs.arraySize > 0) {
EditorGUILayout.HelpBox(
"IDs 1-30: Set via EdgeFusionObject component\n" +
"ID 31: Terrain (objects not in Blend Layers)\n" +
"ID 32: Special Group objects",
MessageType.Info
);
}
}
static int GetClosestPresetIndex (int sampleCount) {
int closestIndex = 0;
int smallestDelta = int.MaxValue;
for (int i = 0; i < k_PresetValues.Length; i++) {
int delta = Mathf.Abs(k_PresetValues[i] - sampleCount);
if (delta < smallestDelta) {
smallestDelta = delta;
closestIndex = i;
}
}
return closestIndex;
}
static int GetBinarySearchStepsForPreset (QualityPreset preset) {
switch (preset) {
case QualityPreset.VeryLow: return 2;
case QualityPreset.Low: return 4;
case QualityPreset.Medium: return 5;
case QualityPreset.High: return 7;
case QualityPreset.VeryHigh: return 8;
default: return 7;
}
}
static int GetEarlyExitHitsForPreset (QualityPreset preset) {
switch (preset) {
case QualityPreset.VeryLow: return 1;
case QualityPreset.Low: return 2;
case QualityPreset.Medium: return 3;
case QualityPreset.High: return 4;
case QualityPreset.VeryHigh: return 5;
default: return 5;
}
}
bool IsEdgeFusionRenderFeaturePresent () {
return GetCurrentURPRendererData() != null && CheckRendererForEdgeFusion(GetCurrentURPRendererData());
}
UnityEngine.Object GetCurrentURPRendererData () {
var pipeline = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
if (pipeline == null) return null;
var serializedAsset = new SerializedObject(pipeline);
var rendererDataList = serializedAsset.FindProperty("m_RendererDataList");
if (rendererDataList == null || !rendererDataList.isArray || rendererDataList.arraySize == 0) return null;
var defaultRendererIndex = serializedAsset.FindProperty("m_DefaultRendererIndex");
int index = defaultRendererIndex != null ? defaultRendererIndex.intValue : 0;
if (index < 0 || index >= rendererDataList.arraySize) return null;
var rendererDataProperty = rendererDataList.GetArrayElementAtIndex(index);
return rendererDataProperty.objectReferenceValue;
}
bool CheckRendererForEdgeFusion (UnityEngine.Object rendererData) {
if (rendererData == null) return false;
var serializedRenderer = new SerializedObject(rendererData);
var featuresProperty = serializedRenderer.FindProperty("m_RendererFeatures");
if (featuresProperty == null || !featuresProperty.isArray) return false;
for (int i = 0; i < featuresProperty.arraySize; i++) {
var featureProperty = featuresProperty.GetArrayElementAtIndex(i);
var feature = featureProperty.objectReferenceValue;
if (feature != null && feature.GetType().Name == "EdgeFusionRenderFeature") {
return true;
}
}
return false;
}
void SelectCurrentURPRenderer () {
var rendererData = GetCurrentURPRendererData();
if (rendererData != null) {
Selection.activeObject = rendererData;
EditorGUIUtility.PingObject(rendererData);
}
}
static bool CameraUsesMsaa () {
if (TryGetPipelineMsaaSamples(GraphicsSettings.currentRenderPipeline, out int pipelineSamples) && pipelineSamples > 1) {
return true;
}
foreach (Camera cam in Camera.allCameras) {
if (cam == null || !cam.allowMSAA) continue;
if (cam.targetTexture != null) {
if (cam.targetTexture.antiAliasing > 1) return true;
}
else if (QualitySettings.antiAliasing > 1) {
return true;
}
}
return false;
}
static bool TryGetPipelineMsaaSamples (RenderPipelineAsset pipeline, out int samples) {
samples = 0;
if (pipeline == null) return false;
var type = pipeline.GetType();
if (type != cachedPipelineType) {
cachedPipelineType = type;
pipelineMsaaProperty = type.GetProperty("msaaSampleCount", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (pipelineMsaaProperty != null && pipelineMsaaProperty.PropertyType == typeof(int)) {
samples = (int)pipelineMsaaProperty.GetValue(pipeline);
return true;
}
return false;
}
}
}