개발/2D 그래픽스

2D 그래픽스 변환 행렬 치트시트

워로디스 2026. 5. 27. 22:52

이 문서는 2D 그래픽스에서 쓰는 affine 변환 행렬을 기준으로 합니다.

이 행렬로 표현할 수 있는 변환은 다음입니다.

  • 이동, translation
  • 스케일, scale
  • 회전, rotation
  • 기울임, shear/skew
  • 반사/뒤집기, reflection/flip
  • 위 변환들의 조합
  • 특정 점을 기준으로 한 회전/스케일
  • 좌표계 변환, local ↔ world, screen ↔ local

단, 다음은 일반적인 2D affine 행렬의 범위를 벗어납니다.

  • 원근 변환, perspective/projective transform
  • 곡선형 왜곡, nonlinear warp
  • 클리핑, 마스크, opacity, blur 같은 렌더링 효과

1. 기준 행렬 형태

이 문서는 열벡터 기준입니다.

점은 다음처럼 본다고 가정합니다.

p = [x, y, 1]^T

변환은 다음처럼 적용합니다.

p' = M * p

2D affine 행렬은 다음 형태입니다.

| a  c  tx |
| b  d  ty |
| 0  0  1  |

좌표 변환식은 다음과 같습니다.

x' = a*x + c*y + tx
y' = b*x + d*y + ty

2. 성분 의미

성분 의미
a, b, c, d 선형변환 부분
tx, ty 이동 부분
0, 0, 1 2D affine 동차좌표 고정 부분

a, b, c, d에는 다음 변환이 섞여 들어갈 수 있습니다.

  • 스케일
  • 회전
  • 기울임
  • 반사

따라서 일반적인 행렬에서는 ad만 보고 스케일이라고 단정하면 안 됩니다.

다만 다음처럼 b = 0, c = 0인 축 정렬 변환에서는:

| a  0  tx |
| 0  d  ty |
| 0  0  1  |

a는 x 방향 스케일, d는 y 방향 스케일로 볼 수 있습니다.

3. TypeScript 기본 코드

type Point = { x: number; y: number };

type Mat2D = {
  a: number; b: number;
  c: number; d: number;
  tx: number; ty: number;
};

const identity = (): Mat2D => ({
  a: 1, b: 0,
  c: 0, d: 1,
  tx: 0, ty: 0,
});

const apply = (m: Mat2D, p: Point): Point => ({
  x: m.a * p.x + m.c * p.y + m.tx,
  y: m.b * p.x + m.d * p.y + m.ty,
});

apply()는 행렬을 점에 적용해서 변환된 점을 반환합니다.

apply(identity(), { x: 2, y: 3 });
// { x: 2, y: 3 }

4. 행렬 곱셈

열벡터 기준에서 multiply(A, B)는 다음을 의미합니다.

A * B
const multiply = (A: Mat2D, B: Mat2D): Mat2D => ({
  a: A.a * B.a + A.c * B.b,
  b: A.b * B.a + A.d * B.b,

  c: A.a * B.c + A.c * B.d,
  d: A.b * B.c + A.d * B.d,

  tx: A.a * B.tx + A.c * B.ty + A.tx,
  ty: A.b * B.tx + A.d * B.ty + A.ty,
});

열벡터 기준에서는 오른쪽 행렬이 먼저 적용됩니다.

M = T * R * S

이면 실제 적용 순서는 다음입니다.

1. S
2. R
3. T

즉, scale → rotate → translate 순서로 적용됩니다.

5. 변환 행렬 생성 함수

const translation = (tx: number, ty: number): Mat2D => ({
  a: 1, b: 0,
  c: 0, d: 1,
  tx, ty,
});

const scaling = (sx: number, sy: number): Mat2D => ({
  a: sx, b: 0,
  c: 0, d: sy,
  tx: 0, ty: 0,
});

const rotation = (rad: number): Mat2D => {
  const cos = Math.cos(rad);
  const sin = Math.sin(rad);

  return {
    a: cos, b: sin,
    c: -sin, d: cos,
    tx: 0, ty: 0,
  };
};

const shearXMatrix = (k: number): Mat2D => ({
  a: 1, b: 0,
  c: k, d: 1,
  tx: 0, ty: 0,
});

const shearYMatrix = (k: number): Mat2D => ({
  a: 1, b: k,
  c: 0, d: 1,
  tx: 0, ty: 0,
});

6. 변환 누적 함수

호출 순서와 실제 적용 순서를 같게 만들려면, 새 변환을 기존 행렬의 왼쪽에 곱합니다.

const thenTranslate = (m: Mat2D, tx: number, ty: number): Mat2D =>
  multiply(translation(tx, ty), m);

const thenScale = (m: Mat2D, sx: number, sy: number): Mat2D =>
  multiply(scaling(sx, sy), m);

const thenRotate = (m: Mat2D, rad: number): Mat2D =>
  multiply(rotation(rad), m);

const thenShearX = (m: Mat2D, k: number): Mat2D =>
  multiply(shearXMatrix(k), m);

const thenShearY = (m: Mat2D, k: number): Mat2D =>
  multiply(shearYMatrix(k), m);

사용 예시는 다음처럼 읽으면 됩니다.

let M = identity();

M = thenScale(M, 2, 1);
M = thenRotate(M, Math.PI / 2);
M = thenTranslate(M, 1, 2);

apply(M, { x: 2, y: 3 });
// approximately { x: -2, y: 6 }

실제 적용 순서도 코드와 같습니다.

1. x 2배, y 1배 스케일
2. 90도 회전
3. (1, 2) 이동

7. 이동 Translation

행렬

| 1  0  tx |
| 0  1  ty |
| 0  0  1  |

예시

const M = translation(5, 2);

apply(M, { x: 2, y: 3 });
// { x: 7, y: 5 }

계산:

x' = 1*2 + 0*3 + 5 = 7
y' = 0*2 + 1*3 + 2 = 5

조정 규칙

원하는 동작 조정
오른쪽 이동 tx 증가
왼쪽 이동 tx 감소
위/아래 이동 좌표계에 따라 ty 조정

수학 좌표계는 보통 y가 위로 증가합니다.
화면 좌표계는 보통 y가 아래로 증가합니다.

8. 스케일 Scale

행렬

| sx  0   0 |
| 0   sy  0 |
| 0   0   1 |

예시

const M = scaling(2, 0.5);

apply(M, { x: 2, y: 3 });
// { x: 4, y: 1.5 }

계산:

x' = 2*2 = 4
y' = 0.5*3 = 1.5

조정 규칙

회전/기울임이 없는 경우:

원하는 동작 조정
x 방향 확대/축소 a 조정
y 방향 확대/축소 d 조정
x 방향 반전 a를 음수로 설정
y 방향 반전 d를 음수로 설정

예:

const M: Mat2D = {
  a: 3, b: 0,
  c: 0, d: 2,
  tx: 0, ty: 0,
};

apply(M, { x: 2, y: 3 });
// { x: 6, y: 6 }

9. 회전 Rotation

행렬

원점 기준 회전입니다.

수학 좌표계, 즉 y가 위로 증가하는 좌표계에서는 양의 각도가 반시계 방향입니다.

| cosθ  -sinθ  0 |
| sinθ   cosθ  0 |
| 0      0     1 |

즉:

a = cosθ
b = sinθ
c = -sinθ
d = cosθ

예시: 90도 회전

const M = rotation(Math.PI / 2);

apply(M, { x: 2, y: 3 });
// approximately { x: -3, y: 2 }

계산:

cos(90°) = 0
sin(90°) = 1

x' = 0*2 + (-1)*3 + 0 = -3
y' = 1*2 + 0*3 + 0 = 2

회전은 a, b, c, d가 함께 바뀝니다.
따라서 회전이 들어간 행렬에서는 ad만 보고 스케일이라고 하면 안 됩니다.

10. 화면 좌표계에서의 회전 방향

위 회전 행렬은 수학 좌표계, 즉 y가 위로 증가하는 좌표계를 기준으로 합니다.

많은 2D 그래픽스 환경에서는 화면 좌표계가 다음과 같습니다.

x: 오른쪽으로 증가
y: 아래쪽으로 증가

이 경우 양의 각도를 넣었을 때 화면상으로는 회전 방향이 반대로 보일 수 있습니다.

좌표계 양의 각도
수학 좌표계, y-up 반시계 방향
화면 좌표계, y-down 시각적으로 시계 방향처럼 보일 수 있음

11. X 방향 기울임 Shear X

행렬

| 1  k  0 |
| 0  1  0 |
| 0  0  1 |

변환식:

x' = x + k*y
y' = y

y 값이 x에 섞입니다.

예시

const M = shearXMatrix(2);

apply(M, { x: 2, y: 3 });
// { x: 8, y: 3 }

계산:

x' = 2 + 2*3 = 8
y' = 3

조정 규칙

원하는 동작 조정
y 값에 따라 x를 밀기 c 조정

12. Y 방향 기울임 Shear Y

행렬

| 1  0  0 |
| k  1  0 |
| 0  0  1 |

변환식:

x' = x
y' = k*x + y

x 값이 y에 섞입니다.

예시

const M = shearYMatrix(0.5);

apply(M, { x: 2, y: 3 });
// { x: 2, y: 4 }

계산:

x' = 2
y' = 0.5*2 + 3 = 4

조정 규칙

원하는 동작 조정
x 값에 따라 y를 밀기 b 조정

13. 반사 / 뒤집기 Reflection / Flip

좌우 반전

y축 기준으로 좌우 반전합니다.

| -1  0  0 |
|  0  1  0 |
|  0  0  1 |
const flipX = (): Mat2D => ({
  a: -1, b: 0,
  c: 0, d: 1,
  tx: 0, ty: 0,
});

apply(flipX(), { x: 2, y: 3 });
// { x: -2, y: 3 }

상하 반전

x축 기준으로 상하 반전합니다.

| 1   0  0 |
| 0  -1  0 |
| 0   0  1 |
const flipY = (): Mat2D => ({
  a: 1, b: 0,
  c: 0, d: -1,
  tx: 0, ty: 0,
});

apply(flipY(), { x: 2, y: 3 });
// { x: 2, y: -3 }

14. 특정 점을 기준으로 회전

원점이 아니라 (cx, cy)를 기준으로 회전하려면 다음 순서로 합성합니다.

M = T(cx, cy) * R(rad) * T(-cx, -cy)

실제 적용 순서:

1. 기준점을 원점으로 옮김
2. 회전
3. 다시 원래 위치로 이동
const rotateAround = (
  cx: number,
  cy: number,
  rad: number
): Mat2D =>
  multiply(
    translation(cx, cy),
    multiply(rotation(rad), translation(-cx, -cy))
  );

const thenRotateAround = (
  m: Mat2D,
  cx: number,
  cy: number,
  rad: number
): Mat2D =>
  multiply(rotateAround(cx, cy, rad), m);

예시

let M = identity();

M = thenRotateAround(M, 10, 5, Math.PI / 2);

apply(M, { x: 12, y: 5 });
// approximately { x: 10, y: 7 }

15. 특정 점을 기준으로 스케일

원점이 아니라 (cx, cy)를 기준으로 스케일하려면 다음 순서로 합성합니다.

M = T(cx, cy) * S(sx, sy) * T(-cx, -cy)

실제 적용 순서:

1. 기준점을 원점으로 옮김
2. 스케일
3. 다시 원래 위치로 이동
const scaleAround = (
  cx: number,
  cy: number,
  sx: number,
  sy: number
): Mat2D =>
  multiply(
    translation(cx, cy),
    multiply(scaling(sx, sy), translation(-cx, -cy))
  );

const thenScaleAround = (
  m: Mat2D,
  cx: number,
  cy: number,
  sx: number,
  sy: number
): Mat2D =>
  multiply(scaleAround(cx, cy, sx, sy), m);

예시

let M = identity();

M = thenScaleAround(M, 10, 5, 2, 3);

apply(M, { x: 12, y: 6 });
// { x: 14, y: 8 }

설명:

중심점: (10, 5)
원래 점: (12, 6)
중심에서 원래 점까지의 차이: (2, 1)

x 2배, y 3배 스케일:
(2, 1) -> (4, 3)

중심에 다시 더함:
(10, 5) + (4, 3) = (14, 8)

16. 임의의 2D affine 변환

다음 행렬 하나로 2D affine 변환을 모두 표현할 수 있습니다.

| a  c  tx |
| b  d  ty |
| 0  0  1  |

표현 가능한 조합:

  • 이동
  • 균일 스케일
  • 비균일 스케일
  • 회전
  • shear
  • flip
  • 회전 + 스케일
  • 회전 + shear
  • flip + 이동
  • 중심점 기준 변환
  • local 좌표계에서 world 좌표계로의 변환

예:

const M: Mat2D = {
  a: 0, b: 2,
  c: -3, d: 0,
  tx: 10, ty: 20,
};

apply(M, { x: 2, y: 3 });
// { x: 1, y: 24 }

계산:

x' = 0*2 + (-3)*3 + 10 = 1
y' = 2*2 + 0*3 + 20 = 24

17. 기존 행렬에서 값 읽기

이동 읽기

const getTranslation = (m: Mat2D) => ({
  tx: m.tx,
  ty: m.ty,
});

tx, ty는 항상 이동 성분입니다.


스케일 크기 읽기

회전 + 스케일 조합이고 shear가 없다는 전제에서는 다음처럼 읽을 수 있습니다.

sx = sqrt(a*a + b*b)
sy = sqrt(c*c + d*d)
const getScale = (m: Mat2D) => ({
  sx: Math.hypot(m.a, m.b),
  sy: Math.hypot(m.c, m.d),
});

예시:

getScale({
  a: 0, b: 2,
  c: -3, d: 0,
  tx: 0, ty: 0,
});
// { sx: 2, sy: 3 }

단, shear가 섞여 있으면 이 값은 단순한 직관적 스케일 크기일 뿐, 완전한 행렬 분해는 아닙니다.


회전 각도 읽기

shear가 없고 flip이 없다는 전제에서는 다음처럼 읽을 수 있습니다.

const getRotation = (m: Mat2D) =>
  Math.atan2(m.b, m.a);

반환값은 radian입니다.


determinant 읽기

det = a*d - b*c
const determinant = (m: Mat2D) =>
  m.a * m.d - m.b * m.c;

의미는 다음과 같습니다.

의미
det > 0 방향 유지
det < 0 반사/뒤집힘 포함
det = 0 한 축 이상이 붕괴된 비가역 변환
abs(det) 면적 스케일 비율

예를 들어 x 2배, y 3배 스케일이면 determinant는 6입니다.
면적이 6배가 됩니다.

18. 역행렬 Inverse Transform

역행렬은 변환을 되돌릴 때 씁니다.

대표적인 사용처:

  • screen 좌표를 local 좌표로 변환
  • 마우스 좌표가 오브젝트 안에 있는지 검사
  • world 좌표를 object 좌표로 되돌리기
const invert = (m: Mat2D): Mat2D => {
  const det = m.a * m.d - m.b * m.c;

  if (Math.abs(det) < 1e-12) {
    throw new Error("Matrix is not invertible");
  }

  return {
    a: m.d / det,
    b: -m.b / det,
    c: -m.c / det,
    d: m.a / det,
    tx: (m.c * m.ty - m.d * m.tx) / det,
    ty: (m.b * m.tx - m.a * m.ty) / det,
  };
};

예시:

const M = translation(10, 20);
const inv = invert(M);

const world = apply(M, { x: 2, y: 3 });
// { x: 12, y: 23 }

const local = apply(inv, world);
// { x: 2, y: 3 }

19. 사각형 변환

회전이나 shear가 들어가면 사각형의 x, y, width, height만 직접 변환하면 안 됩니다.

네 꼭짓점을 변환한 뒤 다시 bounds를 구해야 합니다.

type Rect = { x: number; y: number; w: number; h: number };

const transformRect = (m: Mat2D, r: Rect): Rect => {
  const points = [
    apply(m, { x: r.x, y: r.y }),
    apply(m, { x: r.x + r.w, y: r.y }),
    apply(m, { x: r.x, y: r.y + r.h }),
    apply(m, { x: r.x + r.w, y: r.y + r.h }),
  ];

  const xs = points.map(p => p.x);
  const ys = points.map(p => p.y);

  const minX = Math.min(...xs);
  const maxX = Math.max(...xs);
  const minY = Math.min(...ys);
  const maxY = Math.max(...ys);

  return {
    x: minX,
    y: minY,
    w: maxX - minX,
    h: maxY - minY,
  };
};

20. 빠른 판별 코드

부동소수점 오차를 고려해서 === 0보다는 epsilon 비교를 쓰는 편이 안전합니다.

const nearlyZero = (v: number, eps = 1e-9) =>
  Math.abs(v) < eps;

const isAxisAligned = (m: Mat2D) =>
  nearlyZero(m.b) && nearlyZero(m.c);

const hasTranslation = (m: Mat2D) =>
  !nearlyZero(m.tx) || !nearlyZero(m.ty);

const hasFlip = (m: Mat2D) =>
  determinant(m) < 0;

const isSingular = (m: Mat2D) =>
  nearlyZero(determinant(m));

판별 기준:

조건 해석
tx, ty만 변함 이동
b = 0, c = 0 x/y 축이 서로 섞이지 않음
b = 0, c = 0, a > 0, d > 0 축 정렬 스케일
det < 0 반사/뒤집힘 포함
det = 0 비가역, 한 축 이상 붕괴
b 또는 c가 0이 아님 회전 또는 shear 가능성

21. 행벡터 시스템과의 관계

이 문서는 열벡터 기준입니다.

열벡터 기준 행렬:

| a  c  tx |
| b  d  ty |
| 0  0  1  |

적용 방식:

p' = M * p

행벡터 기준에서는 점을 다음처럼 둡니다.

p = [x, y, 1]

그리고 보통 행렬은 다음처럼 전치된 형태로 씁니다.

| a   b   0 |
| c   d   0 |
| tx  ty  1 |

적용 방식:

p' = p * M

이 경우에도 좌표 변환식은 동일하게 볼 수 있습니다.

x' = a*x + c*y + tx
y' = b*x + d*y + ty

다만 행렬의 배치와 합성 순서가 달라지므로, 문서나 엔진에서 어떤 규약을 쓰는지 반드시 확인해야 합니다.

22. 요약

행렬 형태

| a  c  tx |
| b  d  ty |
| 0  0  1  |

변환식

x' = a*x + c*y + tx
y' = b*x + d*y + ty

핵심 규칙

하고 싶은 일 방법
이동 tx, ty 조정
x/y 축 정렬 스케일 a, d 조정
회전 a,b,c,dcos, sin으로 함께 설정
x 방향 shear c 조정
y 방향 shear b 조정
좌우 반전 a를 음수로 설정
상하 반전 d를 음수로 설정
특정 점 기준 회전 T(center) * R * T(-center)
특정 점 기준 스케일 T(center) * S * T(-center)
변환 되돌리기 inverse matrix 사용
사각형 변환 네 꼭짓점 변환 후 bounds 계산

가장 중요한 결론

단순 스케일 행렬에서는 a와 d가 스케일이다.

하지만 회전이나 기울임이 섞이면
a, b, c, d 전체가 하나의 선형변환 성분이다.

실무 코드에서는 다음 구조가 가장 다루기 좋습니다.

let M = identity();

M = thenScale(M, 2, 1);
M = thenRotate(M, Math.PI / 2);
M = thenTranslate(M, 1, 2);

const p = apply(M, { x: 2, y: 3 });
// approximately { x: -2, y: 6 }
반응형