개요
제가 콘솔창으로 게임을 만들면서 꼭 해보고 싶었던게 있습니다.
바로 Bad Apple!! 출력해보기! 입니다.
유튜브에 'Bad Apple!! but' 키워드로 검색을 하면 아주 많은 미친 프로그래머들이 Bad Apple!! 뮤비를 기상천외한 방법을 사용해서 출력하는 영상을 많이 접할 수 있습니다.
그래서? 저도 해보기로 했죠.
아이디어 정리
Bad Apple!!은 그 뮤비를 봤을때 전부 흑백이죠?
그러니 뭔가 영상의 한 프레임을 읽어온 다음 그 해상도를 강제로 콘솔창에 맞춰 줄이고 한 픽셀씩 읽어와서 0과 1을 구분해 낼 수 있어보였죠.
그래서 그러한 방법을 시도하기 위해 C++ 콘솔에서 영상을 읽어오는 법을 찾아봤는데... 대충 찾아봤는데도 얼마나 길고 복잡하고 하드한 코딩을 요하는지 알게 되었습니다.
그래서 그걸 아주 아주 아주 쉽게 할 수 있게 해주는 OpenCV 라는 외부 라이브러리를 사용해보기로 했습니다.
OpenCV 임포트
일단 OpenCV를 다운로드 해줍시다.
가장 최근에 릴리즈된 4.10.0을 사용해주고 그냥 프로젝트 폴더에다가 다운 받아줬습니다.
그리고 이걸 프로젝트에서 사용할 수 있도록 경로 설정을 해줍시다.
해줘야할 경로 설정은 다음과 같습니다. 프로젝트 설정에서...
(opencv가 깔린 폴더)\opencv\build\include 를 C/C++/일반/추가 포함 디렉터리에 적어줍니다.
경로는 절대경로지만 ${ProjectDir}이라는 매크로?를 사용해서 상대경로로 사용할 수도 있습니다.
저는 이 프로젝트의 소스를 같이 제출해야 하기 때문에 라이브러리를 포함시켜 제출하기 위해서 상대 경로로 작성했습니다.
제 개인적인 의견이지만 상대경로가 절대경로보다 10000배는 좋아요.
그리고 링커/일반에 (opencv가 깔린 폴더)\opencv\build\x64\vc16(버전에 따라 다름)\lib을 적어줍니다.
이러면 OpenCV의 임포트가 끝이났고 이런식으로 include하여 사용할 수 있습니다.
동영상 읽기
OpenCV의 사용법은 아주 쉽습니다.
어... 사용법이 쉬운지는 잘 모르겠지만 Bad Apple!!을 출력하기는 아주 쉽습니다.
놀랍게도 영상을 한 프레임씩 뜯어오는 기능이 존재하죠.
일단 프로젝트 폴더 내에 영상을 넣어주고 다음과 같은 변수들을 선언해줍시다.
//BadAppleScene.h
cv::VideoCapture _cap;
cv::Mat _frame, _grayFrame, _resizedFrame;
cv::VideoCapture라는 클래스는 말 그대로 영상의 정보를 담습니다.
보통 콘솔창 내에서 영상을 출력하는데에 쓰인다고 합니다만... 솔직히 더 깊이 들어가진 않아서 잘 모릅니다.
저도 어짜피 콘솔창에서 영상 출력하는데에 쓸거라 저것만 알아도 상관없죠.
그리고 cv::Mat이라는 변수에 프레임을 담아낼 수 있습니다. 변수가 3개인 이유는 나중에 코드 적으면서 설명하죠.
이제 이런식으로 영상을 읽어올 수 있습니다.
//BadAppleScene::init()
_cap = cv::VideoCapture("Video\\bad_apple.mp4");
이제 이걸 이용하여 render함수 내에서 한 프레임씩 가져와 출력만 해주면 되죠.
이런식으로 영상의 프레임을 읽어올 수 있습니다.
//BadAppleScene::render()
if (_cap.read(_frame))
{
}
그리고 이러한 함수 두개를 사용해서 영상을 강제로 흑백으로 전환 시킨 다음 해상도를 바꿔버릴 수 있습니다.
//BadAppleScene::render()
cv::cvtColor(_frame, _grayFrame, cv::COLOR_BGR2GRAY);
cv::resize(_grayFrame, _resizedFrame, cv::Size(_width, _height));
프레임 변수를 3개 쓴 이유는 그냥 가독성 때문입니다.
하나로 써도 되기야 하겠지만 별로 내키진 않아요.
흑백으로 전환시키는 이유는 아래서 마저 설명하죠.
해상도는 다음과 같이 콘솔창의 크기와 일치시켜줬습니다.
//BadAppleScene::init()
COORD resolution = getConsoleResolution();
_width = resolution.X;
_height = resolution.Y - 1;
그 뒤에 이제 출력만 하면 되죠.
출력
출력 자체도 상당히 쉬운 편입니다.
놀랍게도 읽어온 프레임 안 특정 좌표의 픽셀을 읽어올 수 있거든요.
int pixelValue = _resizedFrame.at<uchar>(y, x);
이 pixelValue라는 녀석은 해당 픽셀의 밝기를 반환합니다.
0은 검정색, 255는 흰색입니다.
그러면 흑백이 아닌 일반 컬러값은 어떻게 사용하냐구요?
그건 at<uchar> 대신 at<cv::Vec3b>를 사용해서 R, G, B 값을 하나하나 가져올 수 있답니다.
물론 흑백이 아니여도 at<uchar>을 사용할 수는 있지만 올바른 사용법은 아닙니다.
OpenCV 라이브러리는 신이 맞습니다.
용량이 말도안되는걸 제외하면요. (어쩔 수 없긴하죠.)
그러면 이제 이렇게 for문을 돌며 하나하나 출력할 수 있겠죠.
//BadAppleScene::render
gotoxy(0,0);
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
int pixelValue = _resizedFrame.at<uchar>(y, x);
cout << (pixelValue > 128) ? "8" : " ";
}
cout << '\n';
}
'8'과 ' '를 나누는 임계점은 대충 설정해줍니다.
어짜피 Bad Apple!! 엔 흰색(255) 아니면 검정색(0)밖에 없어요.
그리고 한 프레임을 출력했으니 다음 프레임이 될때까지 기다려줍시다.
구해뒀던 영상이 30FPS이니 1000/30(33)ms 만큼 기다리면 되겠죠.
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
int pixelValue = _resizedFrame.at<uchar>(y, x);
cout << (pixelValue > 128) ? "8" : " ";
}
cout << "\n";
}
Sleep(1000/30);
근데 이렇게 하고 실행을 해보는 순간 문제가 바로 발생합니다.
오류 잡기
일단 바로 저희를 반겨주는건 링커오류입니다.
이제 슬슬 반갑죠? 없으면 허전하죠?
솔직히 컴파일러도 항상 '확인할 수 없는 외부참조' 라는 말만 앵무새마냥 반복하기 때문에 잡기 어렵지만 에러 메시지 오른쪽에 어렴풋이 떠있는 파일명으로 어느 소스 파일에서 오류가 났는지는 충분히 알아낼 수 있습니다.
그냥 딱봐도 라이브러리 이슈죠.
찾아보니 프로젝트 설정을 덜했습니다.
추가 종속성이란걸 적어줘야하더라구요.
OpenCV는 이렇게 적어주면 됩니다. 뒤에 적힌 4100은 버전이고 그 뒤에 적힌 d는 Debug빌드용 이라는 뜻입니다.
이제 종속성도 적었으니 됐겠죠?
어림도 없지 ㅋㅋ
이런식으로 라이브러리를 사용하려면 저런 dll파일을 exe파일이 있는 빌드 폴더에 동봉시켜줘야 합니다.
opencv\build\x64\vc16\bin 폴더를 보면
이런식의 파일들이 있는데 저희들이 필요한건 .dll 확장자를 가진 파일이고 그중에서 저흰 Debug 빌드를 사용중이니 d가 붙은 파일이 필요합니다.
대충 가져와서 넣어주면 끝입니다.
완...성?
실행해보면 놀랍게도 제가 의도한 대로 잘 출력됩...니..다?
아뇨 제가 의도한대로 출력하지 못합니다.
딱 봤을때 문제가 엄청나게 많죠?
일단 공백으로 출력돼야 할 공간이 '0'으로, '8'로 출력돼야할 공간이 '1'로 출력되는 괴상한 버그가 존재합니다.
그리고 시작할때 뜨는 오류또한 문제고 엄청나게 느려요.
노래랑 싱크를 맞추는게 포인트인데 이렇게 출력하면 보이기는 할지언정 노래랑 싱크는 전혀 안맞을게 뻔합니다.
일단 제 코드를 봤을때 가장 큰 문제는 cout을 기하급수적으로 많이 실행한다는 것 정도가 있습니다.
//BadAppleScene::render()
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
int pixelValue = _resizedFrame.at<uchar>(y, x);
cout << (pixelValue > 128) ? "8" : " ";
}
cout << "\n";
}
널널하게 height : 30, width : 70 정도로만 잡아도 한 프레임마다 cout이 2400번 실행되고 이걸 1000/30. 즉, 33ms마다 한번씩 실행해주는겁니다.
심지어 영상의 프레임을 읽어오고, 흑백, 해상도 변환에 걸리는 시간까지 고려한다면 느려질 수 밖에 없습니다.
그리고 가장 이해할 수 없는 01로만 출력되는 버그...는 아직도 원인 불명입니다.
출력 딜레이를 줄이려고 백준에서나 쓸법한 딜레이 줄이기를 써보기도 했는데 효과는 없다싶이 했습니다.
아무튼, 이 엄청나게 많은 cout을 해결하기 위해 한 글자씩 출력하는 대신 한 프레임을 string변수에 전부 담아서 한번에 출력하도록 하겠습니다.
//BadAppleScene::render()
string buffer;
buffer.reserve(_width * _height * 2);
또 딜레이를 줄이기 위해 reserve()를 사용해서 더하는 시간을 줄여줍니다.
그 후 cout을 한번에 몰아서 해주면 끝입니다.
//BadAppleScene::render()
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
int pixelValue = _resizedFrame.at<uchar>(y, x);
buffer += (pixelValue > 128) ? "8" : " ";
}
buffer += "\n";
}
cout << buffer;
잘 되죠?
서비스인지 0과 1로 출력되던 것도 고쳐졌습니다.
다시 오류잡기
근데 시작할때 이러한 이상한 오류가 잡히죠?
FAILED라고 대문짝만하게 적어둔 메시지를 읽어보면 opencv_videoio_intel_mfx_4100_64d.dll이 없다고 합니다.
...뭔말인지 모르겠어요. 저런 dll은 존재하지 않았단 말이죠.
생각해보니 디버그 빌드용 dll이 하나 부족했던 것 같기도 합니다.
opencv_videoio_ffmpeg4100_64d.dll 이 없더군요.
OpenCV 폴더를 뒤져보고 OpenCV를 재설치 해보고 그냥 삽질을 허공에 30분정도 한 결과 그냥 Debug 빌드는 포기하기로 했습니다.
Release 빌드로 하니 오류가 안뜨고 좋아요.
싱크 맞추기
문제가 있습니다.
싱크가 안맞아요.
초반엔 살짝 맞는것 같아보이다가 점점 밀립니다.
딜레이가 아직 크다는거죠.
그래서 렌더링 하는 프레임의 갯수를 하나 줄여주겠습니다.
그러기 위해 변수 하나를 선언해 준뒤
//BadAppleScene.h
int _curFrame = 0;
update에서 계속 증가시켜주고
//BadAppleScene::update()
_curFrame++;
render에서 짝수일때 그냥 렌더링을 스킵해주면 되죠.
//BadAppleScene::render()
if (_curFrame % 2 == 0) return;
물론 이래도 딜레이는 남아있어서 그냥 야매로다가 해보겠습니다.
//BadAppleScene.h
float _framePerSecond = 31;
이런 변수를 선언해준뒤에 render에서 딜레이를 주는 Sleep을 바꿔줍니다.
//BadAppleScene::render()
Sleep(1000/_framePerSecond);
좀 방법이 레전드긴한데 update에서 0.00025f씩 증가시켜서 생기는 딜레이 만큼 제가 주는 딜레이를 줄여줍니다.
//BadAppleScene::update()
_framePerSecond += 0.00025f;
물론 정확한 수치는 아닐테지만 이래도 충분히 맞는 기분이 들었습니다.
그 외에...
애초에 본게임이 있고 이건 제가 하고 싶어서 해본거기 때문에 게임을 제출 전날에 충분히 완성한 뒤에 만들었습니다.
그래서 살짝 이스터에그같은 느낌으로 타이틀 씬에서 볼 수 있게 연결해주었습니다.
끝입니다.
완성
https://www.youtube.com/watch?v=HGBZGKKKuyo
마치는 말
이번엔 살짝 외부 유입을 고려해서 제목의 폼을 바꿔보았습니다.
이런다고 외부 유입이 이뤄질랑가 모르겠지만 되면 좋은거고 안되면 아쉬운거죠.
그리고 이런거 꼭 한번 해보고 싶었는데 잘되고 발표때 반응도 좋았고 선생님도 극찬해주셔서 너무 뿌듯합니다.
이 글로 Default Defense의 마지막 개발일지를 마치겠습니다.
'개발 > Default Defense' 카테고리의 다른 글
Default Defense | 다섯번째 프로젝트 # 8 : Wave, InGameState (1) | 2024.07.04 |
---|---|
Default Defense | 다섯번째 프로젝트 # 7 : 구조 수정 (0) | 2024.06.30 |
Default Defense | 다섯번째 프로젝트 # 6 : Ally Attack (0) | 2024.06.29 |
Default Defense | 다섯번째 프로젝트 # 5 : Enemy Movement (1) | 2024.06.21 |
Default Defense | 다섯번째 프로젝트 # 4 : Entity (2) | 2024.06.15 |