상세 컨텐츠

본문 제목

SCOP : BMP 이미지 파싱과 텍스처 매핑 구현 - (5)

Computer Graphics/SCOP

by Banjosh 2024. 9. 23. 00:29

본문

이번에는 BMP 이미지를 파싱하고 데이터를 저장한 후, 텍스처로 적용하여 사각형을 렌더링하는 작업을 해보았다.


1. BMP 이미지 파싱

 일반적으로 JPGPNG 파일은 압축되어 있어 파싱하기 까다롭기 때문에, 압축되지 않은 BMP 파일을 파싱해 사용하기로 했다.

 BMP 파일은 파일 헤더, DIB 헤더, 팔레트, 픽셀 데이터로 나뉘는데, 이 중 파일 헤더DIB 헤더에 있는 정보를 이용해 픽셀 데이터를 저장하면 된다.

(팔레트는 bpp가 1바이트 이하일 때 사용하는 방식으로, 내 프로젝트에서는 팔레트를 사용하지 않기로 했다)

 

 위 과정에서, 파일 헤더와 DIB 헤더의 정보를 structread()로 읽어 저장하려고 했으나 문제가 발생했다.

// 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;
}

 

2. 사각형을 그리고 텍스처를 입히기

이제 우리가 받아온 BMP 이미지를 텍스처로 적용해 사각형을 그리는 작업을 진행했다. OpenGL 강의에서 자주 다루는 내용이라 간단하게만 짚고 넘어가자.

  1. VAO 생성 및 바인딩
  2. VBO 생성 및 바인딩
  3. VBO에 정점 정보 저장 (GPU 메모리)
  4. VBO 읽는 법을 VAO에 등록 (location = 0은 position, location = 1은 texcoord)
  5. EBO 생성 및 바인딩 후 인덱스 정보 저장 (GPU 메모리)
  6. Vertex Shader, Fragment Shader 생성 및 컴파일
  7. Program에 Shader들 Attach 후 Linking
  8. BMP 파일을 읽어서 Data 저장
  9. Texture 생성 및 바인딩
  10. Texture에 Data 저장 (GPU 메모리)
  11. Uniform 변수에 Texture 등록
  12. Transform 행렬 생성 (projection * view * model)
  13. Uniform 변수에 Transform 등록
  14. 렌더링

 이 과정을 클래스를 사용하지 않고 쭉 구현해본 결과, 텍스처가 적용된 사각형을 그릴 수 있었다.

텍스처 붙이기 성공

 


3. 디버깅 과정

 사실 위 과정을 따라 바로 성공한 것은 아니었다. 처음에는 텍스처가 제대로 나오지 않고 단색 화면만 나타났다.

OpenGL 디버깅은 처음 해보는 경험이었는데, 문제를 찾기가 쉽지 않았다. 직접 이미지 데이터를 출력해 보고, 내가 사용한 GL 함수들을 다시 살펴본 끝에, 문제는 Fragment Shader에서 texCoord 변수오타였음을 발견했다.

앞으로는 Shader 코드를 작성할 때 더 신경을 써야겠다.

텍스처는 없고 단색으로만...

 


4. 리팩토링 계획

구현을 서둘러 하다 보니 코드가 정리되지 않고 지저분해졌다. .obj 파일 파싱을 구현하기 전에, 먼저 리팩토링을 통해 코드 정리부터 해야겠다.

 

관련글 더보기