Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
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
Archives
Today
Total
관리 메뉴

Longseabear DevLog

Voronoi noise 파헤치기 본문

Graphics Project

Voronoi noise 파헤치기

longseabear 2024. 10. 27. 19:10

Voronoi Noise

Voronoi Noise

Voronoi 노이즈는 컴퓨터 그래픽스와 절차적 텍스처링에서 자주 사용되는 노이즈 생성 기법 중 하나로 voronoi 다이어그램을 기반으로 한 노이즈 패턴이다. 가감없이 뭐든 할 수 있는 멋진 노이즈다. 물 표현, 볼케이노 표현 등 다양한 상황에서 활용 가능하다.

 

Unity shader code(Voronoi Node | Shader Graph | 6.9.2)를 분석하여 연구해보았다. 아래 테스트 코드는 GLSL Editor 에서 코드를 직접 시뮬레이션 해볼 수 있다.

함께 분석하며 노이즈의 세계에 빠져보자 :)

Random Point Sampling

Voronoi는 랜덤 샘플링 된 점을 기준으로 noise pattern을 생성한다. 따라서, 샘플링 된 점을 시각화 할 수 있다면 더 좋은 분석이 가능할 것이다.

 

이를 위해 다음과 같은 기초 fragment shader code를 작성한다. PlotDot 함수는 단순하게 fragment shader에 지정된 위치에 점을 찍는다.

// Author:
// Title:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

float cell_size = 1.0;
float PlotDot(vec2 st, vec2 pos) {
    float dist = distance(st, pos);
    float radius = 0.005/(1./cell_size);
    return smoothstep(radius, radius * 0.5, dist);
}
void main() {
    float seed = 2.0;

    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    //st.x *= u_resolution.x/u_resolution.y;

    gl_FragColor = vec4(0.0);
    gl_FragColor.r = PlotDot(st, vec2(0.5,0.5)); // 0.5, 0.5 지점에 점을 찍으세요
    gl_FragColor.w = 1.0;
}

실행 결과 이미지

하나의 점이 샘플링 되었다. 현재는 (0.5, 0.5) 지점에 고정된 상태로 생성된다. 다음과 같은 함수를 추가하여 랜덤 위치에 생성되도록 바꿔보자.

float seed = 5.;
vec2 rand2(vec2 UV)
{
    mat2 m = mat2(15.27, 47.63, 99.41, 89.98);

    UV = fract(sin(m * (UV + seed)) * 46839.32);
    return UV;
}

void main() {
...
    gl_FragColor.r = PlotDot(fract(st * cell_size), rand2(vec2(0,0)));
...
}

Smooth하게 랜덤성을 주면 더 이쁜 것을 만들 수 있다. 다음 코드를 깊게 분석해보자.

float seed = 5.;
vec2 rand2(vec2 UV, float offset)
{
    mat2 m = mat2(15.27, 47.63, 99.41, 89.98);

    UV = fract(sin(m * (UV + seed)) * 46839.32);
    return vec2(
        sin(UV.y * offset) * 0.5 + 0.5,
        cos(UV.x * offset) * 0.5 + 0.5
    );
}

// MAIN...
    gl_FragColor.r = PlotDot(fract(st * cell_size), rand2(vec2(0,0), u_time));

기존 래덤 함수에 offset이라는 개념이 생겼다. 그리고 랜덤하게 생성된 UV fraction 값(0과 1사이)에 offset을 곱해줬다. UV는 샘플링 된 랜덤 값으로 항상 같은 값이다. 이때, offset을 변수로 바라보면, UV 값은 offset에 대한 기울기이다. 따라서, offset을 천천히 변화시키면 랜덤하게 설정된 기울기에 따라 offset이 변동하면서 -1과 1사이를 왔다리 갔다리 하게 된다. 진동수를 건드린다고 생각해도 좋다.

 

만약 offset이 초단위로 변화한다고 생각해보자. 기울기의 범위가 0에서 1 (fract 함수에 의해)이므로 한번 왔다리 갔다리 하는 시간을 1초에서 무한대(이동 하지 않음!)까지 변화시킬 수 있다. 주목할 점은 1초보다 빠르게 왔다리 갔다리하지는 않는다는 점이다.

 

위 코드를 실행하면 다음과 같은 결과가 나온다.

와따리.. 가따리..

위의 예제는 하나의 그리드에서 voronoi noise pattern의 point를 샘플링한 것이다. Voronoi noise pattern을 구현하기 위해서는 위와 같은 랜덤 샘플링 된 포인트가 여러개 필요하다. 위의 경우는 CellDensity가 1일 때의 샘플링 결과이다. 2x2가 되면 CellDensity가 2, 3x3이 되면 CellDensity가 3이라고 보면 된다.

 

위 메인 코드에서 이미 cell_size라는 이름의 CellDensity parameter를 만들어두었다. 해당 값을 통해 여러 그리드로 위 코드를 확장해보자.

    gl_FragColor.r = PlotDot(fract(st * cell_size), rand2(floor(st*cell_size), u_time));

cell_size를 10.으로 바꿔주자. 다음과 같은 유영하는 랜덤 샘플링 포인트를 얻을 수 있다.

멋지다!

Distance Weighting

랜덤하게 위치를 샘플링 했다면, 이제 실제로 voronoi pattern을 만들어 볼 시간이다. voronoi pattern은 간단하다. 현재 위치 상으로 가장 가까운 샘플링 포인트를 찾아 그 거리가 픽셀의 값이 되면 된다. 현재 그리드 상으로 가장 가까운 랜덤 샘플링이 될 수 있는 후보는 나와 인접해 있는 모든 그리드 지점이다. 해당 지점을 탐색하면서 가장 가까운 거리 값을 픽셀 값으로 설정하면 된다.

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

    float sampling_point = 0.;
    vec4 voronoi_pattern;

    float res = 8.0;
    for (int i=-1; i<=1; i++)
    {
        for(int j=-1;j<=1;j++)
        {
            vec2 b = vec2(i, j);
            vec2 r = b + rand2(floor(st*cell_size + b), u_time) - fract(st*cell_size);
            float w = r.x * r.x + r.y * r.y;
            res = min(res, w);

            sampling_point = max(PlotDot(fract(st * cell_size)-b, rand2(floor(st*cell_size + b), u_time)), sampling_point);    
        }
    }    
    gl_FragColor = vec4(sampling_point, 0.0, 0.0, 1.0) + res;
}

Voronoi pattern

샘플링 된 점을 기준으로 검은색, 원을 형성하며 마치 물결 모양의 텍스처가 나오는 것을 확인할 수 있다. 코드를 간단히 들여다보자.

    vec2 r = b + rand2(floor(st*cell_size + b), u_time) - fract(st*cell_size);

 

위 코드에서, b + rand2의 경우 현재 그리드 기준에서 봤을 때 인접한 그리드에서 샘플링 된 점의 실제 좌표이다. 의사 난수이므로 같은 값이 들어가면 항상 같은 랜덤 값이 도출되는 성질을 이용하여 해당 그리드에서 locally sampling 된 좌표 지점을 받아올 수 있고, b 값을 더해주어 실제 위치로 이동 시킬 수 있다.

 

fract(st*cell_size)는 현재 그리드 기준 내 픽셀의 위치이다. 현재 그리드 기준 내 픽셀 위치에서 랜덤 샘플링 된 점의 위치를 뺴주므로써 벡터를 얻을 수 있고, 거리를 계산하여 가장 작은 값을 픽셀 값에 대입해주면 된다.

            float w = r.x * r.x + r.y * r.y;
            res = min(res, w);

Sampling point는 약간 설명이 복잡한데, 샘플링 포인트 지점이 잘리는 부분을 보정해주도록 변경된 수식이다. (원래 그리드를 넘어가면 짤린다)

 

Application 

Cool voronoi

 

위의 생성된 voronoi pattern을 이용하여 다양한 아트를 만들어 볼 수 있다.

 

 

# 코드에서 부족한 부분은 스터디 용으로 여러분에게 영광을 겸허히 넘기겠습니다.

 

'Graphics Project' 카테고리의 다른 글

나만의 Texture2D class 만들기  (0) 2023.02.01
Color struct 만들기  (0) 2023.01.31
Shader program wrapper class  (0) 2023.01.28
Opengl 3.3 시작  (0) 2023.01.28