행렬(matrix)
벡터에 대해 이해했다면 2D/3D 행렬, 벡터와 행렬 연산, 아핀 변환에 대해 알아보자.
1. Matrix2D
3D 프로그래밍에서 벡터와 행렬에 대한 이해는 필수입니다.
객체의 2D 변환(이동, 크기조절, 회전, 기울리기)에 대해 살펴볼께요.
보통 ‘아핀 변환’이라고 불리우는 중요한 개념입니다.
A. 벡터의 행렬 표현
#### 행행렬(row matrix) = 행벡터 아래 벡터는 한 줄로 되어 있습니다. 즉, 행벡터겠죠.
(x, y, 1)
1행 x 3열 -> **1** x **3** 행벡터입니다.
열행렬(column matrix) = 열벡터
(x)
(y)
(z)
3행 x 1열 -> **3** x **1** 열벡터입니다.
B. 벡터와 행렬 연산
행렬 차수가 같아야 행렬연산이 가능합니다.
마주보는 차수가 같으면 행렬 연산이 가능하고,
양 끝 차수가 합쳐져서 결과 행렬의 차수가 됩니다.
x 3 * 3 x 3 = 1 x 3
행벡터 연산
-
1
x 3 * 3 x 3 = 1 x 3
V | * | TM | = | V’ |
---|---|---|---|---|
(x, y, 1) | * | (m00, m01, m02) | = | (x’, y’, 1’) |
(m10, m11, m12) | ||||
(m20, m21, m22) |
x’ = x * m00 + y * m10 + 1 * m20;
y’ = x * m01 + y * m11 + 1 * m21;
1’ = x * m02 + y * m12 + 1 * m22;
(m02 = 0, m12 = 0, m22 = 1 따라서 1’ = 1)
V * TM = V’ (x * m00)
행벡터 연산은 우측에 곱해집니다.
열벡터 연산
-
3
x 3 * 3 x 1 = 3 x 1
TM | * | V | = | V’ |
---|---|---|---|---|
(m00, m01, m02) | * | (x) | = | (x’, y’, 1’) |
(m10, m11, m12) | (y) | |||
(m20, m21, m22) | (1) |
x’ = m00 * x + m01 * y + m02 * 1;
y’ = m10 * x + m11 * y + m12 * 1;
1’ = m20 * x + m21 * y + m22 * 1;
(m20 = 0, m21 = 0, m22 = 1 따라서 1’ = 1)
TM * V = V’ (m00 * x)
열벡터 연산은 좌측에 곱해집니다.
즉, 행벡터 연산과 열벡터 연산은 다른 결과가 나오겠죠.
AB ≠ BA
행렬의 기본성질 중에서 ‘행렬의 곱은 교환법칙이 성립하지 않는다.’와 관련이 있겠네요.
C. 행렬의 기본 성질
- AB ≠ BA
- A(BC) = (AB)C
- AI = IA = A
- (A-1) * A = I
- A-1 = 역행렬
- I(idntity) = 단위행렬
D. 아핀 변환 (Affine Transform)
아핀 변환은 ‘이동, 크기변환, 회전, 기울리기’를 하나의 행렬로 처리하기 위한 개념입니다.
고등학교 교과서, 그래픽스 서적은 대부분 열벡터 연산을 기준으로 설명합니다. 저도 열벡터 연산을 기준으로 진행해 볼께요.
변환 행렬 (Transform Matrix, TM)
아핀 변환 행렬은 3x3 구조로 정의합니다.
변환 행렬(TM)과 열벡터의 행렬곱 차수를 맞추기 위해 열벡터에 1을 추가합니다. (x, y, 1)
3x 3 * 3 x 1 = 3 x 1
열벡터 연산은 아래와 같이 이루어집니다. (x -> x’)
열벡터 연산은 좌측에 우측으로 곱한다고 했죠? (수학 교과서 방식)
간단히 표기하면,
TM * P = P’
(TM: Transform Matrix, P: Point)
각각의 변환을 개별적으로 알아보고, 마지막에 변환들을 합성해 보겠습니다.
예제는 HTML 캔버스를 이용하여 구현해 봅시다.
캔버스 변환 (Canvas Transform)
HTML5 캔버스는 아핀 변환을 넣을 수 있는 인터페이스가 있습니다.
context.transform(a, b, c, d, tx, ty);
(a, b, c, d, tx, ty 순서로 파마메터를 넣어주는 방식)
1. 이동 (Translate)
T * P = P’
(T: Translate)
50크기의 박스에 아핀 변환을 적용해 보죠. (그리드 한 칸: 50)
context.transform(1, 0, 0, 1, 50, 50);
context.fillStyle = "#ff0000";
context.fillRect(0, 0, 50, 50);
2. 크기 (Scale)
S * P = P’
context.transform(2, 0, 0, 2, 0, 0);
context.fillStyle = "#ff0000";
context.fillRect(0, 0, 50, 50);
3. 회전 (Rotate)
R * P = P’
var A = 45 * Math.PI / 180,
cosA = Math.cos(A),
sinA = Math.sin(A);
context.transform(cosA, sinA, -sinA, cosA, 0, 0);
context.fillStyle = "#ff0000";
context.fillRect(0, 0, 50, 50);
4. 기울이기 (Skew or Shear)
SH * P = P’
var A = 45 * Math.PI / 180,
tanA = Math.tan(A);
context.transform(1, 0, tanA, 1, 0, 0);
context.fillStyle = "#ff0000";
context.fillRect(0, 0, 50, 50);
5. 결합 (Composite)
지금까지 했던 변환을 결합해 봅시다.
SH * R * S * T * P = P’
SH * P = P’
var m = new Matrix2D();
m = m.translate(50, 50);
m = m.scale(2, 2);
m = m.rotate(45 * Math.PI / 180);
m = m.skew(0, 1);
context.transform(m.a, m.b, m.c, m.d, m.tx, m.ty);
변환 행렬을 사용하는 이유?
사각형을 변환하는 예제를 살펴봤는데요. 실제로는 사각형을 구성하는 4개의 꼭지점(Vertex) 좌표를 변환한 겁니다.
TM * P = P’
P -> TM -> P’
예를들어, 3D 모델이 수천개의 정점(Vertex)으로 구성되었다면,
수천개의 정점에 TM을 곱해주면 수천개의 변환 좌표를 한방에 구할 수있습니다.
또한, 모델의 원본 좌표(P)는 변하지 않아서 재사용이 가능하죠.
3D에서 P’는 3D 좌표를 스크린에 투영(project)하기 위해 변환한 2D 좌표입니다.
2. Matrix3D
(작성중)