Perlin Noise 펄린 노이즈 생성 과정 및 알고리즘 - Whitmem
Perlin Noise 펄린 노이즈 생성 과정 및 알고리즘
Graphic Development
2025-01-04 00:59 게시 5cff2b414e3158db4032

0
0
151
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
펄린 노이즈 생성
펄린 노이즈를 생성하기 위해서 일정한 크기 단위인 격자로 나누고, 각 격자 점은 하나의 랜덤 방향 벡터를 가진다. 그리고 퍼를린 노이즈 값을 구하려는 특정 점에 대해 인접하는 그리드의 4개의 꼭짓점과의 방향 벡터를 구하고, 각 특정 점 방향 벡터 4개와 각 격자 점의 랜덤 방향 벡터를 내적하여 각 노이즈의 값을 계산할 수 있다.
구역 별로 이미지 색상을 나누어 출력한 모습
기본적으로 펄린 노이즈를 활용해서 다양한 그래픽 처리에 사용할 수 있다고 한다. 그러기 위해서 펄린 노이즈를 생성하는 방법에 대해서 조금 더 상세히 알아보기로 하였다. 펄린 노이즈는 확실히 노이즈지만, 규칙적인 모습을 띄고 점층적인 노이즈 형태롤 가지고 있기 때문에 어떤 특정한 형태는 유지하되 난수의 처리가 필요할 때 유용하다. 맵을 생성할 때도 펄린 노이즈를 사용하여 밝은 지점을 고지대로, 어두운 지역을 상대적으로 육지로 생성할 수 있고, 특히 점층적인 노이즈를 띄기 대문에 육지와 고지대간의 부드러운 경사를 할당하여 맵을 생성할 수도 있다. 이 과정에서 특정 범위는 강, 바다 등으로 분류하여 생성할 수도 있다.
이런 형태의 노이즈는 특히 쉐이더에서 많이 사용되기도 한다. 화염, 물 등의 노말 맵을 랜덤화 하거나, 무한한 데이터가 필요한 시각적인 정보에 활용할 수도 있다.
계산 자체는 일반적인 수학 계산을 할 줄 안다면 어렵지는 않다. 선형 보간과 벡터의 내적 정도만 숙지하고 있으면 된다.
200*200 펄린 노이즈를 생성하기 위해서 200*200 이미지 내에 격자를 어떻게 나눌 것인가에 대해서 먼저 계산해야 한다. 격자의 밀집도에 따라서 펄린 노이즈의 밀집도는 커질 것이다. 200*200 안에 격자 개당 너비 높이가 10*10일 때, 격자는 총 20*20 개 존재하게 되는 것이다. 그러면 펄린 노이즈를 생성할 때 최대 100개의 픽셀이 하나의 그리드 안에 존재하는 것이며, 그 그리드는 그리드를 구성하는 각 꼭짓점 4개에만 의존하여 노이즈가 생성된다.
기본적으로 그리드를 생성하면서 각 그리드는 고유한 난수 벡터를 가진다. 이 때 이 벡터는 방향 벡터이기 때문에 크기를 1로 고정한다. 즉 X, Y 를 랜덤 -1 ~ 1 범위로 생성하되, 생성후 노말라이즈 작업을 거쳐야 한다. 노말라이즈 작업을 수행하기 위해서 각 벡터의 x, y 를 length 로 나누면 된다. 사실상 이 작업만 완료하면 기본적인 데이터 준비는 완료된다. 노이즈를 구하고자 하는 특정 좌표를 기준으로 벡터를 연산하고, 내적하고, 보간하면 되는 것이다. 즉 200*200 의 노이즈 이미지를 구하려고 할 때, 200*200 픽셀 각각 인접하는 격자의 테두리 4개의 꼭짓점과 서로 연산하면 되는 것이다. 만약 50, 50 좌표의 노이즈 값을 구하기 위해, 먼저 50, 50 좌표에 인접하는 그리드 꼭짓점인 (0,0) (1,0) (0,1) (1,1)을 구하고, 각 그리드 꼭짓점으로부터 50,50 좌표까지의 방향 벡터를 구한다. 그러면 각 꼭짓점마다 50,50 까지의 방향 벡터가 나오는데, 각각의 점까지의 방향 벡터와 꼭짓점의 랜덤 방향 벡터를 내적하여 각각의 내적 크기를 구할 수 있다. 이렇게 구해진 4개의 내적 크기를 보간하면 되는 것이다.
예를 들어 위 이미지의 너비가 200, 높이가 200이라고 할 때, 격자는 약 40*40 단위로 총 존재한다. 각 그리드는 좌상, 우상, 좌하, 우하 꼭짓점이 존재하며, 하나의 꼭짓점은 최대 4개의 그리드 면을 공유한다. 각 그리드 좌표는 난수의 노말라이즈된 방향 벡터를 가진다.
class Vector{ private double x; private double y; public Vector(Point point) { this.x = point.getDoubleX(); this.y = point.getDoubleY(); } public Vector(double x, double y) { this.x = x; this.y = y; } public void setVectorX(double x) { this.x = x; } public void setVectorY(double y) { this.y = y; } public double getVectorX() { return x; } public double getVectorY() { return y; } public double length() { return Math.sqrt(x*x + y*y); } public Vector normalize() { double length = this.length(); Vector nV =new Vector((double)x/length, (double)y/length); return nV; } public Vector directionVectorFromStartToEnd(double startX, double startY, double endX, double endY) { Vector nV =new Vector(endX- startX, endY - startY); return nV; } public Vector subtract(Vector vector) { Vector vec = new Vector(this.x - vector.getVectorX(), this.y - vector.getVectorY()); return vec; } public Vector add(Vector vector) { Vector vec = new Vector(this.x + vector.getVectorX(), this.y + vector.getVectorY()); return vec; } /* * 벡터간 내적을 수행한다. */ public double dot(Vector vector) { double value = vector.getVectorX() * x + vector.getVectorY() * y; return value; } }
double lerp(double startLoc, double startValue, double endLoc, double endValue, double loc) { double t = (double)(loc - startLoc)/(double)(endLoc - startLoc); double value = (endValue - startValue) * t + startValue; return value; }
public double getValue(int x, int y) { double realX = (double)x/(double)pixelPerGrid; double realY = (double)y/(double)pixelPerGrid; int gridX = (int)realX; int gridY = (int)realY; Vector calcVector = new Vector(realX,realY); /* * 그리드 좌표는 왼쪽 위 좌표로 구해지며, 어떤 공간의 그리드 좌표를 구했으면 * 각 x, y + 1 최대로 하여금 격자로 계산을 수행해야 함. */ Vector leftTopRandomValue = randomVectors[gridY][gridX]; Vector rightTopRandomValue = randomVectors[gridY][gridX+1]; Vector leftBottomRandomValue = randomVectors[gridY+1][gridX]; Vector rightBottomRandomValue = randomVectors[gridY+1][gridX+1]; Vector leftTop = new Vector(gridX, gridY); Vector leftTopDirection = calcVector.subtract(leftTop); Vector rightTop = new Vector(gridX+1, gridY); Vector rightTopDirection = calcVector.subtract(rightTop); Vector leftBottom = new Vector(gridX, gridY+1); Vector leftBottomDirection = calcVector.subtract(leftBottom); Vector rightBottom = new Vector(gridX+1, gridY+1); Vector rightBottomDirection = calcVector.subtract(rightBottom); double leftTopValue = leftTopDirection.dot(leftTopRandomValue); double rightTopValue = rightTopDirection.dot(rightTopRandomValue); double leftBottomValue = leftBottomDirection.dot(leftBottomRandomValue); double rightBottomValue = rightBottomDirection.dot(rightBottomRandomValue); double lerpedTop = lerp(gridX,leftTopValue, gridX+1,rightTopValue, realX); double lerpedBottom = lerp(gridX,leftBottomValue, gridX+1,rightBottomValue, realX); double learpedCenter = lerp(gridY, lerpedTop,gridY+1, lerpedBottom, realY); return learpedCenter; }
1. 초기화
그리드 격자에 고유한 랜덤 방향 벡터를 할당한다.
이 때 크기는 1인 방향 벡터를 생성해야 한다.
public void init(int cols, int rows, int pixelPerGrid) { randomVectors = new Vector[rows][cols]; /* * 벡터 난수 할당 */ for(int y=0;y<rows;y++) { for(int x=0;x<cols;x++) { Vector vc = new Vector( Math.random()*2-1, Math.random()*2-1 ); vc = vc.normalize(); randomVectors[y][x] = vc; } } this.pixelPerGrid = pixelPerGrid; }
각 그리드에 대해서 임의의 방향 벡터를 생성한다. 이 때 노말라이즈를 다시 하는 이유는, x가 -1~1 범위, y가 -1 ~ 1 범위로 생성되더라도 둘 다 1 1 인 경우, 크기가 sqrt(2) 가 되기 때문에, 1이 아니게 된다. 따라서 sqrt(2)로 다시 나눠주기 위해 노말라이즈를 한다.
2. 점까지의 방향 벡터 구하기
인접하는 그리드 꼭짓점 4개와 구하고자 하는 좌표의 정점까지의 방향 벡터를 구한다.
만약 빨간 점 (20,20)을 구하고자 하는 경우 20,20이 존재하는 그리드의 꼭짓점으로부터의 방향 벡터를 구한다.
유의할 점은, 그리드 좌표와 실제 점 좌표는 다른 차원이기 때문에, 그리드 좌표를 점 좌표계로 옮기거나, 점 좌표계를 그리드 좌표계로 옮겨야 한다. 예를 들어 그리드 좌표계 기준 1,0 꼭짓점은 점 좌표계 기준으로 40,0이다. 이는 한 그리드당 차지하는 실제 픽셀 개수에 따라 달라진다. 여기서는 각 그리드의 픽셀이 40*40 이다. 그리드 꼭짓점 (0,0) (1,0) (0,1) (1,1) 는 (0,0) (40,0) (0,40) (40,40) 으로 옮길 수 있다. 그리고 옮겨진 각 꼭짓점 좌표는 계산하려는 점까지의 방향 벡터를 구한다.
3. 내적 구하기
각 꼭짓점의 랜덤 방향 벡터와 점까지의 방향 벡터의 내적을 구하기
이제 각 그리드 꼭짓점이 가지고 있는 랜덤 정규화된 방향 벡터와 방금 계산으로 구한 점까지의 방향벡터를 서로 내적을 해줘야 한다. 각 꼭짓점이 가지는 랜덤 벡터와 점까지의 방향벡터를 서로 내적하여 총 4개의 내적 값을 구할 수 있다.
1) 좌측 위 벡터와 점까지의 벡터간 내적,
2) 우측위 벡터와 점까지의 벡터간 내적,
3) 좌측밑 벡터와 점까지의 벡터간 내적,
4) 우측밑 벡터와 점까지의 벡터간 내적, 이렇게 총 4개의 내적을 구한다.
4. 내적을 보간하기
x축끼리 먼저 보간하고, 보간된 나머지 두 내적을 y축끼리 보간하기
이렇게 구해진 내적 값은 아까 그 점까지 위치로 보간을 하여 노이즈 값을 구할 수 있다. 먼저 위 내적 값인 -0.4, 0.5의 x 보간, 0.3, -0.2의 x 보간후, 두 값을 y로 보간하여 해당 이미지 텍스처 픽셀의 세기로 사용하면 된다.
gridX, gridX+1 가 실제 월드 좌표라고 가정했을 때 (여기서는 (20,20)인 빨간 점을 (0.5,0.5)로 본다) 빨간 점이 존재하는 위치의 비율 t를 먼저 구하고, 이 t에 대해서 좌측 위인 leftTop 내적 값과 우측 위인 rightTop 내적 값 사이 보간 값을 구한다. 다음 하단 좌측, 하단 우측 내적도 동일하게 처리하여 결과적으로 상단측 보간된 내적 크기와, 하단측 보간된 내적 크기를 구한다.
그리고 상기 보간1, 보간2 도 구하려고 하는 빨간 점의 y 위치로 하여금 보간한다. y 값의 t 값을 구하고, 보간1, 보간2 간의 보간 값을 구한다.
5. 색으로 칠하기
0,200 좌표까지 모두 좌표 출력하기
각 그리드당 포함되는 픽셀 개수가 적은 경우 위와 같은 결과가 나오고, 그리드 하나당 포함되는 픽셀 개수가 많은 경우 아래와 같은 결과가 나온다.
그리드당 픽셀 개수가 매우 적은 경우 아래와 같은 결과가 나온다.
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.