본문 바로가기
개발/C++

OpenGL & C++ - 셰이더 컴파일에러 잡다가 CS 공부 왕창한 썰

by pandatta 2022. 9. 13.

예전 블로그에서 옮겨온 글입니다. 저 OpenGL도 꽤 열심히 했네요...


0(1) : error c0000: syntax error, $undefined at token "<undefined>"

평화롭게 Learn OpenGL의 Basic Lighting 튜토리얼을 따라하고 있던 나는, 셰이더를 컴파일하던 도중 뜻밖의 문제와 마주치게 된다. 바로 위의 c0000 shader compilation error였다!

잠깐, 사전설명을 하고 가자면, OpenGL에서 점과 면을 처리하는 셰이더(shader)는 C++로 구현하는 것이 아니라, GLSL이라는 OpenGL 전용 소스코드에 구현해서 C++에서 이를 읽은 다음 추상화해서 사용한다. 따라서 셰이더 GLSL 소스를 작성 후에 C++ 소스에서 이를 런타임에 컴파일하는 와중에 에러가 뜬 것! 몇 번 같은 코드로 실행해보니까, 될 때도 있고 안될 때도 있다. 심지어 에러 코드도 expecting "::" at token <var>, unexpected '?' at token '?' 처럼 실행할 때마다 바뀌었다. 런타임 에러라지만 너무 지멋대로인 상황...

일단 위의 에러를 복사해서 stackoverflow에 검색하는 것이 제맛... 찾아보니 얼추 0(1)은 첫번째 줄에 생긴 에러라는 설명... 나에게 처음 주어진 솔루션은 다음과 같았다.

1. 넌 나에게 garbage를 줬어, string이 아니라

결론부터 말하자면, 이 답변이 정답이다. 다만 답변에서 정확한 솔루션을 내주지 않았고, 질문자가 애초에 toStdString()이라는 QT 함수를 쓰다가 조진(?) 것이어서 답변이 눈에 들어오지 않았다. Garbage character가 뭐지? 아하, "€" 글자같이, 원래 읽어오고싶은 방식대로 문자를 못읽어왔을 때 생기는 "깨진 글자"구나... 유니코드로 읽어오다보면 메모리문제 때문에 그럴 수도 있겠다... 이러고 넘어갔다ㅋㅋㅋ

일단 STL string이 문제라고 하길래, 솔직히 그냥 무지성으로 const char*로 파일을 처음부터 읽어올까? 생각을 먼저 했다. 어차피 OpenGL로 셰이더를 컴파일하려면 파일을 언젠가 const char*로 변환해야하기도 하고 말이다. 하지만 이미 STL string으로 한 줄 한 줄 GLSL 파일을 읽는 코드를 짰던 나는 const char*로 바로 파일을 읽어오는 소스를 짜기 귀찮았고, 그렇게 바로 읽어오는게 좋은 소스라고 생각하지도 않아서 실천하진 않았다. 다만, 이 답변이 정답이고 차라리 무지성하게 const char*로 파일을 읽어오는 소스를 짰어야했음을 깨달은건 다음날이 되어서였다...ㅠ 정답이니 자세한 설명은 글 맨 후반에...

2. Windows에서 CRLF로 저장하니까 그렇지 멍청아, LF로 저장해야지!

Windows에서는 텍스트 파일을 저장하면 한 줄의 끝이 CRLF ("\r\n") 라는 두 글자로 저장된다. Linux나 MacOS에서는 한 줄의 끝이 LF ("\n") 라는 한 글자로 저장되고. 자세한 내용으로는 무슨 타자기 시절로 거슬러올라간다고는 하는데... 우리 모두에게는 아마 Windows에서 git commit을 할 때, 갑자기 LF will be replaced by CRLF 같은 warning이 뜨는 걸로 익숙할 것이다.

아까 garbage character라는, 예상치 못한 글자가 추가됐을 수도 있다는 단서를 봤기 때문에, 혹시 그게 "\r", 즉 Windows의 CRLF이기 때문이 아닐까? 싶어서 내가 쓰는 Visual Studio Code extension인 EditorConfig 설정을 CRLF에서 LF로 바꿔보기도 했다. 혹시 Linux처럼 CRLF가 아니라 LF로 저장되면 될까 싶어서. 결과는 실패.

하긴, 이게 정답이면 모두가 이게 정답이라고 했겠지. OpenGL은 Windows 개발자들이 더 많으니까.

3. Character array 끝이 \0으로 안끝나니까 그런거야;;

C++은 char array를 만들 때, 넣고자 하는 char의 총 숫자보다 1개 더 긴 array를 지정해주곤 한다. 그 이유는 char array 마지막이 null로 끝나야지 제대로 array가 끝났다고 생각하기 때문. 따라서 STL string으로 파일을 읽어온 뒤 string.c_str()const char*로 변환한 나는 혹시 이럴 때도 null을 끝에 넣어줘야 하나 의심이 들었다.

다행히(?) 아니었다. STL string.c_str()은 늘 null을 붙여준다는 것. 이게 그나마 설득력있는 답변이었는데, 그럼 도대체 내가 원하는 답은 무엇인가???


고난의 극복

그 이후로 잠을 못 이루면서 stackoverflow를 헤매고, 혹시 이상한 garbage character가 복붙 중에 끼어들어간게 아닌가 싶어서 셰이더를 다 지웠다가 타이핑해서 써보기도 하고... 이를 해결하기 위해 애쓰다가, 한번 garbage character 썰에 운명을 맡기고 내가 만든 shader를 읽어온 string을 출력해보았다.

// 문제의 그 코드
std::string read_shader_source(const char* file_path,
                               const std::string shader_name) {
  // Read GLSL shader source file
  std::string line, shader_source = "";
  std::ifstream file_stream(file_path);
  while (std::getline(file_stream, line)) {
    shader_source += line + "\n";
    std::cout << shader_source << std::endl; // <- 여기서 출력
  }
  file_stream.close();
  return shader_source;
}
#version 330 core\nout vec4 fragColor;\n

...? 잘 출력되는데? 첫 줄에 garbage character가 있으면 \n이 등장하기 전에 뭔가 이상한 공백이 있든지 해야하는거 아닌가? 이런 의문을 가지며 잠을 뒤척이고... 하루가 지난 뒤에야 처음에 말했듯, 결국 garbage character 문제가 맞았음을 퇴근 중에 깨달았다.

문제의 코드는 뒤쪽에 있었다. 바로 저 함수를 활용하는 부분.

// 문제의 그 코드 (진짜)
const char* vertex_shader_source =
  read_shader_source(vertex_shader_file_path, "VERTEX").c_str();

퇴근 중에 이 코드를 깃헙으로 뒤돌아보던 나는 문득 첫 답변이 떠올랐다. "넌 나에게 garbage를 줬어, string이 아니라" 그렇다, shader를 string으로 읽어오는 것까지는 문제가 없었는데, 읽어온 string을 .c_str()으로 전달하는 와중에 문제가 생긴 것이다.

// 디버깅
std::string shader_source =
  read_shader_source(vertex_shader_file_path, "VERTEX");
const char* vertex_shader_source = shader_source.c_str();

"c_str() garbage function" 이런 식으로 검색하니 나온 해결법이다.

someFunction().c_str() 과 같이 함수를 연달아서 실행하면, 뒤에 딸려서 실행되는 함수는 앞의 함수가 return할 값을 포인터로 보고 있지만, 앞의 함수가 return할 값은 앞의 함수가 끝나자마자 사라져버리고 garbage만 남는다는 얘기. 다만, 위 코드처럼 return 값을 다른 변수에 저장해두면 그 값은 보존되기 때문에 다음 함수를 실행할 때도 정상적으로 보존된채 사용될 수 있다는 그런 솔루션. 여기에 좀 더 자세한 설명이 나와있다.

결국 나는 매애애우 쉬운 방법으로 문제를 해결할 수 있었고, 정상적으로 셰이더를 컴파일할 수 있었다. 말하자면 제일 처음 나왔던 솔루션이 결국 정답이었던건데, 검색하자마자 나온 제일 처음 결과를 믿고 따랐어야했다. 이 하루간의 여정

과 고통

에서 느낀 점은,

첫째, stackoverflow는 신이다.

둘째, google 검색 알고리즘은 신이다.

셋째, C++의 메모리 관리는 아름답고 거지같다.

결론, 살려줘

댓글