EFFECT ② · 녹색 메타볼 GOO의 작동 원리 · 비코더용 학습 가이드

녹색 덩어리는 물이 아니라 수학입니다.

우리가 만든 goo에는 물리 시뮬레이션이 없습니다. 대신 네 가지 단순한 아이디어가 겹쳐 있어요 — ① 모양을 거리로 표현하고(SDF), ② 거리들을 부드럽게 섞어 합쳐지게 하고(smooth-min = 끈적임), ③ 그걸 광선으로 그리고(레이마칭), ④ 작은 물리로 움직입니다. 아래 데모들의 슬라이더를 직접 만지면서 각 원리가 화면을 어떻게 바꾸는지 확인해 보세요.

실습 데모 7개그래프 3종 성능 계산기수식 ↔ 코드 매핑 오프라인 동작

관련: ▶ 실제 프로토타입 열기 · 기획서

개요 · 큰 그림

네 개의 블록이 쌓여 goo가 된다

각 블록은 따로 보면 아주 단순합니다. 합쳐졌을 때 "살아있는 끈적한 덩어리"처럼 보일 뿐이에요.

① 거리장 (SDF)

"이 점에서 표면까지 거리"를 알려주는 함수. 구는 거리 = 중심까지거리 − 반지름. 모양을 점·선이 아니라 수식으로 들고 있습니다.

② 끈적임 (smooth-min)

두 거리값을 부드럽게 최소화하면, 가까워진 두 덩어리 사이에 "목(neck)"이 생기며 녹아 붙습니다. 멀어지면 끊어집니다. 이게 메타볼의 핵심.

③ 레이마칭 (그리기)

삼각형 메시가 없으니, 픽셀마다 광선을 쏘고 "현재 표면까지 거리만큼" 한 발씩 전진해 표면을 찾습니다. 매끈한 곡면·광택을 메시 없이 그립니다.

④ 물리 (움직임)

덩어리 중심마다 중력·바닥 반발·킥 임펄스를 주는 ~40줄짜리 코드. 모양은 ②가, 움직임은 ④가 담당. 둘은 분리되어 있습니다.

왜 이렇게 나누나

"진짜 유체 시뮬"은 무겁고 통제가 어렵습니다. 우리는 모양(②)과 움직임(④)을 분리해서, 합쳐짐은 공짜로 얻고(수식) 움직임은 손으로 연출합니다(작은 물리). 그래서 가볍고, 비트에 맞춰 마음대로 조종할 수 있어요.

① 거리장 · Signed Distance Field

모양을 "거리"로 들고 있기

화면의 한 점 p가 있을 때, "표면까지 얼마나 떨어졌나"를 숫자로 돌려주는 함수를 SDF라고 합니다. 구(공)는 세상에서 가장 쉬운 SDF예요.

구의 부호 있는 거리 (Signed Distance) d(p) = | pc | − r   // c = 중심, r = 반지름, |·| = 길이(피타고라스)
| pc | = √((pₓ−cₓ)² + (p_y−c_y)² + (p_z−c_z)²)
부호의 뜻

d > 0 = 표면 바깥, d = 0 = 표면 위(껍질), d < 0 = 안쪽. 그래서 "Signed(부호 있는)" 거리. 표면은 항상 d = 0인 지점들의 모임입니다.

거리장 들여다보기

마우스를 캔버스 위에 올려보세요. 커서에서 표면까지의 거리(흰 선)와 부호를 실시간으로 보여줍니다. 동심원은 같은 거리값(등거리선)이에요.

② 끈적임 · Smooth Minimum (핵심)

합쳐짐의 비밀은 "부드러운 최소값"

두 모양을 합치려면 두 거리 중 더 가까운 것(최소값)을 고르면 됩니다. 그런데 그냥 min을 쓰면 만나는 곳에 날카로운 주름이 생겨요. 부드럽게 최소화하면 — 거기서 끈적임이 태어납니다.

딱딱한 합집합 vs 부드러운 합집합 딱딱: d = min(a, b)  // 만나는 곳에 각진 주름

부드럽게 (iq 다항식 smooth-min):
  h = clamp( 0.5 + 0.5·(ba) / k,  0,  1 )
  smin = mix(b, a, h) − k·h·(1 − h)   // k = 끈적임 반경
한 줄 직관

두 표면이 거리 k 안으로 들어오면, smin이 표면을 바깥으로 살짝 끌어내 둘 사이에 다리(목)를 놓습니다 → 합쳐져 보임. 다시 k보다 멀어지면 다리가 얇아지다 끊어집니다 → 분리. 그래서 k 하나가 "얼마나 끈적한가"를 정합니다. k가 곧 점성 손잡이.

두 덩어리 합치기 — 끈적임 k 조절

두 원을 드래그해 가까이/멀리 옮겨보세요. 끈적임 k를 올리면 더 멀리서도 다리가 생기고, 목이 두꺼워집니다. 흰 윤곽선이 실제 goo 표면(d=0)이에요.

왜 부드러운가 — min vs smin 단면 그래프

두 거리값이 교차하는 지점을, 딱딱한 min(주황, 각짐)smin(녹색, 둥글게)으로 비교합니다. k를 키울수록 모서리가 더 둥글게 깎여요 — 이 "깎인 부분"이 화면에선 목(neck)으로 보입니다.

코드에서

프로토타입의 scene() 안 루프가 모든 덩어리를 d = smin(d, 구거리, uSmoothK)로 차곡차곡 합칩니다. GUI의 "끈적임 K" 슬라이더가 바로 이 k = uSmoothK예요.

③ 레이마칭 · Sphere Tracing

메시 없이 그리는 법

우리에겐 삼각형이 없습니다. 거리 함수만 있죠. 그래서 픽셀마다 카메라에서 광선을 쏘고, "지금 위치에서 표면까지 거리"만큼 안전하게 한 발씩 전진합니다. 거리가 0에 가까워지면 = 표면에 닿음.

광선 행진 (Ray Marching / Sphere Tracing) p = o + d⃗ · t   // o=카메라, d⃗=광선 방향, t=전진 거리
반복:  tt + map(p) · 0.85   // 거리만큼 점프(0.85=안전계수)
멈춤:  map(p) < 0.001 (닿음)  또는  스텝 수 초과 / 너무 멀어짐
왜 "거리만큼" 점프가 안전한가

현재 점에서 가장 가까운 표면이 거리 d만큼 떨어져 있다면, d만큼 전진해도 절대 표면을 뚫고 지나치지 않습니다(반지름 d인 빈 공이 보장됨). 그래서 빈 공간은 성큼성큼, 표면 근처는 촘촘히 — 효율적이에요. 단, 스텝 수가 모자라면 멀리 있는 표면에 닿기 전에 포기 → 잘리거나 띠가 보입니다.

광선이 표면을 찾아가는 과정

왼쪽 눈에서 광선이 출발합니다. 각 단계의 빈 원이 "그 지점에서 가장 가까운 표면까지 거리"예요. 스텝 수를 줄이면 표면에 못 닿는 걸, 안전계수를 1 이상으로 올리면 표면을 뚫는(과전진) 걸 볼 수 있습니다.

④ 빛 · 표면 노멀 & 셰이딩

왜 "젖은 녹색"으로 보이나

표면에 닿으면, 그 점이 어느 방향을 보는지(노멀)를 구해 빛을 입힙니다. 노멀은 거리장의 "기울기"로 공짜로 얻어요.

노멀(법선) = 거리장의 기울기 n = normalize( map(p+εx̂)−map(p−εx̂),  … y, … z )  // 미세 차분

표면 색 = 3가지 빛의 합 확산 = max(0, n·l)  // 빛 방향과 마주봄 → 밝음
프레넬 림 = (1 − n·v)3  // 가장자리 녹색 발광
스페큘러 = max(0, n·h)64  // 젖은 하이라이트(흰 점)
광택·프레넬·색 만져보기

광택을 올리면 젖은 하이라이트가 또렷해지고, 프레넬을 올리면 가장자리가 녹색으로 빛납니다. Hue로 색을, 글로우로 번짐을 조절. 프로토타입의 "룩·광택" 슬라이더와 같은 값이에요.

⑤ 움직임 · 작은 Verlet 물리

튐·바운스·킥은 어디서 오나

덩어리 중심마다 아주 단순한 물리를 매 프레임 적용합니다. 모양(메타볼)과 완전히 분리된, 수십 줄짜리 코드예요.

한 덩어리의 한 프레임 (Δt = 프레임 시간) 중력:  v_yv_yg·Δt
이동:  pp + v·Δt
바닥 반발:  if p_y < 바닥  →  v_y ← −v_y · e  // e=반발계수
점성:  vv · damping  // 1보다 약간 작게 → 느리고 무겁게
킥:  v_yv_y + impulse  // 비트/Space에 위로 발사
크기 → 튐 차이 (작을수록 더 튐) 발사 배율 vs = ( 크기기준 / r )p   // r이 작으면 vs>1 (가벼워서 더 높이)
공 하나 — 높이 그래프로 보기

왼쪽은 공, 오른쪽은 시간에 따른 높이 그래프입니다. 중력↑=빨리 떨어짐, 반발↑=더 오래 통통, 점성↑(감쇠↓)=금세 멈춤. KICK으로 발사해 곡선을 보세요.

오디오 연결

실제 프로토타입에선 비트 = 킥 임펄스, 저음 = 점성·크기↑로 연결됩니다. 즉 위 슬라이더들을 음악이 자동으로 움직이는 셈이에요.

⑥ 레이어 · 독립 goo 군

두 겹이 동시에 벌어지게

레이어는 "서로 섞이지 않는 별개의 goo 그룹"입니다. 같은 레이어끼리만 smin으로 끈적이고, 다른 레이어와는 그냥 겹쳐 보일 뿐이에요.

레이어별로 따로 합치고, 마지막에 합치기 레이어0: d₀ = smin( 레이어0의 구들 )
레이어1: d₁ = smin( 레이어1의 구들 )
최종:  d = min( d₀, d₁ )  // 레이어끼리는 딱딱한 min → 안 섞임
코드의 묘수 — 한 숫자에 두 정보 담기

덩어리 데이터는 vec4(x, y, z, w) 네 칸뿐. 그래서 마지막 칸에 반지름 + 레이어×8을 함께 넣고, 셰이더에서 레이어=floor(w/8), 반지름=w−레이어×8로 풀어 씁니다(반지름은 항상 8보다 작으니 안 겹침). 덕분에 추가 데이터 배열 없이 레이어를 표현해요. 각 레이어엔 다른 (Hue 오프셋)과 깊이를 줘서 "두 겹"으로 읽히게 합니다.

⑦ 성능 · 체계적으로

무엇이 비용을 결정하나

goo의 비용은 거의 하나의 곱으로 설명됩니다. 이 식만 이해하면 미디어월(4K+)에서 60fps를 맞추는 법이 보여요.

프레임당 거리 계산 횟수 ≈ (가장 큰 비용) 비용 ≈ 픽셀수 × 레이 스텝 × 덩어리 수
픽셀수 = (가로 × 내부해상도) × (세로 × 내부해상도)  // 내부해상도는 제곱으로 작용!
가장 강력한 손잡이 = 내부 해상도

픽셀 수는 가로·세로 양쪽에 곱해지므로 내부 해상도를 0.7로 낮추면 비용은 0.7² ≈ 0.49배 — 거의 절반! goo는 경계가 부드러워서 살짝 흐려져도 티가 잘 안 납니다. 그래서 4K 월에선 0.5~0.75로 렌더하고 화면에서 확대하는 게 정석이에요.

성능 계산기 — 우리 머신 기준 추정

곡선 = 내부 해상도에 따른 비용(제곱 곡선). 막대 = 스텝/덩어리의 선형 영향. ● = 현재 설정. fps는 M1 Max 대략 보정값 기준 추정치(실제는 프로토타입에서 확인).

손잡이별 비용 vs 효과

손잡이비용 영향시각 효과미디어월 팁
내부 해상도제곱 (가장 큼)선명도0.5~0.75로 — 비용 절반, 차이 미미
레이 스텝선형먼 표면·깊이 정확도16~32로 충분(goo는 단순). 띠 보이면 +
덩어리 수선형풍성함·디테일16~48 권장. 64는 강한 GPU에서만
끈적임 K거의 없음융합 정도마음껏. 단 너무 크면 레이마칭 띠 → 스텝↑
글로우/광택미미발광·젖은감LED에서 강하게 빛남 — 과하지 않게
출력 해상도제곱월 크기월 실제 픽셀로 고정 + 내부해상도로 보정

병목 3종

필레이트 (Fill-rate)

픽셀수 × 스텝. 4K 풀스크린 레이마칭이 여기서 막힙니다. → 내부 해상도↓ + 스텝↓로 해결.

연산 (ALU)

매 스텝마다 모든 덩어리의 거리+smin. 덩어리 수에 선형 비례. → 덩어리 수 제한, 필요하면 공간 격자 가속.

노멀 계산

표면에 닿은 픽셀은 노멀 때문에 map을 6번 더 호출. 표면이 화면을 많이 덮을수록 가중. → 보통 무시 가능.

⑧ 파라미터 사전

슬라이더 ↔ 수식 ↔ 결과

프로토타입의 각 손잡이가 어떤 수식/코드를 건드리는지, 올리면/내리면 어떻게 보이는지.

슬라이더수식·코드올리면 (↑)내리면 (↓)
끈적임 Ksmin의 k (uSmoothK)더 멀리서 융합, 목 두꺼움, 한 덩어리화또렷이 분리, 개별 공처럼
덩어리 수uBlobCount, 루프 횟수풍성·복잡 (비용↑)단순·깔끔 (비용↓)
레이어 수d₀/d₁/d₂ 분리여러 겹 동시에한 덩어리 군
레이어 색차Hue 오프셋레이어 색 대비 강함색이 비슷
덩어리 크기sizeBase × r전체적으로 큰 덩어리작은 알갱이
크기 다양성r = base·(1+(seed−.5)·2·var)크고 작은 게 섞여 형태 다채균일한 크기
크기→튐 차이vs = (base/r)p작은 건 높이, 큰 건 둔하게크기 무관 동일하게 튐
중력v_y −= g·Δt빨리 떨어짐, 묵직둥둥, 천천히
바닥 반발v_y ← −v_y·e오래 통통 튐바닥에 철퍽 정착
점성·감쇠v ← v·damping관성 유지, 출렁출렁금세 멈춤, 끈적·무거움
킥 발사력v_y += impulse높이 솟구침살짝 들썩
저음→점성bass→damping·r 보정저음에서 크게 뭉침·느려짐음악 영향 적음
광택 / 프레넬 / 글로우spec / fresnel / 누적 halo젖은 광·발광 강매트·차분
레이 스텝 / 내부 해상도성능 손잡이정확·선명 (비용↑)빠름 (띠·흐림 위험)
⑨ 수식·코드 요약

한 장으로 보는 전체 흐름

// 1) 한 덩어리까지의 거리 (SDF)
float sdSphere(p, c, r) return length(p - c) - r;

// 2) 부드러운 최소 = 끈적임 (merge/split)
float smin(a, b, k){
  h = clamp(0.5 + 0.5*(b-a)/k, 0, 1);
  return mix(b, a, h) - k*h*(1-h);
}

// 3) 모든 덩어리를 레이어별로 합침
float scene(p){
  for (덩어리 i) d[layer] = smin(d[layer], sdSphere(p, c_i, r_i), uSmoothK);
  return min(d0, d1, d2);   // 레이어끼리는 안 섞임
}

// 4) 레이마칭 — 표면 찾기
t = 0;
for (i < uSteps){ d = scene(o + dir*t); if(d < 0.001) 닿음; t += d*0.85; }

// 5) 노멀 + 빛 (젖은 녹색)
n = normalize(차분(scene));  diffuse = n·l;  fresnel = (1-n·v)^3;  spec = (n·h)^64;

// 6) 물리 (JS, 매 프레임) — 모양과 분리
v.y -= g*dt;  p += v*dt;  if(바닥) v.y = -v.y*e;  v *= damping;  // 킥: v.y += impulse
기억할 3문장

모양은 거리(SDF)로, 합쳐짐은 smooth-min으로 — k가 끈적임. ② 그리기는 레이마칭 — 거리만큼 점프, 스텝·내부해상도가 성능. ③ 움직임은 별도의 작은 물리 — 중력·반발·킥, 음악이 이 손잡이를 돌린다.

▶ 이제 실제 프로토타입에서 직접 확인하기 · 기획서로