아무것도 안 칠한 탭
libmypaint는 두 입력 사이에 댑을 몇 개 찍을지 세 항을 더해서 정해. 움직인 거리를 실제 반지름으로 나눠 실제 반지름당 댑 수를 곱한 것, 같은 거리를 기본 반지름으로 나눠 기본 반지름당 댑 수를 곱한 것, 흐른 시간에 초당 댑 수를 곱한 것. 이 합이 1을 넘을 때마다 댑이 하나 떨어져. 2강을 떠올려 봐. 획의 첫 입력은 "지난번"보다 10초 뒤에 온 걸로 들어가. 그러면 libmypaint는 붓을 리셋하는데, 그 분기는 댑을 하나도 안 찍고 돌아가. 첫 입력은 붓이 어디 있는지만 알려 줘.
탭은 안 움직여. 그리고 붓 대부분은 초당 댑 수가 0이야. 그래서 합이 1에 못 닿아. 2026-10-03에 이걸 고친 커밋(8973b8a, 19:38)은 피해를 그대로 적었어. "A tap therefore painted nothing with any brush." 같은 산수가 증상 하나를 더 숨기고 있었어. 반지름 두 배 간격으로 찍는 스탬프 붓은 간격보다 짧은 획에서 아무것도 안 칠했어. Photoshop도 Procreate도 펜이 닿는 순간 찍어. 아빠의 Photoshop 붓 세트로 릴리스 빌드에서 재 보니, 탭으로 칠해지는 붓이 세트마다 하나도 없던 데서 대부분이나 전부로 바뀌었어.
수: 240분의 1초 동안, 240분의 1초마다 댑 하나
Painter.layFirstDab(첫 번째 코드 블록)은 첫 입력 바로 뒤에 돌아. 붓의 원래 초당 댑 수를 기억해 두고, 240 × 1.001로 바꾸고, 같은 점을 deltaTime = 1/240로 한 번 더 먹이고, 속도를 되돌려. 1초에 240개가 조금 넘는 속도로 240분의 1초면 댑 하나가 조금 넘어. 0.1% 여유는 반올림 때문에 개수가 1에 털끝만큼 모자라는 걸 막아. 듀얼 브러시는 보조 붓에도 같은 수를 써.
댑이 실제로 떨어지는 자리
함수 위 주석은 libmypaint가 "lays exactly one there"라고 해. 강의에 그대로 옮기기 전에, Ariandel이 들고 있는 libmypaint를 컴파일하고 댑 세는 일만 하는 서피스로 세 봤어(세 번째 코드 블록). 결과는 이래. 두 번째 호출은 아무것도 안 찍어. 댑은 획의 다음 입력에서 떨어지고, 초당 댑 수가 0인 붓이면 거기서 정확히 하나야. 단, 그 입력이 주석에 없는 창 안에 들어와야 해. 닿고 나서 약 4.16 ms부터 5초까지야.
이유는 mypaint-brush.c 안에 있어. 개수를 셀 때 초당 댑 수를 기본값이 아니라 붓의 상태에서 읽어. 리셋이 상태를 전부 0으로 만들었고, 상태는 호출이 끝날 때에야 기본값에서 다시 계산돼. 그러니까 layFirstDab의 호출은 속도를 장전만 하고 쓰진 않아. 다음 입력이 그걸 댑 하나에 쓰고, 그 댑의 상태 갱신이 붓 원래 속도를 되돌려. 1.001 여유는 그래도 제 몫을 해. 펜의 다음 샘플은 대략 240분의 1초 뒤에 오고, 그래야 장전된 개수가 1을 살짝 넘거든. 세 번째 코드 블록은 그 간격을 쭉 바꿔 가며 창의 양 끝을 찾아. 아래쪽 끝은 여유의 산수 그대로야. 장전된 개수는 간격 곱하기 240.24이고, 이게 1에 닿아야 해. 그래서 끝은 240.24분의 1초, 4.1625 ms야. 240분의 1초보다 털끝만큼 앞이지. 4.162 ms는 아무것도 안 찍고, 4.163 ms는 댑을 찍어. 장전은 호출 한 번만 가. 다음 입력이 그보다 빨리 오면 댑 하나에 못 미치고, 그 호출의 상태 갱신이 붓 원래 속도를 그대로 되돌려. 초당 댑 수가 0이면 가만히 있는 펜은 그 댑을 끝내 못 찍어. 위쪽 끝은 Ariandel 게 아냐. 2강에서 본 5초 리셋 그대로야. 마지막 입력보다 5초 넘게 늦은 입력이 오면 libmypaint는 붓을 리셋하고 아무것도 안 찍은 채 돌아가. 장전된 속도도 리셋이 나머지 상태와 같이 버려. 5초는 댑을 찍고, 5.001초와 6초는 아무것도 안 찍어. 두 끝 사이, 4.163 ms 줄부터 5초 줄까지는 전부 정확히 하나야.
다음 입력이 뭐냐가 중요해. libmypaint가 댑을 그쪽으로 보간하니까. 진짜 엔진(빌드 24, 릴리스)을 임시 패키지에 빌드해서 Painter로 탭을 재연해 봤어. 힘 0.8로 닿고, 240분의 1초 뒤에 샘플 하나가 따라오는 경우야.
| 다음 입력 | 닿은 자리의 알파 (32,768 중) |
|---|---|
| 힘 0.8 | 12,681 |
| 힘 0.3 | 3,997 |
| 힘 0 | 149 |
| 닿은 뒤 입력 없음 | 0 |
캔버스에선 탭에 늘 다음 샘플이 있어. 떼는 처리가 떼기 전에 끝 이벤트의 합쳐진 터치를 먼저 먹이거든. 진짜 펜이 그 마지막 샘플들에서 힘을 얼마나 보고하는지는 하드웨어 사실이고, 여기 어떤 테스트도 그걸 안 재. Brush Studio의 Drawing Pad는 끝 이벤트의 합쳐진 터치는 안 먹이고 떼기만 먹여. 그래도 endStroke가 떼는 입력의 힘을 0으로 바꿔서 샘플 하나로 먹여. 그러니까 이동 이벤트 없이 끝난 패드 탭에도 다음 입력은 있어. 힘 0이고, 닿고 나서 탭 길이만큼 지난 샘플이야. 표의 149가 그 경우를 240분의 1초로 잰 거야. 진짜 탭은 그보다 길고, 그럼 답이 달라져. 장전된 댑은 구간 끝이 아니라 구간에 240분의 1초쯤 들어간 자리에 떨어지고, 압력은 두 샘플 사이에서 보간되거든. 세 번째 코드 블록을 조금 바꿔서 댑마다 불투명도를 찍어 보면 보여. 0.8로 닿은 뒤 힘 0짜리 다음 입력이 240분의 1초 뒤면 0.0002, 20 ms 뒤면 0.24, 80 ms 뒤면 0.32야. 다음 입력도 0.8로 누르면 0.35고. 엔진 테스트는 어느 쪽이든 통과해. 힘 0.8짜리 샘플을 일곱 개 먹이니까. 이게 수정을 무르는 건 아냐. 증명된 범위를 좁힐 뿐이야. 엔진에서 탭은 다음 입력이 닿고 약 4.16 ms에서 5초 사이에 올 때 칠해지고, 그 세기는 닿은 자리에서 다음 입력 쪽으로 240분의 1초쯤 들어간 데서 읽혀.
배치 안의 순서
continueStroke는 surface.begin()과 surface.end() 사이에서 샘플마다 같은 순서를 밟아(두 번째 코드 블록).
- Drawing Assist가 점을 고른 가이드 선 위로 붙여. 화면 기준 10포인트를 움직여서 방향이 정해질 때까진 시작점에 잡아 둬.
- 손떨림 보정과 모션 필터링이 위치를 옮겨. 매핑보다 먼저야.
PencilMapping.sample이 libmypaint 인자로 바꿔(2강).- 테이퍼가 기본 반지름을 정하고, 획을 따라 간 거리에 맞춰 압력을 줄여.
setAzimuth가 펜이 기운 방향을 Ariandel 서피스에 알려. 기울기를 따라가는 팁을 위해서야.brush.stroke, 그다음 획의 첫 샘플이면layFirstDab, 그다음 반지름 차이를 얹은 듀얼 보조 붓.
획을 다시 그릴 땐 1, 2단계를 건너뛰어. 뗄 때 테이퍼가 다시 그리면서 먹이는 위치는 이미 붙이고 다듬은 거라서야. end() 뒤엔 5트랙의 배치 단위 합치기가 와.
댑 예산
첫 샘플보다 먼저, beginStroke에서 한도가 하나 더 걸려. 댑 하나의 비용은 그 넓이야. 그래서 획의 비용은 반지름 곱하기 반지름당 댑 수로 커져. Photoshop의 1% 간격은 반지름당 댑 50개고, 릴리스 빌드에서 300픽셀 붓을 1%로 1초 긋는 데 1~2초가 걸렸어(7241359). Painter.dabBudget은 3,000이야. 획을 긋는 동안 실제 반지름당 댑 수를 많아야 max(2, 3000 / radius)로 묶어. 100픽셀이면 30, 300이면 10이고, 획이 끝나면 붓 원래 값이 돌아와.