In-game Terrain Heightmap-editor voor Unity

Unity is uitgerust met een ingebouwde terreineditor, maar wat als je een terreinbewerkingsfunctie wilt toevoegen aan de game?

In deze tutorial laat ik zien hoe je een eenvoudige runtime Terrain-editor toevoegt. Bovendien is er geen sprake van texturen, het is nog steeds een geweldig voorbeeld waarop je kunt voortbouwen.

Runtime Terrain-editor in Unity

Unity versie gebruikt in deze tutorial: Unity 2021.1.0f1 (64-bit)

Stap 1: Maak alle benodigde scripts

Deze tutorial bevat 3 scripts:

SC_TerrainEditor.cs

using UnityEngine;

public class SC_TerrainEditor : MonoBehaviour
{
    public enum DeformMode { RaiseLower, Flatten, Smooth }
    DeformMode deformMode = DeformMode.RaiseLower;
    string[] deformModeNames = new string[] { "Raise Lower", "Flatten", "Smooth" };

    public Terrain terrain;
    public Texture2D deformTexture;
    public float strength = 1;
    public float area = 1;
    public bool showHelp;

    Transform buildTarget;
    Vector3 buildTargPos;
    Light spotLight;

    //GUI
    Rect windowRect = new Rect(10, 10, 400, 185);
    bool onWindow = false;
    bool onTerrain;
    Texture2D newTex;
    float strengthSave;

    //Raycast
    private RaycastHit hit;

    //Deformation variables
    private int xRes;
    private int yRes;
    private float[,] saved;
    float flattenTarget = 0;
    Color[] craterData;

    TerrainData tData;

    float strengthNormalized
    {
        get
        {
            return (strength) / 9.0f;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        //Create build target object
        GameObject tmpObj = new GameObject("BuildTarget");
        buildTarget = tmpObj.transform;

        //Add Spot Light to build target
        GameObject spotLightObj = new GameObject("SpotLight");
        spotLightObj.transform.SetParent(buildTarget);
        spotLightObj.transform.localPosition = new Vector3(0, 2, 0);
        spotLightObj.transform.localEulerAngles = new Vector3(90, 0, 0);
        spotLight = spotLightObj.AddComponent<Light>();
        spotLight.type = LightType.Spot;
        spotLight.range = 20;

        tData = terrain.terrainData;
        if (tData)
        {
            //Save original height data
            xRes = tData.heightmapResolution;
            yRes = tData.heightmapResolution;
            saved = tData.GetHeights(0, 0, xRes, yRes);
        }

        //Change terrain layer to UI
        terrain.gameObject.layer = 5;
        strength = 2;
        area = 2;
        brushScaling();
    }

    void FixedUpdate()
    {
        raycastHit();
        wheelValuesControl();

        if (onTerrain && !onWindow)
        {
            terrainDeform();
        }

        //Update Spot Light Angle according to the Area value
        spotLight.spotAngle = area * 25f;
    }

    //Raycast
    //______________________________________________________________________________________________________________________________
    void raycastHit()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        hit = new RaycastHit();
        //Do Raycast hit only against UI layer
        if (Physics.Raycast(ray, out hit, 300, 1 << 5))
        {
            onTerrain = true;
            if (buildTarget)
            {
                buildTarget.position = Vector3.Lerp(buildTarget.position, hit.point + new Vector3(0, 1, 0), Time.time);
            }
        }
        else
        {
            if (buildTarget)
            {
                Vector3 curScreenPoint = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 200);
                Vector3 curPosition = Camera.main.ScreenToWorldPoint(curScreenPoint);
                buildTarget.position = curPosition;
                onTerrain = false;
            }
        }
    }

    //TerrainDeformation
    //___________________________________________________________________________________________________________________
    void terrainDeform()
    {
        if (Input.GetMouseButtonDown(0))
        {
            buildTargPos = buildTarget.position - terrain.GetPosition();
            float x = Mathf.Clamp01(buildTargPos.x / tData.size.x);
            float y = Mathf.Clamp01(buildTargPos.z / tData.size.z);
            flattenTarget = tData.GetInterpolatedHeight(x, y) / tData.heightmapScale.y;
        }

        //Terrain deform
        if (Input.GetMouseButton(0))
        {

            buildTargPos = buildTarget.position - terrain.GetPosition();

            if (Input.GetKey(KeyCode.LeftShift))
            {
                strengthSave = strength;
            }
            else
            {
                strengthSave = -strength;
            }

            if (newTex && tData && craterData != null)
            {
                int x = (int)Mathf.Lerp(0, xRes, Mathf.InverseLerp(0, tData.size.x, buildTargPos.x));
                int z = (int)Mathf.Lerp(0, yRes, Mathf.InverseLerp(0, tData.size.z, buildTargPos.z));
                x = Mathf.Clamp(x, newTex.width / 2, xRes - newTex.width / 2);
                z = Mathf.Clamp(z, newTex.height / 2, yRes - newTex.height / 2);
                int startX = x - newTex.width / 2;
                int startY = z - newTex.height / 2;
                float[,] areaT = tData.GetHeights(startX, startY, newTex.width, newTex.height);
                for (int i = 0; i < newTex.height; i++)
                {
                    for (int j = 0; j < newTex.width; j++)
                    {
                        if (deformMode == DeformMode.RaiseLower)
                        {
                            areaT[i, j] = areaT[i, j] - craterData[i * newTex.width + j].a * strengthSave / 15000;
                        }
                        else if (deformMode == DeformMode.Flatten)
                        {
                            areaT[i, j] = Mathf.Lerp(areaT[i, j], flattenTarget, craterData[i * newTex.width + j].a * strengthNormalized);
                        }
                        else if (deformMode == DeformMode.Smooth)
                        {
                            if (i == 0 || i == newTex.height - 1 || j == 0 || j == newTex.width - 1)
                                continue;

                            float heightSum = 0;
                            for (int ySub = -1; ySub <= 1; ySub++)
                            {
                                for (int xSub = -1; xSub <= 1; xSub++)
                                {
                                    heightSum += areaT[i + ySub, j + xSub];
                                }
                            }

                            areaT[i, j] = Mathf.Lerp(areaT[i, j], (heightSum / 9), craterData[i * newTex.width + j].a * strengthNormalized);
                        }
                    }
                }
                tData.SetHeights(x - newTex.width / 2, z - newTex.height / 2, areaT);
            }
        }
    }

    void brushScaling()
    {
        //Apply current deform texture resolution 
        newTex = Instantiate(deformTexture) as Texture2D;
        TextureScale.Point(newTex, deformTexture.width * (int)area / 10, deformTexture.height * (int)area / 10);
        newTex.Apply();
        craterData = newTex.GetPixels();
    }

    void wheelValuesControl()
    {
        float mouseWheel = Input.GetAxis("Mouse ScrollWheel");
        if (Mathf.Abs(mouseWheel) > 0.0)
        {
            if (mouseWheel > 0.0)
            {
                //More
                if (!Input.GetKey(KeyCode.LeftShift))
                {
                    if (area < 13)
                    {
                        area += 0.5f;
                    }
                    else
                    {
                        area = 13;
                    }
                }
                else
                {
                    if (strength < 13)
                    {
                        strength += 0.5f;
                    }
                    else
                    {
                        strength = 13;
                    }
                }
            }
            else if (mouseWheel < 0.0)
            {
                //Less
                if (!Input.GetKey(KeyCode.LeftShift))
                {
                    if (area > 1)
                    {
                        area -= 0.5f;
                    }
                    else
                    {
                        area = 1;
                    }
                }
                else
                {
                    if (strength > 1)
                    {
                        strength -= 0.5f;
                    }
                    else
                    {
                        strength = 1;
                    }
                }
            }
            if (area > 1)
                brushScaling();
        }
    }

    //GUI
    //______________________________________________________________________________________________________________________________
    void OnGUI()
    {
        windowRect = GUI.Window(0, windowRect, TerrainEditorWindow, "Terrain Sculptor");

        GUILayout.BeginArea(new Rect(Screen.width - 70, 10, 60, 30));
        showHelp = GUILayout.Toggle(showHelp, "(Help)", new GUILayoutOption[] { GUILayout.Width(60.0f), GUILayout.Height(30.0f) });
        GUILayout.EndArea();

        if (showHelp)
        {
            //Help window properties
            GUI.Window(1, new Rect(Screen.width - 410, 50, 400, 120), HelpWindow, "Help Window");
        }
    }

    //Help window display tips and tricks
    void HelpWindow(int windowId)
    {
        GUILayout.BeginVertical("box");
        {
            GUILayout.Label("- Mouse wheel - area change");
            GUILayout.Label("- Mouse wheel + Shift - strength change");
            GUILayout.Label("- Hold Shift in RaiseLower mode to lower terrain");
        }
        GUILayout.EndVertical();
    }

    void TerrainEditorWindow(int windowId)
    {
        //Detect when mouse cursor inside region (TerrainEditorWindow)
        GUILayout.BeginArea(new Rect(0, 0, 400, 240));
        if (GUILayoutUtility.GetRect(10, 50, 400, 240).Contains(Event.current.mousePosition))
        {
            onWindow = true;
        }
        else
        {
            onWindow = false;
        }
        GUILayout.EndArea();

        GUILayout.BeginVertical();

        //Shared GUI
        GUILayout.Space(10f);
        GUILayout.BeginHorizontal();
        GUILayout.Label("Area:", new GUILayoutOption[] { GUILayout.Width(75f) });
        area = GUILayout.HorizontalSlider(area, 1f, 13f, new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(15f) });
        GUILayout.Label((Mathf.Round(area * 100f) / 100f).ToString(), new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(20f) });
        //Change brush texture size if area value was changed
        if (GUI.changed)
        {
            brushScaling();
        }
        GUILayout.EndHorizontal();

        GUILayout.Space(10f);
        GUILayout.BeginHorizontal();
        GUILayout.Label("Strength:", new GUILayoutOption[] { GUILayout.Width(75f) });
        strength = GUILayout.HorizontalSlider(strength, 1f, 13f, new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(15f) });
        GUILayout.Label((Mathf.Round(strength * 100f) / 100f).ToString(), new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(20f) });
        GUILayout.EndHorizontal();

        //Deform GUI
        GUILayout.Space(10);

        deformMode = (DeformMode)GUILayout.Toolbar((int)deformMode, deformModeNames, GUILayout.Height(25));

        GUILayout.Space(10);

        GUILayout.BeginHorizontal();
        if (GUILayout.Button("Reset Terrain Height", new GUILayoutOption[] { GUILayout.Height(30f) }))
        {
            tData.SetHeights(0, 0, saved);
        }
        GUILayout.EndHorizontal();

        GUILayout.EndVertical();
    }

    void OnApplicationQuit()
    {
        //Reset terrain height when exiting play mode
        tData.SetHeights(0, 0, saved);
    }
}

SC_EditorFlyCamera.cs

using UnityEngine;

public class SC_EditorFlyCamera : MonoBehaviour
{
    public float moveSpeed = 15;
    public float turnSpeed = 3;

    bool freeLook = false;
    bool moveFast = false;
    float rotationY;

    // Use this for initialization
    void Start()
    {
        rotationY = -transform.localEulerAngles.x;
    }

    // Update is called once per frame
    void Update()
    {
        Movement();
    }

    void Movement()
    {
        moveFast = Input.GetKey(KeyCode.LeftShift);

        float speed = moveSpeed * Time.deltaTime * (moveFast ? 3 : 1);

        if (Input.GetKey(KeyCode.W))
        {
            transform.root.Translate(transform.forward * speed, Space.World);
        }
        if (Input.GetKey(KeyCode.S))
        {
            transform.root.Translate(-transform.forward * speed, Space.World);
        }
        if (Input.GetKey(KeyCode.A))
        {
            transform.root.Translate(-transform.right * speed, Space.World);
        }
        if (!Input.GetKey(KeyCode.LeftControl) && Input.GetKey(KeyCode.D))
        {
            transform.root.Translate(transform.right * speed, Space.World);
        }
        if (Input.GetKey(KeyCode.Q))
        {
            transform.root.Translate(transform.up * speed, Space.World);
        }
        if (Input.GetKey(KeyCode.E))
        {
            transform.root.Translate(-transform.up * speed, Space.World);
        }

        if (Input.GetMouseButtonDown(1))
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
        freeLook = Input.GetMouseButton(1);
        if (Input.GetMouseButtonUp(1))
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }

        if (freeLook)
        {
            float rotationX = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * turnSpeed;

            rotationY += Input.GetAxis("Mouse Y") * turnSpeed;
            rotationY = Mathf.Clamp(rotationY, -90, 90);

            transform.localEulerAngles = new Vector3(-rotationY, rotationX, 0);
        }
    }
}

TextureScale.cs

// Only works on ARGB32, RGB24 and Alpha8 textures that are marked readable

using System.Threading;
using UnityEngine;

public class TextureScale
{
    public class ThreadData
    {
        public int start;
        public int end;
        public ThreadData(int s, int e)
        {
            start = s;
            end = e;
        }
    }

    private static Color[] texColors;
    private static Color[] newColors;
    private static int w;
    private static float ratioX;
    private static float ratioY;
    private static int w2;
    private static int finishCount;
    private static Mutex mutex;

    public static void Point(Texture2D tex, int newWidth, int newHeight)
    {
        ThreadedScale(tex, newWidth, newHeight, false);
    }

    public static void Bilinear(Texture2D tex, int newWidth, int newHeight)
    {
        ThreadedScale(tex, newWidth, newHeight, true);
    }

    private static void ThreadedScale(Texture2D tex, int newWidth, int newHeight, bool useBilinear)
    {
        texColors = tex.GetPixels();
        newColors = new Color[newWidth * newHeight];
        if (useBilinear)
        {
            ratioX = 1.0f / ((float)newWidth / (tex.width - 1));
            ratioY = 1.0f / ((float)newHeight / (tex.height - 1));
        }
        else
        {
            ratioX = ((float)tex.width) / newWidth;
            ratioY = ((float)tex.height) / newHeight;
        }
        w = tex.width;
        w2 = newWidth;
        var cores = Mathf.Min(SystemInfo.processorCount, newHeight);
        var slice = newHeight / cores;

        finishCount = 0;
        if (mutex == null)
        {
            mutex = new Mutex(false);
        }
        if (cores > 1)
        {
            int i = 0;
            ThreadData threadData;
            for (i = 0; i < cores - 1; i++)
            {
                threadData = new ThreadData(slice * i, slice * (i + 1));
                ParameterizedThreadStart ts = useBilinear ? new ParameterizedThreadStart(BilinearScale) : new ParameterizedThreadStart(PointScale);
                Thread thread = new Thread(ts);
                thread.Start(threadData);
            }
            threadData = new ThreadData(slice * i, newHeight);
            if (useBilinear)
            {
                BilinearScale(threadData);
            }
            else
            {
                PointScale(threadData);
            }
            while (finishCount < cores)
            {
                Thread.Sleep(1);
            }
        }
        else
        {
            ThreadData threadData = new ThreadData(0, newHeight);
            if (useBilinear)
            {
                BilinearScale(threadData);
            }
            else
            {
                PointScale(threadData);
            }
        }

        tex.Resize(newWidth, newHeight);
        tex.SetPixels(newColors);
        tex.Apply();

        texColors = null;
        newColors = null;
    }

    public static void BilinearScale(System.Object obj)
    {
        ThreadData threadData = (ThreadData)obj;
        for (var y = threadData.start; y < threadData.end; y++)
        {
            int yFloor = (int)Mathf.Floor(y * ratioY);
            var y1 = yFloor * w;
            var y2 = (yFloor + 1) * w;
            var yw = y * w2;

            for (var x = 0; x < w2; x++)
            {
                int xFloor = (int)Mathf.Floor(x * ratioX);
                var xLerp = x * ratioX - xFloor;
                newColors[yw + x] = ColorLerpUnclamped(ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp),
                                                       ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp),
                                                       y * ratioY - yFloor);
            }
        }

        mutex.WaitOne();
        finishCount++;
        mutex.ReleaseMutex();
    }

    public static void PointScale(System.Object obj)
    {
        ThreadData threadData = (ThreadData)obj;
        for (var y = threadData.start; y < threadData.end; y++)
        {
            var thisY = (int)(ratioY * y) * w;
            var yw = y * w2;
            for (var x = 0; x < w2; x++)
            {
                newColors[yw + x] = texColors[(int)(thisY + ratioX * x)];
            }
        }

        mutex.WaitOne();
        finishCount++;
        mutex.ReleaseMutex();
    }

    private static Color ColorLerpUnclamped(Color c1, Color c2, float value)
    {
        return new Color(c1.r + (c2.r - c1.r) * value,
                          c1.g + (c2.g - c1.g) * value,
                          c1.b + (c2.b - c1.b) * value,
                          c1.a + (c2.a - c1.a) * value);
    }
}

Stap 2

  • Maak een nieuwe scène
  • Maak een nieuw terrein door naar GameObject->3D Object->Terrein te gaan
  • Voeg SC_EditorFlyCamera- en SC_TerrainEditor-scripts toe aan de hoofdcamera
  • Wijs Terrain en DeformTexture variabelen toe in SC_TerrainEditor (Terrein zou degene moeten zijn die u in de scène wilt bewerken)

Voor DeformTexture kunt u de onderstaande afbeelding gebruiken of u kunt deze hoogwaardige Terrain Brushes gebruiken:

OPMERKING: Deform Texture moet een Alpha-bron hebben van Grayscale, Lezen/Schrijven ingeschakeld en het formaat moet ook zijn ingesteld op een van deze: ARGB32, RGB24 of Alpha8.

Nadat alles is toegewezen, is het tijd om het te testen in de afspeelmodus:

Selecteer uit drie opties: Verhogen en verlagen (klik met de linkermuisknop om omhoog te gaan, Shift + klik met de linkermuisknop om te verlagen), afvlakken, en Glad.

Gebruik de W-, A-, S- en D-toetsen om rond te vliegen en houd de rechtermuisknop ingedrukt om rond te kijken.

Sharp Coder Video speler

Alles werkt zoals verwacht!

Voorgestelde artikelen
FPC Swimmer - Een alomvattend eenheidsmiddel voor meeslepende wateromgevingen
Muislook-script voor Unity
Script voor het maken van een lichtschakelaar in eenheid
Raycast- en projectielgebaseerd schietscript voor Unity
Joystickcontroller instellen voor beweging in eenheid
2D Melee Attack-tutorial voor Unity
Countdown Timer-tutorial voor Unity