숨 막히게 아름다운 디지털 아트 작품을 보고 어떻게 만들어졌는지 깊이 궁금해한 적이 있으신가요? 디지털 아트에는 다양한 접근 방식이 있지만, 크리에이티브 코딩에 있어서 셰이더(Shaders)는 가장 강력하고 다재다능한 도구 중 하나일 것입니다.
셰이더는 디지털 시대의 붓과 같습니다. 빈 화면을 실시간으로 멋진 애니메이션으로 바꿀 수 있게 해줍니다. 여러분이 좋아하는 비디오 게임이나 영화 속 놀라운 시각 효과를 책임지는 것이 바로 이 셰이더입니다.
크리에이티브 개발자들은 오직 코드를 사용해서 2D 및 3D 렌더링을 만들 수 있으며, 그들의 상상력만이 유일한 한계가 됩니다. 셰이더 아트 코딩은 수학 함수와 알고리즘을 사용하여 픽셀을 조작하고 놀라운 시각 효과를 만들어내는 것입니다. 창의성과 정밀함이 충돌하여 진정으로 매혹적인 것을 만들어내는 예술과 과학의 독특한 융합입니다.
그렇다면 셰이더가 무엇인지 궁금하실 텐데요. 본질적으로 셰이더는 그래픽 카드에서 실행되는 작은 프로그램으로, 캔버스 위 각 픽셀의 색상을 계산하는 역할을 합니다.
셰이더는 현재 픽셀의 위치와 같은 입력을 받아, OpenGL 셰이딩 언어(GLSL)를 사용하여 단일 최종 색상을 계산합니다. 이를 수학적 함수 f(x, y) = (r, g, b)
로 볼 수 있습니다. 여기서 픽셀의 위치 (x, y)가 입력되면, 컴퓨터 그래픽에서 빨강(r), 초록(g), 파랑(b) 채널로 표현되는 출력 색상을 반환합니다.
이 함수는 화면의 모든 픽셀에 대해 병렬로 계산됩니다. 즉, 셰이더는 초당 수백만 번의 계산을 수행하여 놀라운 실시간 그래픽을 만들어낼 수 있습니다.
약 1년 전 셰이더를 발견한 이후, 저도 저만의 작품들을 만들어왔습니다. 새로운 컴퓨터 그래픽 기술을 배우고 그것에 대한 셰이더를 만드는 것은 정말 재미있습니다. 배울 주제가 너무 많고, 발견과 독창적인 창작을 위한 공간이 너무 넓어서 지루할 틈이 없습니다.
셰이더는 멋진 3D, 심지어 용감한 개발자를 위한 4D 장면을 렌더링하는 데 탁월하지만, 2D 이미지와 애니메이션을 렌더링하는 데에도 뛰어납니다. 2D 셰이더는 일반적으로 고성능 GPU가 필요한 무거운 3D 셰이더에 비해 매우 빠르게 실행됩니다.
1년 넘게 저는 2D 기반의 환상적인 애니메이션을 온라인에 공유하며 셰이더 코딩이 더 많은 사람에게 알려지도록 노력했습니다. 그러면서 코드를 사용하여 이러한 시각 효과를 만드는 방법에 대한 튜토리얼과 설명을 요청하는 많은 메시지를 받았습니다.
이 영상에서는 여러분이 셰이더를 사용하여 디지털 아트를 시작하는 데 도움이 될 수 있도록, 이 셰이더 애니메이션을 처음부터 만드는 과정을 안내해 드리겠습니다. 또한 여러분의 예술적 여정에 도움이 될 유용한 참고 자료와 도구도 제공할 것이며, 이는 영상 설명란에 기재될 것입니다.
프로그래밍에 대한 기본 지식이 있다고 가정하지만, 코드에 대해 전혀 몰라도 걱정하지 마세요. 모든 과정을 수학적인 측면을 포함하여 시각 자료를 통해 설명할 것입니다. 참고로 이 시각 자료들도 코드로 만들어졌습니다.
이 튜토리얼을 따라 하려면 Shadertoy.com으로 이동하세요. 이 플랫폼에서는 자신만의 셰이더를 쉽게 작성하고 다른 사람들과 공유할 수 있습니다. 컴퓨터 그래픽과 디지털 아트에 대한 영감과 지식의 보고입니다.
가장 기본적인 셰이더부터 시작하겠습니다. 'New'를 클릭하면 자동으로 생성되는 코드에서 시작할 텐데, 저는 모든 것을 지우고 처음부터 설명하겠습니다. 각 셰이더는 `mainImage` 함수 내에 정의되며 두 개의 매개변수를 가집니다.
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
// 코드는 여기에 작성됩니다.
}
fragCoord
는 입력 매개변수입니다. `vec2` 타입으로, 현재 픽셀의 x와 y 좌표를 담고 있는 벡터입니다. fragColor
는 출력 매개변수이며, 픽셀의 최종 색상을 담는 `vec4` 타입입니다. `vec4`는 빨강(r), 초록(g), 파랑(b) 채널과 투명도를 나타내는 알파(a) 채널, 총 4개의 값을 가집니다.
이 출력 매개변수에 값을 할당해 봅시다. 모든 채널을 0으로 설정하면, 화면 전체가 검은색으로 채워집니다.
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
GLSL에서 색상 값은 0과 1 사이로 정규화됩니다. 따라서 모든 채널을 1로 설정하면 흰색이 됩니다. 이 값들을 조절하여 수백만 가지의 독특한 색상을 만들 수 있습니다. 참고로, Shadertoy unofficial plugin이라는 크롬 확장 프로그램을 사용하면 색상 선택기와 같은 유용한 기능을 쓸 수 있습니다.
이제 픽셀 좌표를 사용하여 화면에 무언가를 그려보겠습니다. 먼저, 좌표를 정규화해야 합니다. 현재 fragCoord
의 범위는 캔버스 크기에 따라 달라집니다. (예: 1600x900). 캔버스 해상도에 의존하지 않도록 이 좌표를 0과 1 사이의 값으로 변환하는 것이 일반적입니다. 이를 위해 `iResolution`이라는 전역 상수를 사용합니다.
vec2 uv = fragCoord / iResolution.xy;
이제 `uv.x`는 빨간색 채널에, `uv.y`는 초록색 채널에 할당하면 왼쪽에서 오른쪽으로, 아래에서 위로 색상이 변하는 그라데이션을 볼 수 있습니다. 다음으로, 좌표 공간의 원점(0,0)이 화면 중앙에 오도록 변환해 보겠습니다. 이렇게 하면 대칭적인 패턴을 만들기 쉬워집니다.
// uv를 -1에서 1 사이의 범위로 변환
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
이렇게 하면 화면 비율이 왜곡될 수 있습니다. 원이 타원처럼 보일 수 있죠. 이를 해결하기 위해 x 좌표에 화면의 가로세로 비율(iResolution.x / iResolution.y
)을 곱해줍니다. 이 수정은 원치 않는 왜곡을 방지합니다.
uv.x *= iResolution.x / iResolution.y;
이제 `length` 함수를 사용해 보겠습니다. 이 함수는 벡터의 크기, 즉 원점으로부터의 거리를 계산합니다. 우리의 중심 좌표계에서 이 함수는 각 픽셀과 화면 중앙 사이의 거리를 계산합니다. 이 거리를 그레이스케일로 시각화하면 중앙에서 바깥으로 퍼지는 방사형 그라데이션을 볼 수 있습니다.
이것은 사실 반지름이 0.5인 원의 부호 있는 거리 함수(Signed Distance Function, SDF)를 나타냅니다. SDF는 특정 지점에서 도형의 경계까지의 최단 거리를 반환하며, 지점이 도형 내부에 있으면 음수, 외부에 있으면 양수 값을 가집니다. 저는 이 주제에 대한 훌륭한 자료를 설명란에 링크해 두었습니다.
step
함수를 사용하면 날카로운 경계를 가진 링을, smoothstep
함수를 사용하면 부드러운 경계를 가진 링을 만들 수 있습니다. sin
함수와 `iTime`을 사용하여 시간에 따라 움직이는 반복적인 링 패턴을 만들 수 있습니다.
float d = length(uv);
d = sin(d * 8. + iTime / 8.);
d = abs(d);
d = 0.02 / d; // 네온 효과를 위한 역함수
fragColor = vec4(vec3(d), 1.0);
이제 흑백에서 벗어나 색상을 추가해 봅시다. 저는 Inigo Quilez의 웹사이트에서 찾은 멋진 팔레트 함수를 사용할 것입니다. 이 함수는 삼각함수를 사용하여 무한히 다양한 색상 그라데이션을 생성합니다. 4개의 제어 매개변수를 사용하여 그라데이션의 구성을 맞춤 설정할 수 있습니다. 이 웹사이트에서 시각적으로 자신만의 팔레트를 만들 수도 있습니다.
이 팔레트 함수를 코드에 추가하고, 원래 픽셀의 거리 값과 시간에 따른 오프셋을 입력으로 사용하여 동적이고 다채로운 색상환을 만듭니다.
vec3 col = palette(length(uv0) + iTime);
//...
finalColor += col * d;
마지막으로 프랙탈 같은 느낌을 주기 위해 반복(iteration)을 추가하겠습니다. `for` 루프를 사용하여 색상 계산 코드를 여러 번 반복합니다. 각 반복마다 `fract` 함수를 사용하여 공간을 반복시키고, 이전 결과를 누적하여 최종 색상을 만듭니다.
vec3 finalColor = vec3(0.0);
for (float i = 0.0; i < 4.0; i++) {
uv = fract(uv * 1.5) - 0.5;
float d = length(uv) * exp(-length(uv0));
vec3 col = palette(length(uv0) + i*.4 + iTime*.4);
d = sin(d*8. + iTime)/8.;
d = abs(d);
d = pow(0.01 / d, 1.2);
finalColor += col * d;
}
fragColor = vec4(finalColor, 1.0);
루프 안에서 공간을 스케일링하고 반복(`fract(uv * 1.5)`)하며, 각 레이어의 색상을 `finalColor` 변수에 더해줍니다. 또한, `exp(-length(uv0))`를 곱하여 중앙에서 멀어질수록 패턴의 크기가 커지는 효과를 줍니다. `pow` 함수를 사용하여 대비를 높여 더욱 선명한 시각적 효과를 만들어냅니다. 이렇게 해서 최종적으로 복잡하고 아름다운 프랙탈 애니메이션이 완성됩니다.
이것으로 튜토리얼을 마칩니다. 셰이더 코딩은 무한한 가능성을 가지고 있습니다. 이 영상이 여러분에게 영감을 주어 자신만의 창의적인 여정을 시작하는 데 도움이 되었기를 바랍니다.