본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Computer Science

베지어 곡선

작성 2026. 10. 17.추가
목차9개 항목
출처와 참고

팔만코딩경(42Seoul 커뮤니티 위키)의 「포토샵에서 곡선을 그리는 방법, Bezier Curve」를 바탕으로, javascript.info(모던 JavaScript 튜토리얼)의 「베지어 곡선」, 「CSS 애니메이션」 글로 보강해 재구성한 노트다.

베지어 곡선이란

베지어 곡선(Bézier curve)은 시작점·끝점과 그 사이의 제어점(control point)으로 곡선 모양을 정하는 매개변수 곡선이다. 르노 자동차의 엔지니어 피에르 베지에(Pierre Bézier)가 차체 설계에 쓰며 알려졌다. 곡선은 시작점과 끝점을 지나고, 제어점은 곡선을 자기 쪽으로 끌어당길 뿐 곡선 위에 있지 않다.

점이 2개면 직선, 3개 이상이면 곡선이 된다. 점이 n+1개면 n차 곡선이다.

차수점제어점쓰이는 곳
1차P₀, P₁없음직선
2차P₀, P₁, P₂1개TrueType 폰트, SVG Q
3차P₀, P₁, P₂, P₃2개포토샵·일러스트레이터·피그마 펜 툴, SVG C, CSS cubic-bezier(), PostScript·OpenType(CFF) 폰트

출발점: 선형 보간

두 점 P₀, P₁을 잇는 선분 위의 점은 매개변수 t(0 ≤ t ≤ 1)로 쓴다.

B(t) = P₀ + t·(P₁ − P₀) = (1 − t)·P₀ + t·P₁
t = 0 → P₀,  t = 0.5 → 중점,  t = 1 → P₁

이것이 선형 보간(linear interpolation, lerp)이다. 베지어 곡선은 이 lerp를 단계마다 한 번씩 겹쳐 적용한 것이 전부다.

2차 곡선: lerp를 두 번

P₀ ────K₁──── P₁ ────K₂──── P₂
          \            /
           ──── K ────          K가 그리는 자취 = 2차 베지어 곡선
 
K₁ = lerp(P₀, P₁, t)
K₂ = lerp(P₁, P₂, t)
K  = lerp(K₁, K₂, t)

대입해서 정리하면 다음과 같다.

B(t) = (1 − t)²·P₀ + 2(1 − t)t·P₁ + t²·P₂

식은 벡터식이라 x와 y에 따로 적용한다. 점이 (0,0), (0.5,1), (1,0)이면 x = t, y = 2t − 2t²가 되어 가운데가 솟은 포물선이 나온다. 꼭짓점의 높이는 제어점의 절반(y=0.5)에 그친다. 제어점은 곡선을 끌어당길 뿐 닿지 않는다.

3차 곡선과 행렬 표현

점 4개에도 같은 과정을 한 단계 더 반복한다.

B(t) = (1 − t)³·P₀ + 3(1 − t)²t·P₁ + 3(1 − t)t²·P₂ + t³·P₃

계수 1, 3, 3, 1은 이항계수이고, 각 항의 가중치를 번스타인 다항식(Bernstein polynomial) 이라 한다. t의 거듭제곱과 상수 행렬로 나누면 다음과 같다.

                     ┌ -1  3 -3  1 ┐ ┌ P₀ ┐
B(t) = [t³ t² t 1] · │  3 -6  3  0 │ │ P₁ │
                     │ -3  3  0  0 │ │ P₂ │
                     └  1  0  0  0 ┘ └ P₃ ┘

가운데 4×4 행렬은 상수라서, 곡선 여러 개를 그릴 때도 제어점만 바꿔 같은 계산을 반복하면 된다. GPU가 곡선을 빠르게 그리는 데 유리한 형태다.

드 카스텔조 알고리즘

식을 외우지 않아도 lerp를 재귀로 적용하면 몇 차 곡선이든 계산할 수 있다. 이것이 드 카스텔조 알고리즘(De Casteljau's algorithm) 이다. 다항식을 바로 계산하는 것보다 수치적으로 안정적이다.

def lerp(a, b, t):
    return (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t)
 
def bezier(points, t):
    if len(points) == 1:
        return points[0]
    next_level = [lerp(points[i], points[i + 1], t) for i in range(len(points) - 1)]
    return bezier(next_level, t)
 
# t를 0→1로 움직이며 점을 찍으면 곡선이 된다
curve = [bezier([(0, 0), (30, 100), (70, 100), (100, 0)], i / 50) for i in range(51)]

성질

성질뜻
양 끝점 통과t=0이면 P₀, t=1이면 마지막 점
접선 방향시작점의 접선은 P₀→P₁, 끝점의 접선은 P₂→P₃ 방향. 펜 툴의 핸들이 바로 이 접선이다
볼록 껍질곡선은 제어점들이 만드는 볼록 다각형 안에 있다. 두 곡선의 볼록 껍질이 겹치지 않으면 곡선도 만나지 않으므로, 교차·충돌 검사와 화면 밖 판정을 싸게 거른다
아핀 불변곡선을 이동·회전·확대하려면 제어점만 변환하면 된다
전역 영향제어점 하나를 움직이면 곡선 전체가 바뀐다. 그래서 긴 경로는 3차 곡선 여러 조각을 이어 붙이고(path), 이음매에서 핸들을 일직선으로 맞춰 매끄럽게 한다

제어점 곡선과 보간 곡선

베지어 곡선은 양 끝점 말고는 제어점을 지나지 않는다. 「이 점들을 모두 지나는 곡선」이 필요하면 베지어가 아니라 보간(interpolation)을 쓴다. 점이 많을 때는 구간마다 낮은 차수 다항식을 이어 붙이는 스플라인 보간(Catmull-Rom 등)이 흔하며, 이 스플라인도 구간별로 3차 베지어로 바꿔 그릴 수 있다.

베지어보간(스플라인)
점의 역할모양을 끌어당기는 손잡이곡선이 지나야 하는 위치
어울리는 곳펜 툴, 폰트, 타이밍 곡선데이터 점을 잇는 차트, 카메라 경로

웹에서 만나는 베지어 곡선

/* 애니메이션 진행률 곡선: P₀=(0,0), P₃=(1,1) 고정, 두 제어점만 지정 */
.box { transition: transform 300ms cubic-bezier(0.25, 0.1, 0.25, 1); } /* = ease */

CSS cubic-bezier(x1, y1, x2, y2)에서 x축은 시간, y축은 진행률이다.

  • 시간은 되돌릴 수 없으므로 x는 0~1 안이어야 한다. 벗어나면 그 값은 무효다.
  • y는 범위를 벗어나도 된다. 1보다 크면 목표를 지나쳤다가 돌아오고(overshoot), 0보다 작으면 시작 전에 뒤로 물러난다.
  • linear는 cubic-bezier(0, 0, 1, 1), 직선이다.
  • 3차 베지어 하나로는 여러 번 튀는 바운스·탄성 곡선을 못 만든다. 이때는 CSS linear() 꺾은선이나 JavaScript 타이밍 함수를 쓴다(→ JavaScript 애니메이션: requestAnimationFrame과 Web Animations API).
<!-- SVG 경로: M 시작점, C 제어점1 제어점2 끝점 -->
<path d="M 0 100 C 30 0, 70 0, 100 100" />

정리

  • 베지어 곡선은 제어점으로 모양을 정하는 매개변수 곡선이고, 양 끝점만 지난다.
  • 원리는 선형 보간의 반복이며, 이를 재귀로 구현한 것이 드 카스텔조 알고리즘이다.
  • 3차 곡선은 번스타인 다항식, 또는 상수 4×4 행렬과 제어점의 곱으로 표현된다.
  • 펜 툴의 핸들, SVG C·Q, CSS cubic-bezier(), 폰트 외곽선이 모두 같은 수학이다.
  • 복잡한 곡선은 고차 곡선 하나가 아니라 3차 곡선 조각을 이어서 만든다.

관련 노트: 변환 행렬: 이동·크기·회전과 동차 좌표, JavaScript 애니메이션: requestAnimationFrame과 Web Animations API, CSS 애니메이션: transition과 @keyframes

원본 기록3

댓글

GitHub 계정으로 댓글을 남길 수 있어요.