사각형까지 렌더링이 되었다. 이제 이 사각형에 이미지를 출력해보려고 한다. 이미지를 출력하기 위해서는 Texture2D
라는 리소스가 필요하다. Texture2D
는 GPU 메모리에 적재되어 Pixel Shader
에서 샘플링될 수 있다. 샘플링된다는 것은 특정 픽셀에서 표현해야할 색상값을 표본으로 가져올 수 있다는 뜻이다. 이미지를 렌더링할 수 있는 Texutre2D에 대해 알아보자.

정점에서 가지는 텍스처 좌표 또한 보간되어 각 픽셀도 텍스처 좌표를 가지게 된다. 이 텍스처 좌표를 가지고 이미지로부터 적절한 색상값을 샘플링해서 표현할 수 있게 된다.
Image 로드하기
가장 우선 해야할 것이 이미지 파일을 로드하는 것이다. 이 부분은 DirectX ToolKit
혹은 DirectX Tex
라이브러리를 사용하면 매우 간단하게 해결된다. 아래의 코드 호출로 끝이다.
ID3D11Texture2D* texture2D = nullptr;
ID3D11ShaderResourceView * shaderResourceView = nullptr;
CoInitialize( nullptr );
DirectX::CreateWICTextureFromFile( ID3D11Device, ImagePath, &texture2D, &shaderResourceView );
이렇게 호출하면 생성된 ID3D11ShaderResourceView
를 렌더링 파이프라인에 연결하면 된다. 위 코드를 사용하는 경우는 Rendering pipeline에 연결하기단락부터 이어서 보면 된다. 이하 이 단락의 내용은 사실상 해당 함수의 코드를 분석하는 내용이며 마이크로 소프트의 내용이기도 하다.
WIC 인스턴스 준비
우선 이미지 로드에 필요한 WIC
( Windows Imaging Component ) 인스턴스들을 생성해야 한다.
IWICImagingFactory* WICFactory = nullptr;
CoCreateInstance( CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, __uuidof( IWICImagingFactory ), ( LPVOID* )&WICFactory );
IWICBitmapDecoder* BitmapDecoder = nullptr;
WICFactory->CreateDecoderFromFilename( ImagePath.c_str(), 0, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &BitmapDecoder );
IWICBitmapFrameDecode* BitmapFrameDecode = nullptr;
BitmapDecoder->GetFrame( 0, &BitmapFrameDecode );
IWICBitmapDecoder
객체를 생성하면서 파일 내용에 대한 로드는 끝이다. IWICBitmapFrameDecode
인스턴스는 이미지에 대한 정보들( Width, Height, Format 등 )를 가져오기 위한 것이다.
Texture생성 및 Image Data를 메모리에 올리기 위한 준비
텍스처 리소스의 크기를 임의로 지정하지 않는 이상 일반적으로 텍스처 리소스의 크기는 이미지의 크기에 종속된다. 그리고 하드웨어 및 API 레벨에서 지원 가능한 텍스처의 크기보다 이미지가 더 큰 경우도 있다. 이런 케이스들에 대응하기 위해 이미지에 대한 정보를 텍스처를 생성하기 위한 정보로 컨버팅이 필요하게 된다.
unsigned int ImageWidth = 0;
unsigned int ImageHeight = 0;
BitmapFrameDecode->GetSize( &ImageWidth, &ImageHeight );
우선 이미지의 가로-세로 크기를 가져온다.
unsigned int maxSize = 0;
switch( ID3D11Device->GetFeatureLevel() )
{
case D3D_FEATURE_LEVEL_9_1:
case D3D_FEATURE_LEVEL_9_2: maxSize = 2048; break;
case D3D_FEATURE_LEVEL_9_3: maxSize = 4096; break;
case D3D_FEATURE_LEVEL_10_0:
case D3D_FEATURE_LEVEL_10_1: maxSize = 8192; break;
default: maxSize = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION; break;
}
API
레벨에서 지원 가능한 최대 크기도 가져온다.
unsigned int TextureWidth = 0;
unsigned int TextureHeight = 0;
if ( ImageWidth > maxSize || ImageHeight > maxSize )
{
float ar = (float)( ImageHeight ) / (float)( ImageWidth );
if ( ImageWidth > ImageHeight )
{
TextureWidth = (unsigned int)( maxSize );
TextureHeight = (unsigned int)( (float)( maxSize ) * ar );
}
else
{
TextureHeight = (unsigned int)( maxSize );
TextureWidth = (unsigned int)( (float)( maxSize ) / ar );
}
if ( TextureWidth > maxSize || TextureHeight > maxSize )
{
GLog.AddLog( "Invalid texture size" );
return false;
}
}
else
{
TextureWidth = ImageWidth;
TextureHeight = ImageHeight;
}
최종적으로 텍스처의 가로-세로크기를 결정한다.
WICPixelFormatGUID WicFormat;
BitmapFrameDecode->GetPixelFormat( &WicFormat );
WIC
기준의 픽셀 포맷을 가져온다.
static WICTranslate g_WICFormats[] =
{
//...
{ .wic= GUID_WICPixelFormat128bppRGBAFloat, .format= DXGI_FORMAT_R32G32B32A32_FLOAT },
{ .wic= GUID_WICPixelFormat64bppRGBAHalf, .format= DXGI_FORMAT_R16G16B16A16_FLOAT },
//...
};
DXGI_FORMAT DxgiFormat = DXGI_FORMAT_UNKNOWN;
for ( size_t i = 0; i < _countof( g_WICFormats ); ++i )
{
if ( memcmp( &g_WICFormats[ i ].wic, &WicFormat, sizeof( GUID ) ) == 0 )
{
DxgiFormat = g_WICFormats[ i ].format;
break;
}
}
WIC
기준의 포맷을 DXGI
포맷으로 변경한다. 이 DxgiFormat
은 나중에 텍스처의 포맷을 지정할 때 사용하게 된다. 변경은 g_WICFormats
을 이용해서 변환한다. 마이크로 소프트에 있는 룩업 테이블을 그대로 이용하였다.
만약 g_WICFormats
에서 찾을 수 없다면 g_WICConvert
룩업 테이블을 이용해서 한번 더 시도를 한다. 마찬가지로 마이크로 소프트에 있는 룩업 테이블을 그대로 가져왔다. 이 룩업테이블은 다양한 WIC
의 포맷을 DXGI
포맷으로 변경가능한 포맷으로 단순화하여 변환해주는 용도이다.
static WICConvert g_WICConvert[] =
{
// Note target GUID in this conversion table must be one of those directly supported formats (above).
{ .source= GUID_WICPixelFormatBlackWhite, .target= GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ .source= GUID_WICPixelFormat1bppIndexed, .target= GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
//...
};
WICPixelFormatGUID ConvertToFormat;
if ( DxgiFormat == DXGI_FORMAT_UNKNOWN )
{
for ( size_t i = 0; i < _countof( g_WICConvert ); ++i )
{
if ( memcmp( &g_WICConvert[ i ].source, &WicFormat, sizeof( WICPixelFormatGUID ) ) == 0 )
{
memcpy( &ConvertToFormat, &g_WICConvert[ i ].target, sizeof( WICPixelFormatGUID ) );
DxgiFormat = ConvertWICToDXGI( g_WICConvert[ i ].target );
break;
}
}
g_WICConvert
전체를 돌며 DXGI
포맷으로 변환가능한 WIC
포멧을 찾는다.
IWICComponentInfo* info = nullptr;
HRESULT hr = WICFactory->CreateComponentInfo( ConvertToFormat, &info );
WICComponentType type;
info->GetComponentType( &type );
IWICPixelFormatInfo* pixelFormat = nullptr;
info->QueryInterface( __uuidof( IWICPixelFormatInfo ), (void**)( &pixelFormat ) );
unsigned int bpp = 0;
pixelFormat->GetBitsPerPixel( &bpp );
info->Release();
pixelFormat->Release();
BPP
( Bit Per Pixel )값도 가져와야 한다.
unsigned int RowPitch = ( TextureWidth * bpp + sizeof( unsigned char ) - 1 ) / sizeof( unsigned char );
unsigned int ImageSize = RowPitch * TextureHeight;
RowPitch
( 이미지에서 가로 한줄의 크기 )와 ImageSize
를 계산한다. 이렇게 필요한 데이터들을 모두 준비했다. 다음은 준비된 데이터들의 목록이다.
데이터 | 설명 |
---|---|
ImageWidth | 이미지의 가로 너비 |
ImageHeight | 이미지의 세로 너비 |
TextureWidth | 텍스처의 가로 너비 |
TextureHeight | 텍스처의 세로 너비 |
WicFormat | WIC기준의 이미지 포맷 |
ConvertToFormat | DirectX 기준 이미지 포맷으로 변환 가능한 WIC포맷으로 필터링 된 포맷 |
DxgiFormat | DirectX 기준 이미지 포맷 |
RowPitch | 이미지의 가로 너비의 바이트 수 |
ImageSize | 이미지 전체의 바이트 수 |
Texture 데이터 메모리에 올리기
이미지 데이터를 올릴 메모리 공간 할당을 우선 해야 한다.
unsigned char* Pixels = new unsigned char[ ImageSize ];
이미지의 정보들과 텍스처의 정보들이 모두 일치할 때
DxgiFormat == ConvertToFormat && ImageWidth == TextureWidth && ImageHeight == TextureHeight
를 충족하는 경우이다. 이 경우는 바로 메모리에 올리면 된다.
BitmapFrameDecode->CopyPixels( nullptr, RowPitch, ImageSize, Pixels );
포맷은 동일하나 이미지의 크기가 텍스처의 크기와 다를 때
ImageWidth != TextureWidth || ImageHeight != TextureHeight
인 경우이다. 이 경우는 이미지를 스케일링 해서 메모리에 올려야 한다.
IWICBitmapScaler* scaler = nullptr;
WICFactory->CreateBitmapScaler( &scaler );
scaler->Initialize( BitmapFrameDecode, TextureWidth, TextureHeight, WICBitmapInterpolationModeFant );
WICPixelFormatGUID scaledWicFormat;
scaler->GetPixelFormat( &scaledWicFormat );
if ( memcmp( &ConvertToFormat, &scaledWicFormat, sizeof(GUID) ) == 0 )
{
scaler->CopyPixels( 0, RowPitch, ImageSize, Pixels );
}
else
{
IWICFormatConverter* converter = nullptr;
WICFactory->CreateFormatConverter( &converter );
converter->Initialize( scaler, ConvertToFormat, WICBitmapDitherTypeErrorDiffusion, 0, 0, WICBitmapPaletteTypeCustom );
converter->CopyPixels( nullptr, RowPitch, ImageSize, Pixels );
}
스케일링된 포맷이 ConvertToFormat
와 일치하면 메모리에 올리면 된다. 일치하지 않는다면 ConvertToFormat
기준으로 변환해서 메모리에 올려야 한다.
포맷이 다른 경우
DxgiFormat != ConvertToFormat
인 경우이다. 이 경우는 이미지를 변환해서 메모리에 올려야 한다.
IWICFormatConverter* converter = nullptr;
WICFactory->CreateFormatConverter( &converter );
converter->Initialize( BitmapFrameDecode, ConvertToFormat, WICBitmapDitherTypeErrorDiffusion, 0, 0, WICBitmapPaletteTypeCustom );
converter->CopyPixels( nullptr, RowPitch, ImageSize, Pixels );
이렇게 텍스처를 생성하기 위한 이미지 데이터들의 모든 준비를 마쳤다. 이제 이 이미지 데이터들을 이용해서 텍스처를 생성하면 된다.
Texture2D 만들기
우선 GPU의 리소스가 할당되어야 하므로 ID3D11Texture2D
객체가 필요하다. 항상 그렇듯 리소스의 생성은 ID3D11Device
로부터 할 수 있다. Texture2D의 경우 CreateTexture2D
함수로 생성한다.
ID3D11Texutre2D* Texture2D = nullptr;
D3D11_TEXTURE2D_DESC td;
ZeroMemory( &td, sizeof( D3D11_TEXTURE2D_DESC ) );
td.Width = TextureWidth;
td.Height = TextureHeight;
td.MipLevels = 1;
td.ArraySize = 1;
td.Format = DxgiFormat;
td.SampleDesc.Count = 1;
td.SampleDesc.Quality = 0;
td.Usage = D3D11_USAGE_DEFAULT;
td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
td.CPUAccessFlags = 0;
td.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA sd;
ZeroMemory( &sd, sizeof( D3D11_SUBRESOURCE_DATA ) );
sd.pSysMem = Pixels;
sd.SysMemPitch = RowPitch;
sd.SysMemSlicePitch = ImageSize;
ID3D11Device->CreateTexture2D( &Desc, nullptr, &Texture2D );
Texture2D는 굉장히 다양한 목적을 위해 사용될 수 있는 리소스이다. 그래서 정점 버퍼나 인덱스 버퍼와 달리 생성할 때 많은 옵션들을 통해 사용 목적에 알맞은 리소스 형태로 생성할 수 있다. D3D11_TEXTURE2D_DESC에 대한 자세한 레퍼런스는 링크를 참고하도록 하자. CreateTexture2D
함수는 마이크로 소프트를 참고하자. 일단 나는 Texture2D를 픽셀 쉐이더에서 샘플링할 목적으로 사용하므로 BindFlags를 D3D11_BIND_SHADER_RESOURCE
로 지정했다. 이 부분이 가장 중요하다.
이미지 파일을 기준으로 텍스처를 생성하므로 Width
, Height
, Format
, pSysMem
, SysMemPitch
, SysMemSlicePitch
는 기존에 이미지를 로드하며 준비했던 데이터들을 이용한다.
ShaderResourceView 만들기
위에서 언급했다시피 생성한 Texture2D 리소스는 픽셀 쉐이더에서 샘플링할 목적으로 사용한다고 했다. 렌더 타겟 준비에서 한번 언급했던 내용을 보자. 백 버퍼를 렌더링 파이프라인의 렌더 타겟 용도로 연결하기 위해 RenderTargetView
라는 것을 생성한 적이 있다. 렌더링 파이프라인에서 몇가지 용도의 리소스는 리소스를 그대로 연결할 수 없다고 언급도 하였다. D3D11_BIND_SHADER_RESOURCE
용도인 ShaderResource
도 마찬가지이다. Texture2D 리소스를 ShaderResource
로 렌더링 파이프라인에 연결하기 위해서는 ShaderResourceView
가 필요하다.
ID3D11ShaderResourceView* ShaderResourceView = nullptr;
D3D11_SHADER_RESOURCE_VIEW_DESC srvd;
ZeroMemory( &srvd, sizeof( D3D11_SHADER_RESOURCE_VIEW_DESC ) );
srvd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srvd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvd.Texture2D.MostDetailedMip = 0;
srvd.Texture2D.MipLevels = 1;
ID3D11Device->CreateShaderResourceView( Texture2D, &srvd, &ShaderResourceView );
D3D11_SHADER_RESOURCE_VIEW_DESC
의 레퍼런스는 마이크로 소프트에서, CreateShaderResourceView
의 레퍼런스는 마이크로 소프트에서 참고하면 된다. CreateShaderResourceView
의 첫번째 파라메터를 보면 알겠지만 ID3D11Resource*
형으로 파라메터를 받는다. 즉, ShaderResourceView로 연결 가능한 리소스는 Texture2D가 아닌 다른 형태의 리소스들도 모두 가능하다는 것이다. ShaderResourceView는 단지 어떤 리소스를 렌더링 파이프라인의 쉐이더 스테이지에 연결해주는 객체라는 것을 알면 된다.
Rendering Pipeline에 연결하기
텍스처를 픽셀 쉐이더에서 샘플링할 용도로 사용하므로 ID3D11DeviceContext
의 PSSetShaderResources
함수를 이용해 렌더링 파이프라인의 픽셀 쉐이더 스테이지에 ShaderResourceView
를 연결한다. 함수 레퍼런스는 마이크로 소프트에서 참고한다.
ID3D11DeviceContext->PSSetShaderResources( 0, 1, &ShaderResourceView );
레퍼런스에 따르면 픽셀 쉐이더에 연결가능한 리소스의 갯수는 D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT
개로 현재 128개까지 가능하다고 되어 있다.
Shader 수정
기존에 사용하던 쉐이더는 정점 데이터에 포함된 색을 출력하도록 되어 있다. 텍스처에서 샘플링해서 픽셀의 색이 결정되도록 쉐이더의 수정이 필요하다. 정점 쉐이더와 픽셀 쉐이더 둘 모두 수정을 해야한다. 혹은 기존 쉐이더를 유지하고 새로운 쉐이더를 생성해서 연결해도 된다.
Vertex Shader 수정
정점 쉐이더의 경우 정점 데이터에서 텍스처 좌표( UV )를 포함할 수 있도록 해야 한다. 픽셀의 색을 결정할 때 텍스처에서 샘플링하므로 기존에 포함되었던 컬러요소는 삭제해도 무방하다.
struct PixelIn
{
float4 position : SV_POSITION;
float2 texCoord : TEXCOORD;
};
PixelIn VS( float4 position : POSITION, float2 texCoord : TEXCOORD )
{
PixelIn output;
output.position = position;
output.texCoord = texCoord;
return output;
}
크게 바뀐건 없고 픽셀 쉐이더에서 샘플링에 사용될 수 있도록 정점 데이터의 텍스처 좌표를 픽셀쉐이더 입력값으로 구성해서 반환한다.
Pixel Shader 수정
픽셀 쉐이더에서는 전달 받은 컬러값을 반환했던 부분을 텍스처에서 좌표값을 이용해 색상값을 샘플링해서 반환하도록 수정한다.
Texture2D psTexture : register( t0 );
SamplerState SampleType : register( s0 );
float4 PS( PixelIn input ) : SV_TARGET
{
float4 textureColor = float4( 1.f, 1.f, 1.f, 1.f );
textureColor = psTexture.Sample( SampleType, input.texCoord );
return textureColor;
}
InputLayout 수정
당연하게도 정점 쉐이더의 내용, 그 중에서 입력받는 시그니처가 변경되었으므로 InputLayout
또한 수정이 되어야 한다.
D3D11_INPUT_ELEMENT_DESC elements[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
Vertex Data 수정
정점 데이터에 텍스처 좌표를 지정해야 한다.
CRVertex GCRVRect[ 4 ] =
{
{ .Position = CRVector( -0.5f, 0.5f, 0.0f ), .TexCoord = CRVector2D( 0.0f, 0.0f ) },
{ .Position = CRVector( 0.5f, 0.5f, 0.0f ), .TexCoord = CRVector2D( 1.0f, 0.0f ) },
{ .Position = CRVector( -0.5f, -0.5f, 0.0f ), .TexCoord = CRVector2D( 0.0f, 1.0f ) },
{ .Position = CRVector( 0.5f, -0.5f, 0.0f ), .TexCoord = CRVector2D( 1.0f, 1.0f ) },
};
이제 프로그램을 실행하면 아래와 같이 이미지가 렌더링되는 모습을 볼 수 있다.

https://github.com/yunei0313/CRY/tree/Texture-rendering 에서 전체 코드를 확인할 수 있다.
이전글 : [DirectX11] 10. 사각형 – Index Buffer에 대해 알아보자.
다음글 : [DirectX11] 12. 변환 – Constant Buffer에 대해 알아보자.