이번에는 BMP 이미지를 파싱하고 데이터를 저장한 후, 텍스처로 적용하여 사각형을 렌더링하는 작업을 해보았다.
일반적으로 JPG나 PNG 파일은 압축되어 있어 파싱하기 까다롭기 때문에, 압축되지 않은 BMP 파일을 파싱해 사용하기로 했다.
BMP 파일은 파일 헤더, DIB 헤더, 팔레트, 픽셀 데이터로 나뉘는데, 이 중 파일 헤더와 DIB 헤더에 있는 정보를 이용해 픽셀 데이터를 저장하면 된다.
(팔레트는 bpp가 1바이트 이하일 때 사용하는 방식으로, 내 프로젝트에서는 팔레트를 사용하지 않기로 했다)
위 과정에서, 파일 헤더와 DIB 헤더의 정보를 struct에 read()로 읽어 저장하려고 했으나 문제가 발생했다.
// file header
struct BMPFileHeader {
uint16_t bfType;
uint32_t bfSize;
uint16_t bfReserved1;
uint16_t bfReserved2;
uint32_t bfOffBits;
};
// DIB header
struct BMPInfoHeader {
uint32_t biSize;
int32_t biWidth;
int32_t biHeight;
uint16_t biPlanes;
uint16_t biBitCount;
uint32_t biCompression;
uint32_t biSizeImage;
int32_t biXPelsPerMeter;
int32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
};
read()로 sizeof(구조체)만큼 파일을 읽었는데, 값이 계속 잘못 읽혀왔다. 문제는 패딩 때문이었다. 파일 헤더는 원래 14바이트여야 하지만, 패딩이 적용되어 16바이트로 읽혀 값이 어긋난 것이다.
이 문제를 깨닫고, 파일을 읽을 때 파일 헤더와 DIB 헤더의 크기인 54바이트만 명시적으로 읽어오도록 수정했다. 그 후, 이미지의 너비, 높이, bpp, 데이터 시작 위치 등을 이용해 픽셀 데이터를 정확히 읽어올 수 있었다.
추가로 높이가 음수로 들어올 수 있는데 이때는 이미지의 상하가 뒤집힌 거라 위 아래 줄을 swap 해주는 로직을 추가하였고, 올바르지 않은 형식의 BMP 파일, 압축된 BMP 파일, 팔레트 기반 BMP 파일에 대해서는 예외 처리를 하였다
아래 있는게 BMP 이미지 파일을 파싱하는 코드이다.
std::unique_ptr<uint8_t[]> glload::loadBmpImg(const char* fileName, int* width, int* height, int* channelCount) {
if (!isBmpFile(fileName)) {
std::cerr << "this file is not bmp format" << std::endl;
return nullptr;
}
std::ifstream fin(fileName, std::ios::binary);
if (!fin.is_open()) {
std::cerr << fileName << " is not exist" << std::endl;
return nullptr;
}
char fileHeader[54];
fin.read(fileHeader, 54);
if (*reinterpret_cast<uint16_t*>(&fileHeader[0]) != 0x4D42) {
std::cerr << fileName << " is invalid bmp file" << std::endl;
return nullptr;
}
uint32_t imgOffBits = *reinterpret_cast<uint32_t*>(&fileHeader[10]);
uint32_t imgSize = *reinterpret_cast<uint32_t*>(&fileHeader[34]);
int32_t imgWidth = *reinterpret_cast<int32_t*>(&fileHeader[18]);
int32_t imgHeight = *reinterpret_cast<int32_t*>(&fileHeader[22]);
uint16_t imgBitCount = *reinterpret_cast<uint16_t*>(&fileHeader[28]);
uint32_t imgCompression = *reinterpret_cast<uint32_t*>(&fileHeader[30]);
if (imgCompression) {
std::cerr << "this bmp file is compressed" << std::endl;
return nullptr;
}
if (imgBitCount <= 8) {
std::cerr << "Palette-based BMP files are not supported." << std::endl;
return nullptr;
}
if (!imgSize) {
imgSize = std::abs(imgWidth * imgHeight * (imgBitCount / 8));
}
std::unique_ptr<uint8_t[]> data = std::make_unique<uint8_t[]>(imgSize);
fin.seekg(imgOffBits, std::ios::beg);
fin.read(reinterpret_cast<char*>(data.get()), imgSize);
if (fin.fail()) {
std::cerr << "Error: Could not read the BMP image data." << std::endl;
return nullptr;
}
if (imgHeight < 0) {
imgHeight = -imgHeight;
int rowSize = imgWidth * (imgBitCount / 8);
std::vector<uint8_t> tmp(rowSize);
for (int i = 0; i < imgHeight / 2; i++) {
uint8_t* upperRow = &data[i * rowSize];
uint8_t* lowerRow = &data[((imgHeight - 1) - i) * rowSize];
std::copy(upperRow, upperRow + rowSize, tmp.begin());
std::copy(lowerRow, lowerRow + rowSize, upperRow);
std::copy(tmp.begin(), tmp.end(), lowerRow);
}
}
*width = imgWidth;
*height = imgHeight;
*channelCount = imgBitCount / 8;
fin.close();
return data;
}
이제 우리가 받아온 BMP 이미지를 텍스처로 적용해 사각형을 그리는 작업을 진행했다. OpenGL 강의에서 자주 다루는 내용이라 간단하게만 짚고 넘어가자.
이 과정을 클래스를 사용하지 않고 쭉 구현해본 결과, 텍스처가 적용된 사각형을 그릴 수 있었다.
사실 위 과정을 따라 바로 성공한 것은 아니었다. 처음에는 텍스처가 제대로 나오지 않고 단색 화면만 나타났다.
OpenGL 디버깅은 처음 해보는 경험이었는데, 문제를 찾기가 쉽지 않았다. 직접 이미지 데이터를 출력해 보고, 내가 사용한 GL 함수들을 다시 살펴본 끝에, 문제는 Fragment Shader에서 texCoord 변수의 오타였음을 발견했다.
앞으로는 Shader 코드를 작성할 때 더 신경을 써야겠다.
구현을 서둘러 하다 보니 코드가 정리되지 않고 지저분해졌다. .obj 파일 파싱을 구현하기 전에, 먼저 리팩토링을 통해 코드 정리부터 해야겠다.
SCOP: 3D 오브젝트 이동, 회전, 색상 및 텍스처 적용 - (7) (0) | 2024.09.27 |
---|---|
SCOP : 기능별 클래스로 리팩토링하기 - (6) (0) | 2024.09.23 |
SCOP : GLM을 대체하는 GLMath 라이브러리 구현 - (4) (0) | 2024.09.20 |
SCOP : 빌드 환경 설정 및 기본 구조 준비 - (3) (0) | 2024.09.19 |
SCOP : 프로젝트 진행 계획 - (2) (1) | 2024.09.18 |