Unity용 게임 내 지형 하이트맵 편집기
Unity 지형 편집기가 내장되어 있지만 game에 지형 편집 기능을 추가하고 싶다면 어떻게 해야 할까요?
이 튜토리얼에서는 간단한 런타임 지형 편집기를 추가하는 방법을 보여 드리겠습니다. 또한 텍스처링이 포함되어 있지 않지만 여전히 기반으로 구축할 수 있는 훌륭한 예입니다.
Unity 이 튜토리얼에서 사용된 버전: Unity 2021.1.0f1(64비트)
1단계: 필요한 모든 스크립트 만들기
이 튜토리얼에는 3가지 스크립트가 포함되어 있습니다.
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);
}
}
2 단계
- 새 장면 만들기
- GameObject->3D Object->Terrain으로 이동하여 새로운 지형을 생성합니다.
- SC_EditorFlyCamera 및 SC_TerrainEditor 스크립트를 기본 카메라에 연결
- SC_TerrainEditor에서 Terrain 및 DeformTexture 변수를 할당합니다(지형은 장면에서 편집하려는 지형이어야 합니다).
DeformTexture의 경우 아래 이미지를 사용하거나 고품질 지형 브러시를 사용할 수 있습니다.
참고: 변형 텍스처에는 회색조의 알파 소스, 읽기/쓰기 활성화가 있어야 하며 형식도 ARGB32, RGB24 또는 Alpha8 중 하나로 설정되어야 합니다.
모든 것이 할당된 후에는 플레이 모드에서 테스트할 차례입니다.
낮게 올리기(올리려면 왼쪽 클릭, 낮추려면 Shift + 왼쪽 클릭), 평탄화, 및 부드럽게의 세 가지 옵션 중에서 선택하세요.
W, A, S, D 키를 사용하여 날아다니고, 마우스 오른쪽 버튼을 누르고 있으면 주변을 둘러볼 수 있습니다.
모든 것이 예상대로 작동합니다!