좋은 임의 숫자 생성기는 각각의 숫자끼리 관계성과 규칙성 없이 숫자를 생성해야한다. 켄 펄린(Ken Perlin)이 고안한 '펄린 노이즈'라는 알고리즘은 이러한 개념을 적용한 알고리즘이다. 펄린 노이즈를 사용하면 구름, 풍경, 대리석의 패턴과 같은 자연의 성질을 가진 다양한 효과를 연출할 수 있다. 펄린 노이즈는 자연적인 질서를 가진(부드러운) 일련의 유사 임의 값을 생성하는 알고리즘으로 굉장히 유기적으로 보인다. 아래의 왼쪽 그래프는 시간의 경과(x축)를 매개변수로 사용해 펄린 노이즈를 표시한 것으로, 굉장히 매끄러운 형태의 곡선이 나온다. 오른쪽 그래프는 시간의 경과에 따라 순수한 임의 값을 추출해 만든 진동이다.
노이즈 함수로 출력되는 범위는 0과 1사이로 정해져 있다. 1차원 펄린 노이즈는 시간에 따른 1개 값의 연속이라고 정의할 수 있다.
특정한 노이즈 값에 접근하고 싶을 때는 특정한 '시간'을 노이즈 함수의 매개변수로 전달해줘야 한다.
float t = 0.0f;
void draw()
{
float n = noise(t);
println(n);
t += 0.01f;
}
매개변수값을 계속 변경시켜줌으로써 계속해서 다른 값을 얻을 수 있다. t를 얼마나 빠르게 변화시키는지도 노이즈의 부드러운 형태에 영향을 준다. t를 빠르게 변화시킨다면 아래의 그림처럼 중간 값들을 뛰어넘으므로 부드럽지 않게 움직일 것이다.
사실 여기에서 실제 시간의 개념은 작용하지 않는다. 노이즈 함수의 구조를 이해하기 위해 시간이라는 개념으로 비유한 것이다. 실제로 관련되어 있는 것은 시간이 아니라 공간이다.
2차원 노이즈
2차원 노이즈도 기본적인 개념은 같다. 다른 점이라면 선 위에 있는 값이 아니라 격자 위에 있는 값을 사용한다는 것이다. 격자 내부에 있는 각각의 사각형에 숫자가 적혀있다고 생각해보자. 모든 값은 이웃한 값(위, 아래, 왼쪽, 오른쪽, 대각선의 8방향)과 비슷한 값이 적혀 있다. 만약 이러한 값이 적혀 있는 격자를 적혀 있는 숫자로 밝기를 나타내서 시각화 하면 구름과 같은 모양이 나온다. 흰색 옆에서는 회색, 회색 옆에는 밝은 회색, 밝은 회색 옆에는 어두운 회색, 어두운 회색 옆에는 검은색 ... 과 같은 식이 된다.
활용
노이즈 함수는 여러분야에서 응용되는데 마인크래프트, 테라리라와 같은 정해진 규칙에 의해 게임 내의 컨텐츠를 자동으로 생성하는 '절차적 컨텐츠 생성(Procedural Content Generation)' 을 사용하는게임에 적용될수있다. 이러한 노이즈 함수는 유니티에서 Mathf.PerlinNoise() 메소드로 제공된다. 그럼 Texture에 노이즈를 그려서 노이즈를 실제 눈으로 확인해보자.
Noise Texture
using UnityEngine;
public class PerlinNoise : MonoBehaviour
{
public int width = 256;
public int height = 256;
public float scale = 20f;
public float offsetX;
public float offsetY;
private void Start()
{
offsetX = Random.Range(0f, 99999f);
offsetY = Random.Range(0f, 99999f);
}
private void Update()
{
Renderer renderer = GetComponent<Renderer>();
renderer.material.mainTexture = GenerateTexture();
}
Texture2D GenerateTexture()
{
Texture2D texture = new Texture2D(width, height);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
Color color = CalculateColor(x, y);
texture.SetPixel(x, y, color);
}
}
texture.Apply();
return texture;
}
Color CalculateColor(int x, int y)
{
float xCoord = (float)x / width * scale + offsetX;
float yCoord = (float)y / height * scale + offsetY;
float sample = Mathf.PerlinNoise(xCoord, yCoord);
return new Color(sample, sample, sample);
}
}
오브젝트를 만들어 텍스쳐를 적용시켜보면 위와 같이 노이즈를 확인할수있습니다. 이러한 노이즈함수는 게임에 다양한 부분에 적용시킬수 있다.
Wave
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Wave : MonoBehaviour
{
float scale;
float heightScale;
int planeSize;
public GameObject cube;
void Start()
{
scale = 0.2f;
heightScale = 2f;
planeSize = 25;
for (int x = 0; x < planeSize; x++)
{
for (int z = 0; z < planeSize; z++)
{
var c = Instantiate(cube, new Vector3(x, 0, z), Quaternion.identity);
c.transform.parent = transform;
}
}
}
void Update()
{
foreach (Transform child in transform)
{
child.transform.position = new Vector3(child.transform.position.x,
heightScale * Mathf.PerlinNoise(Time.time + (child.transform.position.x * scale),
Time.time + (child.transform.position.z * scale)),
child.transform.position.z);
}
}
}
Height Map
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PerlinColor : MonoBehaviour
{
public int size;
public GameObject cube;
public float scale;
public float m;
bool move;
float height;
void Start()
{
move = true;
for (int x = 0; x < size; x++)
{
for (int z = 0; z < size; z++)
{
var c = Instantiate(cube, new Vector3(x, 0, z), Quaternion.identity);
c.transform.parent = transform;
}
}
}
void Update()
{
foreach (Transform child in transform)
{
height = Mathf.PerlinNoise(child.transform.position.x / scale, child.transform.position.z / scale);
child.GetComponent<MeshRenderer>().material.color = new Color(height, height, height, height);
}
if (move)
{
foreach (Transform child in transform)
{
height = Mathf.PerlinNoise(child.transform.position.x / scale, child.transform.position.z / scale);
child.transform.position = new Vector3(child.transform.position.x, Mathf.RoundToInt(height * m), child.transform.position.z);
}
}
}
}
Terrain
using UnityEngine;
public class TerrainGenerator : MonoBehaviour
{
public int depth = 20;
public int width = 256;
public int height = 256;
public float scale = 20f;
public float offsetX = 100f;
public float offsetY = 100f;
private void Start()
{
offsetX = Random.Range(0f, 99999f);
offsetY = Random.Range(0f, 99999f);
}
private void Update()
{
Terrain terrain = GetComponent<Terrain>();
terrain.terrainData = GenerateTerrain(terrain.terrainData);
//offsetX += Time.deltaTime * 5f;
}
TerrainData GenerateTerrain(TerrainData terrainData)
{
terrainData.heightmapResolution = width + 1;
terrainData.size = new Vector3(width, depth, height);
terrainData.SetHeights(0, 0, GenerateHeights());
return terrainData;
}
float[,] GenerateHeights()
{
float[,] heights = new float[width, height];
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
heights[x, y] = CalculateHeight(x, y);
}
}
return heights;
}
float CalculateHeight(int x, int y)
{
float xCoord = (float)x / width * scale + offsetX;
float yCoord = (float)y / height * scale + offsetY;
return Mathf.PerlinNoise(xCoord, yCoord);
}
}
'Programming > Algorithm' 카테고리의 다른 글
아이작에서의 던전 생성 알고리즘 (0) | 2023.03.17 |
---|