DDS란?

마이크로소프트사에서 제공하는 이미지 포맷 형식이다. 이 이미지 포맷은 일반적으로 사용되는 이미지툴에서 지원하는 경우가 많지 않다. 하지만 JPEG, PNG처럼 특별한 압축형식이 없는데다 픽셀 데이터의 구조가 간단해서 2D게임이나 3D 텍스처를 저장하는 용도로 많이 사용된다.  

 

이미지 파일은 일반 텍스트 에디터로는 읽을 수 없는 바이너리 데이터이고, BMP와 PNG, JPG등 이미지 형식에 따라서 압축등 고급 기법이 많이 들어가기 때문에 게임 개발에서는 그나마 다루기 쉬운 DDS를 사용한다.

 

 


 

DDS의 구조

여기서 다루는 DDS의 형식은 32bit ARGB(DirectX용어로는 A8R8G8B8)이다. DDS도 여러 종류가 있지만, 이 형식이 가장 자주 사용된다. ARGB는 이름에서도 알 수 있듯이, 알파 채널, 적색, 녹색, 청색을 의미한다. 그리고 각각 8bit(1byie)씩 사용해서 32비트로 이미지를 표현한다. 이는 unsigned(부호 없는 정수 4byte:32bit)로 표현할 수 있음을 의미한다. 그리고 unsigned char로는 하나의 RGB 색상을 처리할 수 있다.

 

DDS 파일 전체구조
번지 타입 이름 설명
0 DWORD dwMagic 'D', 'D', 'S', '' 4개의 문자 정보
4 DDSURFACEDESC2 ddsd 포맷 정보
128 BYTE bData[] 데이터 정보

 

  • DWORD ( 4byte : 32bit )
    unsigned int 또는 unsigned 
  • BYTE ( 1Byte : 8bit )
     unsigned char

 

표를 보면 0번지에 DWORD 타입의 dwMagic에 문자 DDS+공백 4글자가 적혀있는걸 확인할 수 있다. 이건 DDS파일을 식별하기 위해 들어있는 단어이다. 확장자가 DDS여도 실제 내부 데이터는 다를 수 있기 때문이다. 그리고 4바이트부터 128바이트까지 이미지에 대한 DDSUREFACEDESC2 구조체가 들어있다. 마지막으로 128바이트부터는 실질적인 이미지 데이터에 대한 픽셀 정보가 들어있다.

 

 

DDSURFACEDESC2 포맷 정보
번지      
0 DWORD dwSize 구조체 크기, 124 고정됨
4 DWORD dwFlags 플래그 정보
8 DWORD dwHeight 이미지 높이
12 DWORD dwWidth 이미지 폭
16 DWORD dwPitchOrLinearSize 한 줄 바이트 수
20 DWORD dwDepth 3D 텍스처 깊이
24 DWORD dwMipMapCount 밉맵 수
28 DWORD dwReserved1[11] 미사용
72 DDPIXELFORMAT ddpfPixelFormat 이미지 포맷 구조체
104 DDCAPS2 ddsCaps 기타 정보
120 DWORD dwReserved2 미사용

 

구조체 내에 바이트수를 파악한 후 필요한 정보를 취사해서 이미지 파일을 가져올 수 있다. 만약 여러가지 이미지 포맷 파일을 읽어서 사용하고 싶다면 DDPIXELFORMAT 구조체 내부에 대해 확실히 공부할 필요가 있다.


 

사용 예

#include <iostream>
#include <fstream>  // 파일을 읽어오기
#include "GameLib/Framework.h"
bool sw = true;

void readFile(char** _buffer, int* _size, const char* __fileName);
unsigned getUnsigned(const char* str);
int gImageWidth = 0;
int gImageHeight = 0;
unsigned* gImageData = 0;

namespace GameLib
{
	void Framework::update()
	{
		if (sw)
		{
			char* fileData = nullptr;
			int    fileSize = 0;
			readFile(&fileData, &fileSize, "image.dds");
			gImageHeight = getUnsigned(&(fileData[12]));
			gImageWidth = getUnsigned(&(fileData[16]));
			gImageData = new unsigned[gImageWidth * gImageHeight];
			for (int i = 0; i < gImageWidth * gImageHeight; ++i) {
				gImageData[i] = getUnsigned(&(fileData[128 + i * 4]));
			}
			sw = false;
		}
		unsigned* vram = videoMemory();
		unsigned windowWidth = width();
		for (int y = 0; y < gImageHeight; ++y)
		{
			for (int x = 0; x < gImageWidth; ++x)
			{
				vram[y * windowWidth + x] = gImageData[y * gImageWidth + x];
			}
		}
	}
}

void readFile(char** _buffer, int* _size, const char* __fileName)
{
	using namespace std;
	ifstream input(__fileName, ifstream::binary);
	if (input)
	{
		input.seekg(0, ifstream::end);
		(*_size) = input.tellg();
		input.seekg(0, ifstream::beg);
		(*_buffer) = new char[(*_size)];
		input.read(*_buffer, *_size);
		input.close();
	}
}

unsigned getUnsigned(const char* str)
{
	const unsigned char* temp;
	temp = reinterpret_cast<const unsigned char*>(str);
	unsigned r = temp[0];
	r |= temp[1] << 8;
	r |= temp[2] << 16;
	r |= temp[3] << 24;
	return r;
}

 

실제 사용해서 작성한 코드이다.

getUnsigned

로직을 조금 설명하자면 컴퓨터의 이미지 포맷은 RGB형태의 256 256 256이다. 이걸 컴퓨터가 인식하는 바이너리 형태로 나타내면

11111111 11111111 11111111 <-- 형태.. 띄어쓰기로 적녹청이 구별되어있고 각각의 비트는 8비트 총 1바이트다 

여기서 이미지를 표현하는 경우의수가 각각 256 256 256가지이므로 총 16,777,216가지의 색상표현이 가능하다는 뜻이된다. 

이걸 중복표현하면서 데이터를 누적시키려면 or연산을 사용하면 된다 

그리고 <<는 비트연산이다. 이진부호를 한칸씩 밀어주는 형태이고 한칸이 밀릴때마다 2^n만큼 수가 증가한다. 

즉 256은 2의 8승이니까 저런식으로 표현이 가능하다

 


 

사용 예

#ifndef __IMAGE_FILE_H__
#define __IMAGE_FILE_H__
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;

class ImageFile2D
{
public:
	explicit ImageFile2D() : m_count(0) {}
	~ImageFile2D() {}
	
	int getImageHeight(const int __index) const
	{ 
		if (__index >= m_count) {
			cout << "The index was out of range." << endl;
			return 0;
		}
		return m_height[__index];
	}
	int getImageWidth(const int __index) const
	{
		if (__index >= m_count) {
			cout << "The index was out of range." << endl;
			return 0;
		}
		return m_width[__index];
	}
	unsigned* operator[](const int __index) const
	{
		if (__index >= m_count) {
			cout << "The index was out of range." << endl;
			return 0;
		}
		return m_image[__index];
	}
	void readFile(const char* __fileName)
	{
		ifstream input(__fileName, ifstream::binary);
		if (input)
		{
			int       tSize = 0;
			char*     tBuffer;
			unsigned* temp;
			input.seekg(0, ifstream::end);
			tSize = input.tellg();
			input.seekg(0, ifstream::beg);
			tBuffer = new char[tSize];
			input.read(tBuffer, tSize);
			m_width.push_back( getUnsigned ( &( tBuffer[16] ) ) );
			m_height.push_back( getUnsigned ( &( tBuffer[12] ) ) );
			temp = new unsigned[m_width[m_count] * m_height[m_count] + 1];
			for (int i = 0; i < m_width[m_count] * m_height[m_count]; ++i) {
				temp[i] = getUnsigned(&(tBuffer[128 + i * 4]));
			}
			m_image.push_back(temp);
			++m_count;
			delete[]tBuffer;
			input.close();
		}
	}
protected:
	// RGB를 읽어들이고 색이 채워진 unsigned를 반환한다.
	unsigned getUnsigned(const char* str)
	{
		const unsigned char* temp;
		temp = reinterpret_cast<const unsigned char*>(str);
		unsigned result = temp[0];
		result |= temp[1] << 8;
		result |= temp[2] << 16;
		result |= temp[3] << 24;
		return result;
	}
private:
	vector<unsigned*>  m_image;
	vector<int>        m_width;
	vector<int>        m_height;
	int                m_count;
};

#endif

 

헤더파일을 따로 만들고 하나의 객체로 이미지를 저장하고 관리할 수 있게 싱글톤 패턴으로 다시 만들었다. 사용 예는 아래 코드를 보자

 

#include <iostream>
#include <fstream>  // 파일을 읽어오기
#include "GameLib/Framework.h"
#include "ImageFile.h"
bool gFirst = true;

ImageFile2D imageDataBase;  // 이미지 저장소
enum ImageName { FuJi = 0 };

int gImageWidth = 0;
int gImageHeight = 0;
unsigned* gImageData = 0;

namespace GameLib
{
	void Framework::update()
	{
		if (gFirst)
		{
			gFirst = false;
			imageDataBase.readFile("FuJi.dds");
			gImageWidth = imageDataBase.getImageWidth(ImageName::FuJi);
			gImageHeight = imageDataBase.getImageHeight(ImageName::FuJi);
			gImageData = imageDataBase[ImageName::FuJi];
		}
		unsigned* vram = videoMemory();
		unsigned windowWidth = width();
		for (int y = 0; y < gImageHeight; ++y)
		{
			for (int x = 0; x < gImageWidth; ++x)
			{
				vram[y * windowWidth + x] = gImageData[y * gImageWidth + x];
			}
		}
	}
}

 

  • ImageFile2D imageDataBase;
    이미지를 관리하는 데이터베이스 객체를 선언한다. (복사생성자는 없다) 
    미구현된 대입 연산을 방지하기 위해 생성자는 explicit를 사용한다. 
  • getImageWidth( 인덱스 번호 ) 
    파일의 인덱스 번호를 인수로 넣으면 해당 인덱스의 이미지 폭을 반환해준다.
  • getImageHeight( 인덱스 번호 )
    파일의 인덱스 번호를 인수로 넣으면 해당 인덱스의 이미지 높이를 반환해준다.
  • imageDataBase[ 인덱스 ];
    배열처럼 사용이 가능하다. 들어온 순서대로 오름차순으로 인덱스가 배정되고 인덱스에 해당하는 이미지 파일의 unsigned* 의 주소를 반환한다.