본문 바로가기
셰이더 (Shader)/The Book of Shaders (완)

[GLSL] 11 - Noise

by Minkyu Lee 2023. 4. 26.


현실 세계 무작위성을 가지고 있지만, 랜덤과는 다르다.

이 다양성을 어떻게 표현할까?

 

1980년대 초 영화 "트론"을 위해 보다 사실적인 텍스처를 생성해 달라는 의뢰를 받은 켄 펄린은 이 문제를 해결했다.

이에 대한 해답으로 노이즈 알고리즘을 고안해냈다.

 

다음은 고전적인 펄린 노이즈 알고리즘은 아니지만, 노이즈 생성 방법을 이해하기 위한 기초 예제이다.

float i = floor(x);  // integer
float f = fract(x);  // fraction
y = rand(i); //rand() is described in the previous chapter
y = mix(rand(i), rand(i + 1.0), f); // 선형 보간법
y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f)); // 스무스한 보간법

이전 장에서 했던 것과 비슷한 작업을 한다.

- 연속된 float 값(x)을 정수(i)와 소수점 이하(f)로 나눈다.

floor()를 사용하여 i를 구한다. fract()를 사용하여 f를 구한다.

 

- 그 다음 i에 rand()를 적용한다.

각 정수에 대해 랜덤 값이 부여된다.


- 그 후, 랜덤값을 선형적으로 보간한다.

선형 보간 대신 smoothstep()을 이용해 보간하면 부드러운 결과가 나온다.

 

2D Noise

여기까지 1D에서 노이즈를 보간하는 방법을 알아보았다.

2D로 넘어갈 차례이다.

2D에서는 선의 두 점 사이를 보간하지 않는다.

대신에 평면의 정사각형 영역의 네 모서리 사이를 보간한다.

 

- 두 점 사이 보간

rand(x)

rand(x) + 1.0

 

- 네 모서리 보간

rand(st)

rand(st) + vec2(1.0, 0.0)

rand(st) + vec2(0.0, 1.0)

rand(st) + vec2(1.0, 1.0)

마찬가지로 3D 노이즈를 얻으려면 큐브의 여덟 모서리 사이를 보간해야 한다.

이 기술은 임의의 값을 보간하는 것이다.

이를 value 노이즈라고 한다.

 

 

1D 노이즈 예시와 마찬가지로 부드럽게 보간하는 cubic 보간(smoothstep)을 적용하면 다음과 같다.

 

코드 예제를 살펴보자.

// 2D Random
float random (in vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))
                 * 43758.5453123);
}

float noise (in vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);

    // Four corners in 2D of a tile
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));

    // Smooth Interpolation

    // Cubic Hermine Curve.  Same as SmoothStep()
    vec2 u = f*f*(3.0-2.0*f);
    // u = smoothstep(0.,1.,f);

    // Mix 4 coorners percentages
    return mix(a, b, u.x) +
            (c - a)* u.y * (1.0 - u.x) +
            (d - b) * u.x * u.y;
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;

    // Scale the coordinate system to see
    // some noise in action
    vec2 pos = vec2(st*5.0);

    // Use the noise function
    float n = noise(pos);

    gl_FragColor = vec4(vec3(n), 1.0);
}

좌표를 5로 곱한다.

 

// noise 함수

함수 내에서 좌표를 셀로 분할한다.

셀의 정수 위치(i)와 셀 내부의 소수점 아래(f)를 변수에 담는다.

정수 위치를 사용하여 네 모서리의 좌표를 계산한다.

각 모서리에 대한 임의의 값을 얻는다.

소수점 아래 값을 사용하여 모서리의 임의의 값 4개를 보간한다.

 

Using Noise in Generative Designs

노이즈 알고리즘은 원래 디지털 텍스처에 자연스러운 느낌을 주기 위해 고안되었다.

지금까지 살펴본 1D, 2D 구현은 랜덤한 값 사이의 보간이었다.

이를 Value 노이즈라고 부른다.

하지만 노이즈를 얻는 방법에는 더 많은 방법이 있다.

 

Value 노이즈블록처럼 보이는 경향이 있다.

이러한 블록 효과를 줄이기 위해 1985년 Ken Perlin은 Gradient 노이즈라는 알고리즘을 개발했다.

Ken은 값 대신 랜덤한 gradient를 보간하는 방법을 알아냈다.

이러한 gradient는 단일 값(float) 대신 방향(vec2로 표시)을 반환하는 2D 랜덤 함수의 결과이다.

vec2 random2(vec2 st){
    st = vec2( dot(st,vec2(127.1,311.7)),
              dot(st,vec2(269.5,183.3)) );
    return -1.0 + 2.0*fract(sin(st)*43758.5453123);
}

float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);

    vec2 u = f*f*(3.0-2.0*f);

    return mix( mix( dot( random2(i + vec2(0.0,0.0) ), f - vec2(0.0,0.0) ),
                     dot( random2(i + vec2(1.0,0.0) ), f - vec2(1.0,0.0) ), u.x),
                mix( dot( random2(i + vec2(0.0,1.0) ), f - vec2(0.0,1.0) ),
                     dot( random2(i + vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;
    vec3 color = vec3(0.0);

    vec2 pos = vec2(st*10.0);

    color = vec3( noise(pos)*.5+.5 );

    gl_FragColor = vec4(color,1.0);
}

 

노이즈 구현에 대해 더 많이 알수록 마치 화가처럼 더 잘 사용할 수 있다.

예를 들어 2차원 노이즈 구현시 좌표를 회전하면 나무결 같은 느낌을 만들 수 있다.

float random (in vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))
                * 43758.5453123);
}

float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    vec2 u = f*f*(3.0-2.0*f);
    return mix( mix( random( i + vec2(0.0,0.0) ),
                     random( i + vec2(1.0,0.0) ), u.x),
                mix( random( i + vec2(0.0,1.0) ),
                     random( i + vec2(1.0,1.0) ), u.x), u.y);
}

mat2 rotate2d(float angle){
    return mat2(cos(angle),-sin(angle),
                sin(angle),cos(angle));
}

float lines(in vec2 pos, float b){
    float scale = 10.0;
    pos *= scale;
    return smoothstep(0.0,
                    .5+b*.5,
                    abs((sin(pos.x*3.1415)+b*2.0))*.5);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.y *= u_resolution.y/u_resolution.x;

    vec2 pos = st.yx*vec2(10.,3.);

    float pattern = pos.x;

    // Add noise
    pos = rotate2d( noise(pos) ) * pos;

    // Draw lines
    pattern = lines(pos,.5);

    gl_FragColor = vec4(vec3(pattern),1.0);
}

 

노이즈에서 흥미로운 패턴을 얻는 또 다른 방법도 있다.

노이즈를 디스턴스 필드처럼 취급하는 것이다.

이것은 shapes 챕터에서 설명한 몇 가지 트릭을 적용하는 것이다.

vec2 random2(vec2 st){
    st = vec2( dot(st,vec2(127.1,311.7)),
              dot(st,vec2(269.5,183.3)) );
    return -1.0 + 2.0*fract(sin(st)*43758.5453123);
}

float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);

    vec2 u = f*f*(3.0-2.0*f);

    return mix( mix( dot( random2(i + vec2(0.0,0.0) ), f - vec2(0.0,0.0) ),
                     dot( random2(i + vec2(1.0,0.0) ), f - vec2(1.0,0.0) ), u.x),
                mix( dot( random2(i + vec2(0.0,1.0) ), f - vec2(0.0,1.0) ),
                     dot( random2(i + vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;
    vec3 color = vec3(0.0);

    float t = 1.0;
    // Uncomment to animate
    // t = abs(1.0-sin(u_time*.1))*5.;
    // Comment and uncomment the following lines:
    st += noise(st*2.)*t; // Animate the coordinate space
    color = vec3(1.) * smoothstep(.18,.2,noise(st)); // Big black drops
    color += smoothstep(.15,.2,noise(st*10.)); // Black splatter
    color -= smoothstep(.35,.4,noise(st*10.)); // Holes on splatter

    gl_FragColor = vec4(1.-color,1.0);
}

 

노이즈 함수를 사용하는 세 번째 방법은 도형을 변형하는 것이다.

이 역시 shapes챕터에서 배운 몇 가지 기술이 필요하다.

vec2 random2(vec2 st){
    st = vec2( dot(st,vec2(127.1,311.7)),
              dot(st,vec2(269.5,183.3)) );
    return -1.0 + 2.0*fract(sin(st)*43758.5453123);
}

float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);

    vec2 u = f*f*(3.0-2.0*f);

    return mix( mix( dot( random2(i + vec2(0.0,0.0) ), f - vec2(0.0,0.0) ),
                     dot( random2(i + vec2(1.0,0.0) ), f - vec2(1.0,0.0) ), u.x),
                mix( dot( random2(i + vec2(0.0,1.0) ), f - vec2(0.0,1.0) ),
                     dot( random2(i + vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y);
}

mat2 rotate2d(float _angle){
    return mat2(cos(_angle),-sin(_angle),
                sin(_angle),cos(_angle));
}

float shape(vec2 st, float radius) {
	st = vec2(0.5)-st;
    float r = length(st)*2.0;
    float a = atan(st.y,st.x);
    float m = abs(mod(a+u_time*2.,3.14*2.)-3.14)/3.6;
    float f = radius;
    m += noise(st+u_time*0.1)*.5;
    // a *= 1.+abs(atan(u_time*0.2))*.1;
    // a *= 1.+noise(st+u_time*0.1)*0.1;
    f += sin(a*50.)*noise(st+u_time*.2)*.1;
    f += (sin(a*20.)*.1*pow(m,2.));
    return 1.-smoothstep(f,f+0.007,r);
}

float shapeBorder(vec2 st, float radius, float width) {
    return shape(st,radius)-shape(st,radius-width);
}

void main() {
	vec2 st = gl_FragCoord.xy/u_resolution.xy;
	vec3 color = vec3(1.0) * shapeBorder(st,0.8,0.02);

	gl_FragColor = vec4( 1.-color, 1.0 );
}

 

// 실습 과제

움직임에 노이즈를 적용하는 것도 좋은 방법이다.

Matrices 챕터로 돌아가보라.

"+"로 이동하는 translate 예제를 사용하여 랜덤과 노이즈 움직임을 적용해보라.

 

Improved Noise

펄린은 개선된 노이즈를 발표했다.

 

이 원리는, cubic Hermite curve (f(x) = 3x^2-2x^3 , smoothstep() 함수와 동일)이었던 것을,

quintic interpolation curve(f(x) = 6x^5-15x^4+10x^3 )으로 대체한 것이다.

 

이렇게 하면 커브의 양쪽 끝이 더 평평해져서 각 테두리가 다음 테두리와 부드럽게 이어진다.

즉, 셀 간에 보다 연속적인 전환을 얻을 수 있다.

 

// Cubic Hermite Curve.  Same as SmoothStep() (좌측 이미지)
y = x*x*(3.0-2.0*x);
// Quintic interpolation curve (우측 이미지)
y = x*x*x*(x*(x*6.-15.)+10.);

Simplex Noise

켄 펄린은 알고리즘의 성공만으로는 충분하지 않았다.

그는 더 나은 성능을 내도록 개선하였다.

그는 시그래프 2001에서 이전 알고리즘보다 나은 개선된 심플렉스 노이즈를 발표했다.


- 계산 복잡도가 낮고 곱셈 횟수가 적은 알고리즘.
- 적은 계산 비용으로 더 높은 차원으로 확장되는 노이즈.
- 방향성 아티팩트가 없는 노이즈.
- 상당히 저렴하게 계산할 수 있는 잘 정의되고 연속적인 그라디언트를 갖는 노이즈다.
- 하드웨어에서 구현하기 쉬운 알고리즘.

 

어떻게 알고리즘을 개선하였을까?

우리는 2차원에서 4점(사각형의 꼭짓점)을 보간하는 방법을 배웠다.

그러므로, 3차원과 4차원의 경우, 8점과 16점을 보간해야 한다고 추측할 수 있다.

즉, N차원의 경우, 2에서 N점(2^N)까지 부드럽게 보간해야 한다.

 

그러나 Ken은 공간을 채울 수 있는 확실한 모양은 정사각형이지만,

2차원에서 가장 간단한 모양은 정삼각형이라는 것을 알게 되었다.

그래서 그는 정사각 그리드를 정삼각형의 심플렉스 그리드로 교체하였다.

N 차원 심플렉스 도형은 N + 1개의 꼭짓점이 있다.

즉, 2D로 계산할 때 한 꼭짓점 적게,

3D일때는 네 꼭짓점 적게,
4D일때는 11개의 꼭짓점이 적다.

비용 절감이 된다.

 

// 심플렉스 그리드 만들기 (삼각형)

심플렉스 그리드는 어떻게 만드는가?

그 방법은 모서리 4개 그리드의 셀을 두 개의 이등변 삼각형으로 나눈다.

그리고 삼각형이 등변이 될 때까지 기울이면 구할 수 있다.

스테판 구스타브슨의 논문을 보면 다음과 같이 설명한다.

 

계산하고싶은 지점의 정수로 변환된 좌표(x,y)를 보라.

두 개의 도형 중 어느 셀이 그 점을 포함하는지 알 수 있다.

x와 y의 크기를 비교하여, 점이 상단 심플렉스인지 하단 심플렉스인지 파악한다.

세 개의 정확한 꼭짓점을 횡단하여 자른다.

다음 코드를 보자.

그리드가 어떻게 기울어졌는지 확인해보라.

심플렉스 그리드를 구성하는 방법을 확인할 수 있다.

y("아래쪽" 삼각형) 또는 y > x("위쪽" 삼각형)를 감지하여, 기울어진 사각형을 두 개의 정삼각형으로 분할하는 방법에 유의하라.

vec2 skew (vec2 st) {
    vec2 r = vec2(0.0);
    r.x = 1.1547*st.x;
    r.y = st.y+0.5*r.x;
    return r;
}

vec3 simplexGrid (vec2 st) {
    vec3 xyz = vec3(0.0);

    vec2 p = fract(skew(st));
    if (p.x > p.y) {
        xyz.xy = 1.0-vec2(p.x,p.y-p.x);
        xyz.z = p.y;
    } else {
        xyz.yz = 1.0-vec2(p.x-p.y,p.y);
        xyz.x = p.x;
    }

    return fract(xyz);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    // Scale the space to see the grid
    st *= 10.;

    // Show the 2D grid
    color.rg = fract(st);

    // Skew the 2D grid
    color.rg = fract(skew(st));

    // Subdivide the grid into to equilateral triangles
    color = simplexGrid(st);

    gl_FragColor = vec4(color,1.0);
}

 

simplex noise를 GLSL에서 구현한 것을 보자.

McEwan와 Stefan Gustavson이 만든 예제이다.

// Some useful functions
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float snoise(vec2 v) {

    // Precompute values for skewed triangular grid
    const vec4 C = vec4(0.211324865405187,
                        // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,
                        // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,
                        // -1.0 + 2.0 * C.x
                        0.024390243902439);
                        // 1.0 / 41.0

    // First corner (x0)
    vec2 i  = floor(v + dot(v, C.yy));
    vec2 x0 = v - i + dot(i, C.xx);

    // Other two corners (x1, x2)
    vec2 i1 = vec2(0.0);
    i1 = (x0.x > x0.y)? vec2(1.0, 0.0):vec2(0.0, 1.0);
    vec2 x1 = x0.xy + C.xx - i1;
    vec2 x2 = x0.xy + C.zz;

    // Do some permutations to avoid
    // truncation effects in permutation
    i = mod289(i);
    vec3 p = permute(
            permute( i.y + vec3(0.0, i1.y, 1.0))
                + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(
                        dot(x0,x0),
                        dot(x1,x1),
                        dot(x2,x2)
                        ), 0.0);

    m = m*m ;
    m = m*m ;

    // Gradients:
    //  41 pts uniformly over a line, mapped onto a diamond
    //  The ring size 17*17 = 289 is close to a multiple
    //      of 41 (41*7 = 287)

    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;

    // Normalise gradients implicitly by scaling m
    // Approximation of: m *= inversesqrt(a0*a0 + h*h);
    m *= 1.79284291400159 - 0.85373472095314 * (a0*a0+h*h);

    // Compute final noise value at P
    vec3 g = vec3(0.0);
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * vec2(x1.x,x2.x) + h.yz * vec2(x1.y,x2.y);
    return 130.0 * dot(m, g);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;

    vec3 color = vec3(0.0);

    // Scale the space in order to see the function
    st *= 10.;

    color = vec3(snoise(st)*.5+.5);

    gl_FragColor = vec4(color,1.0);
}

 

마지막으로 응용 예제를 살펴보자.

vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float snoise(vec2 v) {
    const vec4 C = vec4(0.211324865405187,  // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,  // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,  // -1.0 + 2.0 * C.x
                        0.024390243902439); // 1.0 / 41.0
    vec2 i  = floor(v + dot(v, C.yy) );
    vec2 x0 = v -   i + dot(i, C.xx);
    vec2 i1;
    i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod289(i); // Avoid truncation effects in permutation
    vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
        + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
    m = m*m ;
    m = m*m ;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;
    vec3 color = vec3(0.0);
    vec2 pos = vec2(st*3.);

    float DF = 0.0;

    // Add a random position
    float a = 0.0;
    vec2 vel = vec2(u_time*.1);
    DF += snoise(pos+vel)*.25+.25;

    // Add a random position
    a = snoise(pos*vec2(cos(u_time*0.15),sin(u_time*0.1))*0.1)*3.1415;
    vel = vec2(cos(a),sin(a));
    DF += snoise(pos+vel)*.25+.25;

    color = vec3( smoothstep(.7,.75,fract(DF)) );

    gl_FragColor = vec4(1.0-color,1.0);
}

 

'셰이더 (Shader) > The Book of Shaders (완)' 카테고리의 다른 글

[GLSL] 13 - Fractal Brownian Motion  (0) 2023.05.04
[GLSL] 12 - Cellular Noise  (0) 2023.05.03
[GLSL] 10 - Random  (0) 2023.04.25
[GLSL] 9 - Patterns  (0) 2023.04.20
[GLSL] 8 - 2D Matrices  (1) 2023.04.20

댓글