平面を凸凹に見せる
한국어 원문으로 표시합니다.
목표
법선을 텍스처에 담고, 그것으로 평평한 면을 울퉁불퉁하게 보이게 만듭니다. 이 실습이 끝나면 노멀 맵이 왜 그 연보라빛인지, 접선 공간이 왜 필요한지 설명할 수 있게 됩니다.
왜 중요한가
사람이 요철을 알아보는 단서는 대부분 밝기의 변화입니다. 밝기는 법선과 빛 방향의 내적으로 정해지므로, 기하를 바꾸는 대신 법선만 픽셀마다 바꿔치기하면 같은 효과가 납니다. 삼각형 두 개짜리 평면에 벽돌의 요철이 생깁니다.
여기서 중요한 것이 어느 좌표계의 법선을 담느냐 입니다. 월드 공간 법선을 담으면 물체가 회전하는 순간 전부 틀립니다. 그래서 표면을 기준으로 한 좌표계(접선 공간)에 담고, 그 좌표계에서 "평평함" 은 언제나 (0, 0, 1) 입니다. 노멀 맵이 전체적으로 연보라로 보이는 것이 그 값의 색입니다.
단계
/root/normalmap에 도구 상자를 놓습니다./root/normalmap/encode.py— 법선과 색 사이의 변환./root/normalmap/out/normalmap.png— 반구 격자 노멀 맵./root/normalmap/tbn.py— 접선 공간의 세 축./root/normalmap/out/flat.png— 상수 법선으로 비춘 평면./root/normalmap/out/bumped.png— 노멀 맵으로 비춘 같은 평면./root/normalmap/out/l0.png~l5.png와out/07-sweep.txt— 빛을 옮기며.
참고
- 그림은
nohup python3 -m http.server 8080 -d /root/normalmap/out &로 띄우고 웹 미리보기 에서http://localhost:8080/을 엽니다. 여섯 장이 돌아가면 요철이 빛을 따라 반응하는 것이 보입니다. - 이 실습의 평면은 XY 평면에 표준 UV 라 접선 공간이 월드 공간과 같습니다. 그래서 TBN 변환이 항등이 되어 계산이 단순해집니다. 다른 자세의 물체였다면 반드시 거쳐야 합니다.
- 흔한 실수 하나:
decode에서 정규화를 빠뜨리는 것입니다. 8비트로 잘리면서 길이가 1 이 아니게 되어 밝기가 미세하게 틀어집니다. - 흔한 실수 둘: 색을 정수로 자를 때 255 를 넘기는 것입니다. 밝은 곳이 이상한 색으로 뒤집힙니다.
그리기 도구 상자를 놓는다
/root/normalmap/gfxlib.py 를 예시 그대로 저장하고, /root/normalmap/check.py 로 시험 무늬를 그려 /root/normalmap/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) 가 빨강이 됩니다.
이 실습에서는 노멀 맵 자체와 그것으로 비춘 결과를 그림으로 내보내는 데 씁니다.
법선을 색으로
/root/normalmap/encode.py 에 encode(n) 과 decode(rgb) 를 만드십시오. encode 는 (q*0.5 + 0.5) * 255 를 세 성분에 적용한 실수 셋을, decode 는 c/255*2 - 1 을 적용한 뒤 정규화한 단위 벡터를 돌려줍니다.
법선의 각 성분은 -1에서 1 사이인데 텍스처 채널은 0에서 255 사이입니다. 범위를 옮겨 담는 것이 전부입니다.
평평한 부분의 법선 (0, 0, 1) 은 (127.5, 127.5, 255) 가 됩니다. 노멀 맵이 전체적으로 연보라빛으로 보이는 이유가 이것입니다 — 그 색이 곧 '여기는 평평하다' 는 뜻입니다.
decode 에서 정규화를 하는 이유는 8비트로 잘리면서 길이가 정확히 1 이 아니게 되기 때문입니다. 정규화하지 않으면 내적이 코사인이 아니게 되어 밝기가 미세하게 틀어집니다.
길이가 0 이 되는 입력((128,128,128) 부근이 아니라 정확히 원점으로 복원되는 값)에서는 0으로 나누지 않도록 하십시오.
노멀 맵을 만든다
/root/normalmap/genmap.py 로 128x128 노멀 맵을 /root/normalmap/out/normalmap.png 에 쓰십시오. 칸 크기 32 로 나누고 cx = (x%32)/32*2 - 1, cy = (y%32)/32*2 - 1, r2 = cx*cx + cy*cy 를 구해, r2 < 1 이면 법선을 (cx, cy, sqrt(1-r2)) 로, 아니면 (0, 0, 1) 로 두고 encode 해서 저장합니다.
(cx, cy, sqrt(1-r2)) 는 단위 반구 위의 점이자 그 지점의 법선입니다. 반지름 1 인 구에서는 위치 벡터와 법선이 같기 때문입니다.
칸 하나가 32픽셀이므로 128x128 에는 4x4 = 열여섯 개의 돔이 들어갑니다. 칸의 가장자리(r2 >= 1)는 평평한 바닥입니다.
색을 정수로 자를 때 0~255 밖으로 나가지 않도록 하십시오. encode 의 결과는 원래 그 범위 안이지만 반올림에서 넘칠 수 있습니다.
만들어진 그림은 연보라 바탕에 둥근 무늬가 있는 모습이어야 합니다. 그 연보라가 (128,128,255), 즉 평평함입니다.
접선 공간의 세 축
/root/normalmap/tbn.py 에 build_tbn(p0, p1, p2, uv0, uv1, uv2) 를 만들어 (T, B, N) 세 단위 벡터를 돌려주십시오. e1 = p1-p0, e2 = p2-p0, d1 = uv1-uv0, d2 = uv2-uv0, r = 1/(d1x*d2y - d2x*d1y) 로 두고 T = (e1*d2y - e2*d1y)*r, B = (e2*d1x - e1*d2x)*r, N = normalize(cross(e1, e2)) 이며 T 와 B 도 정규화합니다.
이 식은 "UV 가 이만큼 변할 때 위치가 이만큼 변한다" 는 두 방정식을 연립해 푼 것입니다. T 는 표면 위에서 u 가 늘어나는 방향이고 B 는 v 가 늘어나는 방향입니다.
분모가 0 이 되는 경우가 있습니다. 삼각형의 UV 가 한 점으로 뭉쳤거나 한 직선 위에 놓였을 때인데, 그런 삼각형에는 접선 공간이 정의되지 않습니다. 0 이면 T=(1,0,0), B=(0,1,0) 처럼 아무 값이나 정해 주고 넘어가십시오.
XY 평면의 삼각형에 표준 UV 를 붙이면 T=(1,0,0), B=(0,1,0), N=(0,0,1) 이 나옵니다. 즉 접선 공간이 곧 월드 공간이 되는 특수한 경우입니다.
v 방향을 뒤집으면 B 도 뒤집힙니다. 노멀 맵의 초록 채널 관례가 갈리는 지점이 여기입니다.
상수 법선으로 비춘다
/root/normalmap/light.py 로 128x128 평면을 비춰 /root/normalmap/out/flat.png 를 만드십시오. 법선은 모든 픽셀에서 (0,0,1), 빛은 normalize((0.0, 0.3, 0.8)), ambient 0.15, 기본색 (220,200,180) 이고 밝기는 ambient + (1-ambient)*max(0, dot(n, l)) 입니다.
법선이 모든 픽셀에서 같으므로 그림 전체가 한 가지 색이 나옵니다. 그게 정상입니다.
이 단계의 목적은 다음 단계와 비교할 기준을 만드는 것입니다. 기하도 조명도 같은데 법선만 바꿔치기했을 때 무엇이 달라지는지를 보려면 기준이 있어야 합니다.
색을 정수로 자를 때 255 를 넘지 않도록 하십시오.
이 평면은 XY 평면이고 UV 도 표준이라 접선 공간이 곧 월드 공간입니다. 그래서 다음 단계에서 TBN 변환이 항등이 되어 계산이 단순해집니다.
노멀 맵으로 비춘다
같은 평면, 같은 빛으로 법선만 3단계의 normal_at(x, y) 에서 가져와 /root/normalmap/out/bumped.png 를 만드십시오.
기하는 하나도 바뀌지 않았습니다. 여전히 평면 하나입니다. 픽셀마다 법선을 바꿔치기한 것뿐입니다.
그런데 결과는 반구가 격자로 박힌 것처럼 보입니다. 사람이 요철을 알아보는 단서가 밝기의 변화이기 때문입니다.
평균 밝기는 평평한 쪽과 크게 다르지 않아야 합니다. 밝아진 곳과 어두워진 곳이 상쇄되기 때문입니다. 반면 밝기의 흩어짐(분산)은 훨씬 커집니다.
이 평면은 XY 평면에 표준 UV 라 접선 공간이 월드 공간과 같습니다. 그래서 TBN 변환을 생략해도 결과가 같습니다 — 다른 자세의 물체였다면 반드시 거쳐야 합니다.
빛을 움직인다
빛의 x 성분을 -0.9 부터 0.9 까지 여섯 단계(-0.9 + 1.8*k/5)로 바꿔 가며 노멀 맵으로 비춘 그림 /root/normalmap/out/l0.png 부터 l5.png 까지를 만들고, 각 그림에서 좌표 (32,32) 부터 32x32 칸의 왼쪽 절반 평균 밝기에서 오른쪽 절반 평균 밝기를 뺀 값을 /root/normalmap/out/07-sweep.txt 에 a0= 부터 a5= 로 적으십시오. 그리고 여섯 이름을 담은 /root/normalmap/out/index.html 도 만드십시오.
빛의 y, z 성분은 0.3 과 0.8 로 그대로 두고 x 만 바꿉니다.
왼쪽 절반이 밝으면 a 가 양수, 오른쪽이 밝으면 음수입니다. 빛이 왼쪽에서 오른쪽으로 옮겨 가므로 여섯 값이 계속 줄어들어야 합니다. 그것이 요철이 빛을 따라 반응한다는 증거입니다.
평균은 채널까지 다 더해 나눕니다 — 픽셀 하나가 세 값이므로 16*32*3 으로 나누면 됩니다.
돔 하나가 빛을 받는 쪽만 밝아지는 모습이 여섯 장을 이어 보면 살아 움직이는 것처럼 보입니다. 그게 노멀 맵이 하는 일입니다.