Guest User

Unity3D Terrain-to-collider matcher Editor Script

a guest
Jul 10th, 2020
256
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 11.96 KB | None | 0 0
  1. //
  2. // Script originally from user @Zer0cool at:
  3. //
  4. // https://forum.unity.com/threads/terrain-leveling.926483/
  5. //
  6. // Revamped by @kurtdekker as follows:
  7. //
  8. //  - put this on the object (or object hierarchy) with colliders
  9. //  - drag the terrain reference into it
  10. //  - use the editor button to "Stamp"
  11. //  - support for a ramped perimeter, curved as specified
  12. //
  13. using UnityEngine;
  14.  
  15. #if UNITY_EDITOR
  16. using UnityEditor;
  17. #endif
  18.  
  19. public class MatchTerrainToColliders : MonoBehaviour
  20. {
  21.     [Tooltip(
  22.         "Assign Terrain here if you like, otherwise we search for one.")]
  23.     public Terrain terrain;
  24.  
  25.     [Tooltip(
  26.         "Default is to cast from below. This will cast from above and bring the terrain to match the TOP of our collider.")]
  27.     public bool CastFromAbove;
  28.  
  29.     [Header( "Related to smoothing around the edges.")]
  30.  
  31.     [Tooltip(
  32.         "Size of gaussian filter applied to change array. Set to zero for none")]
  33.     public int PerimeterRampDistance;
  34.  
  35.     [Tooltip(
  36.         "Use Perimeter Ramp Curve in lieu of direct gaussian smooth.")]
  37.     public bool ApplyPerimeterRampCurve;
  38.  
  39.     [Tooltip(
  40.         "Optional shaped ramp around perimeter.")]
  41.     public AnimationCurve PerimeterRampCurve;
  42.  
  43.     [Header("Misc/Editor")]
  44.  
  45.     [Tooltip(
  46.         "Enable this if you want undo. It is SUPER-dog slow though, so I would leave it OFF.")]
  47.     public bool EnableEditorUndo;
  48.  
  49.     // This extends the binary on/off blend stencil out by one pixel,
  50.     // making one sheet at a time, then stacks (adds) them all together and
  51.     // renormalizes them back to 0.0-1.0.
  52.     //
  53.     // it simultaneously takes the average of the "hitting" perimeter neighboring
  54.     // heightmap cells and extends it outwards as it expands.
  55.     //
  56.     void GeneratePerimeterHeightRampAndFlange(float[,] heightMap, float[,] blendStencil, int distance)
  57.     {
  58.         int w = blendStencil.GetLength(0);
  59.         int h = blendStencil.GetLength(1);
  60.  
  61.         // each stencil, expanded by one more pixel, before we restack them
  62.         float[][,] stencilPile = new float[distance + 1][,];
  63.  
  64.         // where we will build the horizontal heightmap flange out
  65.         float[,] extendedHeightmap = new float[w, h];
  66.  
  67.         // directonal table: 4-way and 8-way available
  68.         int[] neighborXYPairs = new int[] {
  69.             // compass directions first
  70.             0, 1,
  71.             1, 0,
  72.             0, -1,
  73.             -1, 0,
  74.             // diagonals next
  75.             1,1,
  76.             -1,1,
  77.             1,-1,
  78.             -1,-1,
  79.         };
  80.  
  81.         int neighborCount = 4;                  // 4 and 8 are supported from the table above
  82.  
  83.         float[,] source = blendStencil;         // this is NOT a copy! This is a reference!
  84.         for (int n = 0; n <= distance; n++)
  85.         {
  86.             // add it to the pile BEFORE we expand it;
  87.             // that way the first one is the original
  88.             // input blendStencil.
  89.             stencilPile[n] = source;
  90.  
  91.             // Debug: WritePNG( source, "pile-" + n.ToString());
  92.  
  93.             // this is gonna be an actual true deep copy of the stencil
  94.             // as it stands now, and it will steadily grow outwards, but
  95.             // each time it is always 0.0 or 1.0 cells, nothing in between.
  96.             float[,] expanded = new float[w, h];
  97.             for (int j = 0; j < h; j++)
  98.             {
  99.                 for (int i = 0; i < w; i++)
  100.                 {
  101.                     expanded[i, j] = source[i, j];
  102.                 }
  103.             }
  104.  
  105.             // we have to quit so we don't further expand the flange heightmap
  106.             if (n == distance)
  107.             {
  108.                 break;
  109.             }
  110.  
  111.             // Add one solid pixel around perimeter of the stencil.
  112.             // Also ledge-extend the perimeter heightmap value for those
  113.             // non-zero cells, not reducing them at all (they are like
  114.             // flat flange going outwards that we need in order to later blend).
  115.             //
  116.             for (int j = 0; j < h; j++)
  117.             {
  118.                 for (int i = 0; i < w; i++)
  119.                 {
  120.                     if (source[i, j] == 0)
  121.                     {
  122.                         // serves as "hit" or not too
  123.                         int count = 0;
  124.  
  125.                         // for average of neighboring heights
  126.                         float height = 0.0f;
  127.  
  128.                         for (int neighbor = 0; neighbor < neighborCount; neighbor++)
  129.                         {
  130.                             int x = i + neighborXYPairs[neighbor * 2 + 0];
  131.                             int y = j + neighborXYPairs[neighbor * 2 + 1];
  132.                             if ((x >= 0) && (x < w) && (y >= 0) && (y < h))
  133.                             {
  134.                                 // found a neighbor: we will:
  135.                                 //  - areally expand the stencil by this one pixel
  136.                                 //  - sample the neighbor height for the flange extension
  137.                                 if (source[x, y] != 0)
  138.                                 {
  139.                                     height += heightMap[x, y];
  140.                                     count++;
  141.                                 }
  142.                             }
  143.                         }
  144.  
  145.                         // extend the height of this cell by the average height
  146.                         // of the neighbors that contained source stencil true
  147.                         if (count > 0)
  148.                         {
  149.                             expanded[i, j] = 1.0f;
  150.  
  151.                             extendedHeightmap[i, j] = height / count;
  152.                         }
  153.                     }
  154.                 }
  155.             }
  156.  
  157.             // Copy the new ledge back to the original heightmap.
  158.             // WARNING: this is an "output" operation because it is
  159.             // modifying the supplied input heightmap data, areally
  160.             // adding around the edge by the pixels encountered.
  161.             for (int j = 0; j < h; j++)
  162.             {
  163.                 for (int i = 0; i < w; i++)
  164.                 {
  165.                     var height = extendedHeightmap[i, j];
  166.                        
  167.                     // only lift... this still allows us to lower terrain,
  168.                     // since it is lifting from absolute zero to the altitude
  169.                     // that we actually sensed at this hit neighbor pixels,
  170.                     // and we need this unattenuated height for later blending.
  171.                     if (height > 0)
  172.                     {
  173.                         heightMap[i,j] = height;
  174.                     }
  175.  
  176.                     // zero it too, for next layer (might not be necessary??)
  177.                     extendedHeightmap[i, j] = 0;
  178.                 }
  179.             }
  180.  
  181.             // assign the source to this fresh copy
  182.             source = expanded;          // shallow copy (reference)
  183.         }
  184.  
  185.         // now tally the pile, summarizing each stack of 0/1 solid pixels,
  186.         // copying it to to the stencil array passed in, which will change
  187.         // its contents directly, and renormalize it back down to 0.0 to 1.0
  188.         //
  189.         // WARNING: this is also an output operation, as it modifies the
  190.         // blendStencil inbound dataset
  191.         //
  192.         for (int j = 0; j < h; j++)
  193.         {
  194.             for (int i = 0; i < w; i++)
  195.             {
  196.                 float total = 0;
  197.                 for (int n = 0; n <= distance; n++)
  198.                 {
  199.                     total += stencilPile[n][i, j];
  200.                 }
  201.  
  202.                 total /= (distance + 1);
  203.  
  204.                 blendStencil[i, j] = total;
  205.             }
  206.         }
  207.  
  208.         // Debug: WritePNG( blendStencil, "blend");
  209.     }
  210.  
  211.     void BringTerrainToUndersideOfCollider()
  212.     {
  213.         var Colliders = GetComponentsInChildren<Collider>();
  214.  
  215.         if (Colliders == null || Colliders.Length == 0)
  216.         {
  217.             Debug.LogError("We must have at least one collider on ourselves or below us in the hierarchy. " +
  218.                 "We will cast to it and match terrain to that contour.");
  219.             return;
  220.         }
  221.  
  222.         // if you don't provide a terrain, it searches and warns
  223.         if (!terrain)
  224.         {
  225.             terrain = FindObjectOfType<Terrain>();
  226.             if (!terrain)
  227.             {
  228.                 Debug.LogError("couldn't find a terrain");
  229.                 return;
  230.             }
  231.             Debug.LogWarning(
  232.                 "Terrain not supplied; finding it myself. I found and assigned " + terrain.name +
  233.                 ", but I didn't do anything yet... click again to actually DO the modification.");
  234.             return;
  235.         }
  236.  
  237.         TerrainData terData = terrain.terrainData;
  238.         int Tw = terData.heightmapResolution;
  239.         int Th = terData.heightmapResolution;
  240.         var heightMapOriginal = terData.GetHeights(0, 0, Tw, Th);
  241.  
  242.         // where we do our work when we generate the new terrain heights
  243.         var heightMapCreated = new float[heightMapOriginal.GetLength(0), heightMapOriginal.GetLength(1)];
  244.  
  245.         // for blending heightMapCreated with the heightMapOriginal to form
  246.         var heightAlpha = new float[heightMapOriginal.GetLength(0), heightMapOriginal.GetLength(1)];
  247.  
  248. #if UNITY_EDITOR
  249.         if (EnableEditorUndo)
  250.         {
  251.             Undo.RecordObject(terData, "ModifyTerrain");
  252.         }
  253. #endif
  254.  
  255.         for (int Tz = 0; Tz < Th; Tz++)
  256.         {
  257.             for (int Tx = 0; Tx < Tw; Tx++)
  258.             {
  259.                 // start under the terrain and cast up?
  260.                 var pos = terrain.transform.position +
  261.                     new Vector3((Tx * terData.size.x) / (Tw - 1),
  262.                     -10,
  263.                     (Tz * terData.size.z) / (Th - 1));
  264.  
  265.                 Ray ray = new Ray(pos, Vector3.up);
  266.  
  267.                 // nope, start from above and cast down
  268.                 if (CastFromAbove)
  269.                 {
  270.                     pos.y = transform.position.y + terData.size.y + 10;
  271.                     ray = new Ray(pos, Vector3.down);
  272.                 }
  273.  
  274.                 bool didHit = false;
  275.                 float yHit = 0;
  276.  
  277.                 // scan all the colliders and take the "firstest" distance we hit at
  278.                 foreach (var ourCollider in Colliders)
  279.                 {
  280.                     RaycastHit hit;
  281.                     if (ourCollider.Raycast(ray, out hit, 1000))
  282.                     {
  283.                         if (!didHit)
  284.                         {
  285.                             yHit = hit.point.y;
  286.                         }
  287.  
  288.                         didHit = true;
  289.  
  290.                         // take lowest or highest, as appropriate
  291.                         if (CastFromAbove)
  292.                         {
  293.                             if (hit.point.y > yHit)
  294.                             {
  295.                                 yHit = hit.point.y;
  296.                             }
  297.                         }
  298.                         else
  299.                         {
  300.                             if (hit.point.y < yHit)
  301.                             {
  302.                                 yHit = hit.point.y;
  303.                             }
  304.                         }
  305.  
  306.                     }
  307.  
  308.                     if (didHit)
  309.                     {
  310.                         var height = yHit / terData.size.y;
  311.  
  312.                         heightMapCreated[Tz, Tx] = height;
  313.                         heightAlpha[Tz, Tx] = 1.0f;             // opaque
  314.                     }
  315.                 }
  316.             }
  317.         }
  318.  
  319.         // now we might smooth things out a bit
  320.         if (PerimeterRampDistance > 0)
  321.         {
  322.             // Debug: WritePNG( heightMapCreated, "height-0", true);
  323.             // Debug: WritePNG( heightAlpha, "alpha-0", true);
  324.  
  325.             GeneratePerimeterHeightRampAndFlange(
  326.                 heightMap: heightMapCreated,
  327.                 blendStencil: heightAlpha,
  328.                 distance: PerimeterRampDistance);
  329.            
  330.             // Debug: WritePNG( heightMapCreated, "height-1", true);
  331.             // Debug: WritePNG( heightAlpha, "alpha-1", true);
  332.         }
  333.  
  334.         // apply the generated data (blend operation)
  335.         for (int Tz = 0; Tz < Th; Tz++)
  336.         {
  337.             for (int Tx = 0; Tx < Tw; Tx++)
  338.             {
  339.                 float fraction = heightAlpha[Tz, Tx];
  340.  
  341.                 if (ApplyPerimeterRampCurve)
  342.                 {
  343.                     fraction = PerimeterRampCurve.Evaluate( fraction);
  344.                 }
  345.  
  346.                 heightMapOriginal[Tz, Tx] = Mathf.Lerp(
  347.                     heightMapOriginal[Tz, Tx],
  348.                     heightMapCreated[Tz, Tx],
  349.                     fraction);
  350.             }
  351.         }
  352.  
  353.         terData.SetHeights(0, 0, heightMapOriginal);
  354.     }
  355.  
  356. #if UNITY_EDITOR
  357.     [CustomEditor(typeof(MatchTerrainToColliders))]
  358.     public class MatchTerrainToCollidersEditor : Editor
  359.     {
  360.         public override void OnInspectorGUI()
  361.         {
  362.             MatchTerrainToColliders item = (MatchTerrainToColliders)target;
  363.  
  364.             DrawDefaultInspector();
  365.  
  366.             EditorGUILayout.BeginVertical();
  367.  
  368.             var buttonLabel = "Bring Terrain To Underside Of Collider";
  369.             if (item.CastFromAbove)
  370.             {
  371.                 buttonLabel = "Bring Terrain To Topside Of Collider";
  372.             }
  373.  
  374.             if (GUILayout.Button(buttonLabel))
  375.             {
  376.                 item.BringTerrainToUndersideOfCollider();
  377.             }
  378.  
  379.             EditorGUILayout.EndVertical();
  380.         }
  381. #endif
  382.     }
  383.  
  384.     // debug stuff:
  385.     void WritePNG( float[,] array, string filename, bool normalize = false)
  386.     {
  387.         int w = array.GetLength(0);
  388.         int h = array.GetLength(1);
  389.  
  390.         Texture2D texture = new Texture2D( w, h);
  391.  
  392.         Color[] colors = new Color[ w * h];
  393.  
  394.         // to colors
  395.         {
  396.             float min = 0;
  397.             float max = 1;
  398.  
  399.             if (normalize)
  400.             {
  401.                 min = 1;
  402.                 max = 0;
  403.                 for (int j = 0; j < h; j++)
  404.                 {
  405.                     for (int i = 0; i < w; i++)
  406.                     {
  407.                         float x = array[i,j];
  408.                         if (x < min) min = x;
  409.                         if (x > max) max = x;
  410.                     }
  411.                 }
  412.  
  413.                 // no dynamic range present, disable normalization
  414.                 if (max <= min)
  415.                 {
  416.                     min = 0;
  417.                     max = 1;
  418.                 }
  419.             }
  420.  
  421.             int n = 0;
  422.             for (int j = 0; j < h; j++)
  423.             {
  424.                 for (int i = 0; i < w; i++)
  425.                 {
  426.                     float x = array[i,j];
  427.                     x = x - min;
  428.                     x /= (max - min);
  429.                     colors[n] = new Color( x,x,x);
  430.                     n++;
  431.                 }
  432.             }
  433.         }
  434.  
  435.         texture.SetPixels( colors);
  436.         texture.Apply();
  437.  
  438.         var bytes = texture.EncodeToPNG();
  439.  
  440.         DestroyImmediate(texture);
  441.  
  442.         filename = filename + ".png";
  443.  
  444.         System.IO.File.WriteAllBytes( filename, bytes);
  445.     }
  446.  
  447.     // call this in lieu of doing the actual data
  448.     void Debug_Microtest()
  449.     {
  450.         float[,] heights = new float[3,3] {
  451.             { 0.0f, 0.0f, 0.0f, },
  452.             { 0.0f, 0.5f, 0.0f, },
  453.             { 0.0f, 0.0f, 0.0f, }
  454.         };
  455.         float[,] stencil = new float[3,3] {
  456.             { 0.0f, 0.0f, 0.0f, },
  457.             { 0.0f, 1.0f, 0.0f, },
  458.             { 0.0f, 0.0f, 0.0f, }
  459.         };
  460.  
  461.         {
  462.             WritePNG( heights, "height-0", true);
  463.             WritePNG( stencil, "alpha-0", true);
  464.  
  465.             GeneratePerimeterHeightRampAndFlange(
  466.                 heightMap: heights,
  467.                 blendStencil: stencil,
  468.                 distance: PerimeterRampDistance);
  469.  
  470.             WritePNG( heights, "height-1", true);
  471.             WritePNG( stencil, "alpha-1", true);
  472.         }
  473.     }
  474. }
Add Comment
Please, Sign In to add comment