컴퓨터는 점 몇 개를 어떻게 매끄러운 곡선으로 바꿀까?
디자이너가 손잡이 네 개를 움직인다. 영화 캐릭터의 볼이 매끄러워지고, 자동차 보닛이 휘고, 글자에 곡선이 잡힌다. 손잡이는 곡선 위에 있지 않다 — 자석처럼 곡선을 끌어당길 뿐이다. 하나를 움직이면 그 근처 곡선만 바뀌고 나머지는 가만히 있다. 한 연산을 재귀로 적용하면 이 모두가 나온다. 그 연산은 두 점 사이의 선형 보간 — 그것뿐.
아래 위젯의 손잡이를 끌어보자. t를 슬라이드하면 작도가 한 층씩 무너져 한 점에 닿는다 — 그 점이 곡선을 그린다.
왜 그냥 점들을 잇지 않을까?
컴퓨터에 “이 다섯 점을 지나는 매끄러운 곡선”을 그려달라 해보자. 첫 시도: 다항식 하나로 보간. 점이 두세 개면 잘 된다. 다섯 개를 넘으면 점들 사이에서 심하게 출렁인다 (Runge 현상). 스플라인은 국소 조각을 이어 붙여 이 문제를 푸는데, 조각 하나하나의 모양을 또 정해야 한다. 베지에의 발상은 다르다 — 점들을 지나는 곡선을 찾지 말고, 점들이 모양을 잡는 곡선을 찾자. 점들은 닻이 아니라 손잡이가 된다.
두 점 케이스 — lerp
두 점 와 사이의 선형 보간
# A point is a 2-tuple. Lerp is one line.
def lerp(a, b, t):
return (a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t)
lerp((0, 0), (4, 2), 0.0) # → (0, 0)
lerp((0, 0), (4, 2), 0.5) # → (2, 1)
lerp((0, 0), (4, 2), 1.0) # → (4, 2)재귀적 lerp — 드 카스텔조
위젯에서 층마다 색이 다르다. 점선 회색이 원래 제어 다각형, 주황이 레벨-1 lerp, 갈색이 레벨-2 lerp, 마지막 초록 점이 . 슬라이더를 끌어보면 작도가 무너지면서 초록 점이 움직이고, 그 자취가 그대로 곡선이 된다. 곡선은 별도의 공식으로 그리는 게 아니다 — 재귀의 자취 자체다.
# De Casteljau: lerp every adjacent pair, then again, until 1 point remains.
def bezier(controls, t):
pts = list(controls)
while len(pts) > 1:
pts = [lerp(pts[i], pts[i+1], t) for i in range(len(pts) - 1)]
return pts[0]
# Cubic Bezier — four control points
P = [(0, 0), (1, 2), (3, 2), (4, 0)]
bezier(P, 0.0) # → (0, 0) (= P[0], starts at first)
bezier(P, 1.0) # → (4, 0) (= P[-1], ends at last)
bezier(P, 0.5) # → (2.0, 1.5) (midpoint by recursion)그림에서 읽히는 것들
증명 없이, 작도를 들여다보기만 해도 네 가지 사실이 떨어진다.
- 끝점. 에서 모든 lerp가 왼쪽 점을 돌려주므로 . 대칭으로 . 곡선은 첫 제어점과 마지막 제어점을 지난다.
- 끝점에서의 접선. 부근의 레벨-1 lerp는 선분 위에 놓이고, 곡선은 그 방향으로 출발한다. 따라서 에서 쪽으로 출발하고, 에는 방향으로 도착한다. 디자이너는 이 사실로 두 곡선을 매끄럽게 잇는다 — 끝 손잡이를 정렬하면 된다.
- 볼록껍질. 층마다 lerp는 이전 층의 볼록조합 — 마지막 점은 제어점들의 볼록조합이다. 그래서 곡선은 다각형의 볼록껍질 안에 머물고 밖으로 나가지 않는다. 충돌 판정과 클리핑에 유용하다.
- 제어 다각형은 곡선이 아니다. 곡선을 가두고, 곡선의 방향을 가리키며, 끌어 움직일 수 있는 도구지만, 곡선은 다각형 안쪽에 — 모서리에서 부드럽게 멀어진 채로 — 자리 잡는다.
# Bernstein form — algebraically equivalent to De Casteljau.
def bezier_bernstein(P, t):
s = 1 - t
bx = (s**3 * P[0][0] + 3*s*s*t * P[1][0]
+ 3*s*t*t * P[2][0] + t**3 * P[3][0])
by = (s**3 * P[0][1] + 3*s*s*t * P[1][1]
+ 3*s*t*t * P[2][1] + t**3 * P[3][1])
return (bx, by)
bezier_bernstein(P, 0.5) # → (2.0, 1.5) same answer, different bookkeeping
#
# B'(0) = 3(P[1] - P[0]) → tangent at start points along P0→P1
# B'(1) = 3(P[3] - P[2]) → tangent at end points along P2→P3
# A designer reads "the curve leans into the next handle" off these two facts.왜 모든 그래픽 스택이 이걸 넣고 다닐까
위의 성질이야말로 도구에 정확히 필요한 것들이다. 국소 제어 — 손잡이 하나를 움직이면 근처 곡선만 바뀐다. 아핀 불변성 — 제어점을 변환하면 곡선도 같은 방식으로 따라 변환된다 (회전·스케일·이동에 식 재계산이 필요 없다). 수치 안정성 — 드 카스텔조는 lerp뿐이라 고차 다항식 특유의 상쇄가 없다. 이어붙이기 — 3차 베지에를 끝점·접선 맞춰 줄줄이 잇으면 B-스플라인이 된다 — CAD와 애니메이션의 일꾼.
구체적으로: TrueType은 2차 베지에, PostScript와 현대 폰트 대부분은 3차; SVG의 path 데이터는 약식 표기를 더한 베지에 문법; Figma·Illustrator·Inkscape — 바닥엔 모두 같은 재귀; 픽사의 매끄러운 애니메이션 곡선, CSS의 “ease-in-out” 타이밍 함수 — 사람이 손수 고른 3차 베지에다. 한 가지 알고리즘, 네 개의 손잡이, 곡선 산업 전체.
더 깊은 다리: 베지에 곡선은 매개변수 곡선 모듈을 소비한다. 곡선의 이미지는 매끄러운 경로, 매개변수화는 . 디자이너에게 보이는 것은 보통 이미지뿐이지만, 그것을 한 단계씩 따라가 그려내는 쪽은 매개변수화다.
베지에의 “매끄러움”은 국소 주장이지 전역 주장이 아니다. 제어 다각형이 날카롭게 꺾이는 3차 베지에는 자기 자신과 교차하는 곡선을 만든다 — 미분적으로는 여전히 매끄럽지만 (B’(t) 연속), 시각적으로는 병적이다. 위젯에서 으로 두면 자기 교차하는 Z가 나온다. 디자이너의 “매끄러움”과 미분기하학의 “매끄러움”은 여기서 갈라지지만, 손잡이 네 개라는 추상화는 그 사실을 한 마디도 경고해주지 않는다.
베지에는 lerp의 재귀. 두 점 사이의 한 연산을 인접 쌍에 적용하고, 그 결과 쌍에 또 적용하고, 또 한 번. 손잡이를 움직이면 lerp가 따라가고, 곡선은 lerp를 따라간다.
를 계산하라. 그 다음 . 두 점을 양 끝점 사이에 그려보자.
제어점 인 3차 베지에에서 드 카스텔조로 를 계산하라. 모든 층을 다 보여라.
왜 3차 베지에는 방향으로 출발하고 방향으로 도착할까? 드 카스텔조 작도로 기하적 논증을 하고, 다항식 형태에서 를 계산해 확인하라.
제어 다각형과 베지에 곡선은 첫 점과 마지막 점이 같고 대략 비슷한 모양이다. 둘이 다른 세 가지 방식을 들어라. 차이가 분명히 보이는 예를 위젯으로 만들어보자.
컴퓨터 그래픽 교과서는 보통 베지에를 번스타인 기저 공식 — — 으로 소개한다. 독자가 믿을 만한 근거가 전혀 없는 다항식 합인 채로. Lemma는 반대편 끝에서 출발한다: lerp — 디자이너가 이미 아는 단 하나의 연산. 그 lerp를 재귀로 세 번 적용하면 번스타인 형식이 유도된다. 업계는 공식을 쓰지만, 이해는 재귀에서 시작한다. 번스타인 기저는 결과지, 출발점이 아니다.