TT Lab
시작하기
배우기 러닝패스 코스

3D 수학과 소프트웨어 래스터라이저

벡터·행렬 연산을 직접 만든다

TT Lab 에서 이어서 보기

목표

3차원 벡터 연산과 4x4 행렬 곱을 직접 만들고, 그것으로 도형을 옮기고 돌리고 줄여 봅니다. 이 실습이 끝나면 변환 행렬을 읽고 쓸 수 있고, 곱하는 순서가 왜 중요한지 손으로 확인한 상태가 됩니다.

왜 중요한가

3차원 계산인데 행렬이 4x4 인 이유는 이동이 곱셈이 아니기 때문입니다. 좌표를 하나 늘려 (x, y, z, 1) 로 적으면 마지막 열이 그 1 과 곱해져 결과에 더해지고, 그렇게 평행이동이 곱셈 안으로 들어옵니다. 이것을 동차 좌표라고 하고, w 가 1 이면 점, 0 이면 방향이라는 구분이 여기서 나옵니다.

그리고 행렬로 표현해 두면 여러 변환을 미리 곱해 하나로 만들 수 있습니다. GPU 가 정점마다 MVP 행렬 하나만 곱하는 이유입니다. 다만 행렬 곱은 교환법칙이 성립하지 않아서, 순서를 바꾸면 물체가 제자리에서 도는 대신 궤도를 돕니다. 이 차이를 숫자로 한 번 보고 나면 잊히지 않습니다.

단계

  1. /root/linalg/gfxlib.py 도구 상자를 놓습니다.
  2. /root/linalg/vec.py 에 벡터 연산 일곱 개를 만듭니다.
  3. /root/linalg/mat.py 에 4x4 행렬 곱을 만듭니다.
  4. /root/linalg/xform.py 에 이동·크기·회전 행렬을 만듭니다.
  5. 곱하는 순서를 바꾼 결과를 /root/linalg/out/05-order.txt 에 적습니다.
  6. 변환한 사각형 셋을 /root/linalg/out/square.png 에 그립니다.
  7. 외적으로 법선과 넓이를 구해 /root/linalg/out/07-normal.txt 에 적습니다.

참고

그리기 도구 상자를 놓는다

/root/linalg/gfxlib.py 를 예시 그대로 저장하고, /root/linalg/check.py 로 시험 무늬를 그려 /root/linalg/out/00-check.png 를 만드십시오. 무늬는 64x64 검은 배경에 (0,0)에서 (63,63)까지 흰색(255,255,255) 대각선을 긋고, 그 뒤에 (0,32)에서 (63,32)까지 빨강(255,0,0) 가로선을 겹쳐 그은 것입니다.

이 실습부터는 PNG 인코더를 다시 만들지 않습니다. 첫 실습에서 손으로 만든 것과 같은 코드를 도구로 내려 줍니다 — 여기서 배울 것은 파일 형식이 아니기 때문입니다.

실습 파드에는 볼륨이 없어서 앞 실습에서 만든 파일이 남아 있지 않습니다. 그래서 실습마다 도구 상자를 다시 놓는 것으로 시작합니다.

Canvas(w, h, bg) 를 만들고 line(x0, y0, x1, y1, rgb) 로 두 선을 그은 뒤 write_png(path) 로 저장하십시오. 가로선을 나중에 그어야 교차점 (32,32) 가 빨강이 됩니다.

3차원 벡터 연산

/root/linalg/vec.py 에 add(a,b), sub(a,b), scale(a,s), dot(a,b), cross(a,b), length(a), normalize(a) 일곱 함수를 만드십시오. 벡터는 길이 3짜리 튜플이고, dot 과 length 만 실수를 돌려줍니다.

외적의 정의를 부호까지 정확히 적어야 합니다.

cross(a, b) = (a1*b2 - a2*b1, a2*b0 - a0*b2, a0*b1 - a1*b0)

cross((1,0,0), (0,1,0)) 이 (0,0,1) 이 나오면 부호가 맞는 것입니다. 반대로 나오면 오른손 좌표계가 뒤집힌 것이고, 나중에 조명이 물체 뒤에서 비치는 것처럼 보이게 됩니다.

normalize 는 길이가 0 인 벡터를 받을 수 있습니다. 그때 0으로 나누지 않도록 원래 벡터를 그대로 돌려주십시오.

4x4 행렬 곱

/root/linalg/mat.py 에 identity(), mul(a,b), apply(m,v) 세 함수를 만드십시오. 행렬은 행 우선(길이 4짜리 리스트 4개를 담은 리스트)이고, apply 는 점 (x,y,z) 를 열 벡터 (x,y,z,1) 로 보아 곱한 뒤 (x,y,z,w) 네 성분을 돌려줍니다.

곱셈의 정의는 out[r][c] = sum(a[r][k] * b[k][c] for k in range(4)) 입니다. 인덱스 순서를 한 번 헷갈리면 전치된 행렬이 나오는데, 단위 행렬로 시험하면 안 걸립니다 — 단위 행렬은 전치해도 자기 자신이기 때문입니다.

그래서 시험은 비대칭 행렬로 하십시오. 이동 행렬처럼 마지막 열에만 값이 있는 것이 좋습니다.

apply 는 m[r][0]*x + m[r][1]*y + m[r][2]*z + m[r][3]*1 을 r=0..3 에 대해 계산합니다.

이동·크기·회전 행렬

/root/linalg/xform.py 에 translate(tx,ty,tz), scale(sx,sy,sz), rotate_x(deg), rotate_y(deg), rotate_z(deg) 다섯 함수를 만드십시오. 각도는 도(degree) 로 받고, 오른손 좌표계 기준입니다.

rotate_z 의 왼쪽 위 2x2 는 [[cos, -sin], [sin, cos]] 입니다. 이렇게 두면 rotate_z(90) 이 x축 (1,0,0) 을 y축 (0,1,0) 으로 보냅니다.

같은 규칙으로 rotate_x 는 y 를 z 로, rotate_y 는 z 를 x 로 보냅니다. rotate_y 만 부호가 반대로 들어갑니다 — m[0][2] = sin, m[2][0] = -sin 입니다. 축의 순환 순서가 x→y→z→x 이기 때문인데, 여기서 부호를 통일해 버리면 y축 회전만 반대로 돌게 됩니다.

math.radians 로 도를 라디안으로 바꾸십시오.

곱하는 순서가 결과를 바꾼다

T = translate(2,0,0), R = rotate_z(90) 을 두고 점 (1,0,0) 에 mul(T,R) 과 mul(R,T) 를 각각 적용한 결과를 /root/linalg/out/05-order.txt 에 TR=x,y,z 와 RT=x,y,z 두 줄로 적으십시오. 소수점 아래 여섯 자리까지 적습니다.

열 벡터를 오른쪽에 곱하는 관례에서는 오른쪽 행렬이 먼저 적용됩니다. mul(T, R) 은 회전을 먼저 하고 이동을 나중에 한다는 뜻입니다.

손으로 따라가 보십시오. R 이 (1,0,0) 을 어디로 보내는지 먼저 구하고, 거기에 T 를 더합니다. 반대 순서는 T 를 먼저 적용한 뒤 R 을 겁니다.

한쪽은 물체가 제자리에서 돈 뒤 옮겨진 것이고, 다른 쪽은 원점에서 멀어진 뒤 원점 기준으로 궤도를 돈 것입니다. 결과가 크게 다릅니다.

변환한 사각형을 그린다

/root/linalg/plot.py 로 256x256 PNG 를 /root/linalg/out/square.png 에 쓰십시오. 모델 좌표의 사각형은 (-1,-1,0), (1,-1,0), (1,1,0), (-1,1,0) 이고, 화면 좌표는 sx = 128 + 60*x, sy = 128 - 60*y 로 옮깁니다. 검은 배경에 사각형 셋을 그립니다 — 변환 없는 것은 흰색(255,255,255), rotate_z(30) 을 건 것은 빨강(255,60,60), mul(translate(1.2,-0.8,0), scale(0.5,0.5,1)) 을 건 것은 초록(60,255,60) 입니다.

각 사각형은 네 꼭짓점을 순서대로 이은 뒤 마지막에서 첫 점으로 돌아오는 네 개의 선분입니다. gfxlib.Canvas 의 line(x0, y0, x1, y1, rgb) 를 쓰십시오.

화면 좌표로 옮길 때 y 에 뺄셈이 들어가는 것에 주의하십시오. 수학의 y 는 위로, 이미지의 y 는 아래로 자랍니다. 이걸 빠뜨리면 초록 사각형이 위쪽에 그려집니다.

초록 사각형의 변환은 축소를 먼저 하고 이동을 나중에 합니다 — mul(translate, scale) 의 오른쪽이 먼저입니다.

외적으로 법선과 넓이를 구한다

/root/linalg/normal.py 로 두 삼각형의 단위 법선과 넓이를 구해 /root/linalg/out/07-normal.txt 에 t1_normal=, t1_area=, t2_normal=, t2_area= 네 줄로 적으십시오. 삼각형 1은 A(0,0,0) B(2,0,0) C(0,3,0), 삼각형 2는 A(1,1,1) B(2,1,1) C(1,1,3) 이고, 법선은 cross(B-A, C-A) 를 정규화한 것입니다.

외적의 길이가 두 변이 만드는 평행사변형의 넓이라서, 삼각형 넓이는 그 절반입니다. 즉 area = length(cross(B-A, C-A)) / 2 입니다.

법선의 부호는 꼭짓점을 도는 순서가 정합니다. cross(B-A, C-A) 와 cross(C-A, B-A) 는 정반대 방향입니다. 이 순서가 뒤에서 앞면과 뒷면을 가르는 기준이 되므로, 시키는 대로 맞춰 두십시오.

값은 %.6f 로 적고 벡터는 쉼표로 잇습니다. 예: t1_normal=0.000000,0.000000,1.000000