Link

Imagem de textura

Objetos de imagem no Vulkan representam dados que podem ter uma, duas ou três dimensões e podem ter várias camadas de array ou níveis MIP (ou ambos). Cada elemento dos dados de uma imagem (um texel) também pode ter uma ou mais amostras. A imagem é um tipo de objeto separado porque não consiste necessariamente em apenas um conjunto linear de pixels que pode ser acessado diretamente. As imagens podem ter um formato interno de implementação diferente gerenciado pelo driver.

Imagens podem ser usadas para muitos propósitos diferentes. Podemos usá-las como uma fonte de dados para operações de cópia. Podemos renderizar em imagens, em cujo caso usamos imagens como anexos de cor ou profundidade. Também podemos vincular imagens a pipelines por meio de conjuntos de descritores e usá-las como texturas, como faremos neste capítulo.

Começamos adicionando um novo membro de estrutura em Object3D para armazenar o objeto de imagem:

struct Object3D
{
    Object3D(QSharedPointer<Model> model);

    VkBuffer vertexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE;

    VkImage textureImage = VK_NULL_HANDLE;

    QSharedPointer<Model> model;
};

Para criar uma imagem, precisamos preparar uma estrutura do tipo VkImageCreateInfo. Essa estrutura contém o conjunto básico de parâmetros necessários para criar uma imagem. Como veremos, esta é uma estrutura significativamente mais complexa que a estrutura VkBufferCreateInfo. No final de addTextureImage adicionamos o seguinte código:

VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.pNext = nullptr;
imageInfo.flags = 0;

Os campos comuns, sType e pNext, aparecem na parte superior, como na maioria das outras estruturas Vulkan. O campo pNext deve ser definido como nullptr, a menos que estejamos usando uma extensão. O campo flags dessa estrutura descreve propriedades adicionais de uma imagem. Através deste campo, podemos especificar que a imagem pode ser apoiada por uma memória esparsa. Mas um valor mais interessante é um VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, que nos permite usar a imagem como um cubemap1. Como não teremos requisitos adicionais, definimos esse campo como 0.

1 Um cubemap é basicamente uma textura que contém 6 texturas 2D individuais que representam os reflexos em um ambiente. Essas texturas formam as faces de um cubo imaginário que envolve um objeto; cada face representa a vista ao longo das direções dos eixos do mundo (cima, baixo, esquerda, direita, frente e trás).

imageInfo.imageType = VK_IMAGE_TYPE_2D;

O campo imageType especifica o tipo de imagem que desejamos criar. O tipo de imagem é essencialmente a dimensionalidade da imagem e pode ser VK_IMAGE_TYPE_1D, VK_IMAGE_TYPE_2D ou VK_IMAGE_TYPE_3D para uma imagem 1D, 2D ou 3D, respectivamente. Neste projeto utilizaremos apenas imagens 2D.

imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;

As imagens também têm um formato, que descreve como os dados de texels2 são armazenados na memória e como são interpretados pelo Vulkan. O formato da imagem é especificado pelo campo format e deve ser um dos formatos de imagem representados por um membro da enumeração VkFormat. Vulkan suporta um grande número de formatos (muitos para listar aqui), mas devemos usar o mesmo formato para os texels e os pixels no buffer, caso contrário, a operação de cópia falhará. O formato VK_FORMAT_R8G8B8A8_UNORM3 corresponde ao mesmo formato de imagem que foi previamente carregado pelo objeto QImage.

2 Pixels dentro de um objeto de imagem são conhecidos como texels e usaremos esse nome a partir deste ponto.

3 É possível que o formato VK_FORMAT_R8G8B8A8_UNORM não seja suportado pelo hardware gráfico. Devemos ter uma lista de alternativas aceitáveis e escolher a melhor que é suportada. No entanto, o suporte para esse formato específico é tão difundido que vamos pular essa etapa.

imageInfo.extent.width = image.width();
imageInfo.extent.height = image.height();
imageInfo.extent.depth = 1;

A extensão de uma imagem é seu tamanho em texels. Isso é especificado no campo extent. Esta é uma instância da estrutura VkExtent3D, que possui três membros: width, height e depth. Estes devem ser definidos para a largura, altura e profundidade da imagem desejada, respectivamente. Para imagens 1D, a altura deve ser definida como 1 e, para imagens 1D e 2D, a profundidade deve ser definida como 1.

imageInfo.mipLevels = 1;

O número de níveis de mapa MIP (mipmap) para criar na imagem é especificado em mipLevels. Mapeamento MIP (mipmapping) é o processo de usar um conjunto de imagens pré-filtradas de resolução sucessivamente menor, a fim de melhorar a qualidade da imagem ao subamostrar a imagem. Não usaremos esse recurso neste projeto. Portanto definimos esse campo como 1.

imageInfo.arrayLayers = 1;

O campo arrayLayers especifica o número de camadas na imagem. Nossa textura não será um array, portanto especificamos esse campo como 1.

imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;

Da mesma forma, o número de amostras na imagem é especificado em samples. Isso está relacionado a multisampling. Estaremos usando apenas uma amostra por pixel (VK_SAMPLE_COUNT_1_BIT), o que é equivalente a nenhum multisampling.

imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;

Os próximos campos descrevem como a imagem será usada. O primeiro é o modo de tiling, especificado no campo tiling. Este campo define a estrutura da memória interna de uma imagem (mas não podemos confundir com um layout). Ele é um membro da enumeração VkImageTiling, que contém apenas VK_IMAGE_TILING_LINEAR ou VK_IMAGE_TILING_OPTIMAL. Ao usar VK_IMAGE_TILING_LINEAR, como o nome sugere, os dados de uma imagem são dispostos linearmente na memória, de forma semelhante aos buffers ou arrays C/C++. Isso nos permite mapear a memória de uma imagem e lê-la ou inicializá-la diretamente a partir de nosso aplicativo. Infelizmente, há restrições severas em imagens com esse tipo de estrutura. Por exemplo, a especificação Vulkan diz que apenas imagens 2D devem suportar VK_IMAGE_TILING_LINEAR. Fornecedores de hardware podem implementar suporte para VK_IMAGE_TILING_LINEAR em outros tipos de imagem, mas isso não é obrigatório, e não podemos confiar em tal suporte. Enquanto isso, VK_IMAGE_TILING_OPTIMAL é uma representação opaca usada pelo Vulkan para armazenar dados na memória para melhorar a eficiência do subsistema de memória no dispositivo. Cada tipo de hardware gráfico pode armazenar dados de imagem de uma maneira diferente, que seja ideal para cada um. Isso significa que não sabemos como a memória das imagens é estruturada. Por causa disso, não podemos mapear a memória de uma imagem e inicializá-la ou lê-la diretamente de nosso aplicativo. Nesta situação, somos obrigados a usar recursos de staging (como um buffer ou uma imagem). Mas, dessa forma, podemos criar as imagens que quisermos (não há restrições semelhantes às imagens que usam VK_IMAGE_TILING_LINEAR). Além disso, VK_IMAGE_TILING_OPTIMAL provavelmente terá um desempenho significativamente melhor do que VK_IMAGE_TILING_LINEAR na maioria das operações. E é por isso que é altamente recomendável especificar sempre VK_IMAGE_TILING_OPTIMAL.

imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;

O campo usage é um campo de bits que descreve onde a imagem será usada. Isso é semelhante ao campo de uso na estrutura VkBufferCreateInfo. O campo usage aqui é composto de membros da enumeração VkImageUsageFlagBits, cujos membros são os seguintes:

  • VK_IMAGE_USAGE_TRANSFER_SRC_BIT ou VK_IMAGE_USAGE_TRANSFER_DST_BIT especificam que a imagem será, respectivamente, a origem ou o destino dos comandos de transferência.
  • VK_IMAGE_USAGE_SAMPLED_BIT especifica que a imagem pode ser amostrada em um shader.
  • VK_IMAGE_USAGE_STORAGE_BIT especifica que a imagem pode ser usada para armazenamento de propósito geral, incluindo gravações a partir de um shader.
  • VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT especifica que a imagem pode ser usada como um anexo de cor.
  • VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT especifica que a imagem pode ser vinculada como um anexo de profundidade ou estêncil e usada para teste de profundidade ou estêncil (ou ambos).
  • VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT especifica que a imagem pode ser usada como um anexo temporário, que é um tipo especial de imagem usado para armazenar resultados intermediários de uma operação gráfica.
  • VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT especifica que a imagem pode ser usada como uma entrada especial durante a renderização de gráficos. As imagens de entrada diferem das imagens de amostra ou de armazenamento regulares, pois somente os fragment shaders podem ler a partir deles e apenas em seus próprios locais de pixel.

Semelhante aos buffers, quando criamos uma imagem, precisamos designar todas as maneiras nas quais pretendemos usar a imagem. Não podemos alterá-las mais tarde e não podemos usar a imagem de uma forma que não foi especificada durante a criação. Nossa imagem será usada como destino para a cópia do buffer, portanto, ela deve ser configurada como um destino de transferência, indicamos isso através do uso do sinalizador VK_IMAGE_USAGE_TRANSFER_DST_BIT. Também queremos poder acessar a imagem a partir do shader para colorir nossa malha, portanto, o uso deve incluir VK_IMAGE_USAGE_SAMPLED_BIT.

imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.queueFamilyIndexCount = 0;
imageInfo.pQueueFamilyIndices = nullptr;

O campo sharingMode é idêntico em função ao campo de nome similar na estrutura VkBufferCreateInfo. Se estiver definido como VK_SHARING_MODE_EXCLUSIVE, a imagem será usada com apenas uma única família de filas por vez. Se estiver definido como VK_SHARING_MODE_CONCURRENT, a imagem poderá ser acessada por várias filas simultaneamente. Da mesma forma, queueFamilyIndexCount e pQueueFamilyIndices fornecem uma função semelhante e são usados quando sharingModeVK_SHARING_MODE_CONCURRENT.

imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

Finalmente, as imagens têm um layout, que especifica em parte como ele será usado a qualquer momento. O campo initialLayout determina em qual layout a imagem será criada. As imagens podem ser movidas de um layout para outro. No entanto, as imagens devem ser criadas inicialmente no layout VK_IMAGE_LAYOUT_UNDEFINED ou VK_IMAGE_LAYOUT_PREINITIALIZED. VK_IMAGE_LAYOUT_PREINITIALIZED deve ser usado somente quando tivermos dados na memória que serão vinculados ao recurso de imagem imediatamente. VK_IMAGE_LAYOUT_UNDEFINED deve ser usado quando planejamos mover o recurso para outro layout antes de usá-lo. As imagens podem ser removidas do layout VK_IMAGE_LAYOUT_UNDEFINED a um custo pequeno ou nenhum custo a qualquer momento.

O mecanismo para alterar o layout de uma imagem é conhecido como uma barreira de pipeline (pipeline barrier) ou simplesmente uma barreira. Uma barreira não serve apenas como um meio de alterar o layout de um recurso, mas também pode sincronizar o acesso a esse recurso por diferentes estágios no pipeline do Vulkan e até mesmo por diferentes filas sendo executadas simultaneamente no mesmo dispositivo. As barreiras de pipeline usadas para alterar o layout de uma imagem são discutidas com algum detalhe mais adiante.

Agora que fornecemos valores para todos os parâmetros, podemos criar uma imagem. Isso é feito chamando a função vkCreateImage para a qual precisamos fornecer um identificador de um dispositivo lógico, um ponteiro para a estrutura descrita acima e um ponteiro para uma variável do tipo VkImage na qual o identificador da imagem criada será armazenado:

if (m_object->textureImage) {
   m_deviceFunctions->vkDestroyImage(device, m_object->textureImage, nullptr);
    m_object->textureImage = VK_NULL_HANDLE;
}

VkResult result = m_deviceFunctions->vkCreateImage(device, &imageInfo, nullptr, &m_object->textureImage);
if (result != VK_SUCCESS) {
   qFatal("Failed to create image: %d", result);
}

Antes de criarmos o objeto de imagem, verificamos se ele já foi criado. Caso já tenha sido criado, destruímos primeiro o objeto já criado antes de chamar vkCreateImage. Nós fazemos isso porque pretendemos utilizar essa função mais a frente para carregar dinamicamente imagens de texturas para a geometria.

Alocando memória para imagem

Semelhante aos buffers, as imagens não têm memória própria, por isso, antes de podermos usar as imagens, precisamos alocar a memória a elas. Para isso, definimos um membro de estrutura em Object3D para armazenar o identificar do objeto de memória associado a imagem:

VkImage textureImage = VK_NULL_HANDLE;
VkDeviceMemory textureImageMemory = VK_NULL_HANDLE;

A alocação de memória para uma imagem é semelhante a alocação de memória para um buffer. Só que, em vez de usar vkGetBufferMemoryRequirements, usamos vkGetImageMemoryRequirements; e em vez de vkBindBufferMemory, vkBindImageMemory:

...
VkMemoryRequirements memRequirements;
m_deviceFunctions->vkGetImageMemoryRequirements(
	device,
	m_object->textureImage,
	&memRequirements
);

VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(
	memRequirements.memoryTypeBits,
	VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
);

if (m_object->textureImageMemory != VK_NULL_HANDLE) {
	m_deviceFunctions->vkFreeMemory(
		device,
		m_object->textureImageMemory,
		nullptr
	);
	m_object->textureImageMemory = VK_NULL_HANDLE;
}

result = m_deviceFunctions->vkAllocateMemory(
	device,
	&allocInfo,
	nullptr,
	&m_object->textureImageMemory
);
if (result != VK_SUCCESS) {
	qFatal("Failed to allocate image memory: %d", result);
}

m_deviceFunctions->vkBindImageMemory(
	device,
	m_object->textureImage,
	m_object->textureImageMemory,
	0
);

Da mesma forma que fizemos com o buffer de vértices, especificamos aqui que a memória será local para o dispositivo através do sinalizador VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.


Anterior Próximo