[DirectX11] 11. 이미지 – Texture2D에 대해 알아보자.

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

정점에서 가지는 텍스처 좌표 또한 보간되어 각 픽셀도 텍스처 좌표를 가지게 된다. 이 텍스처 좌표를 가지고 이미지로부터 적절한 색상값을 샘플링해서 표현할 수 있게 된다.





가장 우선 해야할 것이 이미지 파일을 로드하는 것이다. 이 부분은 DirectX ToolKit혹은 DirectX Tex라이브러리를 사용하면 매우 간단하게 해결된다. 아래의 코드 호출로 끝이다.

C++
ID3D11Texture2D* texture2D = nullptr;
ID3D11ShaderResourceView * shaderResourceView = nullptr;

CoInitialize( nullptr );

DirectX::CreateWICTextureFromFile( ID3D11Device, ImagePath, &texture2D, &shaderResourceView );

이렇게 호출하면 생성된 ID3D11ShaderResourceView를 렌더링 파이프라인에 연결하면 된다. 위 코드를 사용하는 경우는 Rendering pipeline에 연결하기단락부터 이어서 보면 된다. 이하 이 단락의 내용은 사실상 해당 함수의 코드를 분석하는 내용이며 마이크로 소프트의 내용이기도 하다.




우선 이미지 로드에 필요한 WIC( Windows Imaging Component ) 인스턴스들을 생성해야 한다.

C++
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 등 )를 가져오기 위한 것이다.




텍스처 리소스의 크기를 임의로 지정하지 않는 이상 일반적으로 텍스처 리소스의 크기는 이미지의 크기에 종속된다. 그리고 하드웨어 및 API 레벨에서 지원 가능한 텍스처의 크기보다 이미지가 더 큰 경우도 있다. 이런 케이스들에 대응하기 위해 이미지에 대한 정보를 텍스처를 생성하기 위한 정보로 컨버팅이 필요하게 된다.

C++
unsigned int ImageWidth  = 0;
unsigned int ImageHeight = 0;

BitmapFrameDecode->GetSize( &ImageWidth, &ImageHeight );

우선 이미지의 가로-세로 크기를 가져온다.


C++
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레벨에서 지원 가능한 최대 크기도 가져온다.


C++
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;  
}

최종적으로 텍스처의 가로-세로크기를 결정한다.


C++
WICPixelFormatGUID WicFormat;
BitmapFrameDecode->GetPixelFormat( &WicFormat );

WIC기준의 픽셀 포맷을 가져온다.


C++
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포맷으로 변경가능한 포맷으로 단순화하여 변환해주는 용도이다.

C++
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포멧을 찾는다.


C++
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 )값도 가져와야 한다.


C++
unsigned int RowPitch  = ( TextureWidth * bpp + sizeof( unsigned char ) - 1 ) / sizeof( unsigned char );  
unsigned int ImageSize = RowPitch * TextureHeight;

RowPitch( 이미지에서 가로 한줄의 크기 )와 ImageSize를 계산한다. 이렇게 필요한 데이터들을 모두 준비했다. 다음은 준비된 데이터들의 목록이다.

데이터설명
ImageWidth이미지의 가로 너비
ImageHeight이미지의 세로 너비
TextureWidth텍스처의 가로 너비
TextureHeight텍스처의 세로 너비
WicFormatWIC기준의 이미지 포맷
ConvertToFormatDirectX 기준 이미지 포맷으로 변환 가능한 WIC포맷으로 필터링 된 포맷
DxgiFormatDirectX 기준 이미지 포맷
RowPitch이미지의 가로 너비의 바이트 수
ImageSize이미지 전체의 바이트 수



이미지 데이터를 올릴 메모리 공간 할당을 우선 해야 한다.

C++
unsigned char* Pixels = new unsigned char[ ImageSize ];

이미지의 정보들과 텍스처의 정보들이 모두 일치할 때

DxgiFormat == ConvertToFormat && ImageWidth == TextureWidth && ImageHeight == TextureHeight를 충족하는 경우이다. 이 경우는 바로 메모리에 올리면 된다.

C++
BitmapFrameDecode->CopyPixels( nullptr, RowPitch, ImageSize, Pixels );

포맷은 동일하나 이미지의 크기가 텍스처의 크기와 다를 때

ImageWidth != TextureWidth || ImageHeight != TextureHeight인 경우이다. 이 경우는 이미지를 스케일링 해서 메모리에 올려야 한다.

C++
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인 경우이다. 이 경우는 이미지를 변환해서 메모리에 올려야 한다.

C++
IWICFormatConverter* converter = nullptr;  
WICFactory->CreateFormatConverter( &converter );  
  
converter->Initialize( BitmapFrameDecode, ConvertToFormat, WICBitmapDitherTypeErrorDiffusion, 0, 0, WICBitmapPaletteTypeCustom );  
  
converter->CopyPixels( nullptr, RowPitch, ImageSize, Pixels );

이렇게 텍스처를 생성하기 위한 이미지 데이터들의 모든 준비를 마쳤다. 이제 이 이미지 데이터들을 이용해서 텍스처를 생성하면 된다.





우선 GPU의 리소스가 할당되어야 하므로 ID3D11Texture2D객체가 필요하다. 항상 그렇듯 리소스의 생성은 ID3D11Device로부터 할 수 있다. Texture2D의 경우 CreateTexture2D함수로 생성한다.

C++
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는 기존에 이미지를 로드하며 준비했던 데이터들을 이용한다.





위에서 언급했다시피 생성한 Texture2D 리소스는 픽셀 쉐이더에서 샘플링할 목적으로 사용한다고 했다. 렌더 타겟 준비에서 한번 언급했던 내용을 보자. 백 버퍼를 렌더링 파이프라인의 렌더 타겟 용도로 연결하기 위해 RenderTargetView라는 것을 생성한 적이 있다. 렌더링 파이프라인에서 몇가지 용도의 리소스는 리소스를 그대로 연결할 수 없다고 언급도 하였다. D3D11_BIND_SHADER_RESOURCE용도인 ShaderResource도 마찬가지이다. Texture2D 리소스를 ShaderResource로 렌더링 파이프라인에 연결하기 위해서는 ShaderResourceView가 필요하다.


C++
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는 단지 어떤 리소스를 렌더링 파이프라인의 쉐이더 스테이지에 연결해주는 객체라는 것을 알면 된다.





텍스처를 픽셀 쉐이더에서 샘플링할 용도로 사용하므로 ID3D11DeviceContextPSSetShaderResources함수를 이용해 렌더링 파이프라인의 픽셀 쉐이더 스테이지에 ShaderResourceView를 연결한다. 함수 레퍼런스는 마이크로 소프트에서 참고한다.

C++
ID3D11DeviceContext->PSSetShaderResources( 0, 1, &ShaderResourceView );

레퍼런스에 따르면 픽셀 쉐이더에 연결가능한 리소스의 갯수는 D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT개로 현재 128개까지 가능하다고 되어 있다.





기존에 사용하던 쉐이더는 정점 데이터에 포함된 색을 출력하도록 되어 있다. 텍스처에서 샘플링해서 픽셀의 색이 결정되도록 쉐이더의 수정이 필요하다. 정점 쉐이더와 픽셀 쉐이더 둘 모두 수정을 해야한다. 혹은 기존 쉐이더를 유지하고 새로운 쉐이더를 생성해서 연결해도 된다.




정점 쉐이더의 경우 정점 데이터에서 텍스처 좌표( UV )를 포함할 수 있도록 해야 한다. 픽셀의 색을 결정할 때 텍스처에서 샘플링하므로 기존에 포함되었던 컬러요소는 삭제해도 무방하다.

C++
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;  
}

크게 바뀐건 없고 픽셀 쉐이더에서 샘플링에 사용될 수 있도록 정점 데이터의 텍스처 좌표를 픽셀쉐이더 입력값으로 구성해서 반환한다.




픽셀 쉐이더에서는 전달 받은 컬러값을 반환했던 부분을 텍스처에서 좌표값을 이용해 색상값을 샘플링해서 반환하도록 수정한다.

C++
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또한 수정이 되어야 한다.

C++
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 },  
};




정점 데이터에 텍스처 좌표를 지정해야 한다.

C++
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 ) },  
};

이제 프로그램을 실행하면 아래와 같이 이미지가 렌더링되는 모습을 볼 수 있다.

Pasted image 20241218180740.png



https://github.com/yunei0313/CRY/tree/Texture-rendering 에서 전체 코드를 확인할 수 있다.




이전글 : [DirectX11] 10. 사각형 – Index Buffer에 대해 알아보자.
다음글 : [DirectX11] 12. 변환 – Constant Buffer에 대해 알아보자.

Leave a Comment