게임 엔진 없이 SDL2로 픽셀을 직접 찍어 삼각형 기하 도구를 만들었다. 꼭짓점을 마우스로 끌면 세 중심(무게중심, 내심, 외심), 내접원, 외접원, 각 변 길이, 내각이 실시간으로 갱신된다. 소스는 약 540줄이다. 만들면서 발생한 버그와 사용한 수학 공식을 정리한다.

꼭짓점 A, B, C 를 끌면 세 중심, 내접원, 외접원, 수치 패널이 실시간으로 갱신된다
좌표계: grid와 픽셀 분리
화면 좌표는 y가 아래로 갈수록 값이 커진다. 수학 좌표와 y 방향이 반대다. 이 도구는 좌표를 두 종류로 나눈다. 정수 격자인 grid 좌표와 화면에 찍히는 픽셀 좌표다.
#define POS_X(x) ((x) * GRID_INTERVAL + g_cx) // grid → 픽셀
#define POS_Y(y) (g_cy - (y) * GRID_INTERVAL) // y 부호 반전
초기에 삼각형이 화면에 표시되지 않았다. 원인은 grid 좌표(-1, -3 같은 작은 정수)를 픽셀로 변환하지 않고 그대로 SDL_Rect에 넣은 것이다. 삼각형이 화면 좌상단에 몇 픽셀 크기로만 찍혔다. 끝점을 픽셀로 변환한 다음 픽셀 공간에서 그려야 한다.
선과 원
Bresenham 선
두 점을 잇는 선은 Bresenham 알고리즘으로 그린다. 정수 오차값 err를 누적해 이상적 직선에서 벗어난 정도를 추적하고 그 부호로 다음 픽셀의 이동 방향을 정한다. 실수 연산, 나눗셈, 반올림, 수직선 예외 처리가 필요 없고 모든 기울기를 반복문 하나로 처리한다.
int dx = abs(x1 - x0), dy = -abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
while (true) {
plot_px(s, x0, y0, thickness, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
초기에는 y = mx + b 형태의 기울기 절편 방식으로 구현했다. 이 방식은 수직선에서 0으로 나누고 가파른 구간에서 픽셀 사이에 구멍이 생겨 경우를 세 가지로 나눠야 했다. Bresenham은 이 분기를 정수 연산으로 대체한다.
Midpoint 원
원은 Midpoint Circle 알고리즘으로 그린다. 제곱근 대신 정수 판별식 d의 부호로 동쪽과 남동쪽 중 원에 가까운 픽셀을 고른다. 원은 여덟 방향이 대칭이라 45도 조각 하나만 계산하고 나머지 일곱 점은 부호를 바꿔 (cx±x, cy±y), (cx±y, cy±x)로 찍는다. 계산량이 8분의 1로 준다.
삼각형 세 중심
세 꼭짓점 A, B, C 와 각 변 길이 a, b, c 로 세 중심을 구한다. 변 길이는 마주 보는 꼭짓점 기준으로 이름을 붙였다.
| 중심 | 공식 | 특징 |
|---|---|---|
| 무게중심 | 세 꼭짓점 좌표의 평균 | 항상 내부 |
| 내심 | (a*A + b*B + c*C) / (a+b+c) | 항상 내부. 마주 보는 변 길이로 가중 |
| 외심 | 수직이등분선의 교점 | 둔각삼각형이면 바깥 |
구현 중 버그를 두 개 확인했다.
내심 가중치를 외적으로 잘못 넣었다. 내심은 각 꼭짓점을 마주 보는 변 길이로 가중한 평균이다. 초기 구현은 가중치에 변 길이 대신 외적(p1.x*p2.y - p1.y*p2.x)을 넣어 다른 점이 나왔다.
외심에서 0으로 나눴다. 외심 식의 분모 D는 세 점이 일직선이면 0이다. 나눗셈을 먼저 하고 if (d) 검사를 뒤에 둬서 일직선일 때 검사 전에 0으로 나눴다. 검사를 나눗셈 앞으로 옮겼다.
float d = 2.0f * (a.x*(b.y-c.y) + b.x*(c.y-a.y) + c.x*(a.y-b.y));
if (d == 0.0f) return false; // 일직선이면 외심 없음. 나누기 전에 검사
내접원과 외접원
원은 중심과 반지름으로 정의한다. 중심은 앞에서 구했고 반지름만 구하면 된다. 내접원 반지름은 넓이 / 반둘레이고 외접원 반지름은 외심에서 꼭짓점까지 거리다. 검증은 화면으로 한다. 외접원이 세 꼭짓점을 지나고 내접원이 세 변에 접하면 맞다.
무게중심에서 세 꼭짓점으로 잇는 노란 선은 중선의 일부다. 중선은 꼭짓점에서 마주 보는 변 중점으로 긋는 선이다. 무게중심은 이 중선을 꼭짓점 쪽 2 대 중점 쪽 1 로 나눈다. 무게중심에서 꼭짓점까지 거리는 중선 길이의 3분의 2다.
마우스 드래그
마우스는 픽셀 좌표를 준다. grid 좌표로 되돌리려면 POS_X/POS_Y를 역으로 계산한다.
int gx = lroundf((float)(px - g_cx) / GRID_INTERVAL);
int gy = lroundf((float)(g_cy - py) / GRID_INTERVAL);
클릭 지점 근처 꼭짓점 판정은 픽셀 공간 거리로 한다. 제곱근을 생략하고 dx² + dy² ≤ r²로 비교한다. 이벤트는 if (SDL_PollEvent)가 아니라 while (SDL_PollEvent)로 프레임마다 큐를 전부 비워야 드래그가 끊기지 않는다.
창 크기가 바뀌면 그리드 원점을 창 중앙으로 다시 계산한다. 초기에는 원점을 컴파일 타임 상수 매크로로 뒀고 이때는 리사이즈해도 원점이 고정됐다. 파일 static 변수(g_cx, g_cy)로 바꾸고 프레임마다 창 크기로 갱신하면 좌표 변환 매크로(POS_X, POS_Y)가 이 변수를 참조해 원점이 따라간다. SDL_GetWindowSurface가 반환한 표면은 창 크기가 바뀌면 무효가 되므로 프레임마다 다시 받는다.
정수 격자와 정삼각형
꼭짓점 드래그가 정수 grid에 스냅해서 완전한 정삼각형이 만들어지지 않는다. 원인은 코드가 아니라 수학 공식이다. 정삼각형 높이는 변 길이 × √3/2이고 √3이 무리수라 세 꼭짓점을 모두 정수 좌표에 놓을 수 없다. 정수 격자 위에 정삼각형이 존재하지 않는다는 것은 증명된 사실이다. 근사 정삼각형은 grid 스냅을 없애 소수 좌표를 허용하면 된다.
정리와 남은 문제
grid 좌표와 픽셀 좌표를 분리하고 y 반전을 고려한다. 선과 원은 Bresenham 계열 정수 알고리즘으로 그린다. 일직선이나 넓이 0 같은 퇴화 케이스는 나눗셈 전에 검사한다.
정보 패널 배경 박스가 고정 크기라 폰트를 키우면 글자가 박스를 벗어난다. 두꺼운 선은 픽셀마다 정사각형을 찍는 방식이라 45도 구간이 각진다. 다음 작업으로 삼각형 내부 채우기(무게중심 좌표 보간으로 꼭짓점 색 그라데이션), 수심과 오일러 선을 계획한다.