CODEONWORT

문제의 원인은 다른 곳에 있다 본문

Season 1/Misc

문제의 원인은 다른 곳에 있다

codeonwort 2015.12.06 00:34

하지만 나는 그 사실을 알아채지 못한다.

 

오늘도 OpenGL로 렌더링 엔진을 만들다가 이해할 수 없는 버그를 만들었다. 셰이딩이 이상해진 것이다.

 

 

원래는 아래와 같이 그려져야 정상이다.

 

 

나는 대충 훑어보고 git 저장소에 커밋-푸쉬를 한 후 집을 나왔기 때문에 동료들을 만나서 다시 실행해보고 나서야 이 문제를 알아챘다. 한 동료가 문제가 생기기 전까지 커밋 목록을 계속 되돌리며 잘못된 지점을 찾아볼 것을 제안했고, 나는 내가 집을 나오기 전 커밋한 코드가 문제임을 발견했다.

 

하지만 무엇이 문제란 말인가? 내가 수정한 코드는 처음 생각하기에 렌더링과 전혀 상관이 없는 부분이었다. 물체의 메시(mesh)는 기하정보(geometry)와 재질(material)로 나누어진다. 기하구조는 물체마다 고유하기에 상관 없지만, 여러 메시에 같은 재질을 입힐 상황은 아주 흔하다. 보통 재질 하나는 셰이더 프로그램 하나와 대응되기 때문에, 재질을 나타내는 객체는 적을 수록 좋다. 하지만 기존에는 3D 모델 파일을 읽어 메시를 생성할 때, 같은 재질이 반복 등장해도 메시마다 새로운 재질 객체를 생성해 할당했다.

 

물론 단순히 재질 객체의 벡터를 만들어놓고 메시마다 알맞은 재질에 대한 참조를 넘기면 되지 않냐는 주장이 당장 나올 것이다. 문제는 C++에서 그런 식으로 했다가는 메모리 해제가 골치아파진다는 것이다. 메시 A와 B가 재질 M을 공유할 때, A의 소멸자에서 delete M을 해버리면 B는 M을 잃어버린다. 이런 문제에는 std::shared_ptr이 자연스러운 해결책이지만, 내가 써본 적이 없어서 임시방편으로 이렇게 구현한 것이다.

 

그리고 집을 나오기 전 수정한 것이 바로 shared_ptr을 써서 재질 객체를 공유하도록 한 것이다.

 

또다시 골치아픈 OpenGL 디버깅 시간이 다가왔다. 이건 여타 버그를 콘솔에 찍어보며 디버깅하는 것과 다른 문제다. 잘못 그려진 화면만 보고 문제를 추측해야 한다. 함수 호출 순서가 잘못되었나? 이상한 걸 바인딩해놓고 드로우 콜을 했나? 매개변수를 틀리게 썼나? 대부분 OpenGL은 프로그램을 죽이지 않는다. 다만 화면을 잘못 그리고 말 뿐이다.

 

물론 OpenGL 함수를 잘못 호출하면 glGetError()가 반환하는 수백 종류의 오류 코드로부터 무엇이 문제인지 짐작할 수 있다. 그런데 대부분의 경우 반환되는 오류 코드는 1282으로, 단순히 "잘못된 명령"이라는 뜻이다. "잘못된 명령"은 문제를 추측하기에 아무 짝에도 도움이 되지 않는다.

 

shared_ptr을 처음 배워서 쓴 것이기 때문에 나는 당장 shared_ptr을 오용했는지를 의심했다. 재질 객체가 참조 횟수가 0이 되어 쓰려는 시점에 이미 삭제되었나? 메시에 재질 포인터를 제대로 넘기지 못한 걸까? shared_ptr을 call by value로 넘겨야 하나 call by reference로 넘겨야 하나? 하지만 문제는 없었다.

 

다음으로 물체가 흰색 또는 검은색으로만 그려지는 것에서 셰이더 프로그램을 잘못 설정한 것으로 추측했다. 셰이더의 유니폼이나 셰이더에 공급하는 버퍼 데이터가 잘못되면 이런 식으로 그려지는 일이 흔하기 때문이다. (이 때는 몰랐는데, 볼링공들은 색깔이 있다. 원래와 색깔이 조금 달라졌지만.)

 

재질은 내부에 셰이더 프로그램을 가지고 있고, 메시로부터 변환 행렬이나 기하구조 등을 받아 알맞게 설정한 후 자신의 프로그램으로 기하구조를 그린다. 한 재질을 여러 메시가 공유하기 때문에, 나는 A, B 메시가 재질 M을 공유할 때 M이 A를 그리고 B를 그릴 때 A에 대한 설정이 남아 있어 B를 잘못 그리게 되는 것으로 추측했다.

 

그래서 재질에 여러 값을 설정하는 setter 메서드들을 눈에 불을 켜고 조사했지만 잘못된 점은 없었다. 애초에 그런 문제였다면 최초의 메시 A만은 제대로 그려지지 않았을까?

 

포기하고 그냥 기존 코드를 쓸까 하는 생각이 들 때 뇌리를 스치는 것이 있었다. 바로 앞서 말한 원래보다 밝게 그려진 볼링공들이다.

 

물체의 라이팅은 재질에 적용될 빛을 등록하여 이루어진다. 즉 재질에 직접 addLight() 메서드를 호출하는 것이다. 그런데 addLight()를 호출하는 코드는 다음과 같다.

 

for (auto sub : mesh->getSubMeshes()) {
    sub->getMaterial()->addLight(L0);
}

 

그리고 이제 여러 메시가 한 재질을 공유한다. 문제가 무엇인지 파악했다. 한 재질에 동일한 빛이 여러 번 적용되는 것이다. 그 결과 색상은 계속 누적되어 흰색이 된다. 같은 재질을 쓰는 볼링공은 몇 개 없어서 원래보다 밝아지긴 했지만 셰이딩을 알아볼 수 있고, 구조물은 너무 많이 적용되어 흰색이 되어버린 것이었다.

 

주어진 정보만으로 문제를 해결하려던 사람은 사기를 당한 기분일 것이다. 라이팅에 대해 언급하기 전에 장황하게 쓴 전제조건과 추론은 문제를 해결하는데 아무 관련이 없었기 때문이다. 하지만 나는 상황이 잘못되었을 때 당장 shared_ptr이 문제라고 생각했고, 그 다음은 재질의 구현을 의심했다. 문제는 재질을 사용하는 응용 코드에서 일어났다. 그런데 내가 고친 것은 재질에 대한 생 포인터 대신 shared_ptr을 쓰도록 한 것이다.

 

생각해보면 이런 식으로 버그를 찾은 경험은 여러 번 있었다. 나는 항상 잘못된 부분이 문제라고 생각했고 원인은 전혀 다른 곳에 있었다. 그러면 내가 이상한 곳에서 헤매고 있다는 것을 어떻게 알 수 있을까? 그리고 나는 진짜 답을 어떻게 찾아낸 걸까? 오늘도 그랬지만 나는 논리적인 추론을 거쳐 최종적으로 문제를 해결한 것이 아니라 헤매기만 하다가 갑자기 해법이 떠올랐기 때문이다.

0 Comments
댓글쓰기 폼