Jorge
Jorge Autor de Aprende Unreal Engine 4.

DirectX 12: Texturas

DirectX 12: Texturas

Vamos a texturizar el triángulo. Para ello cargaremos una textura, creamos los recursos asociados, como los samplers, y se lo pasaremos a los shaders.

Tabla de contenidos

Parte 1. Window

Parte 2. Adapter

Parte 3. Conceptos

Parte 4. Queue

Parte 5. Resources

Parte 6. Descriptores

Parte 7. Pipeline State

Parte 8. Swapchain

Parte 9. Triángulo

Parte 10. Root Signature

» Parte 11. Texturas

Añadir UVs

Cambiar definición de los vértices:

struct Vertex
{
  XMFLOAT3 Position;
  XMFLOAT2 UV;
  XMFLOAT4 Color;
};

Asignar las UVs:

Vertex Vertices[] = {
  // { POS, UV, COLOR }
  { { 0.0f, 0.5f, 0.0f }, { 0.5f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f } },
  { { 0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f, 1.0f, 1.0f } },
  { { -0.5f, -0.5f, 0.0f }, { 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f, 1.0f } }
};

Cambiar el Input Layout:

/* Input Layout */
// vamos a usar un Vertex con position, uv y color
D3D12_INPUT_ELEMENT_DESC pInputElementDescs[] = {
  // SemanticName; SemanticIndex; Format; InputSlot; AlignedByteOffset; InputSlotClass; InstanceDataStepRate;
  {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
  {"UV", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 4 * 3, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
  {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 4 * (3 + 2), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
};

D3D12_INPUT_LAYOUT_DESC InputLayout{};
InputLayout.NumElements = _countof(pInputElementDescs);
InputLayout.pInputElementDescs = pInputElementDescs;

Modifica el shader:

// shaders.hlsl
cbuffer SomeConstants : register(b0)
{
  float4x4 MVP;
}

struct VS2PS
{
  float4 position : SV_Position;
  float2 uv : UV0;
  float4 color : COLOR;
};

VS2PS VSMain(float3 position : POSITION, float2 uv : UV0, float4 color : COLOR)
{
  VS2PS vs2ps;
  vs2ps.position = mul(MVP, float4(position, 1.0));
  vs2ps.uv = uv;
  vs2ps.color = color;
  return vs2ps;
}

float4 PSMain(VS2PS vs2ps) : SV_Target
{
  return float4(vs2ps.uv, 0.0, 1.0);
}

El resultado sería el siguiente:

Triangle UV

Crear el recurso

Vamos a ampliar la clase GPUMem que creamos en el tutorial sobre recursos.

En concreto, añadir el método Texture2D a la clase GPUMem para crear un recurso de tipo textura:

// GPUMem.h
class GPUMem {
  // ...
  static ComPtr<ID3D12Resource> Texture2D(
    ID3D12Device* Device, 
    SIZE_T Width, 
    SIZE_T Height, 
    DXGI_FORMAT Format
  );
  // ...
};
// GPUMem.cpp

ComPtr<ID3D12Resource> GPUMem::Texture2D(
  ID3D12Device* Device, 
  SIZE_T Width, 
  SIZE_T Height,
  DXGI_FORMAT Format
)
{
  ComPtr<ID3D12Resource> Resource;

  D3D12_RESOURCE_DESC ResourceDesc{};
  ResourceDesc.Width = Width;
  ResourceDesc.Height = Height;
  ResourceDesc.DepthOrArraySize = 1;
  ResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
  ResourceDesc.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;
  ResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
  ResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
  ResourceDesc.MipLevels = 1;
  ResourceDesc.SampleDesc.Count = 1;
  ResourceDesc.SampleDesc.Quality = 0;	
  ResourceDesc.Format = Format;

  D3D12_HEAP_PROPERTIES HeapProps{};
  HeapProps.Type = D3D12_HEAP_TYPE_DEFAULT;

  Device->CreateCommittedResource(
    &HeapProps,
    D3D12_HEAP_FLAG_NONE,
    &ResourceDesc,
    D3D12_RESOURCE_STATE_COPY_DEST,
    nullptr,
    IID_PPV_ARGS(&Resource)
  );

  return Resource;
}

Imagina por un momento que hemos cargado la textura (la hemos leído de un fichero de imagen).

La primera tentación sería hacer tal y como hemos venido haciendo hasta ahora con los recursos, esto es:

  1. Crear el recurso en UPLOAD para poder copiar los datos de la textura.

  2. Usar los métodos Map y Unmap del recurso alojado en UPLOAD para copiar los datos de la textura en memoria de la GPU.

  3. Recomendable pero opcional: crear el recurso en DEFAULT y, a través de comandos, que la GPU copie de UPLOAD a DEFAULT.

Sin embargo, las tarjetas gráficas tienen una serie de requisitos para poder trabajar con las texturas. Uno de ellos es que ¡las texturas solo se pueden crear en DEFAULT!

Por tanto, el paso 3 que era recomendado pero opcional cuando trabajamos con textura es un paso obligatorio.

No solo eso, en UPLOAD no podemos crear un recurso de tipo textura, hay que usar un recurso de tipo buffer.

El nuevo modo de proceder sería:

  1. Crear un recurso buffer en UPLOAD. Copiar los datos de la textura a este recurso usando el método Map.
  2. Crear un recurso textura en DEFAULT.
  3. La GPU, mediante comandos, debe copiar el recurso de UPLOAD a DEFAULT.

El paso 3 no es tan trivial como en anteriores tutoriales que usábamos el comando CopyResource ¡aquí no podemos copiar los recursos tal cuál porque son de distinto tipo (buffer vs textura)!

Hay que usar otro comando, en concreto CopyTextureRegion.

Copiar una textura

Para copiar una textura se usa el comando CopyTextureRegion.

Recuerda que una textura puede tener subrecursos, por ejemplo mipmapping. También podemos copiar porciones (regiones) de la textura.

Es por ello que el comando CopyTextureRegion no toma un recurso como un todo, como ocurría con el comando CopyResource, si no una estructura indicando una subregión y subrecurso.

ID3D12CommandList::CopyTextureRegion(
  D3D12_TEXTURE_COPY_LOCATION* DestinoCopia, // recurso + subrecurso
  DstX, DstY, DstZ, // subregion destino
  D3D12_TEXTURE_COPY_LOCATION* CopiarDesde, // recurso + subrecurso
  D3D12_BOX* SrcBox, // subregion origen
);

La estructura D3D12_TEXTURE_COPY_LOCATION contiene una referencia a un recurso y un índice identificando el subrecurso:

D3D12_TEXTURE_COPY_LOCATION DestinoCopia{};
DestinoCopia.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
DestinoCopia.SubresourceIndex = 0;	
DestinoCopia.pResource = TextureResource.Get(); // en DEFAULT

Pero también sirve para interpretar como copiar un recurso. En nuestro caso, tenemos que copiar un buffer a una textura. El byte en la posición i-ésima del buffer ¿en qué componente de qué pixel de la textura destino lo almacenamos? Para saberlo necesitamos datos como ¿cuantos bytes ocupa el ancho, cuanto es el alto? etc.,

Al conjunto de datos Depth, Format, Width, Height y RowPitch (tamaño en bytes del ancho de una textura) que permiten saber como copiar un buffer a una textura se le denomina footprint.

D3D12_TEXTURE_COPY_LOCATION CopiarDesde{};
CopiarDesde.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
CopiarDesde.pResource = UploadBuffer.Get(); // en UPLOAD
CopiarDesde.PlacedFootprint.Footprint.Depth = 1;
CopiarDesde.PlacedFootprint.Footprint.Format = Tex.GetFormat();
CopiarDesde.PlacedFootprint.Footprint.Width = Tex.GetWidth();
CopiarDesde.PlacedFootprint.Footprint.Height = Tex.GetHeight();
CopiarDesde.PlacedFootprint.Footprint.RowPitch = Tex.GetRowPitch();

Añade el siguiente código a DemoApp.cpp:

void DemoApp::LoadTexture()
{
  TextureLoader Tex(L"texture.png"); // TextureLoader es una clase propia que carga una textura

  // Crear el recurso en DEFAULT
  TextureResource = GPUMem::Texture2D(Device.Get(), Tex.GetWidth(), Tex.GetHeight(), Tex.GetFormat());

  // Descriptor Heap para la textura
  // podríamos haber reutilizado la del constantbuffer
  // pero vamos a crear otro descriptor heap para 
  // mayor claridad
  D3D12_DESCRIPTOR_HEAP_DESC DescriptorHeapDesc{};
  DescriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
  DescriptorHeapDesc.NumDescriptors = 1;
  DescriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
  Device->CreateDescriptorHeap(&DescriptorHeapDesc, IID_PPV_ARGS(&TextureDescriptorHeap));

  // Crear el descriptor
  D3D12_SHADER_RESOURCE_VIEW_DESC ResourceViewDesc{};
  ResourceViewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
  ResourceViewDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
  ResourceViewDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
  ResourceViewDesc.Texture2D.MipLevels = 1;	
  Device->CreateShaderResourceView(TextureResource.Get(), &ResourceViewDesc, TextureDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

  // Crear el buffer en Upload
  SIZE_T SizeInBytes = (Tex.Size() + 256) & ~255;
  ComPtr<ID3D12Resource> UploadBuffer = GPUMem::Buffer(Device.Get(), SizeInBytes, D3D12_HEAP_TYPE_UPLOAD);

  // Copiar los datos de la textura a upload
  UINT8* pData;
  UploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&pData));
  memcpy(pData, Tex.Pointer(), Tex.Size());
  UploadBuffer->Unmap(0, nullptr);

  // Empezamos a grabar comandos para copiar de UPLOAD a DEFAULT
  CommandAllocator->Reset();
  CommandList->Reset(CommandAllocator.Get(), nullptr);
    
  D3D12_TEXTURE_COPY_LOCATION Dst{};
  Dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
  Dst.SubresourceIndex = 0;	
  Dst.pResource = TextureResource.Get();

  D3D12_TEXTURE_COPY_LOCATION Src{};
  Src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
  Src.pResource = UploadBuffer.Get();
  Src.PlacedFootprint.Footprint.Depth = 1;
  Src.PlacedFootprint.Footprint.Format = Tex.GetFormat();
  Src.PlacedFootprint.Footprint.Width = Tex.GetWidth();
  Src.PlacedFootprint.Footprint.Height = Tex.GetHeight();
  Src.PlacedFootprint.Footprint.RowPitch = Tex.GetRowPitch();

  CommandList->CopyTextureRegion(&Dst, 0, 0, 0, &Src, nullptr);

  // Una vez copiado, cambiamos el estado del recurso
  GPUMem::ResourceBarrier(
    CommandList.Get(), 
    TextureResource.Get(), 
    D3D12_RESOURCE_STATE_COPY_DEST, 
    D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE
  );

  CommandList->Close();

  ID3D12CommandList* CommandLists[]{ CommandList.Get() };
  CommandQueue->ExecuteCommandLists(_countof(CommandLists), CommandLists);

  FlushAndWait();
}

No olvides añadir también la declaración en DemoApp.h y llamar al método anterior en el constructor.

DemoApp::DemoApp(HWND hWnd, UINT Width, UINT Height)
{
  /* ... */
  CreateVertexBuffer();
  
  LoadTexture();

  CreateConstantBuffer();
}

Cargar la textura

Vamos a usar la siguiente textura (puedes hacer clic con el botón derecho y descargarla)

Texture

Hay muchas librerías para cargar ficheros de imagen en memoria y usarlos como textura.

Dado que estamos en Windows usaremos una librería nativa llamada WIC para leer imágenes. Podríamos haber usado otra.

Haciendo uso de la librería WIC vamos a crear la clase TextureLoader:

class TextureLoader
{
public:
  TextureLoader(const LPCWSTR& Path);

  UINT GetWidth() const { return TextureWidth; }
  UINT GetHeight() const { return TextureHeight; }
  DXGI_FORMAT GetFormat() const { return Format; }
  BYTE* Pointer() const { return ImageData; }
  SIZE_T Size() const { return ImageSize; }
  SIZE_T GetRowPitch() const { return BytesPerRow; }

private:
  int ImageSize;
  int BytesPerRow;
  LPCWSTR Filename;
  UINT TextureWidth;
  UINT TextureHeight;
  DXGI_FORMAT Format;
  BYTE* ImageData;

  /* metodos auxiliares */
};

Esta clase y su implementación están incluidas en el código fuente de este proyecto disponible más abajo para descargar. No vamos a entrar como leer ficheros de imagen (jpg, png, etc.,) y descodificarlo en memoria, usa esta clase como una “caja negra”.

Si tienes curiosidad por como se usa WIC tienes la documentación oficial aquí.

El 99% de la clase TextureLoader está tomada de aquí y aquí.

Shader con textura

Vamos a modificar nuestro shader para que lea una textura.

Para poder leer una textura se hace uso de una referencia a la propia textura y de un sampler.

Un sampler es un objeto que permite leer una textura. Por ejemplo, un sampler dice que hacer si lees una textura fuera de los límites (por ejemplo coordenada uv (2,1), el tipo de interpolación, etc.,). Aquí tienes algunos ejemplos muy ilustrativos para que puedas hacerte una idea de cómo funciona un sampler.

Nuestro shader quedaría:

// shaders.hlsl
cbuffer SomeConstants : register(b0)
{
  float4x4 MVP;
}

Texture2D mainTex : register(t0);
SamplerState samplerTex : register(s0);

struct VS2PS
{
  float4 position : SV_Position;
  float2 uv : UV0;
  float4 color : COLOR;
};

VS2PS VSMain(float3 position : POSITION, float2 uv : UV0, float4 color : COLOR)
{
  VS2PS vs2ps;
  vs2ps.position = mul(MVP, float4(position, 1.0));
  vs2ps.uv = uv;
  vs2ps.color = color;
  return vs2ps;
}

float4 PSMain(VS2PS vs2ps) : SV_Target
{
  float4 col = mainTex.Sample(samplerTex, vs2ps.uv);
  return col;
}

Toca declarar en el root signature estos usos de los registros t0 y s0.


void DemoApp::CreateRootSignature()
{
  // Dos parametros: Un constant buffer para MVP y un descriptor table a la textura
  D3D12_ROOT_PARAMETER RootParameters[2];
	
  // Constant Buffer
  RootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
  RootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
  RootParameters[0].Descriptor.RegisterSpace = 0;
  RootParameters[0].Descriptor.ShaderRegister = 0;
	
  // Texture
  D3D12_DESCRIPTOR_RANGE TexDescriptorRanges[1];
  TexDescriptorRanges[0].BaseShaderRegister = 0;
  TexDescriptorRanges[0].NumDescriptors = 1;
  TexDescriptorRanges[0].OffsetInDescriptorsFromTableStart = 0;
  TexDescriptorRanges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
  TexDescriptorRanges[0].RegisterSpace = 0;

  RootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
  RootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
  RootParameters[1].DescriptorTable.NumDescriptorRanges = _countof(TexDescriptorRanges);
  RootParameters[1].DescriptorTable.pDescriptorRanges = TexDescriptorRanges;

  // Samplers
  D3D12_STATIC_SAMPLER_DESC SamplerDesc{};
	
  SamplerDesc.ShaderRegister = 0;
  SamplerDesc.RegisterSpace = 0;
  SamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;

  SamplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT;
  SamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
  SamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
  SamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
  SamplerDesc.MipLODBias = 0;
  SamplerDesc.MaxAnisotropy = 0;
  SamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
  SamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
  SamplerDesc.MinLOD = 0.0f;
  SamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;	
	
  // Create Root Signature
  D3D12_ROOT_SIGNATURE_DESC SignatureDesc{};
  SignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
  SignatureDesc.NumParameters = _countof(RootParameters);
  SignatureDesc.pParameters = RootParameters;
  SignatureDesc.NumStaticSamplers = 1;
  SignatureDesc.pStaticSamplers = &SamplerDesc;

  ComPtr<ID3DBlob> Signature;
  ComPtr<ID3DBlob> Error;

  D3D12SerializeRootSignature(&SignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &Signature, &Error);
  Device->CreateRootSignature(0, Signature->GetBufferPointer(), Signature->GetBufferSize(), IID_PPV_ARGS(&RootSignature));
}

Commands

Ya solo queda cargar la textura concreta en los registros indicados en el root signature:

void DemoApp::RecordCommandList()
{
  /* ... */
  ID3D12DescriptorHeap* Heaps[]{ TextureDescriptorHeap.Get() };
  CommandList->SetDescriptorHeaps(_countof(Heaps), Heaps);
  CommandList->SetGraphicsRootDescriptorTable(1, TextureDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
	
  CommandList->DrawInstanced(3, 1, 0, 0);

  /* ... */
}

Código fuente

Puedes descargar el código completo del proyecto aquí.

Triangle Textured

Consideraciones

Algunas consideraciones:

  • Solo se pueden crear texturas en DEFAULT.
  • Es imposible llamar directamente a Map en un recurso textura, porque siempre deben estar en DEFAULT.
  • Debes copiar los datos de una textura a un recurso buffer en UPLOAD.
  • Debes copiar el buffer que representa la textura en UPLOAD al recurso en DEFAULT.
  • Cuando copias subrecursos o recursos de distinto tipo no te vale usar el comando CopyResource. En vez de eso debes usar CopyTextureRegion.
  • Para saber que la GPU sepa como copiar un buffer a una textura necesita que declares su footprint.
  • Un footprint contiene el ancho, alto, formato y otros campos importantes para que la GPU sepa como copiar un buffer a un subrecurso de una textura.
  • Dado que puedes querer copiar solo un subrecurso de una textura a veces es difícil obtener el footprint de un subrecurso, para ello se usa ID3D12Device::GetCopyableFootprints.
  • Incluso si vas a usar todo el buffer y por tanto es trivial rellenar la estructura footprint es recomendable seguir usando el método GetCopyableFootprints:
    • para que tenga en cuanto consideraciones acerca del alineamiento
    • en nuestro proyecto no lo usamos por simplicidad, pero sería recomendable su uso.
  • Los samplers tienen un campo específico en el Root Signature.

comments powered by Disqus