unity地形匹配模型

2023-09-04  本文已影响0人  Rayson
1.
//
// Script originally from user @Zer0cool at:
//
// https://forum.unity.com/threads/terrain-leveling.926483/
//
// Revamped by @kurtdekker as follows:
//
//  - put this on the object (or object hierarchy) with colliders
//  - drag the terrain reference into it
//  - use the editor button to "Stamp"
//  - support for a ramped perimeter, curved as specified
//
// Also posted at / about:
// https://twitter.com/kurtdekker/status/1281619776587001856?s=20
// https://www.youtube.com/watch?v=FykNmJ3NIpI
// https://pastebin.com/DYFAYgnE
// 
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

public class MatchTerrainToColliders : MonoBehaviour
{
    [Tooltip(
        "Assign Terrain here if you like, otherwise we search for one.")]
    public Terrain terrain;

    [Tooltip(
        "Default is to cast from below. This will cast from above and bring the terrain to match the TOP of our collider.")]
    public bool CastFromAbove;

    [Header( "Related to smoothing around the edges.")]

    [Tooltip(
        "Size of gaussian filter applied to change array. Set to zero for none")]
    public int PerimeterRampDistance;

    [Tooltip(
        "Use Perimeter Ramp Curve in lieu of direct gaussian smooth.")]
    public bool ApplyPerimeterRampCurve;

    [Tooltip(
        "Optional shaped ramp around perimeter.")]
    public AnimationCurve PerimeterRampCurve;

    [Header("Misc/Editor")]

    [Tooltip(
        "Enable this if you want undo. It is SUPER-dog slow though, so I would leave it OFF.")]
    public bool EnableEditorUndo;

    // This extends the binary on/off blend stencil out by one pixel,
    // making one sheet at a time, then stacks (adds) them all together and
    // renormalizes them back to 0.0-1.0.
    //
    // it simultaneously takes the average of the "hitting" perimeter neighboring
    // heightmap cells and extends it outwards as it expands.
    //
    void GeneratePerimeterHeightRampAndFlange(float[,] heightMap, float[,] blendStencil, int distance)
    {
        int w = blendStencil.GetLength(0);
        int h = blendStencil.GetLength(1);

        // each stencil, expanded by one more pixel, before we restack them
        float[][,] stencilPile = new float[distance + 1][,];

        // where we will build the horizontal heightmap flange out
        float[,] extendedHeightmap = new float[w, h];

        // directonal table: 4-way and 8-way available
        int[] neighborXYPairs = new int[] {
            // compass directions first
            0, 1,
            1, 0,
            0, -1,
            -1, 0,
            // diagonals next
            1,1,
            -1,1,
            1,-1,
            -1,-1,
        };

        int neighborCount = 4;                  // 4 and 8 are supported from the table above

        float[,] source = blendStencil;         // this is NOT a copy! This is a reference!
        for (int n = 0; n <= distance; n++)
        {
            // add it to the pile BEFORE we expand it;
            // that way the first one is the original
            // input blendStencil.
            stencilPile[n] = source;

            // Debug: WritePNG( source, "pile-" + n.ToString());

            // this is gonna be an actual true deep copy of the stencil
            // as it stands now, and it will steadily grow outwards, but
            // each time it is always 0.0 or 1.0 cells, nothing in between.
            float[,] expanded = new float[w, h];
            for (int j = 0; j < h; j++)
            {
                for (int i = 0; i < w; i++)
                {
                    expanded[i, j] = source[i, j];
                }
            }

            // we have to quit so we don't further expand the flange heightmap
            if (n == distance)
            {
                break;
            }

            // Add one solid pixel around perimeter of the stencil.
            // Also ledge-extend the perimeter heightmap value for those
            // non-zero cells, not reducing them at all (they are like
            // flat flange going outwards that we need in order to later blend).
            //
            for (int j = 0; j < h; j++)
            {
                for (int i = 0; i < w; i++)
                {
                    if (source[i, j] == 0)
                    {
                        // serves as "hit" or not too
                        int count = 0;

                        // for average of neighboring heights
                        float height = 0.0f;

                        for (int neighbor = 0; neighbor < neighborCount; neighbor++)
                        {
                            int x = i + neighborXYPairs[neighbor * 2 + 0];
                            int y = j + neighborXYPairs[neighbor * 2 + 1];
                            if ((x >= 0) && (x < w) && (y >= 0) && (y < h))
                            {
                                // found a neighbor: we will:
                                //  - areally expand the stencil by this one pixel
                                //  - sample the neighbor height for the flange extension
                                if (source[x, y] != 0)
                                {
                                    height += heightMap[x, y];
                                    count++;
                                }
                            }
                        }

                        // extend the height of this cell by the average height
                        // of the neighbors that contained source stencil true
                        if (count > 0)
                        {
                            expanded[i, j] = 1.0f;

                            extendedHeightmap[i, j] = height / count;
                        }
                    }
                }
            }

            // Copy the new ledge back to the original heightmap.
            // WARNING: this is an "output" operation because it is
            // modifying the supplied input heightmap data, areally
            // adding around the edge by the pixels encountered.
            for (int j = 0; j < h; j++)
            {
                for (int i = 0; i < w; i++)
                {
                    var height = extendedHeightmap[i, j];
                        
                    // only lift... this still allows us to lower terrain,
                    // since it is lifting from absolute zero to the altitude
                    // that we actually sensed at this hit neighbor pixels,
                    // and we need this unattenuated height for later blending.
                    if (height > 0)
                    {
                        heightMap[i,j] = height;
                    }

                    // zero it too, for next layer (might not be necessary??)
                    extendedHeightmap[i, j] = 0;
                }
            }

            // assign the source to this fresh copy
            source = expanded;          // shallow copy (reference)
        }

        // now tally the pile, summarizing each stack of 0/1 solid pixels,
        // copying it to to the stencil array passed in, which will change
        // its contents directly, and renormalize it back down to 0.0 to 1.0
        //
        // WARNING: this is also an output operation, as it modifies the
        // blendStencil inbound dataset
        //
        for (int j = 0; j < h; j++)
        {
            for (int i = 0; i < w; i++)
            {
                float total = 0;
                for (int n = 0; n <= distance; n++)
                {
                    total += stencilPile[n][i, j];
                }

                total /= (distance + 1);

                blendStencil[i, j] = total;
            }
        }

        // Debug: WritePNG( blendStencil, "blend");
    }

    void BringTerrainToUndersideOfCollider()
    {
        var Colliders = GetComponentsInChildren<Collider>();

        if (Colliders == null || Colliders.Length == 0)
        {
            Debug.LogError("We must have at least one collider on ourselves or below us in the hierarchy. " +
                "We will cast to it and match terrain to that contour.");
            return;
        }

        // if you don't provide a terrain, it searches and warns
        if (!terrain)
        {
            terrain = FindObjectOfType<Terrain>();
            if (!terrain)
            {
                Debug.LogError("couldn't find a terrain");
                return;
            }
            Debug.LogWarning(
                "Terrain not supplied; finding it myself. I found and assigned " + terrain.name +
                ", but I didn't do anything yet... click again to actually DO the modification.");
            return;
        }

        TerrainData terData = terrain.terrainData;
        int Tw = terData.heightmapResolution;
        int Th = terData.heightmapResolution;
        var heightMapOriginal = terData.GetHeights(0, 0, Tw, Th);

        // where we do our work when we generate the new terrain heights
        var heightMapCreated = new float[heightMapOriginal.GetLength(0), heightMapOriginal.GetLength(1)];

        // for blending heightMapCreated with the heightMapOriginal to form
        var heightAlpha = new float[heightMapOriginal.GetLength(0), heightMapOriginal.GetLength(1)];

#if UNITY_EDITOR
        if (EnableEditorUndo)
        {
            Undo.RecordObject(terData, "ModifyTerrain");
        }
#endif

        for (int Tz = 0; Tz < Th; Tz++)
        {
            for (int Tx = 0; Tx < Tw; Tx++)
            {
                // start under the terrain and cast up?
                var pos = terrain.transform.position +
                    new Vector3((Tx * terData.size.x) / (Tw - 1),
                    -10,
                    (Tz * terData.size.z) / (Th - 1));

                Ray ray = new Ray(pos, Vector3.up);

                // nope, start from above and cast down
                if (CastFromAbove)
                {
                    pos.y = transform.position.y + terData.size.y + 10;
                    ray = new Ray(pos, Vector3.down);
                }

                bool didHit = false;
                float yHit = 0;

                // scan all the colliders and take the "firstest" distance we hit at
                foreach (var ourCollider in Colliders)
                {
                    RaycastHit hit;
                    if (ourCollider.Raycast(ray, out hit, 1000))
                    {
                        if (!didHit)
                        {
                            yHit = hit.point.y;
                        }

                        didHit = true;

                        // take lowest or highest, as appropriate
                        if (CastFromAbove)
                        {
                            if (hit.point.y > yHit)
                            {
                                yHit = hit.point.y;
                            }
                        }
                        else
                        {
                            if (hit.point.y < yHit)
                            {
                                yHit = hit.point.y;
                            }
                        }

                    }

                    if (didHit)
                    {
                        var height = yHit / terData.size.y;

                        heightMapCreated[Tz, Tx] = height;
                        heightAlpha[Tz, Tx] = 1.0f;             // opaque
                    }
                }
            }
        }

        // now we might smooth things out a bit
        if (PerimeterRampDistance > 0)
        {
            // Debug: WritePNG( heightMapCreated, "height-0", true);
            // Debug: WritePNG( heightAlpha, "alpha-0", true);

            GeneratePerimeterHeightRampAndFlange(
                heightMap: heightMapCreated,
                blendStencil: heightAlpha,
                distance: PerimeterRampDistance);
            
            // Debug: WritePNG( heightMapCreated, "height-1", true);
            // Debug: WritePNG( heightAlpha, "alpha-1", true);
        }

        // apply the generated data (blend operation)
        for (int Tz = 0; Tz < Th; Tz++)
        {
            for (int Tx = 0; Tx < Tw; Tx++)
            {
                float fraction = heightAlpha[Tz, Tx];

                if (ApplyPerimeterRampCurve)
                {
                    fraction = PerimeterRampCurve.Evaluate( fraction);
                }

                heightMapOriginal[Tz, Tx] = Mathf.Lerp(
                    heightMapOriginal[Tz, Tx],
                    heightMapCreated[Tz, Tx],
                    fraction);
            }
        }

        terData.SetHeights(0, 0, heightMapOriginal);
    }

#if UNITY_EDITOR
    [CustomEditor(typeof(MatchTerrainToColliders))]
    public class MatchTerrainToCollidersEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            MatchTerrainToColliders item = (MatchTerrainToColliders)target;

            DrawDefaultInspector();

            EditorGUILayout.BeginVertical();

            var buttonLabel = "Bring Terrain To Underside Of Collider";
            if (item.CastFromAbove)
            {
                buttonLabel = "Bring Terrain To Topside Of Collider";
            }

            if (GUILayout.Button(buttonLabel))
            {
                item.BringTerrainToUndersideOfCollider();
            }

            EditorGUILayout.EndVertical();
        }
#endif
    }

    // debug stuff:
    void WritePNG( float[,] array, string filename, bool normalize = false)
    {
        int w = array.GetLength(0);
        int h = array.GetLength(1);

        Texture2D texture = new Texture2D( w, h);

        Color[] colors = new Color[ w * h];

        // to colors
        {
            float min = 0;
            float max = 1;

            if (normalize)
            {
                min = 1;
                max = 0;
                for (int j = 0; j < h; j++)
                {
                    for (int i = 0; i < w; i++)
                    {
                        float x = array[i,j];
                        if (x < min) min = x;
                        if (x > max) max = x;
                    }
                }

                // no dynamic range present, disable normalization
                if (max <= min)
                {
                    min = 0;
                    max = 1;
                }
            }

            int n = 0;
            for (int j = 0; j < h; j++)
            {
                for (int i = 0; i < w; i++)
                {
                    float x = array[i,j];
                    x = x - min;
                    x /= (max - min);
                    colors[n] = new Color( x,x,x);
                    n++;
                }
            }
        }

        texture.SetPixels( colors);
        texture.Apply();

        var bytes = texture.EncodeToPNG();

        DestroyImmediate(texture);

        filename = filename + ".png";

        System.IO.File.WriteAllBytes( filename, bytes);
    }

    // call this in lieu of doing the actual data
    void Debug_Microtest()
    {
        float[,] heights = new float[3,3] {
            { 0.0f, 0.0f, 0.0f, },
            { 0.0f, 0.5f, 0.0f, },
            { 0.0f, 0.0f, 0.0f, }
        };
        float[,] stencil = new float[3,3] {
            { 0.0f, 0.0f, 0.0f, },
            { 0.0f, 1.0f, 0.0f, },
            { 0.0f, 0.0f, 0.0f, }
        };

        {
            WritePNG( heights, "height-0", true);
            WritePNG( stencil, "alpha-0", true);

            GeneratePerimeterHeightRampAndFlange(
                heightMap: heights,
                blendStencil: stencil,
                distance: PerimeterRampDistance);

            WritePNG( heights, "height-1", true);
            WritePNG( stencil, "alpha-1", true);
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读