/******************************************************************************/
/*
  Project   - MudBun
  Publisher - Long Bunny Labs
              http://LongBunnyLabs.com
  Author    - Ming-Lun "Allen" Chou
              http://AllenChou.net
*/
/******************************************************************************/

using System;
using System.Linq;

using UnityEditor;
using UnityEngine;

namespace MudBun
{
  [CustomEditor(typeof(MudRendererBase))]
  [CanEditMultipleObjects]
  public class MudRendererBaseEditor : MudEditorBase
  {
    protected SerializedProperty MaxVoxelsK;
    protected SerializedProperty MaxChunks;
    protected SerializedProperty ShowGpuMemoryUsage;
    protected SerializedProperty AutoAdjustBudgetsToHighWaterMarks;
    protected SerializedProperty AutoAdjustBudgetsToHighWaterMarksMarginPercent;

    protected SerializedProperty VoxelDensity;
    protected SerializedProperty ComputeMode;
    protected SerializedProperty TimeSliceMode;
    protected SerializedProperty TimeSliceFrames;
    protected SerializedProperty TimeSliceFramesOffset;
    protected SerializedProperty TimeSlicePeriod;
    protected SerializedProperty TimeSliceTimeOffset;
    protected SerializedProperty HardwareMode;
    protected SerializedProperty RenderMode;
    protected SerializedProperty RayTracedVoxelMode;
    protected SerializedProperty RayTracedVoxelSizeMultiplier;
    protected SerializedProperty RayTracedVoxelSmoothCubeNormal;
    protected SerializedProperty RayTracedVoxelSphereFullness;
    protected SerializedProperty RayTracedVoxelPaddingMode;
    protected SerializedProperty RayTracedVoxelInternalPaddingDistance;
    protected SerializedProperty RayTracedVoxelSizeFadeDistance;
    protected SerializedProperty ShowAdvancedNormalOptions;
    protected SerializedProperty NormalQuantization;
    protected SerializedProperty Normal2dFade;
    protected SerializedProperty Normal2dStrength;
    protected SerializedProperty SmoothNormalBlurRelative;
    protected SerializedProperty SmoothNormalBlurAbsolute;
    protected SerializedProperty EnableAutoSmoothing;
    protected SerializedProperty AutoSmoothingMaxAngle;
    protected SerializedProperty EnableSmoothCorner;
    protected SerializedProperty SmoothCornerSubdivision;
    protected SerializedProperty SmoothCornerNormalBlur;
    protected SerializedProperty SmoothCornerFade;
    protected SerializedProperty InvertNormals;
    protected SerializedProperty ForceEvaluateAllBrushes;
    protected SerializedProperty Enable2dMode;
    protected SerializedProperty SurfaceShift;
    protected SerializedProperty MeshingMode;
    protected SerializedProperty ShowAdvancedGeometryOptions;
    protected SerializedProperty SurfaceNetsDualQuadsBlend;
    protected SerializedProperty SurfaceNetsBinarySearchIterations;
    protected SerializedProperty SurfaceNetsGradientDescentIterations;
    protected SerializedProperty SurfaceNetsGradientDescentFactor;
    protected SerializedProperty SurfaceNetsHighAccuracyMode;
    protected SerializedProperty DualContouringDualQuadsBlend;
    protected SerializedProperty DualContouringRelaxation;
    protected SerializedProperty DualContouringSolverIterations;
    protected SerializedProperty DualContouringBinarySearchIterations;
    protected SerializedProperty DualContouringGradientDescentIterations;
    protected SerializedProperty DualContouringGradientDescentFactor;
    protected SerializedProperty DualContouringHighAccuracyMode;
    protected SerializedProperty ShowAdvancedSplatOptions;
    protected SerializedProperty SplatSize;
    protected SerializedProperty SplatSizeJitter;
    protected SerializedProperty SplatNormalShift;
    protected SerializedProperty SplatNormalShiftJitter;
    protected SerializedProperty SplatColorJitter;
    protected SerializedProperty SplatPositionJitter;
    protected SerializedProperty SplatRotationJitter;
    protected SerializedProperty SplatOrientationJitter;
    protected SerializedProperty SplatOriginalNormalBlend;
    protected SerializedProperty SplatJitterNoisiness;
    protected SerializedProperty SplatCameraFacing;
    protected SerializedProperty SplatNormalsMatchCameraFacing;
    protected SerializedProperty SplatShadowsMatchCameraFacing;
    protected SerializedProperty SplatScreenSpaceFlattening;
    //protected SerializedProperty SplatSmoothNormalBlend;
    protected SerializedProperty CastShadows;
    protected SerializedProperty ReceiveShadows;
    
    protected SerializedProperty SharedMaterial;
    protected SerializedProperty MasterColor;
    protected SerializedProperty MasterEmission;
    protected SerializedProperty MasterMetallic;
    protected SerializedProperty MasterSmoothness;
    protected SerializedProperty RenderMaterialMesh;
    protected SerializedProperty RenderMaterialSplats;
    protected SerializedProperty RenderMaterialDecal;
    //protected SerializedProperty RenderMaterialRayMarchedSurface;
    //protected SerializedProperty RenderMaterialRayTracedVoxels;
    protected SerializedProperty RenderMaterialMode;

    protected SerializedProperty MaxRayMarchSteps;
    protected SerializedProperty RayMarchAccuracy;
    protected SerializedProperty RayMarchMaxRayDistance;
    
    /*
    protected SerializedProperty RayMarchStepSize;
    protected SerializedProperty NumLightMarchSteps;
    protected SerializedProperty RayMarchDistance;
    protected SerializedProperty RayMarchVolumeDensity;
    protected SerializedProperty RayMarchLight;
    protected SerializedProperty RayMarchVolumeAbsorption;
    protected SerializedProperty RayMarchLightAbsorption;
    protected SerializedProperty RayMarchDarknesThreshold;
    protected SerializedProperty RayMarchTransmittanceCurve;
    protected SerializedProperty UseRayMarchNoise;
    protected SerializedProperty RayMarchNoiseThreshold;
    protected SerializedProperty RayMarchNoiseEdgeFade;
    protected SerializedProperty RayMarchNoiseScrollSpeed;
    protected SerializedProperty RayMarchNoiseBaseOctaveSize;
    protected SerializedProperty RayMarchNoiseNumOctaves;
    protected SerializedProperty RayMarchNoiseOctaveOffsetFactor;
    */

    protected SerializedProperty MeshGenerationCreateNewObject;
    protected SerializedProperty MeshGenerationRenderableMeshMode;
    protected SerializedProperty MeshGenerationCreateCollider;
    protected SerializedProperty MeshGenerationForceConvexCollider;
    protected SerializedProperty MeshGenerationColliderSurfaceShift;
    protected SerializedProperty MeshGenerationCreateRigidBody;
    protected SerializedProperty GenerateColliderMeshAssetByEditor;
    protected SerializedProperty GenerateColliderMeshAssetByEditorName;
    protected SerializedProperty MeshGenerationColliderVoxelDensity;
    protected SerializedProperty MeshGenerationAutoRigging;
    protected SerializedProperty MeshGenerationAutoRiggingAlgorithm;
    protected SerializedProperty MeshGenerationAutoRiggingMaxBonesPerVertex;
    protected SerializedProperty MeshGenerationGenerateTextureUV;
    protected SerializedProperty MeshGenerationGenerateLightMapUV;
    protected SerializedProperty MeshGenerationWeldVertices;
    protected SerializedProperty MeshGenerationLockOnStart;
    protected SerializedProperty GenerateMeshAssetByEditor;
    protected SerializedProperty GenerateMeshAssetByEditorName;
    protected SerializedProperty RecursiveLockMeshByEditor;
    protected SerializedProperty RememberLockedMeshMaterialByEditor;

    protected SerializedProperty DrawGenerateSdfGizmos;
    protected SerializedProperty GenerateSdfByEditorName;
    protected SerializedProperty GenerateSdfCenter;
    protected SerializedProperty GenerateSdfDimension;
    protected SerializedProperty GenerateSdfTextureSize;

    protected SerializedProperty UseCutoffVolume;
    protected SerializedProperty CutoffVolumeCenter;
    protected SerializedProperty CutoffVolumeSize;

    protected SerializedProperty ClickSelection;

    protected SerializedProperty AlwaysDrawGizmos;
    protected SerializedProperty DrawRawBrushBounds;
    protected SerializedProperty DrawComputeBrushBounds;
    protected SerializedProperty DrawRenderBounds;
    protected SerializedProperty DrawVoxelNodes;
    protected SerializedProperty DrawVoxelNodesDepth;
    protected SerializedProperty DrawVoxelNodesScale;


    protected void InitSerializedProperties()
    {
      MaxVoxelsK = serializedObject.FindProperty("MaxVoxelsK");
      MaxChunks = serializedObject.FindProperty("MaxChunks");
      ShowGpuMemoryUsage = serializedObject.FindProperty("ShowGpuMemoryUsage");
      AutoAdjustBudgetsToHighWaterMarks = serializedObject.FindProperty("AutoAdjustBudgetsToHighWaterMarks");
      AutoAdjustBudgetsToHighWaterMarksMarginPercent = serializedObject.FindProperty("AutoAdjustBudgetsToHighWaterMarksMarginPercent");

      VoxelDensity = serializedObject.FindProperty("VoxelDensity");
      ComputeMode = serializedObject.FindProperty("ComputeMode");
      TimeSliceMode = serializedObject.FindProperty("TimeSliceMode");
      TimeSliceFrames = serializedObject.FindProperty("TimeSliceFrames");
      TimeSliceFramesOffset = serializedObject.FindProperty("TimeSliceFramesOffset");
      TimeSlicePeriod = serializedObject.FindProperty("TimeSlicePeriod");
      TimeSliceTimeOffset = serializedObject.FindProperty("TimeSliceTimeOffset");
      HardwareMode = serializedObject.FindProperty("HardwareMode");
      RenderMode = serializedObject.FindProperty("RenderMode");
      RayTracedVoxelMode = serializedObject.FindProperty("RayTracedVoxelMode");
      RayTracedVoxelSizeMultiplier = serializedObject.FindProperty("RayTracedVoxelSizeMultiplier");
      RayTracedVoxelSmoothCubeNormal = serializedObject.FindProperty("RayTracedVoxelSmoothCubeNormal");
      RayTracedVoxelSphereFullness = serializedObject.FindProperty("RayTracedVoxelSphereFullness");
      RayTracedVoxelPaddingMode = serializedObject.FindProperty("RayTracedVoxelPaddingMode");
      RayTracedVoxelInternalPaddingDistance = serializedObject.FindProperty("RayTracedVoxelInternalPaddingDistance");
      RayTracedVoxelSizeFadeDistance = serializedObject.FindProperty("RayTracedVoxelSizeFadeDistance");
      ShowAdvancedNormalOptions = serializedObject.FindProperty("ShowAdvancedNormalOptions");
      NormalQuantization = serializedObject.FindProperty("NormalQuantization");
      Normal2dFade = serializedObject.FindProperty("Normal2dFade");
      Normal2dStrength = serializedObject.FindProperty("Normal2dStrength");
      SmoothNormalBlurRelative = serializedObject.FindProperty("SmoothNormalBlurRelative");
      SmoothNormalBlurAbsolute = serializedObject.FindProperty("SmoothNormalBlurAbsolute");
      EnableAutoSmoothing = serializedObject.FindProperty("EnableAutoSmoothing");
      AutoSmoothingMaxAngle = serializedObject.FindProperty("AutoSmoothingMaxAngle");
      EnableSmoothCorner = serializedObject.FindProperty("EnableSmoothCorner");
      SmoothCornerSubdivision = serializedObject.FindProperty("SmoothCornerSubdivision");
      SmoothCornerNormalBlur = serializedObject.FindProperty("SmoothCornerNormalBlur");
      SmoothCornerFade = serializedObject.FindProperty("SmoothCornerFade");
      InvertNormals = serializedObject.FindProperty("InvertNormals");
      ForceEvaluateAllBrushes = serializedObject.FindProperty("ForceEvaluateAllBrushes");
      Enable2dMode = serializedObject.FindProperty("Enable2dMode");
      SurfaceShift = serializedObject.FindProperty("SurfaceShift");
      MeshingMode = serializedObject.FindProperty("MeshingMode");
      ShowAdvancedGeometryOptions = serializedObject.FindProperty("ShowAdvancedGeometryOptions");
      SurfaceNetsDualQuadsBlend = serializedObject.FindProperty("SurfaceNetsDualQuadsBlend");
      SurfaceNetsBinarySearchIterations = serializedObject.FindProperty("SurfaceNetsBinarySearchIterations");
      SurfaceNetsGradientDescentIterations = serializedObject.FindProperty("SurfaceNetsGradientDescentIterations");
      SurfaceNetsHighAccuracyMode = serializedObject.FindProperty("SurfaceNetsHighAccuracyMode");
      SurfaceNetsGradientDescentFactor = serializedObject.FindProperty("SurfaceNetsGradientDescentFactor");
      DualContouringDualQuadsBlend = serializedObject.FindProperty("DualContouringDualQuadsBlend");
      DualContouringRelaxation = serializedObject.FindProperty("DualContouringRelaxation");
      DualContouringSolverIterations = serializedObject.FindProperty("DualContouringSolverIterations");
      DualContouringBinarySearchIterations = serializedObject.FindProperty("DualContouringBinarySearchIterations");
      DualContouringGradientDescentIterations = serializedObject.FindProperty("DualContouringGradientDescentIterations");
      DualContouringHighAccuracyMode = serializedObject.FindProperty("DualContouringHighAccuracyMode");
      DualContouringGradientDescentFactor = serializedObject.FindProperty("DualContouringGradientDescentFactor");
      ShowAdvancedSplatOptions = serializedObject.FindProperty("ShowAdvancedSplatOptions");
      SplatSize = serializedObject.FindProperty("SplatSize");
      SplatSizeJitter = serializedObject.FindProperty("SplatSizeJitter");
      SplatNormalShift = serializedObject.FindProperty("SplatNormalShift");
      SplatNormalShiftJitter = serializedObject.FindProperty("SplatNormalShiftJitter");
      SplatColorJitter = serializedObject.FindProperty("SplatColorJitter");
      SplatPositionJitter = serializedObject.FindProperty("SplatPositionJitter");
      SplatRotationJitter = serializedObject.FindProperty("SplatRotationJitter");
      SplatOrientationJitter = serializedObject.FindProperty("SplatOrientationJitter");
      SplatOriginalNormalBlend = serializedObject.FindProperty("SplatOriginalNormalBlend");
      SplatJitterNoisiness = serializedObject.FindProperty("SplatJitterNoisiness");
      SplatCameraFacing = serializedObject.FindProperty("SplatCameraFacing");
      SplatNormalsMatchCameraFacing = serializedObject.FindProperty("SplatNormalsMatchCameraFacing");
      SplatShadowsMatchCameraFacing = serializedObject.FindProperty("SplatShadowsMatchCameraFacing");
      SplatScreenSpaceFlattening = serializedObject.FindProperty("SplatScreenSpaceFlattening");
      //SplatSmoothNormalBlend = serializedObject.FindProperty("SplatSmoothNormalBlend");
      SplatCameraFacing = serializedObject.FindProperty("SplatCameraFacing");
      CastShadows = serializedObject.FindProperty("CastShadows");
      ReceiveShadows = serializedObject.FindProperty("ReceiveShadows");

      SharedMaterial = serializedObject.FindProperty("SharedMaterial");
      MasterColor = serializedObject.FindProperty("m_masterColor");
      MasterEmission = serializedObject.FindProperty("m_masterEmission");
      MasterMetallic = serializedObject.FindProperty("m_masterMetallic");
      MasterSmoothness = serializedObject.FindProperty("m_masterSmoothness");
      RenderMaterialMesh = serializedObject.FindProperty("RenderMaterialMesh");
      RenderMaterialSplats = serializedObject.FindProperty("RenderMaterialSplats");
      RenderMaterialDecal = serializedObject.FindProperty("RenderMaterialDecal");
      //RenderMaterialRayMarchedSurface = serializedObject.FindProperty("RenderMaterialRayMarchedSurface");
      //RenderMaterialRayTracedVoxels = serializedObject.FindProperty("RenderMaterialRayTracedVoxels");
      RenderMaterialMode = serializedObject.FindProperty("RenderMaterialMode");

      MaxRayMarchSteps = serializedObject.FindProperty("MaxRayMarchSteps");
      RayMarchAccuracy = serializedObject.FindProperty("RayMarchAccuracy");
      RayMarchMaxRayDistance = serializedObject.FindProperty("RayMarchMaxRayDistance");

      /*
      RayMarchStepSize = serializedObject.FindProperty("RayMarchStepSize");
      NumLightMarchSteps = serializedObject.FindProperty("NumLightMarchSteps");
      RayMarchDistance = serializedObject.FindProperty("RayMarchDistance");
      RayMarchVolumeDensity = serializedObject.FindProperty("RayMarchVolumeDensity");
      RayMarchLight = serializedObject.FindProperty("RayMarchLight");
      RayMarchVolumeAbsorption = serializedObject.FindProperty("RayMarchVolumeAbsorption");
      RayMarchLightAbsorption = serializedObject.FindProperty("RayMarchLightAbsorption");
      RayMarchDarknesThreshold = serializedObject.FindProperty("RayMarchDarknesThreshold");
      RayMarchTransmittanceCurve = serializedObject.FindProperty("RayMarchTransmittanceCurve");
      RayMarchNoiseEdgeFade = serializedObject.FindProperty("RayMarchNoiseEdgeFade");
      UseRayMarchNoise = serializedObject.FindProperty("UseRayMarchNoise");
      RayMarchNoiseThreshold = serializedObject.FindProperty("RayMarchNoiseThreshold");
      RayMarchNoiseScrollSpeed = serializedObject.FindProperty("RayMarchNoiseScrollSpeed");
      RayMarchNoiseBaseOctaveSize = serializedObject.FindProperty("RayMarchNoiseBaseOctaveSize");
      RayMarchNoiseNumOctaves = serializedObject.FindProperty("RayMarchNoiseNumOctaves");
      RayMarchNoiseOctaveOffsetFactor = serializedObject.FindProperty("RayMarchNoiseOctaveOffsetFactor");
      */

      MeshGenerationCreateNewObject = serializedObject.FindProperty("MeshGenerationCreateNewObject");
      MeshGenerationRenderableMeshMode = serializedObject.FindProperty("MeshGenerationRenderableMeshMode");
      MeshGenerationCreateCollider = serializedObject.FindProperty("MeshGenerationCreateCollider");
      MeshGenerationForceConvexCollider = serializedObject.FindProperty("MeshGenerationForceConvexCollider");
      MeshGenerationColliderSurfaceShift = serializedObject.FindProperty("MeshGenerationColliderSurfaceShift");
      MeshGenerationCreateRigidBody = serializedObject.FindProperty("MeshGenerationCreateRigidBody");
      GenerateColliderMeshAssetByEditor = serializedObject.FindProperty("GenerateColliderMeshAssetByEditor");
      GenerateColliderMeshAssetByEditorName = serializedObject.FindProperty("GenerateColliderMeshAssetByEditorName");
      MeshGenerationColliderVoxelDensity = serializedObject.FindProperty("MeshGenerationColliderVoxelDensity");
      MeshGenerationAutoRigging = serializedObject.FindProperty("MeshGenerationAutoRigging");
      MeshGenerationAutoRiggingAlgorithm = serializedObject.FindProperty("MeshGenerationAutoRiggingAlgorithm");
      MeshGenerationAutoRiggingMaxBonesPerVertex = serializedObject.FindProperty("MeshGenerationAutoRiggingMaxBonesPerVertex");
      MeshGenerationGenerateTextureUV = serializedObject.FindProperty("MeshGenerationGenerateTextureUV");
      MeshGenerationGenerateLightMapUV = serializedObject.FindProperty("MeshGenerationGenerateLightMapUV");
      MeshGenerationWeldVertices = serializedObject.FindProperty("MeshGenerationWeldVertices");
      MeshGenerationLockOnStart = serializedObject.FindProperty("MeshGenerationLockOnStart");
      GenerateMeshAssetByEditor = serializedObject.FindProperty("GenerateMeshAssetByEditor");
      GenerateMeshAssetByEditorName = serializedObject.FindProperty("GenerateMeshAssetByEditorName");
      RecursiveLockMeshByEditor = serializedObject.FindProperty("RecursiveLockMeshByEditor");
      RememberLockedMeshMaterialByEditor = serializedObject.FindProperty("RememberLockedMeshMaterialByEditor");

      DrawGenerateSdfGizmos = serializedObject.FindProperty("DrawGenerateSdfGizmos");
      GenerateSdfByEditorName = serializedObject.FindProperty("GenerateSdfByEditorName");
      GenerateSdfCenter = serializedObject.FindProperty("GenerateSdfCenter");
      GenerateSdfDimension = serializedObject.FindProperty("GenerateSdfDimension");
      GenerateSdfTextureSize = serializedObject.FindProperty("GenerateSdfTextureSize");

      UseCutoffVolume = serializedObject.FindProperty("UseCutoffVolume");
      CutoffVolumeCenter = serializedObject.FindProperty("CutoffVolumeCenter");
      CutoffVolumeSize = serializedObject.FindProperty("CutoffVolumeSize");

      ClickSelection = serializedObject.FindProperty("ClickSelection");

      AlwaysDrawGizmos = serializedObject.FindProperty("AlwaysDrawGizmos");
      DrawRawBrushBounds = serializedObject.FindProperty("DrawRawBrushBounds");
      DrawComputeBrushBounds = serializedObject.FindProperty("DrawComputeBrushBounds");
      DrawRenderBounds = serializedObject.FindProperty("DrawRenderBounds");
      DrawVoxelNodes = serializedObject.FindProperty("DrawVoxelNodes");
      DrawVoxelNodesDepth = serializedObject.FindProperty("DrawVoxelNodesDepth");
      DrawVoxelNodesScale = serializedObject.FindProperty("DrawVoxelNodesScale");
    }

    private void ShowMaterialOptions()
    {
      var renderer = (MudRendererBase) target;

      Header("Material");

      Property(SharedMaterial, 
        "Shared Material", 
            "External material used as the renderer's master material."
      );

      if (SharedMaterial.objectReferenceValue == null)
      {
        Property(MasterColor, 
          "Master Color", 
              "Master color multiplier."
        );

        Property(MasterEmission, 
          "Master Emission", 
              "Master emission multiplier. Alpha is not used."
        );

        Property(MasterMetallic, 
          "Master Metallic", 
              "Master metallic multiplier."
        );

        Property(MasterSmoothness, 
          "Master Smoothness", 
              "Master smoothness multiplier."
        );

        switch (renderer.RenderModeCategory)
        {
          case MudRendererBase.RenderModeCategoryEnum.Mesh:
            Property(RenderMaterialMesh, "Render Material");
            break;
            
          case MudRendererBase.RenderModeCategoryEnum.Splats:
            Property(RenderMaterialSplats, "Render Material");
            break;

          case MudRendererBase.RenderModeCategoryEnum.Decal:
            Property(RenderMaterialDecal, "Render Material");
            break;

          /*
          case MudRendererBase.RenderModeCategoryEnum.RayMarchedSurface:
            Property(RenderMaterialRayMarchedSurface, "Render Material");
            break;

          case MudRendererBase.RenderModeCategoryEnum.RayTracedVoxels:
            Property(RenderMaterialRayTracedVoxels, "Render Material");
            break;
          */
        }

        Property(RenderMaterialMode, 
          "  Render Material Mode", 
              "A renderer enables & disables its render material's shader keywords to select the correct shader variant, " 
            + "which requires making a local copy of the render material. " 
            + "This means the renderer needs to refresh its copy if the referenced render material has changed. " 
            + "This is done automatically in the editor for convenience, but in builds the renderer uses the render material mode to decide how to refresh the copy.\n\n" 
            + "Static - The render material is not expected to change. It is only copied once. Manual refresh of the copy can be done by calling the MarkMaterialDirty method.\n\n"
            + "Dynamic - The render material is expected to be constantly changing, and it is copied once every frame. This may impact performance.");

        /*
        Indent();
          //Property(MaxRayMarchSteps, "Ray Steps");
          Property(RayMarchStepSize, "Ray Step Size");
          Property(NumLightMarchSteps, "Light Steps");
          //Property(RayMarchDistance, "Total Ray Distance");
          Property(RayMarchVolumeDensity, "Volume Density");
          Property(RayMarchLight, "Light", 
              "Type supported: directional, point.");
          Property(RayMarchVolumeAbsorption, "Volume Absorption");
          Property(RayMarchLightAbsorption, "Light Absorption");
          Property(RayMarchDarknesThreshold, "Darkness Threshold");
          Property(RayMarchTransmittanceCurve, "Transmittance Curve");

          Property(UseRayMarchNoise, "Use Noise");
          if (UseRayMarchNoise.boolValue)
          {
            Indent();
              Property(RayMarchNoiseThreshold, "Threshold");
              Property(RayMarchNoiseEdgeFade, "Edge Fade");
              Property(RayMarchNoiseScrollSpeed, "Scroll Speed");
              Property(RayMarchNoiseBaseOctaveSize, "Base Octave Size");
              Property(RayMarchNoiseNumOctaves, "Octaves");
              Property(RayMarchNoiseOctaveOffsetFactor, "Octave Offset Factor");
            Unindent();
          }
        Unindent();
        */
      }

      Space();
    }

    public virtual void OnEnable()
    {
      InitSerializedProperties();

      EditorApplication.update += Update;
    }

    private void OnDisable()
    {
      EditorApplication.update -= Update;
    }

    private static string IntCountString(long n, bool space = false, string suffix = "")
    {
      if (n < 1024)
        return n.ToString() + (space ? " " : "") + suffix;

      if (n < 1048576)
        return (n / 1024.0f).ToString("N1") + (space ? " " : "") + "K" + suffix;

      return (n / 1048576.0f).ToString("N1") + (space ? " " : "") + "M" + suffix;
    }

    public override void OnInspectorGUI()
    {
      serializedObject.Update();

      var renderer = (MudRendererBase) target;
      bool gpuMode = false;

      {
        Header("MudBun Version " + MudBun.Version);

        if (MudBun.IsFreeVersion)
        {
          Indent();
            Text("Trial Version Limitations:");
            Text("   - Watermark.");
            Text("   - Limited voxel density & triangle count for mesh utilities.");
            Text("   - No full source code.");
            Text("   - No commercial use.");
          Unindent();
        }

        Space();
      }

      if (!renderer.MeshLocked)
      {
        // editor
        {
          Header("Editor");

          Property(ClickSelection, "Click Selection");

          Space();
        } // end: editor

        // budgets
        {
          Header("Memory Budgets");

          Property(MaxVoxelsK, 
            "Max Voxels (K)", 
                "Maximum number of voxels times 1024.\n\n" 
              + "A voxel is a minimum unit where SDFs are evaluated.\n\n"
              + "Try increasing this value if voxels appear missing.\n\n" 
              + "The higher this value, the more GPU memory is used."
          );

          Property(MaxChunks, 
            "Max Voxel Chunks", 
                "Maximum number voxel chunks.\n\n" 
              + "A voxel chunk is a block of space that can contain multiple voxels." 
              + "The larger the space spanned by solids, the more voxel chunks are needed.\n\n" 
              + "Try increasing this value if voxels appear missing.\n\n" 
              + "The higher this value, the more GPU memory is used."
          );

          Property(ShowGpuMemoryUsage,
            "(*) Show/Adjust Usage", 
            "(*): Impacts performance.");

          if (ShowGpuMemoryUsage.boolValue)
          {
            Text("   Used / Allocated: ", FontStyle.Bold);

            long memoryAllocated = renderer.LocalResourceGpuMemoryAllocated;
            long memoryUsed = Math.Min(memoryAllocated, renderer.LocalResourceGpuMemoryUsed);
            float memoryUtilizationPercent = 100.0f * (memoryUsed / Mathf.Max(MathUtil.Epsilon, memoryAllocated));
            Text
            (
                "      GPU Memory - " 
              + IntCountString(memoryUsed, true, "B") + " / " 
              + IntCountString(memoryAllocated, true, "B") + " ("
              + memoryUtilizationPercent.ToString("N1") + "%)"
            );

            int voxelsAllocated = renderer.NumVoxelsAllocated;
            int voxelsUsed = Mathf.Min(voxelsAllocated, renderer.NumVoxelsUsed);
            float voxelUtilizationPercent = 100.0f * (voxelsUsed / Mathf.Max(MathUtil.Epsilon, voxelsAllocated));
            Text
            (
                "      Voxels - " 
              + IntCountString(voxelsUsed) + " / " 
              + IntCountString(voxelsAllocated) + " (" 
              + voxelUtilizationPercent.ToString("N1") + "%)"
            );

            int chunksAllocated = renderer.NumChunksAllocated;
            int chunksUsed = Mathf.Min(chunksAllocated, renderer.NumChunksUsed);
            float chunkUtilizationPercent = 100.0f * (chunksUsed / Mathf.Max(MathUtil.Epsilon, chunksAllocated));
            Text
            (
                "      Voxel Chunks - "
              + IntCountString(chunksUsed) + " / " 
              + IntCountString(chunksAllocated) + " ("
              + chunkUtilizationPercent.ToString("N1") + "%)"
            );

            int vertsAllocated = renderer.NumVerticesAllocated;
            int vertsGenerated = Mathf.Min(vertsAllocated, renderer.NumVerticesGenerated);
            float vertUtilizationPercent = 100.0f * (vertsGenerated / Mathf.Max(MathUtil.Epsilon, vertsAllocated));
            Text
            (
                "      Vertices - " 
              + IntCountString(vertsGenerated) + " / " 
              + IntCountString(vertsAllocated) +  " (" 
              + vertUtilizationPercent.ToString("N1") + "%)"
            );

            Property(AutoAdjustBudgetsToHighWaterMarks, "  Auto-Adjust Budgets");
            Property(AutoAdjustBudgetsToHighWaterMarksMarginPercent, "    Margin Percent");

            EditorGUILayout.HelpBox("Showing GPU memory usage impacts performance.", MessageType.Warning);
          }

          Space();
        } // end budgets

        // render
        {
          Header("Render");

          Property(VoxelDensity, 
            "(*^) Voxel Density", 
                "Number of voxels per unit distance.\n\n" 
              + "Higher density means more pixels and more computation.\n\n" 
              + "(*): Impacts performance.\n" 
              + "(^): Impacts GPU memory usage."
          );

          Property(ComputeMode, 
            "Compute Mode", 
                "Controls the timing and frequency of volumetric re-computation.\n\n" 
              + "Auto - Automatic re-computation upon detection of transform changes to brushes. The downside is that transform changes to the renderer will also trigger re-computation, even if the brushes haven't changed.\n\n" 
              + "Manual - Only perform re-computation if this renderer's MarkNeedsRecompute() method is called in script. If used properly, this mode is the most computationally efficient.\n\n" 
              + "Every Frame - Force re-computation every frame.\n\n" 
              + "Time Sliced - Automatic re-computation on a lower frequency than once every frame. Useful for improving performance with staggered computation, or achieving stopmotion-like visuals."
          );

          if (ComputeMode.intValue == (int) MudRendererBase.ComputeModeEnum.TimeSliced)
          {
            Indent();
              Property(TimeSliceMode, 
                "Time Slice Mode", 
                    "By Frames Auto Offset - Automatic re-compute after this many frames, with frame offset determined by internal renderer index.\n\n" 
                  + "By Frames Manual Offset - Automatic re-compute after this many frames, with explicitly specified frame offset.\n\n" 
                  + "By Period Auto Offset - Automatic re-compute after this many seconds, with time offset determined by internal renderer index.\n\n" 
                  + "By Period Manual Offset - Automatic re-compute after this many seconds, with explicitly specified time offset.\n\n"
              );
              switch ((MudRendererBase.TimeSliceModeEnum) TimeSliceMode.intValue)
              {
                case MudRendererBase.TimeSliceModeEnum.ByFramesAutoOffset:
                  Indent();
                    Property(TimeSliceFrames, "Frames");
                  Unindent();
                  break;

                case MudRendererBase.TimeSliceModeEnum.ByFramesManualOffset:
                  Indent();
                    Property(TimeSliceFrames, "Frames");
                    Property(TimeSliceFramesOffset, "Offset");
                  Unindent();
                  break;

                case MudRendererBase.TimeSliceModeEnum.ByPeriodAutoOffset:
                  Indent();
                    Property(TimeSlicePeriod, "Period");
                  Unindent();
                  break;

                case MudRendererBase.TimeSliceModeEnum.ByPeriodManualOffset:
                  Indent();
                    Property(TimeSlicePeriod, "Period");
                    Property(TimeSliceTimeOffset, "Offset");
                  Unindent();
                  break;
              }
            Unindent();
          }

          Property(CastShadows, "Cast Shadows");

          bool receiveShadowsEffective = false;
          var camera = Camera.main;
          if (camera)
          {
            switch (MudRendererBase.RenderPipeline)
            {
              case ResourcesUtil.RenderPipelineEnum.BuiltIn:
                if (camera.actualRenderingPath == RenderingPath.Forward)
                  receiveShadowsEffective = true;
                break;
            }

            if (receiveShadowsEffective)
              Property(ReceiveShadows, "Receive Shadows");
          }

          Property(ForceEvaluateAllBrushes, 
            "Force Evaluate All Brushes", 
                "Check to force evaluation of all brushes. By default, brushes will be skipped for a voxel tree node if their AABBs are not intersecting." 
              + "Checking this option will force all brushes to be evaluated for every voxel tree node.\n\n" 
              + "This option is automatically checked in 2D mode if the 2D/3D Normal Blend value is larger than zero with the presence of any subtractive solid brush.\n\n" 
              + "This option might need to be manually checked if there are subtraction brushes in 2D mode and the SDF value is used in the shader of the render material."
          );

          /*
          Property(HardwareMode, 
            "Hardware Mode", 
                "GPU - The default and original MudBun implementation. Supports all features. Only actively tested for Windows platforms.\n\n" 
              + "CPU - Less performant implementation than GPU mode. Supports only a subset of features, but has a much wider platform compatibility."
          );
          */

          gpuMode = ((MudRendererBase.HardwareModeEnum) HardwareMode.intValue) == MudRendererBase.HardwareModeEnum.Gpu;

          Property(RenderMode, 
            "Render Mode", 
                "Smooth Mesh - Mesh with smooth normals. More performance intensive than flat mesh and splats.\n\n" 
              + "Flat Mesh - Mesh with flat normals.\n\n" 
              + "Circle Splats - Flat circle splats scattered on solid surface. Good for circular textures.\n\n" 
              + "Quad Splats - Flat quad splats scattered on solid surface. Doubles the triangle count compared to circle splats. Good for square textures.\n\n" 
              + "Decal - Decals composed of brushes.\n\n" 
              + "Ray-Marched Surface - [Experimental / Early URP only] Ray-based render mode that uses ray-marching to render each pixel. Performance is heavily impacted by the number of pixels rendered.\n\n" 
              + "Ray-Traced Voxels - [Experimental / Early URP only] Ray-based render mode that visualizes voxel cells."
          );

          if (gpuMode)
          {
            switch ((MudRendererBase.RenderModeEnum) RenderMode.intValue)
            {
              case MudRendererBase.RenderModeEnum.FlatMesh:
              case MudRendererBase.RenderModeEnum.SmoothMesh:
              case MudRendererBase.RenderModeEnum.CircleSplats:
              case MudRendererBase.RenderModeEnum.QuadSplats:
                Property(MeshingMode, 
                  "Meshing Mode", 
                      "Marching Cubes - Default meshing algorithm. Good balance between speed and quality.\n\n" 
                    + "Dual Quads - Faster than marching cubes. Gives a stylized blocky look.\n\n" 
                    + "Surface Nets - Slightly slower than marching cubes. Comes with the added benefit of blending with dual quads.\n\n" 
                    + "Dual Contouring - Much slower than surface nets, but better at preserving sharp features. Good for edit-time hard-surface modeling. Comes with the added benefit of blending with dual quads."
                );

                Property(Enable2dMode, 
                  "2D Mode", 
                  "Make everything operate on the 2D XY plane."
                );

                if (!Enable2dMode.boolValue)
                {
                  Property(InvertNormals, "Invert Normals");
                }
                break;

              /*
              case MudRendererBase.RenderModeEnum.RayMarchedSurface:
                Property(MaxRayMarchSteps, "Max Ray March Steps");
                Property(RayMarchAccuracy, "Ray March Accuracy");
                Property(RayMarchMaxRayDistance, "Max Ray March Distance");
                EditorGUILayout.HelpBox("The Ray-Marched Surface render mode is experimental and works in early URP only.", MessageType.Warning);
                break;

              case MudRendererBase.RenderModeEnum.RayTracedVoxels:
                Property(RayTracedVoxelMode, "Voxel Mode");
                Property(RayTracedVoxelSizeMultiplier, "Voxel Size");
                switch ((MudRendererBase.RayTracedVoxelModeEnum) RayTracedVoxelMode.intValue)
                {
                  case MudRendererBase.RayTracedVoxelModeEnum.FacetedCubes:
                    Property(RayTracedVoxelSmoothCubeNormal, "    Smooth Cube Normal");
                    break;
                  case MudRendererBase.RayTracedVoxelModeEnum.SmoothSpheres:
                  case MudRendererBase.RayTracedVoxelModeEnum.FlatSpheres:
                    Property(RayTracedVoxelSphereFullness, "    Sphere Fullness");
                    EditorGUILayout.BeginHorizontal();
                      EditorGUILayout.PrefixLabel("      Presets");
                      if (GUILayout.Button("Unit"))
                      {
                        RayTracedVoxelSphereFullness.floatValue = 0.0f;
                      }
                      if (GUILayout.Button("Cell"))
                      {
                        RayTracedVoxelSphereFullness.floatValue = 0.565f;
                      }
                      if (GUILayout.Button("Dice"))
                      {
                        RayTracedVoxelSphereFullness.floatValue = 0.765f;
                      }
                    EditorGUILayout.EndHorizontal();
                    break;
                }
                Property(RayTracedVoxelInternalPaddingDistance, 
                  "Internal Padding", 
                     "Distance within SDF isosurface to create additional internal voxels.");
                Property(RayTracedVoxelSizeFadeDistance, 
                  "Size Fade Distance", 
                     "Distance over which to fade voxel size from zero to full size starting from the isosurface.");
                EditorGUILayout.HelpBox("The Ray-Traced Voxels render mode is experimental and works in early URP only.", MessageType.Warning);
                break;
              */
            }
          }

          bool meshGenerationRenderMode = false;
          switch ((MudRendererBase.RenderModeEnum) RenderMode.intValue)
          {
            case MudRendererBase.RenderModeEnum.FlatMesh:
            case MudRendererBase.RenderModeEnum.SmoothMesh:
            case MudRendererBase.RenderModeEnum.CircleSplats:
            case MudRendererBase.RenderModeEnum.QuadSplats:
              meshGenerationRenderMode = true;
              break;
          }

          serializedObject.ApplyModifiedProperties();

          // advanced meshing options
          Property(ShowAdvancedGeometryOptions, "Advanced Geometry Options");
          if (ShowAdvancedGeometryOptions.boolValue)
          {
            Indent();
              Property(SurfaceShift, 
                "Surface Shift", 
                "Apply a surface shift along surface normals."
              );
              if (gpuMode 
                  && meshGenerationRenderMode)
              {
                switch (renderer.MeshingMode)
                {
                  case MudRendererBase.MeshingModeEnum.SurfaceNets:
                  case MudRendererBase.MeshingModeEnum.DualContouring:
                    switch (renderer.MeshingMode)
                    {
                      case MudRendererBase.MeshingModeEnum.SurfaceNets:
                        Property(SurfaceNetsDualQuadsBlend, "Dual Quads Blend");
                        //Property(SurfaceNetsBinarySearchIterations, "  Binary Search Iterations"); // hidden as this doesn't seem to buy us much
                        //Property(SurfaceNetsGradientDescentIterations, "Gradient Descent Iterations");
                        //Property(SurfaceNetsGradientDescentFactor, "Gradient Descent Factor");
                        //Property(SurfaceNetsHighAccuracyMode, "(*) High Accuracy");
                        break;
      
                      case MudRendererBase.MeshingModeEnum.DualContouring:
                        Property(DualContouringDualQuadsBlend, "Dual Quads Blend");
                        Property(DualContouringRelaxation, "Relaxation");
                        //Property(DualContouringSolverIterations, "Solver Iterations");
                        //Property(DualContouringBinarySearchIterations, "Binary Search Iterations");
                        //Property(DualContouringGradientDescentIterations, "Gradient Descent Iterations");
                        //Property(DualContouringGradientDescentFactor, "Gradient Descent Factor");
                        Property(DualContouringHighAccuracyMode, 
                          "(*) High Accuracy", 
                          "(*): Impacts performance."
                        );
                        break;
                    }
                    break;
                }
              }
            Unindent();
          } // end: advanced meshing options

          // advanced normal options
          if (gpuMode)
          {
            switch (renderer.MeshingMode)
            {
              case MudRendererBase.MeshingModeEnum.MarchingCubes:
              case MudRendererBase.MeshingModeEnum.SurfaceNets:
              case MudRendererBase.MeshingModeEnum.DualContouring:
                switch (renderer.RenderMode)
                {
                  case MudRendererBase.RenderModeEnum.FlatMesh:
                  case MudRendererBase.RenderModeEnum.SmoothMesh:
                    Property(ShowAdvancedNormalOptions, "Advanced Normal Options");
                    if (ShowAdvancedNormalOptions.boolValue)
                    {
                      Indent();
                        if (Enable2dMode.boolValue)
                        {
                          Property(Normal2dFade, 
                            "2D Normal Fade", 
                            "How far inward to fade from outward-pointing normals to flat normals."
                          );
                          Property(Normal2dStrength, 
                            "2D Normal Strength", 
                            "How strong the outward-pointing normals contribute to the final normals."
                          );
                        }
                        Property(NormalQuantization, "Normal Quantization");
                        if (!Enable2dMode.boolValue)
                          Property(EnableAutoSmoothing, "Enable Auto-Smoothing (^)");
                        if (EnableAutoSmoothing.boolValue && !Enable2dMode.boolValue)
                        {
                          Indent();
                            Property(AutoSmoothingMaxAngle, "Auto-Smoothing Max Angle");
                            if (renderer.MeshingMode == MudRendererBase.MeshingModeEnum.DualContouring)
                            {
                              Property(EnableSmoothCorner, 
                                "(*) Enable Smooth Corner", 
                                "(*): Impacts performance."
                              );
                              if (EnableSmoothCorner.boolValue)
                              {
                                Indent();
                                  Property(SmoothCornerSubdivision, 
                                    "(*) Subdivision", 
                                    "(*): Impacts performance."
                                  );
                                  Property(SmoothCornerNormalBlur, "Blur");
                                  Property(SmoothCornerFade, "Fade");
                                Unindent();
                              }
                            }
                          Unindent();
                        }
                        else
                        {
                          if (renderer.RenderMode == MudRendererBase.RenderModeEnum.SmoothMesh)
                            Property(SmoothNormalBlurAbsolute, "Smooth Normal Blur");
                        }
                      Unindent();
                    }
                    break;
                }
                break;
              }
          } // end: advanced normal options

          // advanced splat options
          switch (renderer.RenderModeCategory)
          {
            case MudRendererBase.RenderModeCategoryEnum.Splats:
              Property(ShowAdvancedSplatOptions, "Advanced Splats Options");
              if (ShowAdvancedSplatOptions.boolValue)
              {
                Indent();
                  Property(SplatSize, "Size");
                  Property(SplatSizeJitter, "Size Jitter");
                  Property(SplatNormalShift, "Normal Shift");
                  Property(SplatNormalShiftJitter, "Normal Shift Jitter");
                  Property(SplatColorJitter, "Color Jitter");
                  Property(SplatPositionJitter, "Position Jitter");
                  Property(SplatRotationJitter, "Rotation Jitter");
                  Property(SplatOrientationJitter, "Orientation Jitter");
                  Property(SplatOriginalNormalBlend, "  Original Normal Blend");
                  Property(SplatJitterNoisiness, "Jitter Noisiness");
                  Property(SplatCameraFacing, "Camera Facing");
                  Property(SplatNormalsMatchCameraFacing, "  Normals Match Camera Facing");
                  Property(SplatShadowsMatchCameraFacing, "  Shadows Match Camera Facing");
                  Property(SplatScreenSpaceFlattening, "Screen-Space Flattening");
                  //Property(SplatSmoothNormalBlend, "Smooth Normal Blend");
                Unindent();
              }
              break;
          } // end: advanced splat options

          Space();
        } // end: render

        ShowMaterialOptions();

        // shaders
        {
          Header("Resources");

          if (GUILayout.Button(new GUIContent("Reload (Needed After Shader Edits)", "Reload shaders and GPU resources. This is necessary after editing shaders.")))
          {
            MudRendererBase.ReloadAllShaders();
          }

          Space();
        } // end: shaders

        // mesh utilities
        if (gpuMode 
            && !Enable2dMode.boolValue)
        {
          Header("Mesh Utilities");

          if (MudBun.IsFreeVersion)
          {
            Text("Trial Version Limitations:");
            Text("  - Voxel density limited to " + ((int) MudRendererBase.MaxMeshGenerationVoxelDensityFreeVersion));
            Text("  - Triangle count limited to " + MudRendererBase.MaxMeshGenerationTrianglesFreeVersion);
          }
          
          Property(MeshGenerationLockOnStart, 
            "Lock On Start", 
                "Lock mesh on start in play mode. This can save file size by not saving the mesh, but the performance will take a hit on lock."
          );

          Property(MeshGenerationCreateNewObject, 
            "Create New Object", 
                "Check to create a new object when locking mesh."
          );

          Property(MeshGenerationCreateCollider, 
            "Create Collider", 
                "Check to create a collider when locking mesh."
          );
          if (MeshGenerationCreateCollider.boolValue)
          {
            Indent();
              Property(MeshGenerationColliderVoxelDensity, 
                "Collider Voxel Density", 
                    "Voxel density used for creating collider."
              );
              Property(MeshGenerationForceConvexCollider, 
                "Force Convex Collider"
              );
              Property(MeshGenerationColliderSurfaceShift, 
                "Surface Shift"
              );
              Property(MeshGenerationCreateRigidBody, 
                "Create Rigid Body (Convex)"
              );
              Property(GenerateColliderMeshAssetByEditor, 
                "Generate Mesh Asset", 
                   "Generate a mesh asset. This is needed for prefabs that contain collider meshes."
              );
              if (GenerateColliderMeshAssetByEditor.boolValue)
              {
                if (renderer.GenerateColliderMeshAssetByEditorName.Equals(""))
                {
                  serializedObject.ApplyModifiedProperties();
                  renderer.ValidateAssetNames();
                  serializedObject.Update();
                }
                Indent();
                  Property(GenerateColliderMeshAssetByEditorName, "File Name" );
                Unindent();
              }
            Unindent();
          }

          Property(MeshGenerationRenderableMeshMode, 
            "Renderable Mesh Mode", 
                "None - No renderable mesh is created.\n\n" 
              + "Procedural - Run compute shader once and keep rendering from computed data. No standard mesh will be generated nor serialized.\n\n" 
              + "Mesh Renderer - Run compute shader once and generate a standard mesh. The mesh will be serialized and rendererd using Unity's MeshRenderer or SkinnedMeshRenderer."
          );

          if ((MudRendererBase.RenderableMeshMode) MeshGenerationRenderableMeshMode.intValue == MudRendererBase.RenderableMeshMode.MeshRenderer)
          {
            Indent();
              Property(MeshGenerationAutoRigging, 
                "Auto-Rigging", 
                    "Check to auto-rig locked mesh with brushes flagged as bones."
              );
              /*
              if (MeshGenerationAutoRigging.boolValue)
              {
                Property(MeshGenerationAutoRiggingAlgorithm, 
                  "  Algorithm", 
                      "Default - Takes brush blends into account.\n\n"
                    + "Legacy - Ignores brush blends and computes bone weights differently.");
              }
              */
              Property(MeshGenerationAutoRiggingMaxBonesPerVertex, 
                "Max Bones Per Vertex", 
                    "Maximum number of bones each vertex can be bound to."
              );
              Property(MeshGenerationGenerateTextureUV, 
                "Generate Texture UV (UV1)"
              );
              Property(MeshGenerationGenerateLightMapUV, 
                "Generate Light Map UV (UV2)"
              );
              Property(MeshGenerationWeldVertices, 
                "Weld Vertices"
              );
              Property(GenerateMeshAssetByEditor, 
                "Generate Mesh Asset", 
                    "Generate a mesh asset. This is needed for prefabs that contain locked meshes."
              );
              if (GenerateMeshAssetByEditor.boolValue)
              {
                if (renderer.GenerateMeshAssetByEditorName.Equals(""))
                {
                  serializedObject.ApplyModifiedProperties();
                  renderer.ValidateAssetNames();
                  serializedObject.Update();
                }
                Indent();
                  Property(GenerateMeshAssetByEditorName, "File Name" );
                Unindent();
              }
            Unindent();
          }

          Property(RecursiveLockMeshByEditor, 
            "Recursive Lock", 
                "Recursively lock all renderers within this renderer's hierarchy."
          );

          /*
          if (GUILayout.Button("Rectify Non-Unit-Scaled Parents"))
          {
            renderer.RectifyNonUnitScaledParents();
          }
          */

          if (GUILayout.Button("Lock Mesh"))
          {
            LockMesh();
            renderer.MeshGenerationLockOnStartByEditor = true;

            InitSerializedProperties();
          }

          Space();
        } // end: mesh utilities

        // SDF utilities
        if (gpuMode)
        {
          Header("SDF Utilities");

          Property(DrawGenerateSdfGizmos, "Draw SDF Generation Gizmos" );

          if (renderer.GenerateSdfByEditorName.Equals(""))
          {
            serializedObject.ApplyModifiedProperties();
            renderer.ValidateAssetNames();
            serializedObject.Update();
          }
          Property(GenerateSdfByEditorName, "File Name" );

          Property(GenerateSdfCenter, 
            "SDF Center", 
                "Point in SDF mapped to center of SDF texture"
          );
          Property(GenerateSdfDimension, 
            "SDF Dimension", 
                "Dimension in SDF mapped to size of SDF texture"
          );
          Property(GenerateSdfTextureSize, "SDF Texture Size" );

          if (GUILayout.Button("Generate SDF Texture"))
          {
            GenerateSdf();
          }

          Space();
        } // end: SDF utilities

        // optimization
        {
          // TODO
          /*
          Header("Optimization");

          Property(UseCutoffVolume, "Use Cutoff Volume");
          if (UseCutoffVolume.boolValue)
          {
            Indent();
              Property(CutoffVolumeCenter, "Center");
              Property(CutoffVolumeSize, "Size");
            Unindent();
          }

          Space();
          */
        } // end: optimziation

        // debug
        {
          Header("Debug");

          Property(AlwaysDrawGizmos, "Always Draw Gizmos");

          Property(DrawRawBrushBounds, 
            "Draw Raw Brush Bounds", 
                "Draw raw bounding volumes for each brush."
          );

          Property(DrawComputeBrushBounds, 
            "Draw Compute Bounds", 
                "Draw the expanded bounding volume actually used for computation for each brush."
          );

          Property(DrawRenderBounds, 
            "Draw Render Bounds", 
                "Draw the bounding volume used for culling the renderer."
          );

          Property(DrawVoxelNodes, 
            "Draw Voxel Nodes", 
                "Draw hierarchical voxel nodes."
          );
          Indent();
            Property(DrawVoxelNodesDepth, 
              "Node Depth", 
                  "Draw voxel nodes at a specific hierarchical depth.\n\n" 
                + "-1 means drawing all depths."
            );
            Property(DrawVoxelNodesScale, "Node Scale");
          Unindent();

          Space();
        } // end: debug

        // extra info
        {
          Header("Extra Info");
          Text($"Current Tracked Version: {renderer.CurrentTrackedVersion}");
          Text($"Previous Tracked Version: {renderer.PreviousTrackedVersion}");
          Text($"First Tracked Version: {renderer.FirstTrackedVersion}");
          Space();
        } // end: extra info

        // legends
        {
          Header("Legends");

          Text("(*): Impacts performance.");
          Text("(^): Impacts GPU memory usage.");
        } // end: legends
      }
      else // mesh locked?
      {
        if (MeshGenerationRenderableMeshMode.intValue == (int) MudRendererBase.RenderableMeshMode.Procedural)
          ShowMaterialOptions();

        Header("Mesh Utilities");

        Property(RecursiveLockMeshByEditor, 
          "Recursive Unlock", 
              "Recursively unlock all renderers within this renderer's hierarchy."
        );

        Property(RememberLockedMeshMaterialByEditor, "Remember Material");

        if (GUILayout.Button("Unlock Mesh"))
        {
          foreach (var r in targets.Select(x => (MudRenderer) x))
          {
            if (r == null)
              continue;

            if (!r.MeshLocked)
              continue;

            if (RememberLockedMeshMaterialByEditor.boolValue)
              renderer.RememberLockedMeshMaterial();

            UnlockMesh(r.transform, RecursiveLockMeshByEditor.boolValue);
          }
        }
      }

      serializedObject.ApplyModifiedProperties();
    }

    protected virtual void LockMesh() { }

    private void UnlockMesh(Transform t, bool recursive)
    {
      if (t == null)
        return;

      if (recursive)
      {
        for (int i = 0; i < t.childCount; ++i)
          UnlockMesh(t.GetChild(i), recursive);
      }

      var renderer = t.GetComponent<MudRenderer>();
      if (renderer == null)
        return;

      if (!renderer.MeshLocked)
        return;

      renderer.UnlockMesh();
    }

    protected virtual void GenerateSdf() { }

    private static readonly float RepaintInterval = 0.2f;
    private float m_repaintTimer = 0.0f;
    private void Update()
    {
      m_repaintTimer += Time.deltaTime;
      if (m_repaintTimer < RepaintInterval)
        return;

      m_repaintTimer = Mathf.Repeat(m_repaintTimer, RepaintInterval);

      try
      {
        if (Selection.activeGameObject != ((MudRendererBase) target).gameObject)
          return;
      }
      catch (MissingReferenceException)
      {
        // renderer has been destroyed
        return;
      }

      UpdateGpuMemoryUsage();
    }

    private void UpdateGpuMemoryUsage()
    {
      if (!ShowGpuMemoryUsage.boolValue)
        return;
        
      Repaint();
    }

    protected void DestroyAllChildren(Transform t, bool isRoot = true)
    {
      if (t == null)
        return;

      var aChild = new Transform[t.childCount];
      for (int i = 0; i < t.childCount; ++i)
        aChild[i] = t.GetChild(i);
      foreach (var child in aChild)
        DestroyAllChildren(child, false);

      if (!isRoot)
        DestroyImmediate(t.gameObject);
    }

    public bool HasFrameBounds()
    {
      return true;
    }

    public Bounds OnGetFrameBounds()
    {
      var renderer = (MudRendererBase) target;
      var bounds = Aabb.Empty;
      foreach (var brush in renderer.Brushes)
        if (brush != null)
          bounds.Include(brush.BoundsRs);

      if (bounds.IsEmpty)
        return new Bounds(renderer.transform.position, Vector3.one);

      bounds.Expand(renderer.SurfaceShift);
      bounds.Transform(renderer.transform);

      return new Bounds(bounds.Center, bounds.Size);
    }
  }
}

