Geometria 3D
Antes de podermos carregar vértices de um modelo 3D, precisamos atualizar a estrutura Vertex
para que as coordenadas sejam vetores tridimensionais. Também devemos alterar o campo format
na estrutura VkVertexInputAttributeDescription
correspondente às coordenadas:
struct Vertex {
QVector3D pos;
QVector3D color;
QVector2D texCoord;
...
static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions = {};
attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format =
VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);
...
}
};
Atualizamos também a entrada do vertex shader que corresponde às coordenadas dos vértices. Não podemos nos esquecer de recompilar o shader depois.
layout(location = 0) in vec3 inPosition;
...
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}
Vamos carregar os vértices do arquivo de modelo agora, então devemos remover os vértices definidos no membro vertices
de Model
:
QVector<Vertex> vertices;
Queremos carregar modelos 3D dinamicamente, para isso, precisamos fazer algumas modificações no código.
Primeiro, vamos criar uma função auxiliar chamada drawObject
em Renderer
. Vamos mover parte do código que está em startNextFrame
para essa função:
void Renderer::drawObject() {
if (!m_object) {
return;
}
if (m_object->vertexBuffer == VK_NULL_HANDLE) {
initObject();
}
updateUniformBuffer();
VkCommandBuffer commandBuffer = m_window->currentCommandBuffer();
m_deviceFunctions->vkCmdBindPipeline(
commandBuffer,
VK_PIPELINE_BIND_POINT_GRAPHICS,
m_graphicsPipeline
);
m_deviceFunctions->vkCmdBindDescriptorSets(
commandBuffer,
VK_PIPELINE_BIND_POINT_GRAPHICS,
m_pipelineLayout,
0,
1,
&m_object->descriptorSet,
0,
nullptr
);
VkBuffer vertexBuffers[] = {m_object->vertexBuffer};
VkDeviceSize offsets[] = {0};
m_deviceFunctions->vkCmdBindVertexBuffers(
commandBuffer,
0,
1,
vertexBuffers,
offsets
);
m_deviceFunctions->vkCmdDraw(
commandBuffer,
static_cast<uint32_t>(m_object->model->vertices.size()),
1,
0,
0
);
}
Na função drawObject
primeiro verificamos se o membro m_object
já foi inicializado. Caso não tenha sido, simplesmente retornamos da função. Se m_object
for diferente de nullptr
mas o seu membro vertexBuffer
for igual a VK_NULL_HANDLE
, então chamamos a função initObject
para inicializar o objeto 3D.
Nossa função startNextFrame
fica assim:
void Renderer::startNextFrame() {
VkRenderPassBeginInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = m_window->defaultRenderPass();
renderPassInfo.framebuffer = m_window->currentFramebuffer();
renderPassInfo.renderArea.offset.x = 0;
renderPassInfo.renderArea.offset.y = 0;
const QSize swapChainImageSize = m_window->swapChainImageSize();
renderPassInfo.renderArea.extent.width = swapChainImageSize.width();
renderPassInfo.renderArea.extent.height = swapChainImageSize.height();
std::array<VkClearValue, 2> clearValues = {};
clearValues[0].color = {0.0f, 0.0f, 0.0f, 1.0f};
clearValues[1].depthStencil = {1.0f, 0};
renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
renderPassInfo.pClearValues = clearValues.data();
VkCommandBuffer commandBuffer = m_window->currentCommandBuffer();
m_deviceFunctions->vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport;
viewport.x = 0;
viewport.y = 0;
viewport.width = swapChainImageSize.width();
viewport.height = swapChainImageSize.height();
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
m_deviceFunctions->vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor;
scissor.offset.x = 0;
scissor.offset.y = 0;
scissor.extent.width = viewport.width;
scissor.extent.height = viewport.height;
m_deviceFunctions->vkCmdSetScissor(
commandBuffer,
0,
1,
&scissor
);
drawObject();
m_deviceFunctions->vkCmdEndRenderPass(commandBuffer);
m_window->frameReady();
m_window->requestUpdate();
}
Agora removemos a chamada para função initObject
em initResources
. Simplesmente não precisaremos mais dela lá:
void Renderer::initResources() {
VkDevice device = m_window->device();
m_deviceFunctions = m_window->vulkanInstance()->deviceFunctions(device);
createDescriptorSetLayout();
initPipeline();
createTextureSampler();
}
Também devemos remover as duas primeiras linhas de initObject
. Com isso nossa função initObject
ficará assim:
void Renderer::initObject() {
createObjectVertexBuffer();
createUniformBuffer();
addTextureImage(":/textures/texture.png");
}
Em model.h
criamos uma nova função auxiliar chamada isValid
para indicar se o modelo é válido, ou seja, se possuí vértices carregados no membro vertices
:
bool isValid() const { return vertices.size(); }
Para adicionarmos dinamicamente um modelo em Renderer
criamos a função publica addObject
que recebe como parâmetro um ponteiro para um objeto do tipo Model
:
public:
...
void addObject(QSharedPointer<Model> model);
void Renderer::addObject(QSharedPointer<Model> model) {
}
Iniciamos a função verificando se o modelo é valido, para isso chamamos nossa função isValid
. Caso seja válido, criamos um novo objeto 3D e armazenamos-o no membro m_object
:
if (model->isValid()) {
if (m_object)
releaseObjectResources();
m_object = new Object3D(model);
m_window->requestUpdate();
}
Também verificamos se m_object
já foi criado. Caso isso tenha ocorrido, chamamos nossa função releaseObjectResources
para liberar os recursos já alocados para o objeto 3D anteriormente carregado, antes de criarmos um novo.