模様を貼ってサンプルを取り出す
한국어 원문으로 표시합니다.
목표
텍스처를 만들고, 두 가지 표본화 방식을 구현하고, 원근 보정 보간의 차이를 눈으로 봅니다. 마지막에는 축소에서 생기는 지글거림과 밉맵이 그것을 어떻게 없애는지 숫자로 확인합니다.
왜 중요한가
정점마다 색을 붙이면 삼각형 하나에 색 세 개뿐입니다. 텍스처는 색을 기하에서 떼어 내 별도의 이미지에 두고, 표면의 각 지점이 그 이미지의 어디를 가리키는지만 정합니다. 그 좌표가 UV 이고, 래스터라이저가 픽셀마다 보간해 줍니다.
여기서 두 가지가 특히 중요합니다. 하나는 원근 보정입니다. 화면 좌표에서 UV 를 그냥 섞으면 원근이 들어간 면에서 무늬가 뒤틀립니다. 화면에서 균등한 간격이 3차원 공간에서는 균등하지 않기 때문입니다. 다른 하나는 축소입니다. 한 픽셀이 텍셀 수십 개를 덮으면 그중 하나만 골라서는 무늬를 대표하지 못하고, 카메라가 조금만 움직여도 화면이 지글거립니다. 밉맵이 그 답입니다.
단계
/root/texture에 도구 상자를 놓습니다./root/texture/tex.py와out/checker.png— 체커 무늬./root/texture/sample.py— 최근접 표본화./root/texture/bilinear.py— 쌍선형 표본화./root/texture/out/affine.png— 보정 없는 보간./root/texture/out/correct.png와diff.png— 원근 보정./root/texture/out/mip0.png,mip1.png,out/07-mip.txt— 축소와 밉맵.
참고
- 그림은
nohup python3 -m http.server 8080 -d /root/texture/out &로 띄우고 웹 미리보기 에서http://localhost:8080/을 엽니다. affine.png 와 correct.png 를 나란히 보는 것이 이 실습의 핵심 장면입니다. - 흔한 실수 하나: 쌍선형에서 0.5 를 빼지 않는 것입니다. 텍셀의 중심이
(i+0.5)/폭에 있어서, 이 반 칸을 빼먹으면 그림이 밀립니다. - 흔한 실수 둘: 음수 UV 에서
int()로 자르는 것입니다.int(-0.3)은 0 이고floor(-0.3)은 -1 이라 한 칸이 어긋납니다.
그리기 도구 상자를 놓는다
/root/texture/gfxlib.py 를 예시 그대로 저장하고, /root/texture/check.py 로 시험 무늬를 그려 /root/texture/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/texture/tex.py 에 SIZE = 64, CELL = 8, LIGHT = (240,240,240), DARK = (40,40,60) 을 두고, 64x64 텍셀을 행 우선으로 담은 TEX 리스트와 texel(i, j) 함수를 만드십시오. 칸의 색은 ((i//CELL) + (j//CELL)) % 2 == 0 이면 LIGHT, 아니면 DARK 이고, texel 은 인덱스를 % SIZE 로 감쌉니다. 그리고 /root/texture/out/checker.png(64x64)로 내보내십시오.
행 우선이므로 TEX[j * SIZE + i] 가 (i, j) 텍셀입니다. 앞 코스의 픽셀 배치와 같은 규칙입니다.
texel 이 인덱스를 감싸는 것이 곧 반복 감싸기 모드입니다. 여기서 한 번 정해 두면 표본화 함수들이 범위를 신경 쓰지 않아도 됩니다.
체커 무늬를 고른 이유는 뒤틀림과 지글거림이 가장 잘 보이는 무늬이기 때문입니다. 그래서 텍스처 관련 시험 이미지로 오래 쓰여 왔습니다.
64x64 에 8픽셀 칸이면 가로세로 여덟 칸씩입니다.
최근접 표본화
/root/texture/sample.py 에 sample_nearest(u, v) 를 만드십시오. i = floor(u * SIZE), j = floor(v * SIZE) 를 정수로 구해 texel(i, j) 를 돌려줍니다. 범위 밖의 UV 는 texel 의 감싸기가 처리합니다.
math.floor 를 쓰고 int 로 바꾸십시오. 음수 UV 에서 int() 만 쓰면 0 쪽으로 잘려서 한 칸이 어긋납니다 — int(-0.3) 은 0 이지만 floor(-0.3) 은 -1 입니다.
감싸기는 texel 안의 % SIZE 가 합니다. 파이썬의 % 는 음수에 대해서도 양수 나머지를 주므로 그대로 반복 모드가 됩니다.
u = 1.5 는 u = 0.5 와 같은 결과가 나와야 합니다. 무늬가 되풀이된다는 뜻입니다.
쌍선형 표본화
/root/texture/bilinear.py 에 sample_bilinear(u, v) 를 만드십시오. x = u*SIZE - 0.5, y = v*SIZE - 0.5 로 옮긴 뒤 i0 = floor(x), fx = x - i0 를 구하고, 네 텍셀 (i0,j0), (i0+1,j0), (i0,j0+1), (i0+1,j0+1) 을 fx, fy 비율로 섞은 실수 세 개를 돌려줍니다.
0.5 를 빼는 것이 핵심입니다. 텍셀의 중심이 (i+0.5)/SIZE 에 있기 때문입니다. 이 반 칸을 빼먹으면 그림이 반 텍셀만큼 밀리는데, 확대해 보기 전에는 눈치채기 어렵습니다.
섞는 식은 이렇습니다.
c00*(1-fx)*(1-fy) + c10*fx*(1-fy) + c01*(1-fx)*fy + c11*fx*fy
네 가중치의 합이 1 이므로 네 텍셀이 모두 같은 색이면 그 색이 그대로 나옵니다. 이걸로 구현을 확인할 수 있습니다.
텍셀 중심에 정확히 놓인 UV(예: 0.5/SIZE)에서는 그 텍셀의 색이 그대로 나와야 합니다.
보정 없이 붙인다
/root/texture/quad.py 로 사각형에 무늬를 붙여 /root/texture/out/affine.png(256x256, 검은 배경)를 만드십시오. 네 꼭짓점은 화면 좌표·w·uv 순으로 (30,40)/1.0/(0,0), (226,90)/4.0/(1,0), (226,166)/4.0/(1,1), (30,216)/1.0/(0,1) 이고 삼각형은 (0,1,2) 와 (0,2,3) 입니다. uv 는 무게중심 좌표로 그냥 보간하고 최근접 표본화로 색을 꺼냅니다.
w 는 카메라로부터의 거리라고 보면 됩니다. 오른쪽 두 꼭짓점의 w 가 4 라는 것은 그쪽이 멀리 있다는 뜻입니다.
이 단계에서는 그 사실을 일부러 무시하고 uv 를 화면에서 균등하게 섞습니다. 그래서 무늬가 뒤틀립니다. 초기 콘솔 게임의 바닥 무늬가 출렁이던 것이 바로 이것입니다.
삼각형 두 개로 나뉘어 있으므로 대각선을 경계로 뒤틀림의 방향이 꺾입니다. 그 꺾임이 이 방식의 특징적인 증상입니다.
무게중심 좌표와 경계 상자 훑기는 앞 코스에서 만든 것과 같습니다.
원근 보정 보간
같은 사각형을 u/w, v/w, 1/w 를 보간해 앞의 둘을 뒤의 것으로 나누는 방식으로 다시 그려 /root/texture/out/correct.png 를 만들고, 두 그림의 픽셀 차이를 세 배로 키워 /root/texture/out/diff.png 로 내보내십시오.
보정 공식은 이렇습니다.
iw = b0/w0 + b1/w1 + b2/w2
u = (b0*u0/w0 + b1*u1/w1 + b2*u2/w2) / iw
무엇을 보간할지가 바뀔 뿐 보간 자체는 그대로입니다. 화면 좌표에서 선형인 양은 uv 가 아니라 u/w 와 1/w 라는 것이 요점입니다.
차이 이미지는 min(255, abs(a - b) * 3) 을 채널마다 계산하면 됩니다. 세 배로 키우는 이유는 그냥 빼면 어두워서 잘 안 보이기 때문입니다.
두 그림을 나란히 보십시오. 보정하지 않은 쪽은 대각선을 경계로 무늬가 꺾이고, 보정한 쪽은 멀어질수록 촘촘해지는 자연스러운 모습이 나옵니다.
축소와 밉맵
/root/texture/mip.py 로 64x64 의 한 텍셀짜리 체커((i+j)%2==0 이면 LIGHT, 아니면 DARK)를 만들고, 2x2 평균으로 32x32 밉 한 단계를 만드십시오. 두 텍스처를 각각 128x128 화면에 u = (x+0.5)/128*5, v = (y+0.5)/128*5 로 최근접 표본화해 /root/texture/out/mip0.png 와 mip1.png 를 만들고, 가로로 이웃한 픽셀의 채널 평균 절대차를 /root/texture/out/07-mip.txt 에 energy0=, energy1=, level1_uniform=yes|no 로 적으십시오.
한 텍셀짜리 체커는 가장 높은 주파수의 무늬입니다. 2x2 평균을 한 번만 내면 밝은 텍셀 둘과 어두운 텍셀 둘이 섞여 모든 텍셀이 같은 색이 됩니다. 그래서 level1_uniform 은 yes 가 나옵니다.
에너지는 이렇게 셉니다 — 모든 y 와 x=0..126 에 대해 |픽셀(x+1,y) - 픽셀(x,y)| 를 세 채널 더하고, 전체를 (개수 × 3)으로 나눕니다.
energy0 은 90 이 넘고 energy1 은 정확히 0 이 나옵니다. 그 차이가 지글거림의 크기입니다. 카메라가 조금만 움직이면 energy0 쪽 그림은 무늬가 통째로 바뀌지만 energy1 쪽은 그대로입니다.
밉맵이 메모리를 3분의 1 더 쓰는 대가로 사는 것이 이 안정성입니다.