float n = fbm(q + vec2(0., -t*1.8));
n += 0.5*fbm(q*2.3 + vec2(t*.3, -t*3.2));
float flame = clamp(
n*2.1*base - uv.y*1.15, 0., 1.);
col = vec3(1.5*flame,
1.5*pow(flame,1.9), pow(flame,3.5));
5옥타브 노이즈를 위로 흘려보낸 불 · 색은 온도 램프
03BLACK HOLE
float r = length(p);
float a = atan(p.y, p.x)
+ 0.5/(r + 0.12) - t*.1;
vec2 q = vec2(cos(a), sin(a)) * r;
float disk =
exp(-pow((r-.42)*7., 2.));
각도를 1/r만큼 비틀어 만든 중력 렌즈 + 강착원반
04GOLD FIELD
float d1 = length(p - vec2(0., .42));
float d2 = length(p + vec2(0., .42));
float w = sin(d1*d2*16. - t*2.2
+ fbm(p*2.)*1.5);
float g = 0.035/(abs(w)+0.02);
col = g * vec3(1., .62, .18);
두 초점 거리곱(카시니 곡선)의 등고선을 빛으로
풀고 있는 식 — 픽셀 하나에 8나노초
위 네 장면에 반복문이 안 보이는 이유는 반복이 코드가 아니라 칩 안에 있기 때문입니다.
화면은 GPU 에게 “픽셀 하나를 받으면 색 하나를 내놓는 함수”만 건네고, 나머지는 하드웨어가 200만 번 돌립니다.
색 = f(픽셀 좌표, 시간) ← 셰이더가 쓰는 함수의 전부. 이웃 픽셀을 못 본다
예산 = 1000 ms / 60 fps = 16.67 ms
1080p 픽셀 = 1920 × 1080 = 2,073,600
픽셀 하나당 = 16.67 ms / 2,073,600 = 8.04 ns3 GHz 코어라면 약 24 클럭
이 24 클럭이 셰이더 문법을 통째로 설명합니다. 분기(if)를 쓰면 같은 무리의 픽셀들이 서로 다른 길을 가면서
비용이 두 배가 되고, 그래서 셰이더 코드는 if 대신 곱셈으로 씁니다 —
step(), smoothstep(), mix() 가 어디에나 나오는 게 그 흔적입니다.
조건을 0과 1 사이의 수로 바꿔 곱해 버리는 것이 이 예산 안에서 사는 방법입니다.
이웃을 못 본다는 제약
셰이더 함수는 자기 픽셀 좌표 말고는 아무것도 모릅니다. 옆 픽셀이 무슨 색인지, 이전 프레임이 어땠는지 볼 수 없습니다.
그래서 모든 형태를 좌표의 함수로 다시 써야 합니다 — 위 장면들의 번개도, 물결도, 도형 목록이 아니라
“이 좌표가 도형 안쪽인가”를 답하는 거리 함수로 표현돼 있습니다.
제약처럼 보이지만 이게 속도의 원천입니다. 픽셀끼리 서로를 안 보기 때문에 순서가 상관없고,
순서가 상관없으니 수천 개를 동시에 돌릴 수 있습니다. 아래 사진의 그 반복되는 블록들이 그래서 쓸모가 있습니다.
이 코드가 실제로 도는 곳
위 그림들은 전부 화면의 픽셀마다 같은 짧은 함수를 한 번씩 돌려 만든 것입니다.
그 “한 번씩”을 동시에 해내는 물건이 어떻게 생겼는지, 라이선스가 확인되는 사진으로 짚습니다.
① 같은 것이 수없이 반복된다 — 그래서 픽셀마다 돌릴 수 있다
ATI RV670 그래픽 칩의 다이(뚜껑을 벗긴 실리콘 알맹이)입니다.
눈에 먼저 들어오는 건 같은 무늬가 계속 반복된다는 점입니다.
CPU 사진처럼 서로 다른 큰 블록 몇 개가 있는 게 아니라, 거의 같은 단위가 격자로 깔려 있습니다.
이 반복이 위 그림들의 전제입니다 — 셰이더를 “픽셀 하나에 대한 함수”로만 쓰면 되는 이유가
여기 있습니다. 나머지는 저 똑같은 단위들이 서로 다른 픽셀을 맡아 동시에 처리합니다.
코드에 반복문이 안 보이는데 화면 전체가 칠해지는 건, 반복이 코드가 아니라 이 사진 안에 있기 때문입니다.