LabHub
배우기 러닝패스 코스

3D数学とソフトウェアラスタライザ

PPMとPNGを手で書く

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

이미지 파일을 라이브러리 없이 직접 써서, 자기가 계산한 픽셀을 브라우저로 볼 수 있게 만듭니다. 이 실습이 끝나면 이 패스의 나머지 전부에서 결과를 눈으로 확인할 수 있습니다.

왜 중요한가

그래픽스 공부가 중간에 멈추는 가장 흔한 이유는 결과를 볼 방법이 없어서입니다. 그런데 이미지 파일 하나를 쓰는 일은 어렵지 않습니다. PPM 은 머리글 세 줄 뒤에 픽셀을 나열한 것이 전부이고, PNG 도 서명 8바이트에 청크 셋이면 됩니다. 압축과 CRC 는 표준 라이브러리의 zlib 이 해 줍니다.

직접 써 보면 따라오는 것이 있습니다. 픽셀이 행 우선으로 놓인다는 것, 좌표 (x, y) 의 위치가 (y * 폭 + x) * 3 이라는 것, 이미지의 y 는 아래로 자라는데 3D 수학의 y 는 위로 자란다는 것입니다. 이 세 가지는 뒤의 모든 실습에서 계속 나옵니다. 그리고 셰이더가 이상할 때 중간 값을 색으로 칠해 뽑아 보는 것이 가장 빠른 진단법인데, 그러려면 이 함수가 손에 있어야 합니다.

단계

  1. 4x4 P6 PPM 을 /root/gfx/out/tiny.ppm 에 씁니다.
  2. 256x256 그레이디언트를 /root/gfx/out/gradient.ppm 에 씁니다.
  3. zlibstruct 만으로 PNG 를 써서 /root/gfx/out/gradient.png 를 만듭니다.
  4. /root/gfx/out/index.html 을 만들고 HTTP 서버를 띄워 웹 미리보기로 봅니다.
  5. 원을 채워 /root/gfx/out/shapes.png 를 만듭니다.
  6. 직선 12개를 그어 /root/gfx/out/lines.png 를 만듭니다.
  7. 둘을 합쳐 /root/gfx/out/final.png/root/gfx/out/report.txt 를 만듭니다.

참고

4x4 짜리 PPM 하나

/root/gfx/tiny.py 를 만들어 4x4 크기의 P6 PPM 을 /root/gfx/out/tiny.ppm 에 쓰십시오. 왼쪽 위 (0,0) 은 빨강(255,0,0), 오른쪽 위 (3,0) 은 초록(0,255,0), 왼쪽 아래 (0,3) 은 파랑(0,0,255), 오른쪽 아래 (3,3) 은 흰색(255,255,255), 나머지 열두 픽셀은 검정입니다.

P6 의 머리글은 세 줄입니다. P6, 4 4, 255 를 줄바꿈으로 이어 붙이고 그 뒤에 픽셀 바이트 48개를 그대로 씁니다.

픽셀은 왼쪽 위부터 오른쪽으로, 한 행이 끝나면 다음 행으로 갑니다. 좌표 (x, y) 의 시작 위치는 (y * 4 + x) * 3 입니다.

파일은 반드시 이진 모드(open(path, "wb"))로 여십시오. 텍스트 모드로 열면 바이트 값 10(줄바꿈)이 플랫폼에 따라 변형될 수 있습니다.

256x256 그레이디언트

/root/gfx/gradient.py 로 256x256 P6 PPM 을 /root/gfx/out/gradient.ppm 에 쓰십시오. 좌표 (x, y) 의 색은 정확히 (x, y, 64) 입니다.

앞 단계와 같은 구조인데 크기만 커집니다. 이중 반복문으로 y 를 바깥, x 를 안쪽에 두면 바이트가 나가는 순서가 곧 파일 순서가 됩니다.

bytearray+= 로 이어 붙이는 편이 매번 인덱스를 계산하는 것보다 빠릅니다. 파일 크기는 머리글 15바이트 + 256×256×3 = 196,608 바이트가 됩니다.

왼쪽에서 오른쪽으로 갈수록 붉어지고 위에서 아래로 갈수록 초록이 짙어지는 그림이 나와야 합니다.

zlib 과 struct 만으로 PNG 를 쓴다

/root/gfx/png.pywrite_png(path, w, h, px) 함수를 만들고, 앞 단계와 같은 그림을 /root/gfx/out/gradient.png 로 쓰십시오. 색 타입은 2(트루컬러), 비트 깊이는 8, 행 필터는 전부 0(None) 입니다.

PNG 는 8바이트 서명 \x89PNG\r\n\x1a\n 으로 시작하고, 그 뒤로 청크가 이어집니다. 청크 하나는 길이(4바이트 빅엔디안) + 종류(4글자) + 내용 + CRC(4바이트) 이고, CRC 는 종류와 내용에 대해서만 계산합니다(zlib.crc32).

IHDR 의 내용은 struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0) 입니다. IDAT 의 내용은 각 행 앞에 필터 바이트 0 을 하나 붙여 이어 붙인 다음 zlib.compress 로 압축한 것입니다. IEND 의 내용은 빈 바이트열입니다.

행마다 붙는 필터 바이트를 빠뜨리면 그림이 대각선으로 밀려 보입니다. 그게 이 형식에서 가장 흔한 실수입니다.

브라우저로 내 그림을 본다

/root/gfx/out/index.html 을 만들어 gradient.png<img> 로 걸고, python3 -m http.server 8080 -d /root/gfx/out 을 띄운 뒤 웹 미리보기에서 http://localhost:8080/ 을 여십시오.

HTML 은 몇 줄이면 됩니다. 이미지 경로는 같은 폴더이므로 gradient.png 처럼 상대 경로로 씁니다.

서버는 터미널을 붙잡으므로 python3 -m http.server 8080 -d /root/gfx/out & 처럼 뒤에 & 를 붙여 백그라운드로 띄우십시오. 화면 위쪽의 웹 미리보기 단추를 누르면 포트 8080 을 골라 볼 수 있습니다.

채점은 서버가 아니라 index.html 파일과 PNG 를 봅니다 — 떠 있다 사라지는 것으로 판정하면 같은 답이 어떤 날은 통과하고 어떤 날은 실패하기 때문입니다.

원을 채운다

/root/gfx/shapes.py 로 256x256 PNG 를 /root/gfx/out/shapes.png 에 쓰십시오. 배경은 흰색(255,255,255), 중심 (128,128) 반지름 60 의 원 안쪽은 파랑(40,80,220) 으로 채우고, 이미지의 바깥 테두리 1픽셀은 검정(0,0,0) 입니다.

원 안쪽인지는 제곱근을 쓰지 않고 판정합니다. (x - 128) ** 2 + (y - 128) ** 2 <= 60 ** 2 이면 안쪽입니다. 제곱근은 느리고, 여기서는 필요도 없습니다.

테두리는 x 나 y 가 0 또는 255 인 픽셀입니다. 원을 먼저 그리고 테두리를 나중에 그리면 겹치는 자리에서 테두리가 이깁니다.

PNG 쓰기는 앞 단계에서 만든 png.pywrite_pngfrom png import write_png 로 가져다 쓰십시오.

직선을 긋는다

/root/gfx/line.py 로 256x256 PNG 를 /root/gfx/out/lines.png 에 쓰십시오. 배경은 검정이고, 중심 (128,128) 에서 길이 100 의 흰색 직선 12개를 30도 간격(0도, 30도, ... 330도)으로 뻗습니다. 각도 t 의 끝점은 (128 + 100*cos(t), 128 - 100*sin(t)) 입니다.

브레젠험 직선 알고리즘을 쓰면 정수 연산만으로 그릴 수 있습니다. 오차값 err = dx + dy 를 두고(dx = abs(x1-x0), dy = -abs(y1-y0)) 한 걸음마다 2*errdy, dx 와 비교해 x 를 갈지 y 를 갈지 정합니다.

끝점의 y 에 뺄셈이 들어간 것에 주의하십시오. 수학의 y 는 위로 자라는데 이미지의 y 는 아래로 자라기 때문입니다.

끝점을 반올림해 정수로 만든 뒤 그리십시오.

합쳐 그리고, 센 값을 적는다

/root/gfx/report.py 로 원과 직선을 한 장에 합쳐 /root/gfx/out/final.png(256x256, 흰 배경, 파란 원, 검은 직선 12개, 검은 테두리)를 만들고, /root/gfx/out/report.txtwidth=, height=, background=r,g,b, nonbackground= 네 줄을 적으십시오. nonbackground 는 배경색이 아닌 픽셀의 개수입니다.

앞의 두 단계를 이어 붙이면 됩니다. 원을 먼저 채우고 그 위에 직선을 검정으로 그은 다음 테두리를 그립니다.

nonbackground 는 지어내지 말고 다 그린 뒤에 픽셀을 세어 적으십시오. 채점기가 PNG 를 직접 열어 세고 당신이 적은 값과 맞춰 봅니다. 값이 서로 어긋나면 둘 중 하나가 틀린 것입니다.

background 는 쉼표로 이어 붙인 세 정수입니다. 예: background=255,255,255