Post Lists

2018년 12월 31일 월요일

Height Maps

https://lwjglgamedev.gitbooks.io/3d-game-development-with-lwjgl/content/chapter14/chapter14.html

Height Maps
이 챕터에서, 우리는 height maps를 사용하여 복잡한 테레인을 어떻게 만드는지를 배울 것이다. 우리가 시작하기 전에, 너는 어떤 리팩토링이 된 것을 알아야 할 것이다. 우리는 어떤 새로운 패키지들을 만들었고, 그것들을 더 잘 정리하기 위해 어떤 클래스들을 옮겼다. 너는 소스 코드에서 그 변화들을 체크할 수 있다.

그래서 height map이 무엇이냐? height map은 surface elevation data를 얻기 위해 픽셀 컬러를 사용하는 3D terrain을 생성하기 위해 사용되는 한 이미지이다. Height maps image는 보통 gray scale을 사용하고, Terragen같은 프로그램에 의해 생성될 수 있다. height map 이미지는 이것 처럼 보인다.

위의 이미지는 마치 너가 위에서 땅을 보고 있는 것처럼 보인다. 그 이미지로, 우리는 정점으로 구성된 삼각형으로 만들어진 한 mesh를 만들 것이다. 각 정점의 altitude는 이미지 픽셀들의 각각 컬러에 따라서 계산될 것이다. 블랙 컬러는 가장 낮은 값을 나타내고, 하얀색은 가장 높은 것을 나타낸다.

우리는 정점들의 한 grid를 만들 것인데, 그 이미지의 각 픽셀에 대해 하나씩이다. 그러한 정점들은 다음 그림에서 보여지듯이 mesh를 구성할 삼각형들을 형성하기 위해 사용될 것이다.

그 mesh는 x와 z축을 가로질러서 y축에서 elevation을 바꾸기 위해 픽셀 컬러를 사용하여 렌더링될 큰 quad를 형성할 것이다.

height map으로부터 3D terrain을 만드는 프로세스는 다음으로 요약될 수 있다:

  • height map을 포함하는 image를 불러와라 (우리는 각 픽셀에 접근하기 위해 BufferedImage instance를 사용할 것이다).
  • 각 이미지 픽셀에 대해, 픽셀 컬러를 기반으로 그것의 높이를 가진 정점을 만들어라.
  • vertex에 정확하 texture coordinate를 반환해라.
  • 정점과 과련된 삼각형을 그리기 위해 인덱스들을 설정해라.
우리는 위에서 설명된 단계들을 수행하여, height map image를 기반으로 Mesh를 만드는 HeightMapMesh라 이름이 있는 한 클래스를 만들 것이다. 그 클래스를 위해 정의된 상수들을 처음에 리뷰해보자.

private static final int MAX_COLOUR = 255 * 255 * 255;

우리가 위에서 설명했듯이, 우리는 height map으로 사용된 이미지의 각 픽셀 컬러를 기반으로 각 정점의 높이를 계산할 것이다. 이미지들은 보통 greyscale이고, PNG image에 대해서이다. 그리고 이것은 각 픽셀에 대한 RGB 컴포넌트가 0 ~ 255이다. 그래서 우리는 다른 높이를 정의하는 256개의 discrete values를 가진다. 이것은 너에게 충분한 정밀도일지도 모르지만, 만약 그것이 아니라면, 우리는 좀 더 intermediate values를 갖기 위해 세 개의 RGB components들을 사용할 수 있다. 이 경우에, 그 높이는 0 ~ 255^3의 범위를 형성하도록 계산되어질 수 있다. 우리는 두 번째 접근법을 선택할 것이다. 그래서 우리는 greyscale images로 제한되지 않는다.

다음 상수는:

private static final float STARTX = -0.5f;
private static final float STARTZ = -0.5f;

그 메쉬는 정점들의 한 집합으로 형성될 것인데 (픽셀당 하나씩), 그것의 x와 z 좌표들은 다음의 범위에 있을 것이다:
  • [-0.5, 0.5], 즉, [STARTX, -STARTX] for the x axis.
  • [-0.5, 0.5] 즉, [STARTZ, -STARTZ] for the z axis.
그 값들에 대해서 너무 걱정하지 말아라. 나중에 최종 메쉬는 그것의 사이즈가 world에 맞게 하기 위해 스케일될 수 있다. y축과 관련해서, 우리는 두 개의 파라미터 minY and maaxY를 설정할 것인데, y좌표가 가질 수 있는 가장 낮고 높은 값을 설정하기 위한 것이다. 이러한 파라미터들은 상수는 아니다. 왜냐하면 우리가 그것들을 run time에 바꾸기를 원할지도 모르기 때문이다. 적용된 스케일링과 독립적으로. 마지막으로, 테레인은 [STARTX, -STARTX], [STARTZ, -STARTZ], [minY, maxY] 범위의 cube안에 포함될 것이다.

그 메쉬는 HeightMapMesh 클래스의 생성자에서 만들어질 것인데, 이것처럼 정의된다.

public HeightMapMesh(float minY, float maxY, String heightMapFile, String TextureFile, int textInc) throws Exception{ }

그것은 y축에 대한 최소와 최대값을 받는다. height map으로서 사용될 이미지를 포함하는 파일의 이름과, 사용될 텍스쳐 파일도. 그것은 또한 우리가 나중에 이야기할 textInc라는 정수를 받는다.

우리가 그 constructor에서 할 첫 번째 것은 height map image를 Buffered Image instance로 불러오는 것이다.

그러고나서 우리는 texture file을 ByteBuffer로 불러오고, Mesh를 구성할 변수들을 준비한다. incx와 incz 변수들은 x와 z 좌표에서 각 정점에 적용될 increment를 가질 것이다. 그래서 그 Mesh는 위에서 명시된 범위를 다룬다.

그 이후에, 우리는 이미지에 대해 반복할 준비가 된다. 그리고 각 픽셀 당 한 정점을 만들고, 그것의 텍스쳐좌표를 준비하고, Mesh를 구성할 삼각형을 정확히 정의할 인덱스들을 준비한다.

정점 좌표를 만드는 프로세스는 self-explanatory하다. 이번에 왜 우리가 텍스쳐 좌표를 한 숫자로 곱하고 그 height가 어떻게 계산되는지를 무시하자. 각 정점에 대해 우리가 두 개의 삼각형들의 정점을 정의하고 있다는 것을 볼 수 있다 (우리가 마지막 row or column있다는 것을 제외하고). 3x3 image로 그것을 시각화해보자, 그것들이 어떻게 구성하는지를 시각화 하기 위해서. 3x3 image는 9개의 정점들을 구성하고. 그리고 따라서 2x4 삼각형에 의해 형성된 4개의 quads들이 있다. 다음의 그림은 그 grid를 보여준다. 다음의 사진은 그 그리드를 보여주고, Vrc (r: row, c: column)의 형태로 각 정점을 네이밍한다.

우리가 첫 번째 정점(v00)을 처리할 때, 우리는 빨갛게 칠해진 두 삼각형의 인덱스들을 정의한다.

우리가 두 번째 정점 (V01)을 처리할 때, 우리는 빨갛게 칠해진 두 삼각형들의 인덱스들을 정의한다. 그러나, 우리가 세 번째 정점 (V02)를 처리할 떄, 우리는 정점들을 더 정의할 필요가 없다. 그 행에 대한 삼각형들이 이미 정의가 되어있다.

너는 쉽게 그 프로세스가 나머지 정점들에 대해 처리되는지를 볼 수 있따. 이제, 우리가 모든 정점 위치, 텍스쳐 좌표, 인덱스들 만들기만 한다면, 우리는 Mesh를 만들필요가 있다. 그리고 모든 그 데이터가 있는 관련된 Material들도.

너는 우리가 입력으로 vertex positions을 취하는 normals를 계산하는 것을 볼 수 있다. normals이 어떻게 계산되는지를 보기전에, heights가 어떻게 얻어지는지를 보자. 우리는 getHeight라고 불려지는 한 method를 만드는데, 그것을 vertex에 대한 height를 계산한다.

그 메소드는 한 픽셀에 대한 x와 z 좌표들을 받고, image의 width와 Byte Buffer를 받는데, RGB 컬러를 반환하는 buffer이다 (개별 RGB 컴포넌트들의 합) 그리고 minY and maxY 사이에 포함된 값을 반환한다. (black 컬러에 대해 minY와 white color에 대해 maxY).

너는 BufferedImage를 사용해서 좀 더 간단한 버전을 개발할지도 모른다, 그것이 RGB 값을 얻는 편리한 방법을 포함하고. 그러나 우리는 AWT를 사용할 것이다. AWT는 OSX와 섞이지 않는다는 것을 기억해라. 그래서 그것들의 클래스를 사용하는것을 피해라.

이제 텍스쳐 좌푣르이 어떻게 계산되는지를 봐보자. 첫 번째 옵션은 전체 메쉬에 따라 텍스쳐를 wrap하는 것인데, top left vertex는(0,0) 텍스쳐 좌표를 가질 것이고, bottom right vertex는 (1,1) 텍스쳐 좌표를 가질 것이다. 이 접근법의 문제는 그 텍스쳐가 좋은 결과를 제공하기 위해서 커야할 것이다는 것이다. 만약 그렇지 않다면 그것은 너무 stretched될 것이다.

그러나, 우리는 매우 효율적인 기법을 사용하여 좋은 결과를 가진 작은 텍스쳐를 사용할 수 있다. 만약 우리가 텍스쳐 좌표들이 [1,1] 범위를 넘어가도록 설정한다면, 우리는 origin에 돌아와서 처음부터 다시 계산할 수 있다. 다음의 사진은 이 behavior를 보여주는데, 몇 개 쿼드들에서 같은 텍스쳐를 tiling하는 것을 보여준다. [1.1] 범위를 넘어가는.

이것이 텍스쳐 좌표를 설정할 떄 우리가 할 것이다. 우리는 텍스쳐 좌표를 factor로 곱할 것이다, textInc parameter인데, 텍스쳐의 픽셀의 개수를 증가시키기 위해서이다 adjacent vertices 사이에 사용될

{
저렇게 하면 쉐이더 코드에서 이렇게 하면 된다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// shadertype=glsl
#version 330 core

out vec4 FragColor;

in vec3 fragPos;
in vec3 normal;
in vec2 TexCoords;
in float ypos;

uniform vec3 cameraPos;

uniform sampler2D terrTex;

void main()
{
 float newTexX = TexCoords.x > 1.0 ? TexCoords.x - floor(TexCoords.x) : TexCoords.x;
 float newTexY = TexCoords.y > 1.0 ? TexCoords.y - floor(TexCoords.y) : TexCoords.y ;
 

 // test Texture
 vec3 texColor = texture(terrTex, vec2(newTexX, newTexY)).xyz;
 FragColor = vec4(texColor, 1.0);
}

- 사용한 텍스쳐


- 결과물 1 (멀리서)

- 결과물 2 (확대)
- 결과물 3 (전체 Terrain 텍스쳐 좌표를 0 ~ 1로 설정했을 때)

뭐든 코드로 조종한다면 다 할 수 있을듯 하다.
텍스쳐 좌표를 0 ~ grid vertex 범위까지 해놓는다면, 각 한 개의 grid quad에 접근하여 뭔가 하고 싶을 때는 grid width와 height를 나누어서 접근할 수도 있겠다. 내가 원하는대로 하면 된다.

}

이제 남아있는 유일한 것은 normal 계산이다. 우리가 normals이 필요하다는 것을 기억해라. 그래서 light는 terrain에 정확하게 적용되어질 수 있다. normals 없이, 우리의 terrain은 같은 컬러로 렌더링 될 것이다, light가 각 지점에 어떻게 닿든. 우리가 여기에서 사용할 방법은 height maps에 대해 가장 효율적이지만, 그것은 normals이 어떻게 자동 계산되는지를 이해하느넫 도와줄 것이다. 만약 너가 다른 솔루션들을 검색해본다면, 외적 연산을 하지 않고 인접 점들의 높이를 사용하는 좀 더 효율적인 접근법을 찾을지도 ㅁ ㅗ른다. 그럼에도 불구하고, 이것은 startup에서 되기 때문에, 여기에서 보여지는 메소드는 성능을 많이 잡아먹지 않을 것이다.

그래픽적으로 normal이 어떻게 계산되는지 설명해보자. 우리가 P0이라는 정점을 가지고 있다고 상상해보자. 우리는 처음에 주변을 둘러싸는 정점들 가각에 대해 계산한다 (P1, P02, P3, P4), 이러한 점들을 연결하는 표면에 접선이 벡터들이다. 이러한 벡터들 (V1, V2, V3, V4)들은 P0으로부터 인접 점들 각각을 빼서 계싼된다 (V1 = P1- P0, etc).

그러고나서 우리는 인접 점들을 연결하는 평면들의 각가에 대한 normal를 계산한다. 이것은 이전에 계산된 벡터들에 대한 외적을 연산해서 된다. 예를들어, P1과 P2를 연결하는 표면의 normal (blue로 칠해진) V1과 V2 사이의 외적으로 연산된다.

만약 우리가 표면들의 나머지에 대해 normals를 계산한다면, P0에 대한 normal은 그 주변 surfaces의 모든 normal의 합계가 (normalized)가 될 것이다.

{
normal calculate에 대해서 위에서 처럼 해도 된다. 근데 이게 왜 이렇게 계산되는지에 대해서 알고 싶었는데,

https://www.leadwerks.com/community/topic/16244-calculate-normals-from-heightmap/?do=findComment&comment=106666

이 답변과

https://stackoverflow.com/questions/49640250/calculate-normals-from-heightmap

이 답변과

James Stewart Calculus 8판 PDF기준으로, Chapter 14의 Section 6인 Directional Derivatives and the Gradient Vector에 설명이 자세히 나와있다. 특히 Subsection인 Tangent Planes to Level Surfaces가 normal를 구하는데 쓰이는 수학이다.

그 수학 내용이 스택오버플로우에 적혀져있어서, Calculus 텍스트북을 공부해야 그 답변을 이해가 가능할 것이다.

여튼 적용해봤는데, 확실히 Perlin Noise를 evalute하면서 구한 normal과는 차이가 난다. 그래도 어쨋든 이정도면 타협할만하다. 저기 답변에서 주의해야할 것이 있는데

나는 여러가지 답변들을 다 시도해보았는데,
https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/perlin-noise-computing-derivatives

여기에서 설명한 것이 가장 깔끔하고, 코드도 깔끔하다. 어떤 답변을 실행하든 normal은 거기에서 거기다. analytical partial derivative가 아닌 이상 크게 차이가 안난다. 그래서 코드는 다음 처럼 깔끔하게 되었다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Normals
for (unsigned j = 0; j <= m_terrainSubDepth; ++j)
{
 for (unsigned i = 0; i <= m_terrainSubWidth; ++i)
 {
  int centerIndex = i + j * (m_terrainSubWidth + 1);
  int rightIndex = centerIndex + 1;
  int downIndex = centerIndex + (m_terrainSubWidth + 1);

  // Handle Edge Case.
  if (i == m_terrainSubWidth) rightIndex = centerIndex;
  if (j == m_terrainSubDepth) downIndex = centerIndex;

  float centerV = vertices[centerIndex].y;
  float rightV = vertices[rightIndex].y;
  float downV = vertices[downIndex].y;

  // calculate the normal using the gradient vector of partial derivative math
  normals[centerIndex] = glm::normalize(glm::vec3((rightV - centerV) *  -1.f, 1.f, (downV - centerV) * -1.f));
 }
}

이게 height map으로 부터 normal를 만드는 최종 코드가 될 것이다.

간단한 directional light를 했다.

}



마지막으로, 더 큰 terrains을 구성하기 위해, 우리는 두 가지 옵션들을 가진다:

  • 더 큰 height map을 만들어라
  • height map을 재사용하고, 그것을 3D space를 통해 타일링해라. 그 height map은 타일같이 world를 가로질러서 이동할 수 있는 terrain block처럼 보일 것이다. 그렇게 하기 위해서, height map의 edge의 픽셀들은 같아야 한다 (left edge는 right side와 동일하고 top edge는 bottom과 동일하고), 그 타일들간의 gaps를 피하기 위해서.
우리는 두 번째 접근법을 사용할 것이다 (그리고 적절한 height map을 선택할 것이다). 이것을 지원하기 위해서, 우리는 height map tiles의 square를 만들 Terrain이라고 이름지엊인 class를 만들 것이다.


전체 프로세스를 설명해보자. 우리는 다음의 좌표들을 가진 블럭들을 가지고 있다 (x와 ㅋz에 대해, 위에서 정의된 상수들과 함께).

3x3 blocks grid에 의해 형성된 한 테레인을 만든다고 가정하자. 또한 우리가 그 terrain blocks을 스케일링 하지 않는다고 가정하자 (즉, 그 변수 blocksPerRow는 3이 될 ㅓㄳ이고, scale 변수는 1이다). 우리는 grid가 (0,0) 좌표에 중심을 잡길 원한다.

우리는 그 블럭들을 평행이동 시키길 원한다. 그래서 그 정점들은 다음의 좌표들을 가질 것이다.

그 평행이동은 setPosition method에 의해서 얻어지지만, 우리가 설정한 것이 position이 아닌 displacement라는 것을 기억해라. 만약 너가 위의 표를 다시 본다면, 너는 central block이 어떤 displacement를 요구하지 않는다는 것을 알 것이다.  그것은 이미 적절한 좌표에 위치되어있다. 초로색에 그려진 정점은 displacement가 필요하다. x좌표에 -1정도. 파란색으로 그려진 정점은 +1의 displacement가 필요하다. x displacement를 계산하는 공식은, 그 스케일과 block width를 고려해서 이것이다:

xDisplacement = (col - (blocksPerRow - 1) / 2) * scale * width

만약 우리가 DummyGame class같은 Terrain instance를 만든다면, 우리는 이것과 같은 것을 얻는다.

너는 테레인 주위로 카메라를 움직일 수 있고, 그것이 어떻게 렌더링 되는지 볼 수 있따. 우리는 여전히 충돌 탐지를 구현하지 않았기 때문에, 너는 그것을 통과해서 가고 그것으로 부터 바라볼 수 있따. 우리는 face culling을 활성화 했기 때문에, 테레인의 어떤 부분들은 아래에서 볼 때 렌더링 되지 않는다.

게임 일정 (2주차)

내가 원하는 대로 진도가 나가지 않아서, 일정을 항상 봐야겠다.

분기점1 : Terrain and Model
분기점2 : Graphics Details
분기점3 : Physics and Animation
분기점4 : Game UI and Effect
분기점5 : Optimization and Convenience

이번주부터가 분기점 2를 시작해야 하지만, 분기점 1의 Terrain도 끝내지 못했다. 어차피 완벽한 것은 추구하지 않는다. 그러니 나는 내가 가야할 길을 천천히 가보도록 하자.

Terrain Rendering 쪽에 좀 걸리는 것이 있는데 아래의 튜토리얼로 끝내자.


Terrain Physics가 이제 가장 큰 발목인데, 아래의 것을 보고 시도해보고.


만약 내가 원하는게 없다면 BulletPhysics 코드를 처음부터 까기 시작해서 해야할 것이다.
그래서 이런식으로 해서, Terrain Rendering과 Physics를 끝내자.

Terrain Editing에 대해서 내가 만들고 싶은 기능은
- 클릭으로 해당 Terrain 지역 높이 위 아래 높게 만들기
- Terrain Texture 바꾸기
- 그림 그리듯이, 마우스로 그림 Terrain 위에 Painting 하는 기능 (근데 별 필요없을듯)

여튼 내 생각엔 분기점 1~2는 반드시 하고 싶은 것이니, 분기점 3 ~ 5가 기한 내에 못하더라도 천천히 구현해 나가도록 하자.

어차피, 나는 계속 이런 내용들을 공부해 나갈 내용이니, 취업하더라도 계속 이어나가면 되니까.

Perlin Noise: Part 2, Improved Perlin Noise

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/improved-perlin-noise

Improved Perlin Noise
몇 년 뒤에 (사실 2002년에, 그래서 Perlin이 noise에 대한 그의 첫 번째 논문을 출판한지 17년 뒤에), Perlin은 그의 noise function의 새로운 개선된 버전을 제안했었다. 그 원래 noise function은 두 가지 문제들을 가지고 있다.

The smoothstep function is not ideal and ehre is why...
첫 번째로, 우리가 gradients와 cell내에서 점을 향하는 벡터 사이의 내적의 결과를 보간하기 위해 사용했던 interpolatns는 smoothstep function이라는 것을 기억해라. 이 함수는 3t^2 - 2t^3의 형태를 가진다. 또한 우리가 이 함수의 도함수인 (6t - 6t^2)을 noise function의 편도함수를 연산하기 위해 사용했다는 것을 기억해라. 이전 챕터에서 설명했었 듯이, 이러한 도함수들은 noise function으로 옮겨진 정점들의 "true" normal를 연산하는데 유용하다 (수학적으로 올바른). 이제 이것과 관련한 문제는 그 smoothstep function derivative의 도함수가 (우리가 수학에서 f''(x)의 2차 도함수라고 부르는) t = 0과 t = 1에서 연속하지 않다는 것이다. 그 smoothstep function의 2차 도함수는 6 - 12t이다. 그래서 t = 0일 때, f''(x) = 6이고, t = 1일 때, f''(x) = -6이다. 이것은 문제인데, 왜냐하면 옮겨진 메쉬의 normal를 연산하기 위해 사용된 함수의 도함수가 연속이지 않다면, 그러면 그것은 이러한 normals들에서 불연속을 도입할 것이기 때문이다 ,t = 0이고 t = 1인 부분에서. 그것을 옮겨진 메쉬의 정점들이 noise grid cells의 edges와 정렬할 때 발생한다 (즉, 정점좌표들이 정수값을 가질 때). 이러한 시각적 artifacts들은 명백히 아래의 (왼쪽) 이미지에서 보인다.

{

- 생각 1
왜 smoothstep 3t^2 -2t^3 함수의 2차 도함수가 불연속인지 이해가 안된다. 글이나 페를린이 설명하는 것을 보면 2차 도함수의 가 t = 0일 때와 t = 1일 때 값이 6과 -6이라는 것이 연속하지 않다라고 말하는 이유인데. 이것으로 따지자면, noise texture의 입장에서 도함수들의 값이 계속 같아야 연속하지 않는 것으로 말하는 것 같다. 정확히 왜 불연속인지 모르겠다. 어쨋든 이미지상으로는 그런 끊기는게 확인되니 이렇게 알고 넘어가자.

- 생각 2
페이스북 생활코딩에 물어봐서 해결했다. 역시 세상에는 고수들이 많이 있다.
https://www.facebook.com/groups/codingeverybody/permalink/2595918853781968/

요점은 어떤 한 vertex에서 다른 vertex로 넘어갈 때 이다.
즉 smoothstep의 2차도함수가 6 - 12t인데 그것의 t가 0일 때 6이고 t가 1일 때 -6인데
이 때, 어떤 vertex v0과 v1의 smoothstep되는 그래프를 그려서 2차도함수를 본다면
v0의 t = 1 장소와 v1의 t = 0 장소에서  2차 도함수 값이 다르기 때문에 당연히 불연속이 될 것이다.

}

noise function deriavtive를 연산하는 analytical solution 수학적으로 옳은 반면에, 이것이 그 결과가 반드시 시각적으로 우리가 찾는 것이 아니라는 것을 이해하는 것이 중요하다. 이것은 수학적 구성일 뿐이고, 이 construct는 우리가 좋아하지 않는 특성을 가질지도 모른다. 그래서 그 결과는 수학적으로 옳을지라도, 그것들은 우리가 피하고싶은 시각적 artefacts를 만든다.

게다가, 이 함수의 문제를 언급하는 대부분의 문서들은 왜 우리가 이 문제를 갖는지를 설명하지 않고 그리고 그것을 시각화하는 방법을 제공하지 안흔다. 왜 그 문제가 첫 번째 장소에 있는지를 더 잘 이해하기 위해서, 너는 함수들과 함수 도함수들에 대한 세부사항을 기억할 필요가 있다. 물리학에서, 공통 함수들은 너가 움직이는 오브젝트의 위치를 시간의 함수로서 연산하게 해주는 함수이다. 이 함수를 p(t)라고 부르자. 만약 너가 물리학 강의를 기억한다면, 너는 이 오브젝트의 속도를 이 함수의 시간에 대한 도함수로 연산할 수 있다. 다시 말해서,  p'(t) = P(t)/\delta t. 그리고 마지막으로 너는 그 오브젝트 가속도를 시간에 대한 그 속도함수의 도함수로 연산할 수 있다, 또는 p''(t) = p'(t) / \delta t. 그래서 본질적으로 너는 함수 f(x)와 그것의 도함수 f'(x) and f''x()를 개별적으로 한 오븢게트의 psotiion, speed, acceleration으로 볼 수 있다. 우리의 예제에서, smoothstep function은 분석적으로 position이고, tangent를 연산하기 위해 사용했던 smoothstep function의 1차 도함수는 spped이고, smoothstep function의 마지막 이차 도함수는 가속도이다. 물론 이러한 것은 analogies이다. 하지만 요점은, 만약 그 가속도가 불연속성을 가진다면, 그러면 우리는 그 속도가 급작스럽게 때때로 변한다는 것을 기대할 수 있다. 우리의 특별한 예제에서, 이것은 만약 그 smoothstep function second order derivative가 불연속성을 가진다면, 그러면 우리는 그 접선이 때때로 급작스럽게 변하는 것을 기대할 수 있다. 그리고 그것은 정확히 cell boundaries에서 visual artefacts가 발생하는 것이다. 이러한 여역들의 normals들은 급작스럽게 바뀐다.

컴퓨터 그래픽스와 수학에서, original function이 연속일 떄, 우리는 geometry continuity라고 말하고, 우리는 그 함수를 C0이라고 말한다 (또는 G0, 우리가 기하를 다룰 때). 만약 그것의 도함수가 연속이라면, 우리는 tangent continuity라고 말한다 (C1 or G1). 그리고 그 함수 2차 도함수가 또한 연속일 때, 우리는 curvature continuity라고 말한다. 다시 말해서, 두 곡선 또는 두 평면이 join하는 curvature은 접촉 점에서의 곡선들 또는 두 표면에 대해서 같다.

아래의 이미지는 noise function의 slice를 보여준다. 그리고 그것의 analytically 연산된 normals를 보여준다. normals의 orientation과 이러한 normals의 directions이 cell boundary의 각각에서 같지 않다는 것이 명백하다.

바라건대 해결책이 있다. 만약 유일한 문제가 그 smootstep function의 이차 도함수가 불연속하는 거라면, 그 문제를 고치기 위해 우리가 할 필요가 있는 것은 명백히 2차 도함수가 연속인 interpolant에 대한 함수를 고르는 것이다. smoothstep 함수와 유사한 도형을 가진 것들이 꽤 있지만, Perlin은 이 quintic function을 고른다 (그림 1):

6t^5 - 15t^4 + 10t^3.

그것의 1차 도함수는

30t^4 - 60t^3 + 30t^2

그리고 그것의 2차 도함수는

120t^3 - 180t^2 + 60t

너가 볼 수 있듯이 이 도함수는 연속이다 (t = 0과 t = 1일 때, 그 함수가 0이다).

Choosing random gradients is not ideal and here is why...
그 다음 문제는 gradients가 무작위로 선택되기 때문에, 가끔씩 그 gradient directions는 같은 축을 따라 또는 그 cell의 대각선을 따라 정렬된다 ,그림 2에서 [missing] 보여지듯이. 그 noise function은 이러한 영역 (1에 가까운)에서 매우 높은 값을 갖는 경향이 있고, 그 영역은 부분적으로 원래 Noise implementation의 "splotchy appearance"를 만든다.

바라건대, 이 문제는 서로 다른 12개의 방향의 한 집합으로 256개의 random directions를 대체하여 쉽게 해결될 수 있다 (그리고 이러한 경사들이 같은 방향을 따라서 가리키는 상황을 제외하고). 그 permutation table은 풍부한 randomness를 제공한다. 그래서 실제로, 만약 이러한 12개의 벡터들이 미리 정해져있는지 아닌지를 중요하지 않다. 그의 2002 논문에서 Perlin은 다음의 벡터들을 선택하기로 결정했었다:

(1, 1, 0), (-1, 1, 0), (1, -1, 0), (-1, -1, 0),
(1,0,1,), (-1,0,1),(1,0,-1), (-1,0,-1),
(0,1,1), (0,-1,1), (0,1,-1), (0,-1,-1)

그런데, 우리의 permutation table이 [0,255] 범위의 정수들을 반환한다는 것을 기억해라. 그래서 이러한 벡터들 중 하나를 선택하기 위해서, 우리는 P[Hash[x,y,z]] %12와 같은 것을 할 필요가 있다. 너가 알듯이 뤼가 우리는 modulo operator를 사용하길 좋아하지 않는다. 왜냐하면 비트 연산자보다 더 느리기 때문이고, 그것은 음수를 다르게 처리하기 때문이다 (비트 연산자는 이 문제를 고친다). 그 modulo 연산자를 비트 연산자로 바꾸기 위해서, Perlin은 12개의 벡터들의 배열을 16개의 벡터들로 확장하는 것으로 제안한다. 그래서 다음의 4개의 방향을 처음 12개의 추가한다:

(1,1,0), (-1,1,0), (0, -1,1), (0, -1,-1).

우리는 이미 이러한 방향들을 상요했지만, Perlin은 처음 12개의 방향들이 regular tetrahedron(4면체)를 형성하기 때문에, 이러한 방향을 중복으로 추가하는 것은 텍스쳐에 어떤 bias를 도입하지 않는다고 주장한다.

최종 결과는 original distribution과 같은 non-directional apperance를 갖지만, 덜 clumping(무리지어 지는)이 된다.

splotchy 같은 apperance는 더 이상 보이지 않는다 (아래의 비교이미지를 체크해보아라). 이 변화를 구현하는 것은 실제로 꽤 간단하다. 우리의 해쉬 함수는 [0,255]의 범위의 숫자들을 반환한다. 그래서 우리는 이 숫자를 [0,15]의 범위로 설정하기 위해 또 다른 비트 연산자를 사용한다.

uint8_t p = hash(xi, yi, zi) & 15;

그러고나서 모든 방향들이 0과 1로 설정되어 있기 때문에, 그 내적이 오른손 좌표계 벡터의 좌표의 합으로 간단하게 된다.

// float a = dot(c000, p000);
// float a = c000.x * p000.x + c000.y * p000.y + c000.z * p000.z;
// for the gradient (1, 1, 0) for instance, this simplifies to:
float  a = 1 * p000.x + 1 * p000.y + 0 * p000.z = p000.x + p000.y;

우리는 이 코드를 그것 자신의 함수로 옮길 수 있다.

아래의 이미지는 원래의 Perlin noise 구현과 개선된 버전 사이의 비교를 보여준다:

크게 다르지는 않지만, 두개의 구현은 다른 결과를 준다. 개선된 버전은 vertical and horizontal lines를 만든다. 비록 너가 원래 버전에서 못볼지라도. 그 사실로 인해서, 개선된 버전에서, gradients들은 xy, xz and yz 평면을 따라 정렬읻 ㅚㄴ다. 반면에 interpolant로서 quintic function의 선택은 visual artefacts를 줄어주고 개선물로서 불려질 수 있지만, 우리는 random gradients에 대해 미리 정의된 gradients를 사용하는 것이 반드시 좋은 것이라고 생각하지 않느다. 너는 이 강의의 마지막 챕터에서 개선된 버전의 완전한 구현을 찾을 수 있다.

What's Next?
이것은 Perlin noise에 대한 우리의 강의를 결론짓는다. 이 강의는 꽤 이미 광범위 하지만, 일반적으로 노이즈 함수에 대해 말할 것이 더 많다. 첫 째로, procedural noise를 생성하는 좀 더 많은 방법들이 꽤 존재한다. 몇가지 다른 방법들은 wavelet noise (Pixar에 의해 개발된), Gabor noise, simplex noise이다. simplex noise 또한 Ken Perlin에 의해 개발된 노이즈의 종류이다. 우리는 나중에 이러한 기법들 각각에 대한 강의를 쓸 것이다.

마지막으로, 다른 것들에 비해 noise implementation의 퀄리티를 판단할 수 있는 것이 중요하다. 우리는 그것을 어떻게 하는가? 우리는 이미 한 noise function이 가져야 하는 퀄리티들을 언급해었다. 이러한 퀄리티들 중에서, 그 noise는 가능한 균일한 frequencies의 distribution을 가져야하고, 그러나 여전히 random하고 smooth해 보여야 한다. 한 주어진 noise function이 이 기준에 맞는지를 찾으려는 한 가지 방법은 그것의 output을 frequency 여역에서 분석하는 것이다. 우리는 또한 나중에 기법에 이후의 강의에서 배울 것이다.

How to Use the Noise Function?
일반적으로 procedural noise function은 많은 것들에 사용될 수 있다:

  • Terrain : 이 강의에서, 우리는 그 함수 자체의 구현에 대해서 공부했다. 비록 우리가 또한 테레인 같은 것을 만들기 위해서 그것을 어떻게 사용하는지를 보여줬을지라도. 우리는 noise function을 사용하여 아직 정말 복잡하고 현실적인 surface details를 가진 테레인을 어떻게 만드는지 보여주지 않았다. 우리는 이 기법을 아직 나오지 않은 별개의 강의에서 보여줄 것이다.
  • Water surfaces: 그것은 또한 water surface를 재현하는데 사용될 수 있다. 그 입력 점을 y좌표를 offsetting하여, 파도같은 이동된 표면을 animate하는 것이 가능하다. 이것은 잘 작동하는 간단한 기법이다. 비록 water surfaces를 재현하는 더 좋은 방법들이 있을지라도.
  • Cloud : 구름 같은 volumes에 surface details를 추가하는데 사용될 ㅜㅅ 있다. 우리는 다양한 cloudy shapes의 외관을 재현하기 위해 volume rendering의 강의에서 noise function을 사용한다.
  • Texturing : 마지막으로 표면에 texture details를 더하기 위해 사용될 수 있다. 컬러, specular, 같은 쉐이더의 입력을 조절하거나, 한 오브젝트의 표면에 bump를 추가하기 위해 사용될 수 있다.
  • Animation: noise function은 또한 애니메이션에 어떤 noise를 더하기 위해 사용되어질 수 있다. 예를들어 일반적인 사용 예시는 어떤 카메라 shakes를 재현하기 위해 camera animation에 noise를 더하는 것이다.


===================================================
Simple Directional Light를 추가해서, Perlin Noise Generator로 렌더링했다.


















Perlin Noise: Part 2, Computing Derivatives

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/perlin-noise-computing-derivatives

Computing Derivatives
직관적인 방법으로 Perlin noise method를 설명하는 책과 웹의 문서들이 거의 없다; 그러나  Perlin noise derivatives를 어떻게 연산하는지에 대한 정보를 찾는 것은 심지어 더 어렵다 (특히, analytical 방법으로). 이러한 derivatives가 무엇인지 모르고 그것이 왜 유용한지 모르는 사람들에게, derivatives를 빠르게 봐보자.

A Quick Introduction to (Partial) Derivatives
어떤 함수의 Derivatives(도함수) (그것이 1, 2, 3 차원의 함수이든), 매우 유용하다. 그러나 우리가 예제를 주기전에, 처음에 그것들이 무엇인지 한 번 봐보자. 만약 너가 2D noise의 한 image를 만들고, 그 이미지의 top위에 어떤 regular grid를 적용한다면, 그러면 우리는 그 noise function이 x와 y 방향을 따라 그리드의 각 점에서 얼마나 변하는지를 알기를 원할지도 모른다 (그림1). 아마도 이 아이디어는 벌써 친숙하게 들리지 않는가? 이전 챕터에서, 우리가 한 mesh를 옮기기 위해 2D noise function의 결과를 사용했다는 것을 기억해라. 그러나, 우리가 여기에서 얻으려고하는 것으로 돌아가보자: 어떻게 x축과 y축을 따라서 우리의 2D noise function의 변화율을 알 수 있는가? 이 문제에 대한 갇난한 솔루셔은 그 점에서 noise 값을 취하는 것으로 구성되는데, 그 점에서, 너는 이 변화를 연산하길 원한다 (이 점은 Gn_x라고 부르자), 그리고 Gn_x의 한 칸 옆에 있는 점의 noise value를 취하고 (이것을 두 번쨰 점Gn_x+1 이라고 부르자), 그리고 두 번째 값을 첫 번째 것으로 부터  뺀다. 예를들어, 만약 Gn_11에 있는 noise가 0.1과 같고, 그 Gn_12에 있는 noise value가 0.7이라면, 그러면 우리는 noise가 Gn_11에서 Gn_12로 (x축을 따라) 0.6만큼 변했다고 가정할 수 있다 (그림1). 방정식의 형태로, 우리는 다음을 쓸 수 있다:



물른 y축을 따라 변화율을 연산하기 위해서, 우리는 같은 것을 할 것이지만, 아래로 내려가야 할 것이다. 우리의 방정식을 다음처럼 보일 것이다



기술적으로, 이 차이를 normalize하는 것이 최고인데, 우리가 grids들에 있는 두 점을 분리하는 거리에 상관없이 일관된 결과를 얻로록 하기위해서 이다 (만약 그 결과가 normalized 되어있따면, 다른 grid spacing으로 만들어진 measurements가 그러고나서 서로서로 비교되어질 수 있다). 이 결과를 normalize하기 위해서, 우리는 G_11과 G_12사이의 거리로 그 차이를 나눌 필요가 있다. 그래서 만약 grid에 있는 두 점 사이의 거리가 예를들어 2라고 하면, 그러면 우리는 다음을 쓸 수 있다:




수학에서, 이 기법은 forward difference라고 불려진다. 왜냐하면 우리가 다음 연산될 점을 취하고, 다음 점의 값으로 부터 현재의 점에 있는 값을 빼기 때문에 forward라고 한다.

Backward differencing은 또한 가능하다: 너는 당므 점 대신에 이전 점을 사용한다. 너는 또한 central difference를 사용할 수 있다.

수학적으로, 우리는 이 개념을 다음의 방정식으로 공식화한다:



f'(x) 표기는 이것이 f(x)함수의 도함수라는 것을 의미한다.

이것은 우리가 그 함수 f(x)의 도함수를 우리가 방금 소개했던 forward difference 기법을 사용하여 연산할 수 있다는 것을 의미한다. 그러나 이 도함수 값이 두 점 사이의 거리가 더 작아질 수록 (이론 상으로 h가 0을 향한다면) 더 정확해진다는 것도 의미한다. 그 spacing(간격)이 클 때, 너는 주어진 점 x에서 도함수 값에 대해 어느정도 매우 조잡한 값을 얻게 된다. 그러나 그 spacing이 작아질 수록, 이 근사는 향상된다. 우리의 noise image의 경우에, 그 grid의 spacing은 꽤 크다. 그래서 사실, 너는 만약 그 grid spacing이 더 작다면 (그림2), grid의 각 점에서 noise function의 변화에 대한 더욱 좋은 근사를 얻을 것이다.

이 개념은 1D example로 더 쉽게 이해된다. 그림 3은 1차원 함수의 profile을 보여준다. 우리가 이제 이 함수가 P의 proximity(가까움) 내에서 얼마나 변하는지를 알길 원한다고 가정하자. forward differencing의 원리를 사용해서, 우리는 example x1과 같은 x축을 따라 한 점을 취할 수 있고, 그 점에서 그 함수의 값을 연산하고, 그래소 f(x1) 를 f(x)로 뺀다. 우리가 f(x0에서 f(x1)으로 가는 한 line을 따라 갈 때, 그 line은 x에서 그 함수에 tangent이다. 그것은 왜냐하면 사실 1차원 함수의 도함수는 그 함수에 대해 접선인 직선의 기울기(slope)를 준다. 그 함수에서, 그 함수의 도함수가 연산된다. 그래서 우리는 어떻게 기하학적으로 한 함수의 도함수를 해석할지를 알았으니, 너는 쉽게 만약 우리가 x1보다 더 먼 점을 취한다면, 예를들어 x2, 그러면 x와 x2 사이의 그 직선이 x에 x-x1보다 접선이지 않다는 것을 알 수 있다. 결론으로, 만약 너가 한 함수의 도함수를 연산하기 위해 forward difference를 사용한다면, 그러면 x와 x+h사이의 거리가 더 작을수록 더 좋다.

우리가 이제까지 무엇을 배웠는가?

  • 우리는 일차원 함수 f(x)의 도함수 f'(x)가 x에서 f(x)의 접하는 line의 slope로서 해석될 수 있다는 것을 배웠다.
그 기울기 외에도, 도함수 f'(x)는 또한 x에서 함수 f(x)의 instantaneous 변화율로 고려될 수 있다.
  • 우리는 또한 그 기울기의 "근사"를 연산하기 위해 우리가 forward difference라고 불리는 기법을 (일반적인 방법은 finite difference라고 불려진다) 사용할 수 있다는 것을 배웠다. 뿐만 아니라, 이 근사가 forward difference 방정식에서 h가 더 작아질수록 더 좋아진다고 배웠다.
우리가 어떻게 이 챕터의 나중에서 도함수를 사용할지 (실제로 편미분)에 대해 이해하기 위해서 이해해야할 정말 중요한 것이 있다. 지금까지, 우리는 한 함수의 도함수가 x의 어떤 값에서 함수 f(x)의  slope로서 해석될 수 있다고 설명했다. 우리가 x에서 (우리가 함수 f(x)의 도함수를 연산하는 곳) 그 접선을 추적하는 방식은 간단히 그 함수의 점에서 y = mx의 직선을 그리는 것인데, 그 함수에서, 우리는 그 함수의 도함수를 연산했다. 그 m 값은  여기에서 물론 우리가 연산한 함수의 slope이다. 왜 이것이 중요한가? x = 1일 때 y = m이라는 것을 주목해라. 이것은 이 관찰을 사용해서, 우리가 그 점에서 곡선의 2D vector tangent가 Vec2f(1, m)과 같다고 말할 수 있다. 너는 동의 하는가? 물론, 그러고나서 너는 이 벡터를 표준화 할 필요가 있지만, 그럼에도 불구하고, 이 벡터가 그 점에 어떻게 접하는지를 주목해라. 이 아이디어를 이해하는 것이 중요하다 (그림 4에서 보여지는).

너는 3D perlin noise function을 서로에 수직인 2개의 2D functions이라고 볼 수 있다. 그래서 만약 원한다면, xy 평면에서 2D function의 도함수를 연산하는 1D function을 가질 것이고, yz 평면에서 2D noise function의 도함수를 연산하는 또 다른 1D function을 가질 것이다. 그림 5에서 보여지듯이. 이제 만약 우리가 이러한 함수들의 각각에서 접선을 연산하기 위해서 배웠던 기법을 적용한다면, 두 개의 얻어진 접선들이 서로에 수직하다는 것에 주목해라. 그러나 좀 더 흥미롭게도, 이러한 두 벡터의 외적을 취해서, 너는 2D noise function이 원래 연산되는 그 점에 접하는 평면에 수직인 벡터를 얻게된다. 이 벡터가 P에서 우리의 함수의 normal이다. 바라건데, 너가 그것을 이해하기 시작하기 바란다. 이러한 도함수들은 3D or 2D noise function으로 옮겨진 우리의 mesh의 normals를 연산하는데 유용할 것이다.

이것은 중요한 결과인데, 왜냐하면 이것은 우리가 3D noise function에 의해 옮겨진 한 mesh의 normal를 어떻게 연산할지에 대한 방법이기 때문이다. 우리는 어떤 주어진 점 (가령 P)에서 그 함수의 도함수를 evaluate할 것이고, 그러고나서 그 x축을 따라서 P에 접하는 벡터를 연산할 것이다 (이 벡터를 T_x라고 하자). 그러고나서 우리는 P에서 그 z축을 따라서 noise function의 도함수를 연산할 것이다. 그러고나서, z축을 따라서 P에 접선을 연산할 것이다 (이것을 T_z라고 부르자). 마지막으로 우리는 P에서의 normal를 구하기 위해서 외적에서 그 두 벡터들을 사용할 것이다:


이것은 간단하고 우아하다. 이제 한 가지 말할 것과 질문이 있다.

Remark : 우리가 여기에서 noise function의 도함수를 정말로 연산하지 않는다느 ㄴ것에 주목해라. 우리는 어느정도 x축을 따라서 그 함수의 도함수를 연산하고 그러고나서 z축을 따라서 또 다른 도함수를 연산하여 cheat를 한다. 흥미로운 것은, 우리가 그것을 할 때, 그 양의 오직 하나만이 변한다. 예를들어, x축을 따라서 3D noise function의 도함수를 연산할 때, 물론 우리가 얻는 값은 z축을 따라서 그 값의 변화 때문에 변하지 않는다 ( 왜냐하면 우리는 xy 평면에서 그 noise function을 evalutate하기 때문이다 - z에서 어떠한 변화도 없다고 알려져있듯이). 수학에서, 너가 몇 가지 변수가 있는 함수를 가질 때, 그러나 너가 그것의 변수들 중 하나로만 관련하여 그것의 도함수를 연산할 때, 다른 것들은 상수로 고정되고, 그러면 우리는 그 함수의 partial derivatives를 연산한다고 말한다. 예제를들어 보자. 만약 너가 다음의 함수를 가지고 있다면:


그러고나서 만약 너가 y를 상수로 유지하면서 그 함수의 도함수를 연산하길 원한다면, 너는 다음을 얻는다:


너는 그 함수를 x와 관련된 f(x,y)의 편도함수라고 읽을 수 있다. 다시 말해서, 우리는 우리의 예제에서 y^2 같은 x가 나타나지 않느 항들을 모든 항들을 무시하는데, 그러고나서 x가 나타나는 각 항에 대해 x의 도함수를 연산한다. 예를들어, 만약 그 항들 중 하나에서, 우리가 x^2y를 가지고 있다면, 우리는 그 편도함수에서 그것을 2xy로 바꾼다. 만약 우리가 xy를 가진다면, 우리는 그 항을 편도함수에서 y로 바꾼다. 간단하지 않은가?

질문: 우리는 이러한 편도함수들을 어떻게 계산하는가?

한 방법은 forward difference technique을 사용하는 것으로 구성된다 (그리고 이것이 우리가 이 챕터의 초기에 그것에 대해 배운 이유이다). 만약 우리가 우리의 옮겨진 메쉬들의 예제를 가지고 온다면, 그러면 우리는 vertex V_x+1,x에서 noise value의 도함수를 Vx,z정점의 noisevalue로부터 빼서 vertex Vx,z에서 그 도함수를 연산할 수 있다 (그림 6). 다시 말해서, 우리는 다음으로 쓸 수 있다:

그 질문은 이제, 우리가 이 실수 값을 어떻게 벡터로 (도함수가 x와 z축을 따라 연산되는 점에서의 noise function에 접하는 vector) 변환하는가 이다. 이것은 간단하다. 우리가 x축을 따라서 편도함수를 연산할 때, xy 평면에서 작업한다는 것을 깅거해라. 따라서, 그 x축을 따라가는 noise function에 접하는 벡터의 z좌표는 반드시 0이다:

T_x = {?, ?, 0}.

다른 두 좌표들을 연산하기 위해서, 너는 다시 그림 4를 볼 필요가 있다. 거기에서 우리는 한 평면에서 그 함수의 접선을 연산하기 위해서, 너는 x 좌표를 1로 설정하고 그 벡터의 y좌표를 편도함수 값으로 설정할 필요가 있다 (만약 너가 yz평면에서 그 벡터의 접선을 연산하기 원한다면, 그러면 너는 그 접선으 z좌표를 1로 설정하고, 그 벡터의 y좌표를 z와 관ㄹ녀된 편도함수의 값으로 설정하고, 접선 벡터의 x좌표를 0으로 설정한다). 마지막으로 우리는 다음을 갖는다:

T_x = {1, N_x, 0}
T_z = {0, N_z, 1}.


마지막으로, 그 정점의 normal를 연산하기 위해서, 너가 이제 할 필요가 있는 것은 이러한 두 벡터의 외적을 연산하는 것이다 (그 외적의 결과는 두 개의 input vectors가 표준화 되어 있지 않더라도 올바르다: 그 최종 벡터가 두 개의 입력 벡터에 대해 수직일 것이다, 비록 그것이 자체로 표준화되어있지 않을지라도):



이 기법은 훌륭한 작동하지만 :

  • 우리가 grid의 edges에 있는 정점에 대한 도함수를 연산하길 원할 때 처음에 무슨일이 발생하는가? 우리는 할 수 없다.
  • 우리느 또한 (그림 3) forward difference를 사용하여 도함수를 연산할 때 두 샘플들 사이의 공간이 더 작을 수록, 그 결과가 더 정확해진다고 설명했었다. 다시 말해서, 그 정점들 사이의 공간이 더 클 수록, 편도함수의 연산은 덜 정확해진다. 이 챕터의 나중에, 우리는 forward difference로 연산된 편도함수와, 우리가 이제 공부할 analytical solution 사이의 차이를 보여줄 것이다.
Analytical Partial Derivatives of the Perlin Noise Function
그래서 이러한 편도함수를 연산하는 더 좋은 방법이 있다. 이 기법은 오직 수학에만 의존하고, "정확한" 솔루션을 제공한다 (그 항의 수학적인 의미에서). quick remider: 다음의 방정식의 편도함수:


x축에 관해서는:

이제 그 Noise function을 조금 다시 써보자. 모든 내적을 문자로 바꿔보자 (아래에 보이듯이):


a = dot(c000, p000)
b = dot(c100, p100)
c = dot(c010, p010)
d = dot(c110, p110)
e = dot(c001, p001)
f = dot(c101, p101)
g = dot(c011, p011)
h = dot(c111, p111)

연습을 위해서, 파라미터들 u,v,w들이 다음과 같이 연산된다는 것을 다시 기억해보자 (우리는 smoothstep function을 사용한다):

u = tx * tx * (3 - 2 * tx)
v = ty * ty * (3 - 2 * ty)
w = tz * tz * (3 - 2 * tz)

이제 Perlin noise function의 다른 interpolations들을 하나의 single line으로 써보자 (첫 번째 챕터를 보라):

lerp(
   lerp(
       lerp(a, b, u),
       lerp(c, d, u),
       v)
   lerp(
       lerp(e, f, u),
       lerp(g, h, u),
       v),
   w)

lerp(a,b,t)의 호출을 실제 코드 (a(1-t) + bt)로 바꿔보자:

((a(1 - u) + bu)(1 - v) + (c(1 - u) + du)v)(1 - w) +
((e(1 - u) + fu)(1 - v) + (g(1 - u) + hu)v)w

((a - au + bu)(1 - v) + (c - cu + du)v)(1 - w) +
((e - eu + fu)(1 - v) + (g - gu + hu)v)w

(a - au + bu - av + auv - buv + cv - cuv + duv)(1 - w) +
(e - eu + fu - ev + euv - fuv + gv - guv + huv)w

a - au + bu - av + auv - buv + cv - cuv + duv -
aw + auw - buw + avw - auvw + buvw - cvw + cuvw - duvw +
ew - euw + fuw - evw + euvw - fuvw + gvw - guvw + huvw

그러고나서 마지막으로 그 항들을 다음과 같이 묶어보자:

a + u(b - a) + v(c - a) + w(e - a) +
uv(a + d - b - c) + uw(a + f - b - e) + vw(a + g - c - e) +
uvw(b + c + e + h - a - d - f - g)

너도 볼 수 있듯이 (그리고 예ㅊ측되듯이) 이것은 세 함수들의 한 함수이다 :u ,v 그리고 w. 만약 우리가 변수들 중의 하나와 관련하여 한 함수의 편도함수를 연산하기 위해 배웠떤 기법을 적용한다면, 우리는 궁금한 변수를 포함하지 않는 모든항을 제거할 필요가 있고, 나머지 항에서 그 변수를 그것의 도함수로 대체 한다. 예를들어, 만약 우리가 u와 관련하여 noise function 편도함수를 연산하길 원한다면 우리는 다음을 얻는다:

u'(b - a) +
u'v(a + d - b - c) + u'w(a + f - b - e) +
u'vw(b + c + e + h - a - d - f - g)

u'((b - a) + v(a + d - b - c) + w(a + f - b - e) + vw(b + c + e + h - a - d - f - g))

유사하게, v와 w와 과련된 편도함수들은:

// partial derivative with respect to v
v'(c - a) +
uv'(a + d - b - c) + v'w(a + g - c - e) +
uv'w(b + c + e + h - a - d - f - g)

v'((c - a) + u(a + d - b - c) + w(a + g - c - e) + uw(b + c + e + h - a - d - f - g))

// partial derivative with respect to w
w'(e - a) +
uw'(a + f - b - e) + vw'(a + g - c - e) +
uvw'(b + c + e + h - a - d - f - g)

w'((e - a)  + u(a + f - b - e) + v(a + g - c - e) + uv(b + c + e + h - a - d - f - g))

남은 질문은 u', v' 그리고 w'의 도함수가 무엇이냐는 것이다. 간단한 u, v, w는 다음으로 연산된다:

u = 3tx^2 - 2tx^3
v = 3ty^2 - 2ty^3
w = 3tz^2 - 2tz^3

따라서 이러한 함수들의 도함수들은:

u' = 6tx - 6tx^2
v' = 6ty - 6ty^2
w' = 6tz - 6tz^2

해냈다1 너가 이제 필요한 모든 것은 이러한 도함수들을 연산하고, 그러고나서 우리가 여기에서 제공한 기법을 사용하여 그 함수가 evaluated되는 그 점에 접선이 벡터들을 구하는 것이다. 여기에 3D noise function을 evalutes하고 주어진 위치에서 그것의 편도함수를 구하는 eval function의 변경된 버전이 있다:


 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
float eval(const glm::vec3& p, glm::vec3& derivs) 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);

 float a = dot(c000, p000);
 float b = dot(c100, p100);
 float c = dot(c010, p010);
 float d = dot(c110, p110);
 float e = dot(c001, p001);
 float f = dot(c101, p101);
 float g = dot(c011, p011);
 float h = dot(c111, p111);

 float k0 = (b - a);
 float k1 = (c - a);
 float k2 = (e - a);
 float k3 = (a + d - b - c);
 float k4 = (a + f - b - e);
 float k5 = (a + g - c - e);
 float k6 = (b + c + e + h - a - d - f - g);

 float du = smoothstepDeriv(tx);
 float dv = smoothstepDeriv(ty);
 float dw = smoothstepDeriv(tz);

 derivs.x = du * (k0 + v * k3 + w * k4 + v * w * k6);
 derivs.y = dv * (k1 + u * k3 + w * k5 + u * w * k6);
 derivs.z = dw * (k2 + u * k4 + v * k5 + u * v * k6);

 return a + u * k0 + v * k1 + w * k2 + u * v * k3 + u * w * k4 + v * w * k5 + u * v * w * k6;
}

Analytical Solution vs Forward Difference
우리는 이제 그 옮겨진 mesh의 두 개의 버전을 compete할 수 있다. 하나는 그 mesh의 vertex normals를 계싼하기 위해 geometric solution을 사용한 것. 그리고 analytic solution을 사용한 것. 이러한 solutions들 중 하나를 연산하는 코드는 다음과 같다:ㅣ

{
Analytical Normals

Vec3f tangent(1, derivs.x, 0); // tangent
Vec3f bitangent(0, derivs.z, 1); // bitangent
// equivalent to bitangent.cross(tangent)
poly->normals[i] = Vec3f(-derivs.x, 1, -derivs.z);
poly->normals[i].normalize();

Geometry Normal
세점으로 이루어진 삼각형 두 변을 이용하여 외적을 해서 normal

}

여기에 그것들의 연관된 vertex normals가 있는 두 개의 메쉬들의 이미지가 있다 (오른쪽에 보여지는):

vertex normals들이 mesh의 edge에서 정의되지 않았다는 것에 주목해라. 거기에서 normals들이 forward difference method를 사용해서 연산되었다 (geometric solution). 또한 vertex normals의 방향이 상당히 두 메쉬 사이에 다르다는 것에 주목해라 비록 그것들의 모야이 같을지라도). 이것은 명백히 두 개의 메쉬들의 shading이 눈에 띄게 또한 다르게 한다 (normals이 shading에 사용된다는 것을 기억해라).

2018년 12월 27일 목요일

Perlin Noise: Part 2, Using Perlin Noise to Create a Terrain Mesh

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/perlin-noise-terrain-mesh

Using Perlin Noise to Create a Terrain Mesh
이 챕터에서, terrain을 만드는 mesh의 vertex를 옮기기 위해서 2D Perlin noise를 사용하는 것으로 구성된 재미있는 기법을 배울 것이다. noise에 대한 첫 번째 강의에서 언급했듯이, noise function은 매우 유용한 "procedural texture" primitive인데, 거기에서 좀 더 복잡한 procedural textures들이 만들어질 수 있따, 예를들어 fractal or turbulence pattern같은. 우리는 우선 Perlin noise를 사용할 것이고, 그러고나서 대신에 fractal pattern을 사용하여 생성된 terrain의 예시를 보여줄 것이다.

이 기법은 다음 챕터의 주제인 Perlin noise function의 도함수를 연산하는 것의 중요성을 더 잘 이해하는데 유용할 것이다.

이 챕터를 읽은 후에, 너는 아래의 이미지를 만들어낼 수 있을 것이다.

이 기법 뒤에 있는 아이디어는 매우 간단하고, 우리가 displacement mapping이라고 부르는 것과 유사하다. 만약 너가 위에서 grid를 바라본다면, 만약 너가 noise image를 grid로 덮어씌운다면 너는 완벽히 일치한다는 것을 볼 수 있을 것이다 : grid의 각 정점이 noise image의 pixel과 일치한다. 너가 알듯이, 우리는 픽셀들의 좌표를 어떤 normal device coordinates의 종류에 정의할 수 있다 (pixel 좌표들은 그러고나서 [0.1]의 범위에 있다). 그 같은 것은 grid vertices에 되어질 수 있다: 이러한 것은 기술적으로 texture coordinates라고 불려진다. grid를 만들기 위해 우리가 사용할 함수들을 봐보자:


 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
PolyMesh* createPolyMeshPlane(
 uint32_t width = 1,
 uint32_t height = 1,
 uint32_t subdivisionWidth = 40,
 uint32_t subdivisionHeight = 40
)
{
 PolyMesh* poly = new PolyMesh;
 poly->numVertices = (subdivisionWidth + 1) * (subdivisionHeight + 1);
 poly->vertices = new Vec3f[poly->numVertices];
 poly->st = new Vec2f[poly->numVertices];

 float invSubdivisionWidth = 1.f / subdivisionWidth;
 float invSubdivisionHeight = 1.f / subdivisionHeight;
 for (uint32_t j = 0; j <= subdivisionHeight; ++j)
 {
  for (uint32_t i = 0; i < subdivisionWidth; ++i)
  {
   poly->vertices[j * (subdivisionWidth + 1) + i] = Vec3f(width * (i * invSubdivisionWidth - 0.5), 0, height *(j * invSubdivisionHeight - 0.5));
   poly->st[j * (subdivisionWidth + 1) + i] = Vec2f(i * invSubdivisionWidth, j * invSubdivisionHeight);
  }
 }

 poly->numFaces = subdivisionWidth * subdivisionHeight;
 poly->faceArray = new uint32_t[poly->numFaces];
 for (uint32_t i = 0; i < poly->numFaces; ++i)
  poly->faceArray[i] = 4;

 poly->verticesArray = new uint32_t[4 * poly->numFaces];
 for (uint32_t j = 0, k = 0; j < subdivisionHeight; ++j)
 {
  for (uint32_t i = 0; i < subdivisionWidth; ++i)
  {
   poly->verticesArray[k] = j * (subdivisionWidth + 1) + i;
   poly->verticesArray[k + 1] = j * (subdivisionWidth + 1) + i + 1;
   poly->verticesArray[k + 2] = (j + 1) * (subdivisionWidth + 1) + i + 1;
   poly->verticesArray[k + 3] = (j + 1) * (subdivisionWidth + 1) + i;
   k += 4;
  }
 }

 return poly;
}

vertex의 텍스쳐 좌표는 line 16에서 연산된다. (원문기준). 이것은 또한 좌표들이 [0, 1]의 범위에 있는 한 공간이다. gird의 upper left corner에 있는 vertex는 텍스쳐 좌표로 (0,0)이다. 반면에, lower-right coordinate에 있는 정점은 텍스쳐 좌표[1,1]이다. 따라서 noise image에서 lookup하기 위해 vertex texture coordinates를 사용하는 것이 쉽게 된다.

이 예제에서, 우리가 2D noise function으로 부터 값을 읽기위해 noise image를 사용하지만, 만약 원한다면 우리가 2D noise function을 직접적으로 evaluate를 할 수 있따는 것에 주목해라. 우리는 2D noise function의 결과를 image file로 output하기 위해 첫 번째 챕터에서 만든 그 배열을 재사용하기로 결정했다. 한 image가 한 오브젝트의 정점을 움직이게 하기 우해 사용될 때, 우리는 이 이미지를 height map이라고 말한다.

height map의 경우에, 우리는 일반적으로 displacement의 amplitude를 제어하기 위해 픽셀들의 color의 밝기 (예를들어, luminance)를 사용한다. 일반적으로 픽셀이 밝을수록, displacement는 더 커진다. 비록 물론 너가 pixel value를 너가 바란다면 완전히 다른 방식으로 displacement로 map할 수 있을지라도. 그것은 모두 너가 만들고자 의도하는 효과에 달려있따. 너가 기억할 필요가 있는 것은 양을 어느정도 조절하기 위해 한 이미지를 사용한다는 것이고. 그 양에서, 오브젝트들의 정점들이 옮겨지거나 예를들어, 그것들의 normal을 위해 움직여진다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
for (unsigned j = 0; j < imageHeight; ++j)
{
 for (unsigned i = 0; i < imageWidth; ++i)
 {
  // Perlin noise is in the range [-1:1]
  float perlinNoise = PerlinNoise::evalAtPoint(Vec3f(i, j, 0) * (1 / 128.f));
  noiseMap[j * imageWidth + i] = (perlinNoise + 1) * 0.5;
 }
}

// displace
for (uint32_t i = 0; i < poly->numVertices; ++i)
{
 Vec2f st = poly->st[i];
 uint32_t x = std::min(static_cast(st.x * imageWidth), imageWidth - 1);
 uint32_t y = std::min(static_cast(st.y * imageHeight), imageHeight - 1);
 poly->vertices[i].y = 2 * noiseMap[y * imageWidth + x] - 1;
}

Perlin noise의 값들이 [-1,1]의 범위에 있다는 것을 명심해라. 그러나 우리는 이러한 값들을 [0,1]로 remapped한다. 우리가 이미지 버퍼에 그 값들을 저장할 때 (line 5).

우리가 (line 15)에서 정점들을 나중에 옮길 때 그 값들이 다시 [-1,1]로 mapped될 지라도, 그 mesh는 y축을 따라서 원점 주변에 있을 것이다. 우리는 그 정점들을 위 (만약 그 값들이 0보다 크다면) 또는 아래로(그 값들이 0보다 작다면) 밀 것이다. 그리고 만약 그 값이 0 이라면 그 정점 y좌표는 0으로 될 것이다.

만약 너가 이 mesh를 texture map으로서 꼭대기에 noise image와 함께 렌더링한다면, 너는 이 챕터의 첫 번째 이미지와 비슷한 것을 얻는다. noise image의 white/bright area가 mesh에서 bump와 어떻게 일치하는지를 주목해라. 반면에 image에서 어두운 부분들은 dents or valleys와 일치한다 (그리고 displacement의 양은 그 픽셀 값에 비레한다는 것에 주목해라).

이 챕터의 introduction에서 언급되었듯이, 너는 mesh vertices를 옮기기 위해 좀 더 흥미로운 procedural patterns을 사용할 수 있다, fractal pattern같은. 그리고 이것은 noise layers들의 weighted sum으로 구성될 수 있다. noise function을 사용하여 fractal pattern을 생성하는 것을 배우기 위해 noise에 대한 이전 강의를 체크해라. 여기에 mesh를 옮기기 위해 사용된 fractal image를 생성하는 코드가 있다.

fractal image는 일반적으로 single layer를 가진 noise에서 보다 더 높은 frequency의 세부사항을 포함한다. 따라서, displacement에서 이러한 세부사항을 보기 위해, 너가 그 mesh의 density를 그 자체로 증가시켜야만 할 가능성이 높다. 여기에 fractal image로 displaced 된 mesh의 render가 있다.

이전 강의에서 제안되었듯이, 이 기법은 현실적인 terrains을 생성하는데 사용될 수 있다 (바라건데 위의 이미지가 충분히 설득적이기를 바란다. 이 예제에서 우리가 사용한 (fractal) procedural pattern은 꽤 간단하다. 너는 terrain의 look을 변경하기 위해 그 파라미터들 가지고 놀 수 있지만, 우리는 또한 terrain의 realism을 증가시키기 위해 erosion같은 효과들을 어떻게 추가할지를 배울 것이다.

final note로서, 우리는 displacement후에 mesh의 normal를 연산하지 않았다. 우리는 어떻게 그것을 하는가? 이것은 우리의 다음 챕터의 주제이다. 우리는 displacement 후에 vertex position에서 "true" normal를 연산하기 위해 noise function derivatives를 어떻게 사용하는지를 배울 것이다.

===================================================
여기까지 하는데 시간이 걸렸지만, 일단은 Terrain 그렸다.












2018년 12월 26일 수요일

Perlin Noise: Part 2, Perlin Noise

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2

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 사이를 보간할 것이다. 예를들어:
X0 = (int)(-1) & 255; // = 255( (right corner of the 3D lattice of last cell)
X1 = (X0 + 1) & 255; // = 0 (left corenr of the 3D lattice of first cell)
  • 내적은 [-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 강의를 체크해라).