오늘은 3D 환경에서 Terrian 을 통해 랜덤으로 맵을 생성하는 방법에 대해 알아보겠습니다.

- TerrainGenerator
- 지형의 높이맵 생성, 습도 맵 생성, 바이옴 적용, 나무,바위,풀 등의 오브젝트를 배치하는 역할을 합니다.
- BiomeGenerator
- 높이와 습도 값을 기반으로 각 지형 구간의 바이옴 유형을 결정하는 역할을 합니다.
- Biome
- 각 바이옴의 속성 (높이 범위,습도 범위, 지형 텍스쳐, 해당 지형 프리팹)을 정의하는 역할을 합니다.
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[RequireComponent(typeof(Terrain))]
public class TerrainGenerator : MonoBehaviour
{
[Header("Terrain Settings")]
public int width = 512; // 지형의 너비
public int depth = 512; // 지형의 깊이
public int height = 100; // 지형의 최대 높이
public float scale = 20f; // 노이즈 스케일
[Header("Noise Settings")]
public int octaves = 4; // 노이즈 층 수
[Range(0, 1)]
public float persistence = 0.5f; // 진폭 감소율
public float lacunarity = 2f; // 주파수 증가율
[Header("Moisture Settings")]
public float moistureScale = 30f; // 습도 맵 스케일
public Vector2 moistureOffset; // 습도 맵 오프셋
[Header("Biome Settings")]
public BiomeGenerator biomeGenerator;
[Header("Object Settings")]
public GameObject[] treePrefabs;
public int treeCount = 1000;
public GameObject[] rockPrefabs;
public int rockCount = 500;
public float rockHeightOffset = 0.1f;
public GameObject[] grassPrefabs;
public int grassCount = 1000;
public float grassHeightOffset = 0.05f;
[Header("Cave Settings ------- 사용 안해용")]
public int caveWidth = 512;
public int caveHeight = 512;
public int initialFillProbability = 45;
public int smoothingIterations = 5;
[Header("River Settings ------- 사용 안해용")]
public int riverCount = 3;
public int riverLength = 300;
public float riverDepth = 0.02f;
[Header("Lake Settings ------- 사용 안해용")]
public int lakeCount = 5;
public int lakeRadius = 50;
public float lakeDepth = 0.05f;
[Header("Water Settings ------- 사용 안해용")]
public GameObject waterPrefab; // 인스펙터에서 할당할 물 프리팹
private Vector2 offset;
private Terrain terrain;
private TerrainData terrainData;
private float[,] heights;
private float[,] moistureMap;
void Start()
{
terrain = GetComponent<Terrain>();
offset = new Vector2(Random.Range(0f, 9999f), Random.Range(0f, 9999f));
moistureOffset = new Vector2(Random.Range(0f, 9999f), Random.Range(0f, 9999f));
GenerateTerrain();
GenerateMoistureMap();
ApplyBiomes();
//GenerateCaves(); // 오류가 있음..
//GenerateRivers(); // 자연스럽지가 않음..
//GenerateLakes(); // 자연스럽지가 않음..
PlaceObjects();
}
public void GenerateTerrain()
{
terrainData = terrain.terrainData;
terrainData.heightmapResolution = Mathf.Max(width, depth) + 1; // 높이맵 해상도는 가장 큰 축 기준
terrainData.size = new Vector3(width, height, depth);
heights = GenerateHeights();
terrainData.SetHeights(0, 0, heights);
}
float[,] GenerateHeights()
{
float[,] heights = new float[depth, width]; // [z, x] 인덱싱
for (int z = 0; z < depth; z++)
{
for (int x = 0; x < width; x++)
{
float amplitude = 1;
float frequency = 1;
float noiseHeight = 0;
for (int i = 0; i < octaves; i++)
{
float sampleX = (x + offset.x) / scale * frequency;
float sampleZ = (z + offset.y) / scale * frequency;
float perlinValue = Mathf.PerlinNoise(sampleX, sampleZ) * 2 - 1;
noiseHeight += perlinValue * amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
heights[z, x] = Mathf.Clamp01(noiseHeight * 0.5f + 0.5f);
}
}
return heights;
}
float[,] GenerateMoistureMap()
{
moistureMap = new float[depth, width];
for (int z = 0; z < depth; z++)
{
for (int x = 0; x < width; x++)
{
float sampleX = (x + moistureOffset.x) / moistureScale;
float sampleZ = (z + moistureOffset.y) / moistureScale;
moistureMap[z, x] = Mathf.PerlinNoise(sampleX, sampleZ);
}
}
return moistureMap;
}
void ApplyBiomes()
{
if (biomeGenerator == null || biomeGenerator.biomes.Length == 0)
{
return;
}
// 바이옴에 따른 텍스처 적용
ApplyBiomeTextures();
}
void ApplyBiomeTextures()
{
Terrain terrain = GetComponent<Terrain>();
TerrainData terrainData = terrain.terrainData;
// 각 바이옴의 텍스처 레이어 추가
foreach (Biome biome in biomeGenerator.biomes)
{
if (biome.terrainLayer != null && !terrainData.terrainLayers.Contains(biome.terrainLayer))
{
terrainData.terrainLayers = terrainData.terrainLayers.Concat(new TerrainLayer[] { biome.terrainLayer }).ToArray();
}
}
// 텍스처 매핑
float[,,] alphaMaps = new float[terrainData.alphamapWidth, terrainData.alphamapHeight, terrainData.terrainLayers.Length];
for (int z = 0; z < terrainData.alphamapHeight; z++)
{
for (int x = 0; x < terrainData.alphamapWidth; x++)
{
// 높이맵과 모이스처 맵의 좌표 변환
int heightmapX = Mathf.RoundToInt((float)x / terrainData.alphamapWidth * width);
int heightmapZ = Mathf.RoundToInt((float)z / terrainData.alphamapHeight * depth);
heightmapX = Mathf.Clamp(heightmapX, 0, width - 1);
heightmapZ = Mathf.Clamp(heightmapZ, 0, depth - 1);
float heightNormalized = heights[heightmapZ, heightmapX];
float moisture = moistureMap[heightmapZ, heightmapX];
BiomeType biomeType = biomeGenerator.GetBiome(heightNormalized, moisture);
Biome currentBiome = GetBiomeByType(biomeType);
for (int i = 0; i < terrainData.terrainLayers.Length; i++)
{
if (terrainData.terrainLayers[i] == currentBiome.terrainLayer)
{
alphaMaps[z, x, i] = 1f;
}
else
{
alphaMaps[z, x, i] = 0f;
}
}
}
}
terrainData.SetAlphamaps(0, 0, alphaMaps);
}
void GenerateCaves()
{
CaveGenerator caveGenerator = gameObject.AddComponent<CaveGenerator>();
caveGenerator.caveWidth = caveWidth;
caveGenerator.caveHeight = caveHeight;
caveGenerator.initialFillProbability = initialFillProbability;
caveGenerator.smoothingIterations = smoothingIterations;
caveGenerator.GenerateCave();
caveGenerator.ApplyCaveToTerrain();
}
void GenerateRivers()
{
RiversGenerator riversGenerator = gameObject.AddComponent<RiversGenerator>();
riversGenerator.riverCount = riverCount;
riversGenerator.riverLength = riverLength;
riversGenerator.riverDepth = riverDepth;
riversGenerator.waterPrefab = waterPrefab;
riversGenerator.GenerateRivers(terrainData);
}
void GenerateLakes()
{
LakesGenerator lakesGenerator = gameObject.AddComponent<LakesGenerator>();
lakesGenerator.lakeCount = lakeCount;
lakesGenerator.lakeRadius = lakeRadius;
lakesGenerator.lakeDepth = lakeDepth;
lakesGenerator.waterPrefab = waterPrefab;
lakesGenerator.GenerateLakes();
}
void PlaceObjects()
{
PlaceTrees();
PlaceRocks();
PlaceGrass();
}
void PlaceTrees()
{
if (biomeGenerator == null || biomeGenerator.biomes.Length == 0)
{
return;
}
Vector3 terrainPos = terrain.transform.position;
for (int i = 0; i < treeCount; i++)
{
float x = Random.Range(0f, terrainData.size.x);
float z = Random.Range(0f, terrainData.size.z);
// 월드 좌표를 높이맵 좌표로 변환
int heightmapX = Mathf.RoundToInt((x / terrainData.size.x) * width);
int heightmapZ = Mathf.RoundToInt((z / terrainData.size.z) * depth);
heightmapX = Mathf.Clamp(heightmapX, 0, width - 1);
heightmapZ = Mathf.Clamp(heightmapZ, 0, depth - 1);
float y = terrain.SampleHeight(new Vector3(x, 0, z)) + terrainPos.y;
float heightNormalized = heights[heightmapZ, heightmapX];
float moisture = moistureMap[heightmapZ, heightmapX];
BiomeType biomeType = biomeGenerator.GetBiome(heightNormalized, moisture);
Biome currentBiome = GetBiomeByType(biomeType);
Vector3 position = new Vector3(x, y, z) + terrainPos;
if (currentBiome.vegetationPrefabs.Length > 0)
{
GameObject vegetation = Instantiate(
currentBiome.vegetationPrefabs[Random.Range(0, currentBiome.vegetationPrefabs.Length)],
position,
Quaternion.identity
);
vegetation.transform.parent = this.transform;
}
}
}
void PlaceRocks()
{
if (rockPrefabs.Length == 0)
{
return;
}
Vector3 terrainPos = terrain.transform.position;
for (int i = 0; i < rockCount; i++)
{
float x = Random.Range(0f, terrainData.size.x);
float z = Random.Range(0f, terrainData.size.z);
// 월드 좌표를 높이맵 좌표로 변환
int heightmapX = Mathf.RoundToInt((x / terrainData.size.x) * width);
int heightmapZ = Mathf.RoundToInt((z / terrainData.size.z) * depth);
heightmapX = Mathf.Clamp(heightmapX, 0, width - 1);
heightmapZ = Mathf.Clamp(heightmapZ, 0, depth - 1);
float y = terrain.SampleHeight(new Vector3(x, 0, z)) + terrainPos.y;
Vector3 position = new Vector3(x, y + rockHeightOffset, z) + terrainPos;
GameObject rock = Instantiate(rockPrefabs[Random.Range(0, rockPrefabs.Length)], position, Quaternion.identity);
rock.transform.parent = this.transform;
}
}
void PlaceGrass()
{
if (grassPrefabs.Length == 0)
{
return;
}
Vector3 terrainPos = terrain.transform.position;
for (int i = 0; i < grassCount; i++)
{
float x = Random.Range(0f, terrainData.size.x);
float z = Random.Range(0f, terrainData.size.z);
// 월드 좌표를 높이맵 좌표로 변환
int heightmapX = Mathf.RoundToInt((x / terrainData.size.x) * width);
int heightmapZ = Mathf.RoundToInt((z / terrainData.size.z) * depth);
heightmapX = Mathf.Clamp(heightmapX, 0, width - 1);
heightmapZ = Mathf.Clamp(heightmapZ, 0, depth - 1);
float y = terrain.SampleHeight(new Vector3(x, 0, z)) + terrainPos.y;
Vector3 position = new Vector3(x, y + grassHeightOffset, z) + terrainPos;
GameObject grass = Instantiate(grassPrefabs[Random.Range(0, grassPrefabs.Length)], position, Quaternion.identity);
grass.transform.parent = this.transform;
}
}
Biome GetBiomeByType(BiomeType type)
{
foreach (Biome biome in biomeGenerator.biomes)
{
if (biome.type == type)
return biome;
}
return biomeGenerator.biomes[0]; // 기본값
}
#if UNITY_EDITOR
//public void OnValidate()
//{
// if (terrain == null)
// {
// terrain = GetComponent<Terrain>();
// }
// GenerateTerrain();
// GenerateMoistureMap();
// ApplyBiomes();
//
//}
#endif
}
- 랜덤 맵 생성기 기능의 핵심이 되는 클래스입니다.
- 높이 맵 생성 : Perlin Noise를 이용하여 지형의 높이 데이터를 생성해줍니다.
- 습도 맵 생성 : 지형의 습도를 나타내는 맵을 생성하여 바이옴을 구분합니다.
- 바이옴 적용 : 높이와 습도의 데이터를 통해 각 지형 구간에 맞는 바이옴 텍스쳐를 적용합니다.
- 객체 배치 : Biome에서 정의한 프리팹을 배치합니다.
- 동굴 및 강,호수의 경우에는 오류가 있거나 자연스럽게 설치가 되지않아 주석처리 하였습니다..
using UnityEngine;
public class BiomeGenerator : MonoBehaviour
{
public Biome[] biomes;
public float[,] moistureMap;
public BiomeType GetBiome(float height, float moisture)
{
foreach (Biome biome in biomes)
{
Debug.Log($"height : {height} || moisture : {moisture}");
if (height >= biome.minHeight && height <= biome.maxHeight &&
moisture >= biome.minMoisture && moisture <= biome.maxMoisture)
{
return biome.type;
}
}
return BiomeType.Forest; // 기본값
}
}
- 지형의 각 지점에서 높이와 습도 데이터를 판단하여 바이옴 유형을 결정하는 클래스 입니다.
- 모든 바이옴을 돌면서, 입력받은 높이와 습도가 바이옴의 범위에 속하는지 확인합니다.
using UnityEngine;
public enum BiomeType { Forest, Desert, Mountain, Snow }
[System.Serializable]
public class Biome
{
public BiomeType type;
public float minHeight;
public float maxHeight;
public float minMoisture;
public float maxMoisture;
public TerrainLayer terrainLayer;
public GameObject[] vegetationPrefabs;
}
- 바이옴의 속성을 정의하는 클래스입니다.
- 켄 퍼린이 개발한 그레이디언트 노이즈 알고리즘 입니다.
- 연속성
- 인접한 지점 간에 부드럽게 변화하는 값을 생성합니다.
- 자연스러움
- 자연에서 관찰되는 다양한 패턴(예: 구름, 산맥, 바다)을 모사할 수 있습니다.
- 반복성
- 주기적인 패턴을 생성할 수 있어 무한히 반복되는 텍스처를 만들 수 있습니다.
- 그리드 생성 : 격자를 생성합니다. 각 격자 점은 고유한 방향 벡터를 가집니다.
- 벡터 계산 : 특정 지점에서 가장 가까운 격자 점들을 찾고
각 격자 점과 지점 사이의 벡터를 계산합니다.- 내전 계산 : 각 격자 점의 방향 벡터와 지점과의 벡터 간의 내적을 계산합니다.
- 보간 : 계산된 내적 값을 부드러운 보간 함수를 통해 결합하여 최종 노이즈 값을 생성합니다.
- 그리드 벡터 설정 -> 지점과 격자 점 벡터 계산 -> 내적 계산 -> 보간 -> 최종 노이즈 값
- 특징
- 부드러운 변화
- 다양한 스케일 지원
- 다차원 지원
- 장점
- 자연스러운 퍁펀
- 효율성
- 확장성



- 테스트 영상을 올릴 예정이였는데 값을 바꾸면 몇 분간 세팅하다보니 이미지로 대체하였습니다.