Perlin Noise: Part 2
Keywords : Perlin noise, gradient noise, permutation, hashing function, deriavtives, interpolant, height map, displacement.
Perlin Noise
1985년에, Ken Perlin은 "An Image Synthesizer"라는 Siggraph 논문을 썼다. 거기에서 그는 이전 강의에서 우리가 공부한 것과 유사한 noise function의 타입을 (Noise Part 1) 발표했지만 조금 더 좋았다. 이 noise function은 몇년간 Ken Perlin 그 자신 뿐만 아니라 다른 사람들에 의해서 개선되었고 확장되었다. 우리는 이 강의에서 이러한 개선물 몇몇을 연구할 것이다. 이 noise function은 이제 Perlin Noise의 이름 아래에서 알려져있다.
Perlin noise는 이전 강의에서 우리가 공부한 noise의 유형과 유사하다. 이전 강의에서 우리가 공부한 value noise와 유사하게, 그것은 lattice system에 의존한다. lattice cells의 corner에서, 우리는 어떤 random values를 정의하는데, 그 값들은 그러고나서 공간에서 한 점의 위치에 있는 noise value를 연산하기 위해 보간된다. 이전 강의에서, 우리는 1D and 2D value noise를 만드는 것을 공부했다. 이 강의에서, 우리는 Perlin noise function의 3D version을 구현할 것이다. 2D or 심지어 4D function을 구현하는 것은 사용자에게 남아있지만, 이전 강의의 코드를 재 사용하여, 이것은 간단한 예제가 될 것이다. 그러나 어쨋든, 요점은 value noise와 Perlin noise 둘 다 lattice 기반 noise functions이라는 것이다.
그래서 만약 그것들이 같은 원리로 작동한다면, 그 둘 사이의 차이점은 무엇인가? 음 그 차이점은 lattice의 corners에서 "random values"를 어떻게 연산하는가 이다. value noise의 경우에, 우리는 간단히 lattice cells의 corners에 있는 random numbers를 할당하고, 점이 falls into하는 cell 내에서 점의 위치를 사용하여 이러한 값을 보간한다. 이 프로세스는 이전 강의에서 바라건대 잘 설명 되었다. Perlin noise에서, Ken Perlin은 cell의 corners에 있는 random values들을 gradient로 바꿀 것을 제안한다. 그가 gradients라고 부르는 것은 random 3D normalized vectors 이다 (3D noise function의 경우에). 이것은 생성하기에 복잡하지 않다. 우리의 Noise function constructor class 내에서 random numbers를 생성하는 대신에, 우리는 random float generation을 random 3D vector의 생성으로 대체한다. 3D random vector를 만드는 것은 쉽다: 너는 [0:1] 범위에 있는 세 개의 random float를 생성하고, 이러한 random numbers를 [-1,1]로 remap하고, 그러고나서 최종 벡터 좌표들을 normalize한다.
이제 이 접근법에 작은 문제가 있다. 균일하게 분산된 random normalized directions을 생성하는 것은 uniform distribution을 가진 단위 sphere에서 random positions 생성하는 것과 같다. 위에서 설명된 접근법에서 문제가 그것이 정말로 random normalized directions를 하겠지만, 그것들은 구의 표면에 균일하게 분산되어있지 않다. 그리고 이것이 문제이다 (왜냐하면 그것은 다른 것 보다 어떤 방향을 좀 더 선호할 것이기 때문이고, 이것은 결국에 우리의 noise가 균일하지 않을 것이라는 것을 의미한다 - 그리고 우리는 그것을 원하지 않는다). 그러나 지금은 이 점을 무시하자. 우리는 이 문제를 나중에 고칠 것이다.
이제 그 문제는 cells의 cornser에서 interpolate할 random number를 갖기 보다는, 우리가 gradients/vectors를 가졌다는 것이다. 그리고 noise function은 real value를 반환하기 때문에, 우리는 어떻게 3D vector에서 real or float로 갈까? Ken Perlin은 위치에 있는 점 P에 대한 cell의 각 corner의 위치 사이의 방향을 연산하는 것을 제안했다. 거기에서, 우리는 noise function을 측정하기를 원한다. 이것은 우리에게 3D에서 8개의 벡터를 제공한다 (2D case에서는 4개). 그는 그러고나서 한 cell의 corner에서 gradient와 그 corner에서 P로가는 벡터 사이의 내적을 사용했다. 두 벡터의 내적은 실수를 주기 때문에, 우리는 간신히 우리의 gradients or directions을 어떤 random onumbers로 또 다시 바꿀 수 있다. 2D case의 경우에, 우리의 "가상의" 3D grid의 local coordinate에서 점 P의 좌표를 계산하기 위해서, 우리는 point 좌표를 float에서 integer values로 캐스팅할 것이고, 그러고나서 이러한 정수 좌표 modulo N을 할 것이다, N은 우리의 random directions 배열의 크기이다 (value noise lesson에서 우리는 N을 256라고 선택했다). 우리는 이전 lesson에서 모든 이러한 기법들을 설명했다 (어떻게 C++ operator modulo %가 이진 연사자 &로 바꾸는지를 포함해서, 만약 그 테이블의 크기가 2의 제곱꼴이라면). 우리가 이전 강의에서 설명했듯이, 우리는 256x256x256 방향의 lattice를 생성하고 싶지 않다. 그래서 우리는 256 random directions의 1D array를 사용하고, 그 point의 정수 좌표를 hash function이 있는 permutation table의 index로 바꿔서 이 테이블에 저장된 방향 중 하나를 "randomly pickup" 하는 permutation table 기법을 사용한다. 다시 한 번, 모든 이러한 기법들은 이전 강의에서 설명되었다.
{
그냥 잘 이해되도록 그림을 그리고 싶었다.
}
우리는 cell의 corner에 있는 8개의 방향들을 8개의 signed random values로 어떻게 바꾸는지 알기 때문에 (그것들은 random이다, 왜냐하면 cell의 corner의 방향은 randomly하게 선택되기 때문이다), 해야 할 것은 이러한 값들을 trilinear interpolation을 사용하여 보간하는 것이다. (3D case 경우에, 2D case 경우에 bilinear interpolation 한다). Trilinear and bilinear interpolation은 Interpolation 강의에서 설명되었다.
{
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | class PerlinNoise { static const unsigned tableSize = 256; static const unsigned tableSizeMask = tableSize - 1; glm::vec3 gradients[tableSize]; unsigned permutationTable[tableSize * 2]; int hash(const int&x, const int& y, const int& z) const { return permutationTable[permutationTable[permutationTable[x] + y] + z]; } public: PerlinNoise(unsigned seed = 2018) { std::mt19937 generator(seed); std::uniform_real_distribution<float> distribution; auto dice = std::bind(distribution, generator); float gradientLen2; for (unsigned i = 0; i < tableSize; ++i) { do { gradients[i] = glm::vec3(2 * dice() - 1); gradientLen2 = glm::dot(gradients[i], gradients[i]); } while (gradientLen2 > 1); gradients[i] /= sqrtf(gradientLen2); // normalize gradient permutationTable[i] = i; } std::uniform_int_distribution<int> distributionInt; auto diceInt = std::bind(distributionInt, generator); // create permutation table for (unsigned i = 0; i < tableSize; ++i) { std::swap(permutationTable[i], permutationTable[diceInt() & tableSizeMask]); permutationTable[i + tableSize] = permutationTable[i]; } } float eval(const glm::vec3& p) const { int xi0 = ((int)std::floor(p.x)) & tableSizeMask; int yi0 = ((int)std::floor(p.y)) & tableSizeMask; int zi0 = ((int)std::floor(p.z)) & tableSizeMask; int xi1 = (xi0 + 1) & tableSizeMask; int yi1 = (yi0 + 1) & tableSizeMask; int zi1 = (zi0 + 1) & tableSizeMask; // gradients at the corner of the cell const glm::vec3& c000 = gradients[hash(xi0, yi0, zi0)]; const glm::vec3& c100 = gradients[hash(xi1, yi0, zi0)]; const glm::vec3& c010 = gradients[hash(xi0, yi1, zi0)]; const glm::vec3& c110 = gradients[hash(xi1, yi1, zi0)]; const glm::vec3& c001 = gradients[hash(xi0, yi0, zi1)]; const glm::vec3& c101 = gradients[hash(xi1, yi0, zi1)]; const glm::vec3& c011 = gradients[hash(xi0, yi1, zi1)]; const glm::vec3& c111 = gradients[hash(xi1, yi1, zi1)]; float tx = p.x - ((int)std::floor(p.x)); float ty = p.y - ((int)std::floor(p.y)); float tz = p.z - ((int)std::floor(p.z)); // generate vectors going from the grid points to p /*** 18-12-28 Chanhaneg Lee To clear why this code is as it is, if you assume c000 is (0, 0, 0), then the vector from grid points to p is Vector = P - (Grid Point c000). And tx, ty tz is the thing, c000 because p.x - ((int)floor(p.x)) makes it. And the reason x1 is subtracted by 1, such as p100, the coordinate c100 is (1, 0, 0). The equation of calculating the vector P - (Grid Point c100). Therefore, tx should be added by -1 (c100 is added by 1, compared c000) ***/ float x0 = tx, x1 = tx - 1; float y0 = ty, y1 = ty - 1; float z0 = tz, z1 = tz - 1; glm::vec3 p000 = glm::vec3(x0, y0, z0); glm::vec3 p100 = glm::vec3(x1, y0, z0); glm::vec3 p010 = glm::vec3(x0, y1, z0); glm::vec3 p110 = glm::vec3(x1, y1, z0); glm::vec3 p001 = glm::vec3(x0, y0, z1); glm::vec3 p101 = glm::vec3(x1, y0, z1); glm::vec3 p011 = glm::vec3(x0, y1, z1); glm::vec3 p111 = glm::vec3(x1, y1, z1); float u = smoothStep(tx); float v = smoothStep(ty); float w = smoothStep(tz); // linear interpolation float a = lerp(glm::dot(c000, p000), glm::dot(c100, p100), u); float b = lerp(glm::dot(c010, p010), glm::dot(c110, p110), u); float c = lerp(glm::dot(c001, p001), glm::dot(c101, p101), u); float d = lerp(glm::dot(c011, p011), glm::dot(c111, p111), u); float e = lerp(a, b, v); float f = lerp(c, d, v); return lerp(e, f, w); } }; |
p000 ~ vector를 구하는게 이해 안되어서 손으로 해보고, 주석을 달아놓았다.
마지막의 trilinear interpolation은 링크 걸려져있는 trilinear interpolation 강좌를 읽어보면, 그리고 bilinear interpolation이 이미 이해가 되어있는 상태라면 쉽게 이해할 수 잇다.
lerp식을 나를 위해서 한 번 더 정리하자.
식은 a(1-t) + bt인데,
이 식을 원래 1D라고 생각하고, 직관적인 식으로 바꾸자면
a + (b - a)t (0<= t <= 1) 이다.
간단하게 생각해서 a와 b 어떤 값 사이에 어떤 지점 그러니까 a b사이의 전체 길이를 1로 scale down했을 때, a 에다가 그 길이의 비율 만큼 t를 곱해 값을 구하는 것이다.
직관성을 위해서 내 lerp 식을 그냥 저렇게 바꿔야 겠다.
}
이 구현에서, array gradients는 tableSize 크기를 갖는다는 것에 주목해라. 이것은 Perlin implementation에서와 같지 않다. 그것은 tableSize * 2의 크기를 가진다. 이것은 왜냐하면 그의 원래 구현에서 그가 사용한 hash function이
return permutationTable[permutationTable[x] + y] + z;
였기 때문이다.
그리고 이것은 0과 tableSize * 2의 사이의 값을 반환한다. permutation[]에 대한 어떤 lookup은 0과 tableSize의 범위의 값을 반환할 것이고, 우리는 0 ~ tableSize범위에 있는 그것에 z를 더하기 때문에, 최종 숫자는 0 ~ tableSize * 2의 범위에 있다. 이제 우리는 이 숫자를 gradients 배열에서 인덱스로서 이 숫자를 사용하기 때문에, gradients는 tableSize * 2의 크기를 가질 필요가 있다. 그러나 hash function을 위해 우리가 사용한 우리의 구현에서:
return permutationTable[permutationTable[permutationTable[x] + y] + z]
그리고 이것은 0 ~ tableSize * 2의 값을 반환하고 따라서 우리의 gradients 배열은 오직 tableSize 크기를 가질 것을 요구한다. 상세하지만, 주목할 가치가있다.
{
마지막에 0 ~ tableSize 크기라고 적어야 되지 않나 싶다.
}
보통, cells의 corners에 있는 값들을 부드럽게 보간하기 위해서, 우리는 interpolates를 remap할 smoothstep function을 사용한다 (우리는 tx, ty, tz를 u,v,w로 각각 remap한다 - lines 55 ~ 57).
너가 볼 수 있듯이, 이것은 value noise function과 매우 유사하다. 우리는 cells의 corner에 있는 random values 값을 random direction과 cell의 corners에서 P로 가는 방향의 내적으로 바꾸었다 (lines 86 ~ 89).
코드를 옳게 하는 것은 쉽지 않다. 우리가 신경쓸 필요가 있는 몇 가지 함정들이 있따. 이제, 위의 이미지는 너가 무엇이 일어나는지를 이해하는데 (바라건대) 도와준다 (그것은 그런데 2D case를 나타낸다). grid cell의 corners에 있는 "gradient vectors"를 주목하고, 우리가 cell의 corner에서 point로 가는 벡터를 어떻게 연산하는지를 주목해라. 거기에서 우리는 noise function을 연산한다. 또한 다음을 주목해라:
- 너는 grid cell x좌표의 lower left corner를 연산하기 위해 (int)(floor(P.x)) & 255를 사용할 필요가 있다 (y좌표에 대해서 P.x를 P.y로 바꾼다). 그 질문은 왜 우리가 floor function을 쓰느냐이다 (그리고 이것은 stl C++의 함수이다). 이것은 그 점 좌표의 어떤 값이 음수일 때 유용하다. int(P.x)의 문제는 그것이 좌표의 값이 1.1 또는 1.999 라면 1을 반환하는 것이다 (그리고 이것은 좋다), 그러나 그것은 -1.2 또는 -1.999 일 때 -1을 반환한다. 우리는 이 특별한 경우에 -2를 원한다. 그 점이 있는 grid cell의 가장 작은 좌표가 항상 cell의 lower left corner가 되어야 한다는 것을 명심해라. 위의 이미지에서 보여지는대로 그 cell이 world Cartesian coordinate system의 양수 또는 음수 side에 어디든. floor(P.x) 함수를 사용하는 것은 좌표 기호와 상관없이 우리가 정확한 결과를 얻는 것을 보장한다. 실제로 (int)(floor(-1.2)는 예를들어 우리가 원하는 -2를 반환한다.
- 우리가 grid cell의 lower-left coordinate를 연산했다면, 그러면 우리는 그것의 upper-right coordinate 또한 연산할 필요가 있따. 우리는 이것은 간단히 lower left 좌표에 1을 더해서 할 수 있찌만, 이 값은 grid size를 초과할지도 모른다. 따라서 우리는 또한 이 값의 modulo를 취할 필요가 있는데, grid coordinates가 [0:255]의 범위에 있도록 하기 위해서이다.
- 또 다시, gradient array의 사이즈가 제한되어 있다는 것을 기억해라 (256 값들). 우리는 그 프로세스를 시각화하는 한 방법은 3D grid를 상상하는 것이라고 설명했는데, 거기에서 그 grid의 정점들은 random gradients를 할당받는다. 그러나, 그 noise function은 x,y,z 값에 대해 작동하는데, 그것은 무한이다 (grid 차원은 유한이지만, noise function을 연산하기 위해 사용되는 그 점의 좌표는 아니다). 그래서 너는 noise function을 또한 그림 2에서 보여지는 모든 방향에서 이 3D grid의 반복이라고 볼 수 있다. 이것이 실제로 의미하는 것은, 한 점이 그 grid의 upper right corner에 위치할 때, (이것은 예를들어 그 점의 좌표중 어떤 것이 [0,-1], [-255,-256], [255,256] 등에 있을 때 발생한다), 그러면 우리는 좌표(255,255)에 grid에 저장된 gradient와 (0,0)좌표 grid에 저장되어 있는 gradient value 사이를 보간할 것이다. 예를들어:
X1 = (X0 + 1) & 255; // = 0 (left corenr of the 3D lattice of first cell)
Uniform Distribution of Gradients
이제, 균일하게 분산된 random directions을 생성하는 것은 (그것들은 모두 생성될 동일한 확률을 가진다) 간단한 것처럼 보이지만, 그것을 옳게 하는 것은 보기보다 더 까다롭다. 우리가 사용했던 "naive" 기법은 정말로 random directions을 발생시키지만, 이러한 방향들이 균일하게 분산되어있지 않다. 그것들은 한 cube의 volume 내에서의 random directions를 발생시킨다, sphere의 volume이 아니라. 이 이유 때문에, 그것들은 흥미가 있는 도형 (sphere)과 관련해서 균일하게 분산되어 있지 않다. 또 다른 naive 기법은 구면좌표계 𝝓, 𝜃를 무작위로 생성하고, 이렇나 구면좌표계를 Cartesian 좌표계로 바꾸는 것으로 구성된다:
이것은 우아하게 들릴지라도, 이것은 작동하지 않는다. 너가 random 구면좌표계를 생성할 때, 이러한 좌표들은 그 공간 내에서 멋지게 정말로 분산되거나, 이러한 좌표들이 정의되는 것에 지배된다. 그것은 𝝓에 대해 [0,2𝛑]이고, 𝜃에 대해 [0,𝛑] 이다. 너가 이 nice rectangle를 sphere로 wrap할 떄, 너는 그 rectangle이 sphere의 poles에서 squeezed 되는 것을 볼 수 있다. 다시 말해서, rectangle의 surface에 멋지게 분산되는 점들은 이제 poles에서 채워진다는 것이다 (아래 이미지). 명백히 pole에 있는 점들의 density는 sphere에 있는 어디보다더 더 크다. 그리고 따라서 우리는 명백히 이 distribution이 균일하지 않다고 볼 수 있다.
우리가 여기에서 해결하려고 하는 것이 균일하게 분산된 random unit directions을 만드는 것이라는 것을 기억해라. 그리고 이것은 어느정도로 우리가 Global Illumination and Path Tracing 강의에서 이미 공부한 것과 같은 것이다. 거기에서 우리는 hemisphere에 대해 어떻게 random samples를 만드는지를 배웠었다. 우리는 이 강의에서 많은 양의 세부사항을 배웠다. 그래서 우리는 이 프로세스를 여기에서 다시 완전히 설명하지 않을 것이다. 한 함수를 sample하기 위해서, 우리는 처음에 그 함수의 PDF를 연산할 필요가 있다는 것을 기억해라. 그러고나서 그것의 CDF를 연산하고, 마지막으로 CDF의 invert를 연산해야 한다. 이제 우리가 sphere에 대해 samples 하길 원하는 특별한 경우에서, 그러나 hemisphere 예제 처럼, 우리는 우리의 probability function or PDF를 표현하기 위해 solid angle로 시작할 것이다. 너도 알듯이 (바라건대), 구에서 4𝛑 steradians (unit for solid angle)이 있다 (만약 너가 solid angle 개념에 익숙하지 않다면, Mathematics and Physics for Computer Graphics section에서 radiometry 강의를 꼭 봐라). 우리는 또한 PDF가 1로 적분된다는 것을 안다. 그래서 그 sphere의 경우에, 우리는 다음을 쓸 수 있다:
만약 너가 Global Illumination 강의에서 설명된 단계들을 따른다면, 너는 다음을 얻는다:
hemisphere의 PDF가 1/2𝛑인 것과의 차이를 주목해라
다른 강의에서 처럼, 우리는 극좌표 (𝜃와 𝝓)의 관점으로 그 PDF를 표현하고 싶다:
그러고나서, differential solid angle dw가 다음으로 정의될 수 있다는 것을 회상해라 (또 다시, 이것은 다른 강의에서 설명된다):
만약 우리가 얻은 이전 방정식에서 이 방정식을 대체한다면:
그 방정식의 각 변에서 d𝝓d𝜃 항은 cancel되고 다음을 얻는다:
그러고나서 global illumination 강의에서 설명되었듯이 𝝓에 대해 p(𝝓,𝜃)를 적분해라. 그리고 우리는 다음을 얻는다:
우리가 얻은 𝝓의 PDF에 대해
우리가 𝝓,𝜃에 대한 PDF들을 가졌으니, 우리는 그것들의 각각 CDF를 연산하고 inverse시킨다:
적분의 기본 정리를 사용해서 우리는 다음을 얻는다:
p(𝝓)의 CDF는 더 간단하다:
마지막으로 이러한 CDF들을 역수를 취해주자:
2y - 1을 주목해라. 거기에서 y는 [0,1] 범위에 (균일하게 분산된) random number이고, 1-2y는 같은 결과를 준다.
그리고 𝝓에 대해:
이 경우에 y는 [0,1] 범위에서 균일하게 분산된 수를 나타낸다. 다시말해서, sphere의 표면에 균일하게 random points를 생성하기위해서 다음의 코드를 사용할 필요가 있다:
다른 방법들이 존재하지만, 그것들이 수학적으로 왜 작동하는지를 설명하는 것은 이 강의 범위 내에서 너무 복잡할 것이다. 그래서 지금은, 우리는 우리의 Perlin noise function의 코드에서 이것을 사용할 것이다.
다음의 이미지에서 Perlin noise (right)가 얼마나 value noise(left)보다 더 좋아 보이게 느껴지는지를 주목해라. 그래서 그 질문은 이제 이것이 왜 사실이냐 이다.
{
위 이미지는 강의에 나온대로 Uniformly Distributed gradients를 이용한 사진이고 아래는 그렇지 않은 사진이다.
확실히 Uniformly Distributed gradients로 만들어진게 더 자연스러워 보인다.
}
Why is Perlin/Gradient Noise better than Value Noise?
왜 Perlin noise가 value noise in 1D 보다 더 좋은지 이해하는 것은 다소 더 쉽다. value noise의 경우에, 우리는 "line"을 따라서 integer positions에 random values를 생성하고, 이러한 정수 위치들 사이에 이러한 값들을 보간한다 (그림 5와 6에 보여지듯이). 그런데, 우리가 이러한 값들을 무작위로 선택했기 때문에, 몇 가지 연속하는 값들이 매우 유사할지도 모른다는 것에 주목해라 (그림 6에서 보여지듯이). 이것은 좋지 않다. 왜냐하면 noise의 어떤 부분들이 빠르게 달라질 것이기 때문이다 (연속하는 값들이 서로 매우다를 때) 그리고 어떤 부분들은 느리게 변할 것이다 ( x축을 따라서 연속하는 값들이 비슷할 때: 예를들어, 같은 기호를 갖는). values가 느리게 변하는 noise functions의 부분들은 values가 빠르게 변하는 noise의 부분들과 비교해서 낮은 frequency를 가진다고 말해진다. 빠르게 변하는 noise는 높은 frequency를 가진다. 일반적으로, 이것은 value noise가 (너가 noise가 만들어진 frequencies를 체크할 때) high and low frequencies로 구성된다는 것을 의미한다. 좋은 noise는 random하게 보이고, locally에서는 부드럽게 변하지만, 또한 일반적으로 꽤 homogeneous look을 보이는 noise라는 것을 명심해라. 다시 말해서, noise가 만들어지는 features가 일반적으로 비슷한 크기 (비슷한 frequency)를 가져야 한다는 것이다. 그것은 우리가 설명했던 이유 때문에 value noise는 아니다.
Perlin noise 기법은 line을 따라서 정수 위치에서 랜덤 값을 선택하는 대신에 "gradients"를 선택하지만 value noise와 매우 유사하다. Gradients는 lattice points에서 1D noise function에의 "접선"으로 볼 수 있다. 너가 그림 7과 8에서 볼 수 있듯이, 그 gradient가 어떤 방향을 가리키는지는 중요하지 않다. 왜냐하면 만약 그것이 curve가 그 lattice point에서 한 방향으로 올라가게 한다면 (그림 8에서 보여지듯이 한 lattice point의 오른쪽에서), 그것은 curve가 그 같은 점의 다른 side에서 내려가도록한다. (그 점의 왼쪽에서) worse case에서, 만약 두 개의 연속하는 lattice points가 급격하게 반대방향을 목표로 하는 gradients를 가진다면 (하나는 위를 가리키고, 다른 하나는 아래를 가리키고), 그러면 그 noise function은 두 점 사이에 "S" 같은 도형을 가질 것이다 (그림 8a에서 보여지듯이). 다른 경우에, 그 curve는 위 아래 둘 주 ㅇ하나로 갈 것이다 (그림 8b 와 c). 그러나 하나는 쉽게 이 구성 때문에 모든 features가 다소 같은 사이즈를 가질 것을 볼 수 있다. 그것은 두 개의 연속하는 lattice points사이에 bump or dent or"S" 같은 모양중의 하나이다. 결과적으로, Perlin noise의 frequenceis의 분산은 value noise의 frequency spectrum보다 더 규칙적이다 (특히, 나중에 너가 알 수 있는 low frequencies를 제거한다). Perlin이 그의 논문에서 언급했듯이:
위의 텍스쳐는 그것에 band-limited character를 가진다; 즉, 어떤 사이즈의 범위 밖에 어떠한 세부사항이 없다.
이것은 중요한 특성이다, 특히 함수를 filtering할 때 (filtering 강의를 체크해라).
- 내적은 [-1, 1] 사이의 범위의 값을 반환한다 (관련된 벡터들이 normalized 되었따고 가정한다). 그래서 우리는 이미 Perlin noise 함수가 또한 음수와 양수의 값을 반환할 것이라는 것을 기대할 수 있다. 우리는 cell의 corner에 있는 방향과 corner에서 P로 가는 벡터가 반대 방향을 가리킬 때 음수값을 얻게 된다 (그림 1에 보여지듯이).
- 마지막으로, cell의 코너에서 P로 가는 벡터 V는 P가 정확히 그 corner에 있을 때 물론 (0, 0, 0)이다. 그래서 그 corner에서의 gradient의 내적과 이 특별한 경우의 V는 반드시 0이다. 이것은 아래의 이미지에서 보여질 수 있는 것인데, 만약 우리가 noise function의 결과의 결과에 대해 latticer를 겹쳐놓는다면). 왜냐하면 Perlin noise function은 [-1, 1]의 값을 반환하기 때문에, 우리는 그것들을 [0, 1]의 범위로 remap해야만 하는데, 한 이미지로 결과를 저장해야해서 이다. 그리고 이것은 각 cell의 corner에서의 noise가 remapped되면 0.5라는 것을 의미한다 (그러나 그것을 remapping하는 것이 0이 되기전에?? - 그림 3)
{
마지막 문장은 설명을 식으로 설명했으면 더 쉬웠을 것 같다 그러니까 -1 <= x <= 1의 범위는 거기에 + 1을 더하고, 0 <= x + 1 <= 2. 그러고나서 0.5를 곱해서 2로 나누는 것과 같기 때문에, 0 <= (x + 1) * 0.5 <= 1. 만약 corner의 noise function value가 0이면 거기에 1을 더하고 2로 나누기 때문에 항상 0.5가 된다.
}
Uniform Distribution of Gradients
이제, 균일하게 분산된 random directions을 생성하는 것은 (그것들은 모두 생성될 동일한 확률을 가진다) 간단한 것처럼 보이지만, 그것을 옳게 하는 것은 보기보다 더 까다롭다. 우리가 사용했던 "naive" 기법은 정말로 random directions을 발생시키지만, 이러한 방향들이 균일하게 분산되어있지 않다. 그것들은 한 cube의 volume 내에서의 random directions를 발생시킨다, sphere의 volume이 아니라. 이 이유 때문에, 그것들은 흥미가 있는 도형 (sphere)과 관련해서 균일하게 분산되어 있지 않다. 또 다른 naive 기법은 구면좌표계 𝝓, 𝜃를 무작위로 생성하고, 이렇나 구면좌표계를 Cartesian 좌표계로 바꾸는 것으로 구성된다:
1 2 3 4 5 | float phi = 2 * drand48() * M_PI; float theta = drand48() * M_PI; float x = cos(phi) * sin(theta); float y = sin(phi) * sin(theta); float z = cos(theta); |
이것은 우아하게 들릴지라도, 이것은 작동하지 않는다. 너가 random 구면좌표계를 생성할 때, 이러한 좌표들은 그 공간 내에서 멋지게 정말로 분산되거나, 이러한 좌표들이 정의되는 것에 지배된다. 그것은 𝝓에 대해 [0,2𝛑]이고, 𝜃에 대해 [0,𝛑] 이다. 너가 이 nice rectangle를 sphere로 wrap할 떄, 너는 그 rectangle이 sphere의 poles에서 squeezed 되는 것을 볼 수 있다. 다시 말해서, rectangle의 surface에 멋지게 분산되는 점들은 이제 poles에서 채워진다는 것이다 (아래 이미지). 명백히 pole에 있는 점들의 density는 sphere에 있는 어디보다더 더 크다. 그리고 따라서 우리는 명백히 이 distribution이 균일하지 않다고 볼 수 있다.
우리가 여기에서 해결하려고 하는 것이 균일하게 분산된 random unit directions을 만드는 것이라는 것을 기억해라. 그리고 이것은 어느정도로 우리가 Global Illumination and Path Tracing 강의에서 이미 공부한 것과 같은 것이다. 거기에서 우리는 hemisphere에 대해 어떻게 random samples를 만드는지를 배웠었다. 우리는 이 강의에서 많은 양의 세부사항을 배웠다. 그래서 우리는 이 프로세스를 여기에서 다시 완전히 설명하지 않을 것이다. 한 함수를 sample하기 위해서, 우리는 처음에 그 함수의 PDF를 연산할 필요가 있다는 것을 기억해라. 그러고나서 그것의 CDF를 연산하고, 마지막으로 CDF의 invert를 연산해야 한다. 이제 우리가 sphere에 대해 samples 하길 원하는 특별한 경우에서, 그러나 hemisphere 예제 처럼, 우리는 우리의 probability function or PDF를 표현하기 위해 solid angle로 시작할 것이다. 너도 알듯이 (바라건대), 구에서 4𝛑 steradians (unit for solid angle)이 있다 (만약 너가 solid angle 개념에 익숙하지 않다면, Mathematics and Physics for Computer Graphics section에서 radiometry 강의를 꼭 봐라). 우리는 또한 PDF가 1로 적분된다는 것을 안다. 그래서 그 sphere의 경우에, 우리는 다음을 쓸 수 있다:
만약 너가 Global Illumination 강의에서 설명된 단계들을 따른다면, 너는 다음을 얻는다:
hemisphere의 PDF가 1/2𝛑인 것과의 차이를 주목해라
다른 강의에서 처럼, 우리는 극좌표 (𝜃와 𝝓)의 관점으로 그 PDF를 표현하고 싶다:
그러고나서, differential solid angle dw가 다음으로 정의될 수 있다는 것을 회상해라 (또 다시, 이것은 다른 강의에서 설명된다):
만약 우리가 얻은 이전 방정식에서 이 방정식을 대체한다면:
그 방정식의 각 변에서 d𝝓d𝜃 항은 cancel되고 다음을 얻는다:
그러고나서 global illumination 강의에서 설명되었듯이 𝝓에 대해 p(𝝓,𝜃)를 적분해라. 그리고 우리는 다음을 얻는다:
우리가 얻은 𝝓의 PDF에 대해
우리가 𝝓,𝜃에 대한 PDF들을 가졌으니, 우리는 그것들의 각각 CDF를 연산하고 inverse시킨다:
적분의 기본 정리를 사용해서 우리는 다음을 얻는다:
p(𝝓)의 CDF는 더 간단하다:
마지막으로 이러한 CDF들을 역수를 취해주자:
2y - 1을 주목해라. 거기에서 y는 [0,1] 범위에 (균일하게 분산된) random number이고, 1-2y는 같은 결과를 준다.
그리고 𝝓에 대해:
이 경우에 y는 [0,1] 범위에서 균일하게 분산된 수를 나타낸다. 다시말해서, sphere의 표면에 균일하게 random points를 생성하기위해서 다음의 코드를 사용할 필요가 있다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | std::mt19937 generator(seed); std::uniform_real_distribution<float> distribution; auto dice = std::bind(distribution, generator); for (unsigned i = 0; i < tableSize; ++i) { float theta = acos(2 * dice() - 1); float phi = 2 * dice() * PI; gradients[i].x = cos(phi) * sin(theta); gradients[i].y = sin(phi) * sin(theta); gradients[i].z = cos(theta); permutationTable[i] = i; } |
다른 방법들이 존재하지만, 그것들이 수학적으로 왜 작동하는지를 설명하는 것은 이 강의 범위 내에서 너무 복잡할 것이다. 그래서 지금은, 우리는 우리의 Perlin noise function의 코드에서 이것을 사용할 것이다.
다음의 이미지에서 Perlin noise (right)가 얼마나 value noise(left)보다 더 좋아 보이게 느껴지는지를 주목해라. 그래서 그 질문은 이제 이것이 왜 사실이냐 이다.
{
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int main() { const int width = 512; const int height = 512; float* noiseMap = new float[width * height]; PerlinNoise testPerlin; for (unsigned j = 0; j < height; ++j) { for (unsigned i = 0; i < width; ++i) { float result = testPerlin.eval(glm::vec3(i, j, 0) * (1/ 128.f)); noiseMap[i + j * width] = (result + 1) * 0.5f; } } savePNGfile(noiseMap, width, height, "perlinTest.png"); delete[] noiseMap; return 0; } |
위 이미지는 강의에 나온대로 Uniformly Distributed gradients를 이용한 사진이고 아래는 그렇지 않은 사진이다.
확실히 Uniformly Distributed gradients로 만들어진게 더 자연스러워 보인다.
}
Why is Perlin/Gradient Noise better than Value Noise?
왜 Perlin noise가 value noise in 1D 보다 더 좋은지 이해하는 것은 다소 더 쉽다. value noise의 경우에, 우리는 "line"을 따라서 integer positions에 random values를 생성하고, 이러한 정수 위치들 사이에 이러한 값들을 보간한다 (그림 5와 6에 보여지듯이). 그런데, 우리가 이러한 값들을 무작위로 선택했기 때문에, 몇 가지 연속하는 값들이 매우 유사할지도 모른다는 것에 주목해라 (그림 6에서 보여지듯이). 이것은 좋지 않다. 왜냐하면 noise의 어떤 부분들이 빠르게 달라질 것이기 때문이다 (연속하는 값들이 서로 매우다를 때) 그리고 어떤 부분들은 느리게 변할 것이다 ( x축을 따라서 연속하는 값들이 비슷할 때: 예를들어, 같은 기호를 갖는). values가 느리게 변하는 noise functions의 부분들은 values가 빠르게 변하는 noise의 부분들과 비교해서 낮은 frequency를 가진다고 말해진다. 빠르게 변하는 noise는 높은 frequency를 가진다. 일반적으로, 이것은 value noise가 (너가 noise가 만들어진 frequencies를 체크할 때) high and low frequencies로 구성된다는 것을 의미한다. 좋은 noise는 random하게 보이고, locally에서는 부드럽게 변하지만, 또한 일반적으로 꽤 homogeneous look을 보이는 noise라는 것을 명심해라. 다시 말해서, noise가 만들어지는 features가 일반적으로 비슷한 크기 (비슷한 frequency)를 가져야 한다는 것이다. 그것은 우리가 설명했던 이유 때문에 value noise는 아니다.
Perlin noise 기법은 line을 따라서 정수 위치에서 랜덤 값을 선택하는 대신에 "gradients"를 선택하지만 value noise와 매우 유사하다. Gradients는 lattice points에서 1D noise function에의 "접선"으로 볼 수 있다. 너가 그림 7과 8에서 볼 수 있듯이, 그 gradient가 어떤 방향을 가리키는지는 중요하지 않다. 왜냐하면 만약 그것이 curve가 그 lattice point에서 한 방향으로 올라가게 한다면 (그림 8에서 보여지듯이 한 lattice point의 오른쪽에서), 그것은 curve가 그 같은 점의 다른 side에서 내려가도록한다. (그 점의 왼쪽에서) worse case에서, 만약 두 개의 연속하는 lattice points가 급격하게 반대방향을 목표로 하는 gradients를 가진다면 (하나는 위를 가리키고, 다른 하나는 아래를 가리키고), 그러면 그 noise function은 두 점 사이에 "S" 같은 도형을 가질 것이다 (그림 8a에서 보여지듯이). 다른 경우에, 그 curve는 위 아래 둘 주 ㅇ하나로 갈 것이다 (그림 8b 와 c). 그러나 하나는 쉽게 이 구성 때문에 모든 features가 다소 같은 사이즈를 가질 것을 볼 수 있다. 그것은 두 개의 연속하는 lattice points사이에 bump or dent or"S" 같은 모양중의 하나이다. 결과적으로, Perlin noise의 frequenceis의 분산은 value noise의 frequency spectrum보다 더 규칙적이다 (특히, 나중에 너가 알 수 있는 low frequencies를 제거한다). Perlin이 그의 논문에서 언급했듯이:
위의 텍스쳐는 그것에 band-limited character를 가진다; 즉, 어떤 사이즈의 범위 밖에 어떠한 세부사항이 없다.
이것은 중요한 특성이다, 특히 함수를 filtering할 때 (filtering 강의를 체크해라).
댓글 없음:
댓글 쓰기