| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 아트
- Codeforce
- opengl 3.3
- C++
- shader
- Texture2D
- Color struct
- OpenGL
- gsls
- opengl3.3
- ShaderProgram
- string_view
- voronoi
- Today
- Total
Longseabear DevLog
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;
}

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

위의 생성된 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 |