Post Lists

2018년 12월 25일 화요일

Value Noise and Procedural Patterns: Part 1, Creating a Simple 1D noise

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1/creating-simple-1D-noise

Value Noise and Procedural Patterns: Part 1

Creating a Simple 1D Noise

noise는 어떤 입력 x에 대해 [0:1]의 범위의 float를 반환하는 함수이다 (x는 float, 2D, 3D, or 4D 점이 될 수 있지만, 이 챕터에서, 우리는 오직 1D case만 볼 것이다. 우리의 입력 position은 또한 양수 그리고 음수가 될 수 있고, 0에서 무한 또는 음의 무한으로 확장될 수 있다). 우리는 random number generator를 사용하여 이러한 floats를 생성할 수 있지만, 첫 번째 챕터에서 그 함수가 호출될 때 마다 서로 다른 값들을 반환한다고 설명했다. 이것은 texture generation에 적절하지 않은 white noise라고 알려진 패턴을 만든다 (white noise patterns는 smooth하지 않지만, 반면에 대부분의 자연의 패턴들은 smooth하다).

우리가 대신에 할 것은 규칙적인 간격으로 공간을 차지하는 (drand48()를 사용하여) random points의 연속을 만드는 것이다. 만약 너가 2D에서 작업한다면, 우리는 regular grid의 정점들에 이러한 랜덤 값들을 만들고, 만약 너가 1D에서 작업한다면 이 grid는 ruler처럼 보일 수 있다. 상황을 간단하게 하기 위해, 우리는 그 grid의 정점들 또는 ruler에 있는 ticks들이 x와 y축을 따라서 좌표에 만들어진다고 가정한다. 그리고 그 축은 정수값을 가진다. 이러한 랜덤 숫자들은 이러한 위치에 오직 한 번씩 생성된다 (noise function이 초기화 될 때).

만약 우리가 우리의 1D 예제로 계속한다면, 우리는 우리가 일련의 dots 또는 ruler에 integer 위치에 정의된 값들과 함꼐 있는 것을 볼 수 있따. 예를들어, x = 0에 대한 그 함수의 결과가 0.36이고, x = 1에 대한 겨로가가 0.68 등등이다. 그러나 x가 정수가 아닐 때의 그 함수의 결과는 무엇인가? x 축에서 어떤 점에 대한 한 값을 연산하기 위해서, 우리가 할 필요가 있는 모든 것은 두 개의 가장 근접한 integer positions을 x축에서 이 입력값에 대해 찾는 것이고, 이러한 두 개의 위치와 관련된 랜덤 값들을 사용하는 것이다. 예를들어, 만약 x가 0.5와 같다면, x를 둘러싸는 정수 값을 가진 두 점들은 0 과 1이다. 그리고 이러한 두 점들은 연관된 랜덤 값 0.36과 0.68을 가진다. x = 0.5일 때 x에 대한 함수의 결과는 점 1에서 정의된 0.36값 과 점 2에서 정의된 0.68값의 mix이다. 이 숫자를 연산하기 위해, 우리는 linear interpolation 라고 불려지는 interpolation technique을 사용할 수 있다. 선형 보간은 a와 b의 값들의 어떤 값 t에 대해서 한 mix를 반환하는 간단한 함수이다.  t는 [0:1]의 범위이다.

우리의 noise function의 겨웅에, 우리는 a와 b를 x를 둘러싸는 정수 위치에 정의된 랜덤 값으로 대체할 것이다 (만약 x가 1.2라면 그 x = 1과 x = 2에 대한 랜덤 값들을), 그리고 x로부터 t를 연산할 것인데, 간단히 x 그자 체로부터 x에 대해 발견되ㅐㄴ 최소 정수 값을 빼서 된다. (t = 1.2 - 1 = 0.2).


int xMin = (int)x;
float t = x - xMin;

그리고 선형 보간법을 사용하는 한 값을 연산하는 코드가 여기에 있다. 대부분의 shading language에서 이 함수는 left라고 불려진다 (물론 linear interpolation을 위해서):


template<typename T = float>
inline T lerp (const T& lo, const T& hi, const T& t)
{
   return lo * (1 - t) + hi * t;
}

mix function은 CG 프로그래머들에 의해서 보통 Lerp 함수로 (linear interpolation) 알려져  있다. 만약 너가 한 renderer의 소스코드에서 Lerp function을 찾거나 또는 한 책에서 언급된 것을 찾는다면, 너는 그것이 여기에서 사용된 mix function과 같은 것이라는 것을 알아야 한다.

범위 [0:1]에 있는 어떤 x에 대해, linear interpolation을 사용하여 한 값은 연산하는 것은 점1과 점2까지의 직선을 그리는 것과 같다. 만약 우리가 범위 [1:2], [2:3] 등등에 있는 모든 점들에 대해 이 프로세스를 반복한다면, 우리는 그림 4의 곡선을 얻는다. 너는 이제 왜 우리가 이러한 유형의 noise를 value noise라고 부르는지 이해할지도 모른다 (noise는 어떤 다른 방법들로 만들어질 수 있다). 그 아이디어는 ruler (1D) or a grid(2D)에서 규칙적인 간격에서 어떤 값을 만들고, linear interpolation을 사용하여 그것들을 보간하는 것이다.

우리는 이제 매우 간단한 noise function을 만드는데 필요한 모든 bits and pieces를 가졌다. 그 함수를 초기화 할 때, 우리는 float array에 저장할 random values의 series를 만들 것이다 (figure 4의 바닥 박스에 쓰여져있는 숫자들). 너가 코드에서 볼 수 있듯이, 그 배열의 길이는 쉽게 바뀔 수 있다. 이것은 나중에 중요할 것이지만, 이제 데모를 간단하게 하기 위해, 우리는 10개의 값들을 만들 것이다 (0에서 9까지). 그 배열에서 한 숫자의 인덱스는 ruler에서의 그것의 위치와 동일하다. array에서 첫 번째 숫자는 0과 일치하고, 그 두 번째는 x = 1 등등. x에 대한 noise value를 연산하기 위해, 우리는 처음에 x에 대한 integer boundaries를 연산할 것이다 (x에 대한 최소, 최대 정수값). 우리는  그러고나서 이러한 두 개의 정수 값을 random numbers에를 저장한 배열에 index positions으로서 사용할 수 있다. 우리가 얻은 두 숫자들 a와 b는 이러한 인덱스 위치에 저장된 두 개의 랜덤 값들이다. 우리는 또한 위에서 설명된 기법을 사용하여 x로부터 t를 찾을 필요가 있다 (x로부터 x에 대해 최소 정수를 빼라). 그 마지막 단계는 t를 사용하여 a와 b의 선형 보간을 수행하는 것이다. 그리고 너는 x에 대한 noise function의 결과를 얻는다.

{
나는 image로 결과를 얻기위해 다음과 같은 코드를 썼고 다음의 결과를 얻었다.


#include <iostream>
#include <fstream>
#include <sstream>
#include <cmath>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#define STBI_MSC_SECURE_CRT
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

#include "GPED_random.h"

#define CHANNEL_NUM 3
struct Color3f
{
 float r, g, b;

 float operator[] (unsigned i)
 {
  assert(i >= 0 && i <= 3);
  return (&r)[i];
 }
};

inline unsigned min(const unsigned& a, const unsigned& b) { return (a < b ? a : b); }
template <typename T = float>
inline T lerp(const T& lo, const T& hi, const T& t) { return lo * (1 - t) + hi * t; }

class ValueNoise1D
{
public:
 ValueNoise1D(unsigned seed = 2011)
 {
  GPED::Random rand(seed);

  for (unsigned i = 0; i < kMaxVertices; ++i)
   r[i] = rand.randomReal();
 }
 
 // Evaluate the noise function at position x
 float eval(const float& x)
 {
  int xMin = (int)x;
  assert(xMin <= kMaxVertices - 1);
  float t = x - xMin;

  if (xMin + 1 >= kMaxVertices)
   return lerp(r[xMin], r[0], t);
  else
   return lerp(r[xMin], r[xMin + 1], t);
 }

 static const unsigned kMaxVertices = 20;
 float r[kMaxVertices];
};

int main()
{
 const int width = 1000;
 const int height = 1000;


 uint8_t* pixels = new uint8_t[width * height * CHANNEL_NUM];
 memset(pixels, 0, sizeof(uint8_t) * width * height * CHANNEL_NUM);

 ValueNoise1D myNoise;

 float step = (float)myNoise.kMaxVertices / (float)width;
 float widthStep = 1.f / step;
 for(float i = 0; i < (float)myNoise.kMaxVertices; i += step)
 {
  float result = myNoise.eval(i);

  int WidthIndex = i * widthStep * CHANNEL_NUM;
  int HeightIndex = result * height;
  int index = WidthIndex + width * (height - HeightIndex) * CHANNEL_NUM;

  pixels[index++] = 255;
  pixels[index++] = 255;
  pixels[index++] = 255;
 }
 
 stbi_write_png("stbpng.png", width, height, CHANNEL_NUM, pixels, width * CHANNEL_NUM);
 
 
 delete[] pixels;
 return 0;
}


}

우리는 ㅌ = 0에서 시작하여 x축에서 각 integer position에서 정의된 10개의 random values를 가진다. 그래서 우리는 오직 range [0:10]에서 어떤 x에 대한 값을 연산할 수 있다. [0:9] 대신에 왜 [0:10]인가? x가 [9:10]의 범위에 있을 때, 우리는 noise value를 연산하기 위해 index 9와 index 0에서의 값을 사용할 것이다. 너가 그림 6에서 볼 수 있뜻이, 만약 너가 이것을 한다면, 커브의 시작과 끝은 같다. 다시 말해서, x = 0일 때와 x = 10일 떄의 noise는 같다 (우리의 예제에서 0.36).  그 curve의 복사본을 만들고 존재하는 것의 왼쪽 또는 오른쪽으로 움직여보자. 그 존재하는 커브 (curve 1)은 범위 [0:10]에 정의되어있고, 새로운 copy (curve2)는 range[10:20]에 정의되어 있다.

너는 curves가 결합되는 곳에 어떠한 불연속성도 없는 것을 볼 수 있다. 왜? curve 1의 끝의 noise value가 curve 2의 시작에서의 noise value와 같기 때문이다. 그것은 왜냐하면 x가 9:10의 범위 이 ㄹ떄, 우리는 x = 9, x = 0의 값으로 보간하기 때문이다. curve의 성공적인 copies사이에 어떠한 불연속성이 없기 때문에, 우리는 우리가 원하는 많은 복사본을 만들어 낼 수 있고, 우리의 noise function을 무한으로 확장할 수 있다. x에 대한 값은 더 이상 [0:10]으로 제한 되지 않는다. 그것은 0 또는 무한으로 (또는 음의 무한대) 가는 어떠한 양수 또는 음수 값이든 받을 수 있다. 그 noise function은 음의 값에도 또한 작동해야만 한다).

우리는 어떻게 이것이 코드에서 가능하게 만드는가? 우리는 x가 [0:9] 범위일 때 noise value를 어떻게 연산하는지를 안다. 그러나 x가 9보다 더 클 때 (그리고 그 같은 것은 x가 음수일 때 적용된다), 가령 9.35라고 하자. 우리는 그 random position을 x = 9와 x = 0에 있는 random position을 보간하길 원한다. 위에 설명되었듯이. 만약 우리가 x에 대한 최소와 최대 정수를 취한다면, 우리는 9와 10을 얻는다. 그러나 10을 사용하는 대신에, 우리는 0을 사용해야만 한다. 우리가 여기에서 필요한 것은 modulo 연산자이다. modulo 연산자는 다른 수에 의해 나눠진 수의 나머지를 준다 (우리의 경우에 10으로 나눠진 x의 나머지). 만약 너가 수학을 한다면, 10으로 나눠진 9의 나머지는 9이다. 그리고 10으로 나눠진 10의 나머지는 0이다.  다시 말해서, x = 9.35에 대한 최소와 최대 정수 값에 대한 modulo 연산자를 사용하는 것ㅇㄴ 우리에게 9와 0을 주고, 이것은 우리가 정확히 원하는 것이다. 그리고 만약 너가 테스트 해본다면, 너는 이 연산자를 사용하는 것은 10보다 더 큰 어떤 x에 대해 또는 0 보다 더 작은 어떤 x에 대해 올바른 정수 boundaries를 반환한다는 것을 볼 것이다 (특별한 care는 음수의 경우에 취해져야만 하지만, 그 원칙은 같다).

이 기법을 사용하여, 우리는 우리가 x축을 이동함에 따라 반복적으로 noise functions을 순환시킬 수 있다 (그리고 이것은 원래 curve의 복사본을 만드는 것과 유사하다). 우리는 noise function이 periodic하다고 첫 번째 섹션에서 언급했었다. 이 경우에, 그 함수의 주기는 10이다 (그것은 그 자체로 x의 음수와 양수에 대해 원점의 각 면에 대해 10units 마다 반복된다). 우리는 이제 modulo 연산자를 사용하지만 나중에 우리는 이것이 간단화 될 수 있는 것을 볼 것이고, 좀 더 효율적으로 만들 것이다 (다음의 코드가 0보다 더 작은 x의 ㄱ밧에 작동하지 않는다는 것을 인지해라) 이 제한은 이 코드 snippet의 마지막 버전에서 제거될 것이다):

{
이제 period를 추가한 나의 코드이다 그리고 아래는 결과이다. period를 3으로 주었다.


#include <iostream>
#include <fstream>
#include <sstream>
#include <cmath>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#define STBI_MSC_SECURE_CRT
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

#include "GPED_random.h"

#define CHANNEL_NUM 3
struct Color3f
{
 float r, g, b;

 float operator[] (unsigned i)
 {
  assert(i >= 0 && i <= 3);
  return (&r)[i];
 }
};

inline unsigned min(const unsigned& a, const unsigned& b) { return (a < b ? a : b); }
template <typename T = float>
inline T lerp(const T& lo, const T& hi, const T& t) { return lo * (1 - t) + hi * t; }

class ValueNoise1D
{
public:
 ValueNoise1D(unsigned seed = 2011)
 {
  GPED::Random rand(seed);

  for (unsigned i = 0; i < kMaxVertices; ++i)
   r[i] = rand.randomReal();
 }
 
 // Evaluate the noise function at position x
 float eval(const float& x)
 {
  int xi = (int)x; 
  int xMin = xi % (int)kMaxVertices;
  float t = x - xi;
  int xMax = (xMin == kMaxVertices - 1) ? 0 : xMin + 1;

  return lerp(r[xMin], r[xMax], t);
 }

 static const unsigned kMaxVertices = 10;
 float r[kMaxVertices];
};

int main()
{
 const int width = 1000;
 const int height = 1000;


 uint8_t* pixels = new uint8_t[width * height * CHANNEL_NUM];
 memset(pixels, 0, sizeof(uint8_t) * width * height * CHANNEL_NUM);

 ValueNoise1D myNoise;

 int period = 3;
 float step = (float)myNoise.kMaxVertices / (float)width * period;
 float widthStep = 1.f / step;
 for(float i = 0; i < myNoise.kMaxVertices * period; i += step)
 {
  float result = myNoise.eval(i);

  int WidthIndex = i * widthStep * CHANNEL_NUM;
  int HeightIndex = result * height;
  int index = WidthIndex + width * (height - HeightIndex) * CHANNEL_NUM;

  pixels[index++] = 255;
  pixels[index++] = 255;
  pixels[index++] = 255;
 }
 
 
 stbi_write_png("stbpng.png", width, height, CHANNEL_NUM, pixels, width * CHANNEL_NUM);
 
 
 delete[] pixels;
 return 0;
}


}

이 결과에 잘못 된 것은 없다 (기술적으로 그것은 옳은 것을 했다), 매우 자연스럽지 않은 톱 이빨 같은 곡선처럼 보이는 것을 제외하고. 만약 너가 바다의 표면 (파도)의 profile과 같은 랜덤인 것처럼 보이는 자연의 패턴을 본다면, 그것들은 보통 이러한 saw-toothed profile을 갖고 있지 않다. 그것들의 옆모습은 부드럽다 (둥글다). 우리가 이 곡선의 외관을 바꾸기 위해서 할 수 있는 것은 ("S" 모양을 갖는 curve를 만드는 함수) smooth profile를 갖는 또 다른 함수를 사용하여 우리의 입력 t값을 remap하는 것이다. t를 remap하기 위해 흔히 사용되는 두 개의 S curve 함수들은 cosine과 smoothstep 함수이다. random values를 보간하는 코드가 변하지 않는 것을 이해하는 게 중요하다. 우리가 하는 것은 간단히 t의 값을  remap하는 것인데, 우리가 mix function을 사용하기 전에 한다. 다시 한 번, 우리는 mix function을 한 "S" curve function으로 바꾸지 않는다. 우리는 smooth function을 사용하여 t를 remap하고, 그리고 나서 t의 이 remapped value를 가지고 linear interpolation을 한다. 여기에 우리가 슈도 코드로 얻는 것이 있다:


float smoothNoise(const float& a, const float &b, const float& t)
{
    assert(t >= 0 && t <= 1); // t should be in the range[0:1]
    float tRemap = smoothFunc(t); // remap t input value
    return lerp(a, b, tRemap); // return interpolatino of a-b using new t
}

Cosine
범위 [0:2Pi]에 있는 cosine 함수를 그려보자 (단위 원 주위를 완전히 도는 것). 너는 확실히 이 곡선의 옆면에 친숙하다 (그림 7, 왼쪽 이미지). 너도 볼 수 있듯이, 그 곡선이
x가
[0:Pi/2] :  1->0으로 가고,
[Pi/2:Pi] : 0 -> -1으로 가고,
[Pi:3/2Pi] : -1 -> 0으로 가고,
[3/2Pi:2Pi] : 0 -> 1으로 간다.

함수가 1 에서 -1로 변하는 (x가 [0: Pi] 구간에 있을 때) curve의 섹션은 두 개의 값들을 보간하는데 사용될 수 있다. 왜냐하면 t는 [0:1]의 구간에 있기 때문에, 우리는 그것을 remap을 할 건데, cosine 함수에 그것을 사용하기 전에 한다, 그것에 간다히 Pi를 곱해서 한다 (우리는 x가 구간 [0:Pi] 들어가기를 원한다는 것을 기억해라).

그러나 우리는 우리의 remapping function의 결과가 0 ~ 1이 되기를 원하지만, x가 [0:Pi]일 때의 cosine 함수는 1에서 -1까지 이다. trick은 1 - cos(t * Pi)를 쓰는 것인데 이것은 우리에게 0 에서 2 사이의 값을 준다. 만약 우리가 이 결과를 2로 나누면 (또는 0.5를 곱한다면) 우리의 remapping function은 마침내 [0:1] 범위의 갑승ㄹ 반환한다 (그림 7, 오른쪽 이미지). 그 코드를 완성짓기 위해서, 우리는 r1과 r2를 mix function으로 보간할 것이다, t의 값을 사용해서, 그리고 그 t는 cos function으로 remapped 된다. 여기에 우리가 사용할 코드가 있다.

{

class ValueNoise1D
{
public:
 ValueNoise1D(unsigned seed = 2011)
 {
  GPED::Random rand(seed);

  for (unsigned i = 0; i < kMaxVertices; ++i)
   r[i] = rand.randomReal();
 }
 
 // Evaluate the noise function at position x
 float eval(const float& x)
 {
  int xi = (int)x; 
  int xMin = xi % (int)kMaxVertices;
  float t = x - xi;
  int xMax = (xMin == kMaxVertices - 1) ? 0 : xMin + 1;

  // return lerp(r[xMin], r[xMax], t);
  return consineRemap(r[xMin], r[xMax], t);
 }
 
 float consineRemap(const float& a, const float& b, const float& t)
 {
  assert(t >= 0 && t <= 1);
  float tRemapCosine = (1 - std::cosf(t * glm::pi<float>())) * 0.5f;
  return lerp(a, b, tRemapCosine);
 }


 static const unsigned kMaxVertices = 10;
 float r[kMaxVertices];
};

period = 1

period = 3 (더 부드럽게 보이기 위해 widthheight 1000/1000에서 3000/3000으로 늘림

확실히 smooth해졌다!
}

만약 우리가 이 코드를 사용한다면, 곡선의 옆 모습은 같게 남아 있지만 (우리가 같은 위치에 있는 미리 정의된 값들을 보간하고 있기 때문에), 그러나 두 연속적인 값들 사이의 변화가 더 부드러워 진다 (그림 8).

Smoothstep
smooth step 함수는 noise functions의 구현에서 흔히 사용된다 (그것은 Perlin noise라고 알려진 Ken Perlin에 의해 쓰여진 noise function의 인기있는 구현에서 사용된다). 그 함수 자체에 대해서는 별로 할 말이 없는데, 그것의 옆모습이 ([0:1]의 범위내에서) 정확히 우리가 관심있는 모습의 종류를 가지고 있다는 사실밖에 할 말이 없다. 여기에 그 함수와 그것의 그림이 있따 (그림 9, 오른쪽 이미지. Cosine remapping equation ((1-cos(t*Pi)*0.5)는 거의 같은 profile을 갖는다는 것을 알아라).

너가 smoothstep function을 코드에 넣을 때 신경을 써야 하는데, t가 제곱과 세제곱될 필요가 있기 때문이다. 이러한 연산들을 다음의 구현으로 다소 최적화하는 것이 가능하다:

{


class ValueNoise1D
{
public:
 ValueNoise1D(unsigned seed = 2011)
 {
  GPED::Random rand(seed);

  for (unsigned i = 0; i < kMaxVertices; ++i)
   r[i] = rand.randomReal();
 }
 
 // Evaluate the noise function at position x
 float eval(const float& x)
 {
  int xi = (int)x; 
  int xMin = xi % (int)kMaxVertices;
  float t = x - xi;
  int xMax = (xMin == kMaxVertices - 1) ? 0 : xMin + 1;

  // return lerp(r[xMin], r[xMax], t);
  // return consineRemap(r[xMin], r[xMax], t);
  return smoothstepRemap(r[xMin], r[xMax], t);
 }
 
 float consineRemap(const float& a, const float& b, const float& t)
 {
  assert(t >= 0 && t <= 1);
  float tRemapCosine = (1 - std::cosf(t * glm::pi<float>())) * 0.5f;
  return lerp(a, b, tRemapCosine);
 }

 float smoothstepRemap(const float& a, const float& b, const float& t)
 {
  // float tRemapSmoothstep = t * t * (3 - 2 * t);

  // Ken Perlin ver.
  float t3 = std::powf(t, 3);
  float tRemapSmoothstep = 6 * t3 * t * t - 15.f * t3 * t + 10 * t3;
  return lerp(a, b, tRemapSmoothstep);
 }


 static const unsigned kMaxVertices = 10;
 float r[kMaxVertices];
};

1. smoothStep basic
2. Ken Perlin Ver.
확실히 smoothStep마다 그림이 조금씩 다르다. }

그림 10은 smoothstep function을 사용한 noise function의 그림을 보여준다. 너가 볼 수 있듯이, 우리가 cosine 함수를 사용하여 얻은 것과 유사하다.

Advanced : Ken Perlin은 smoothstep function을 다음의 것으로 바꾸라고 제안했다:

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

좀 더 세부사항을 위해 Noise Part 2를 체크해라.

A Simple 1D Noise Function
이 섹션에서, 우리는 빠르게 noise function의 결과를 바꿀 다른 방법들을 보여줄 것이다. 처음에 우리는 우리의 함수가 어떻게 생겼는지를 보여줄 것이다. 이제, 우리는 여전히 우리의 noise pattern을 생성하기 위해 10개의 random number를 사용할 것이다 (그림 10에서 처럼). 그리고 그것은 우리의 함수가 x축에서 0에서 10까지 변하는 것을 의미한다 (range[9:10]의 값에 대해, 우리는 lattice 9와 0을 사용할 것이다). 그 이후에, 그것은 10 단위로 주기를 반복할 것이다. 우리는 이전에 noise function이 periodic function이라고 언급했었다 (그림 5가 이 아이디어를 보여준다). 실제 프로그램에서, 그러한 짧은 주기는 만족스럽지 않다. 우리의 noise function의 최종 버전은 더 큰 주기를 다룰 것이다 (256; 우리는 우리가 왜 이 숫자를 사용할 것인지를 설명할 것이다). 우리는 또한 그 코드를 써야만 하는데, 그것이 x에 대해 음수값과도 작동하도록 하기 위해서 이다. 우리는 지금까지 noise function의 period에 대해 순환하기 위해 modulo 연산을 썼었다. 그러나 이것은 x가 음수일 때 작동하지 않을 것이다. 그리고 그것은 더 복잡하게 만든다. (더 느리고). 코드의 최종 버전에서, 너는 이 경우를 좀 더 우아하게 어떻게 다룰지를 배울 것이다. 마지막으로, 그 noise가 범위 [0:1]에서 y축을 따라 여전히 어떻게 변하는지를 주목해라.

Scaling
입력 값 x를 scaling하거나 그 함수 결과 자체에 곱하여, 그 함수의 모양을 꽤 쉽게 바꾸는 것이 가능하다. 그 첫 번째 연산은 그 함수의 주기성을 바꿀 것이다. x에 1보다 더 큰 값을 곱하는 것은 그 함수의 주기성을 증가시킬 것이다 (너는 noise function의 주파수를 변경한다). 요악해서, 너는 그 curve를 압축시킬 것이다 (끄것을 더 짧게 보이게 만들 것이다). 이것은 다음 챕터에서 2D noise의 예제와 더 명백해질 거싱다. 만약 우리가 x와 1보다 더 작은 수를 곱한다면, 이것은 그 curve를 x-axis를 따라 stretch하는 효과를 갖는다, 그리고 이것은 더 길게 만든다 (그림 11). 이 연산은 noise pattern의 frequency를 제어하는데 매우 유용하다.

float frequency = 0.5;
float freqNoise = valueNoise1D.eval(x * frequency);

{
frequency = 0.5 -> RED
frequency = 1.0 -> GREEN
frequency = 1.5 -> BLUE


이것을 하다보니, pixel index에 잘못 접근해 컬러가 섞이는 현상이 생겼었는데 다음과 같은 코드를 추가해서 해결했다. modulo 연산이 들어가 시간이 더 걸리지만..


float frequency = 0.5;
for (float i = 0; i < myNoise.kMaxVertices * period; i += step)
{
 float result = myNoise.eval(i * frequency);

 int WidthIndex = i * widthStep * CHANNEL_NUM;
 int HeightIndex = result * height;
 int index = WidthIndex + width * (height - HeightIndex) * CHANNEL_NUM;

 // adjust index
 int modulo = index % CHANNEL_NUM;
 index += (CHANNEL_NUM - modulo);

 pixels[index++] = 255;
 pixels[index++] = 0;
 pixels[index++] = 0;
}
}

두 번째 가장 기본적인 연산은 간다히 noise function의 결과 그 자체에 곱하는 것이고, 이것은 그것의 amplitude(진폭)을 변화하게 만든다. 만약 너가 한 curve를 animate하기 위해 1D noise를 사용한다면, 그 효과는 너무 강력할 것이다. 그것은 그것의 amplitude를 스케일링 다운 하여 감쇠되어질 수있다 (그림 12).

float amplitude = 0.5;
float ampNoise = valueNoise1D.eval(x) * amplitude;

{
amplitude = 0.3 -> RED
amplitude = 0.5 -> GREEN
amplitude = 0.8 -> BLUE

내 코드는 result값을 기반으로 정해진 pixel height에 비례하여 채우므로 1.0을 넘어가면 pixel array의 범위를 넘으므로 위와 같이 설정할 수 밖에 없었다.

}

Offsetting
noise function의 입력에 어떤 값을 더하는 것은 매우 유용할 수 있다. 이것은 그 곡선을 왼쪽 (x에 양수가 더해진다면) 또는 오른쪽으로 (x에 음수가 더해진다면) 움직이는 효과를 갖는다. x에 offset을 더하여 noise function을 움직이는 결과는 시간에 따라서 함수를 animate하는데 유용하다 (매 프레임 에 offset value를 증가시켜서).

float offset = frameNumber;
float offNoise = valueNoise1D.eval(x + offset);

{
offset= 0 -> RED
offset= 1 -> GREEN
offset= 2-> BLUE


확실히 offset을 더하면 시작점이 오른쪽에서 시작하므로 왼쪽으로 흘러가는 것 같은 효과를 준다.
}

Signed Noise
보통 noise 함수들은 range[0:1]의 값들을 반환한다. 그러나 그것은 그럴 필요는 없다. 그것은 그것이 어떻게 구현되었는지에 의존한다. 예를들어, 너가 랜덤값을 [-1:1] 사이에 있는 정수 lattice point로 할당할 수 있다. 그리고 이것은 같은 범위에 있는 값을 반환하는 noise function을 만든다. 그러나, 우리가 말했듯이, convention은 보통 range[0:1]의 값을 반환하는 것이지만, 그 값을 range[-1:1]로 remap하는 것은 충분히 쉽다. 그 최종 noise는 가끔씩 signed noise라고 불려진다. 여기에 코드와 우리의 noise function의 그림이 있다. 그리고 그것은 [-1:1]의 범위로 remapped 되었다 (그림 14). 이것은 오브젝트들을 절차적으로 animate 하려고 하거나 한 파라미터를 다양하게 하기 위해 noise를 사용할 때 또 다시 유용할 수 있다. 만약 한 오브젝트의 y의 위치가 3이고, 너가 그것에 noise를 적용한다면, 너는 그 오브젝트가 0 ~ 6의 범위에 변하기를 원할지도 모른다. 그 경우에 너는 그 오브젝트의 y position에 대해 signed noise를 적용하는데, 너는 y position에 3을 곱한다. 그래서 그것이 범위 [-3:3]의 범위가 되도록 하고, 거기에 3을 더해서, 그 오브젝트가 [0:6]의 범위에서 y축을 따라 변하도록 만들 것이다.

float signedNoise = 2 * valueNoise1D.eval(x) - 1;

{
이전의 하고있는 simple terrain에 camera position만 valueNoise1D를 적용해보았다. 그냥 0 ~ 1 위치만 y값을 할당 했다. 어쨋든 이렇게 한다면, 여러가지 기능들을 구현할 수 있겠다.
}

A Complete 1D Noise Function
10개의 random number를 사용하는 것은 매우 짧은 주기를 만든다. 이것은 만약 우리가 그 함수를 x의 값들의 큰 범위에 대해 상요한다면 그 함수가 꽤 종종 그 자체를 반복할 거라는 것을 의미한다. (가령 0에서 100까지면 그것은 10번 반복할 것이다). 시각적으로 너가 이 패턴을 감지할 수 있는 강한 가능성이 있고, 이것은 우리의 결과를 매우 자연스럽게 보이도록 만들지 않을 것이다. 우리는 첫 번째 챕터에서, noise 특성들 중 하나가 periodic하게 보이지 않아야 한다는 것이라고 언급했었다. 이것을 달성하기 위해, 우리는 간단히 속임수를 쓰고, noise function의 주기가 매우 크게 만들 것이다.

이전 챕터에서 (noise properties의 문단에서), 우리는 너가 zoom out할 때 noise가 너무 작아서 반복이 거의 보이기 어렵게 되도록 한 것을 언급했었다. 이것은 사실 어떤 무엇보다도
하나의 트릭이다. 만약 너가 그림 15의 밑에 있는 curve를 본다면, 우리는 noise function을 0에서 256까지 했다 (한 주기). 너가 볼 수 있뜨이, 만약 우리가 이 curve를 오른쪽과 왼쪽에 복사하고, 이러한 세 개의 커브들이 나란히 보도록 zoom out을 한다면, 너는 아마도 이 line들이 같은 curve의 세 개의 copies로 만들어졌는지를 구분할 수 없을 것이다. 그 트릭은 너가 zoom out했을 때 pattern을 알아차리기 불가능할 정도로 충분히 긴 noise의 주기에 대한 size를 찾는 것이다. 그러나 random value array가 충분히 합리적으로 작도록 해야한다 (그 메모리 사용 소비가 낮게 유지하면서). 256 or 512는 그 결과를 달성하는데 좋은 것처럼 보인다.

noise function의 대부분의 현대 구현은 256 점들의 lattice grid를 사용한다 (또는 512). 그 256 value는 우연히 선택된 것이 아니다. 왜 256인가 255나 257이 아니라? 왜냐하면 이 숫자는 2의 제곱 수이다 (2^8). 우리가 그러한 숫자를 다룰 때, 우리는 modulo C 연산자를 (%) bit 연산자 (&)로 대체할 수 있다. 과거에 프로그래머들은 더 느린 % 연산자를 사용하는 것 보다 arithmetic operations를 선호했었따. 현대 프로세서/컴파일러가 이것을 잘 다룰지라도. 그래서 x가 음수일 때 상황을 더 간단하게 만든다는 사실을 제외하고& 연산자로 바꿀 특정할 필요는 없다.

독자로부터의 질문: 너는 왜 &가 더 좋은지와 그것이 어떻게 작동하는지를 설명할 수 있는가?

C/C++에서 -10 % 256은 -10 & MASK와 같은 것이 아니다 (MASK = 255). 하나는 -10을 반환하고, 다른 것은 246을 반환하는데, 256이 x= -10일 때 우리가 noise function에서 사용하길 원하는 값이다. (만약 우리의 noise가 256 period를 갖는다면, 랜덤 값의 배열에서 첫 번쨰 원소 (index 0)은 x = -256과 일치한다. x = -10이라면 index position은 246이다). 이론상으로 컴파일러가 modulo가 2의 제곱꼴일 때 코드를 최적화한다 할지라도, x의 음수 값에 대해 작동하지 않는다. &은 그렇게 할지라도.

그것이 어떻게 작동하는가는 좀 더 까다롭지만, 우리는 너가 깊이 있는 설명을 위해 Bitwise Arithmetic의 강의를 읽기를 충고한다. 그러나, 만약 너가 이미 이 주제에 친숙하다면, 다음의 코드는 너가 답을 찾는데 도움이 될 것이다:


#include <bitset> 
using namespace std; 
#define MASK 256 - 1 
int a = -10; 
int b = a & MASK; 
bitset<16> ab(a); 
bitset<16> bb(b); 
bitset<16> mb(MASK); 
cout << ab << "=" << a << "\n" << mb << "=" << MASK << "\n" << bb << "=" << b << "\n"; 
1111111111110110=-10 
0000000011111111=255 
0000000011110110=246 

line 18에서 x가 음수 일 때 우리가 어떻게 x를 minimum integer value로 rounding 하는 것을 신경쓰는지에 주목해라 (int(-1.2)는 -1이고, 반면에 우리는 -2를 원한다. 그래서 만약 오른쪽에 있는 boolean test가 true라면, 그러면 그것은 -1을 결과에 더할 것이다: -1 + -1 = -2). 만약 너가 궁금하고, 너의 함수가 정말 주기적인지 체크하고 싶다면, 그러면 너는 다음의 코드를 쓸 수 있다.

{


class ValueNoise1D
{
public:
 ValueNoise1D(unsigned seed = 2011)
 {
  GPED::Random rand(seed);

  for (unsigned i = 0; i < kMaxVertices; ++i)
   r[i] = rand.randomReal();
 }
 
 // Evaluate the noise function at position x
 float eval(const float& x)
 {
  // Floor
  int xi = (int)x - (x < 0 && x != (int)x);
  float t = x - xi;

  // Modulo using &
  int xMin = xi & kMaxVerticesMask;
  int xMax = (xMin + 1) & kMaxVerticesMask;

  // return lerp(r[xMin], r[xMax], t);
  // return consineRemap(r[xMin], r[xMax], t);
  return smoothstepRemap(r[xMin], r[xMax], t);
 }
 
 float consineRemap(const float& a, const float& b, const float& t)
 {
  assert(t >= 0 && t <= 1);
  float tRemapCosine = (1 - std::cosf(t * glm::pi<float>())) * 0.5f;
  return lerp(a, b, tRemapCosine);
 }

 float smoothstepRemap(const float& a, const float& b, const float& t)
 {
  // float tRemapSmoothstep = t * t * (3 - 2 * t);

  // Ken Perlin ver.
  float t3 = std::powf(t, 3);
  float tRemapSmoothstep = 6 * t3 * t * t - 15.f * t3 * t + 10 * t3;
  return lerp(a, b, tRemapSmoothstep);
 }

 static const unsigned kMaxVertices = 256;
 static const unsigned kMaxVerticesMask = kMaxVertices - 1;
 float r[kMaxVertices];
};

확실히 최적화가 되었다. kMaxVertices를 256이라 하면 image에 어떻게 나타나는지 모르기 때문에, kMaxVertices를 16으로 한 것과 256을 한 것을 같이 올린다.

- width : 1000, height : 1000, kMaxVertices : 16
- width : 500, height : 500, kMaxVertices : 256
확실히 알아볼 수가 없다.
}

너가 볼 수 있뜻이, 그것의 주기의 배수인 x에 대한 한 값으로 noise function을 호출하는 것은 항상 같은 값을 반환한다 (그리고 이것은 index 0 에 저장된 random number의 값이다). 너는 또한 그 함수의 주기성을 체크하는 또 다른 방법인 다음의 코드를 쓸 수 있다.


 std::cout << myNoise.eval(-512) << std::endl;
 std::cout << myNoise.eval(-256) << std::endl;
 std::cout << myNoise.eval(0) << std::endl;
 std::cout << myNoise.eval(256) << std::endl;
 std::cout << myNoise.eval(512) << std::endl;

One Last Very Important Thing to Know
noise function의 구현은 random values의 한 배열을 만드는 것에 의존하는데, 거기에서 이러한 값들 각각은 ruler에서 integer positions에 위치되어있다고 고려된다. 이것은 중요한 관찰인데, 나중에 noise function을 filter링할 때 극히 유용하다. 이 강의의 첫 챕터에서, 우리는 noise pattern이 너무 작을 때, 그것은 다시 white noise를 만들고 aliasing으로 알려진 visual artifact를 만든다고 언급했었다. frequency가 너무 높을 때 그 noise function을 필터링하여 이 aliasing을 없애는 것이 가능하다. 질문은 언제 "너무 높은지"를 아는 것이다. 이 질문에 대한 답은 정확히 ruler에서 미리 정의된 random values 각각을 분리시키는 거리에 의존한다: 두 개의 연속적인 random numbers가 1단위씩 떨어져있다. 너가 noise function의 이 특성을 기억하는 것은 매우 중요하다 (noise를 필터링하는 챕터는 lesson Noise Part 2에서 발견될 수 있다).

댓글 없음:

댓글 쓰기