Link

Carregando vértices do modelo

Agora, vamos escrever uma função membro chamada readOBJFile em Model que usa a biblioteca tinyobjloader para preencher o contêiner de vértices com os dados de vértice da malha.

void Model::readOBJFile(QString const &filePath) {

}

Essa função recebe como parâmetro um objeto do tipo QString com o caminho para o arquivo OBJ.

Um modelo é então carregado nas estruturas de dados da biblioteca chamando a função tinyobj::LoadObj:

tinyobj::attrib_t attribs;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;

std::string warn;
std::string err;

bool result = tinyobj::LoadObj(
	&attribs,
	&shapes,
	&materials,
	&warn,
	&err,
	filePath.toStdString().c_str()
);

if (!warn.empty()) {
	qDebug(warn.c_str());
}

if (!err.empty()) {
	qDebug(err.c_str());
}

if (!result) {
	qFatal("Could no open file: %s", filePath.toStdString().c_str());
}

O código acima mostra que tinyobj::LoadObj recebe seis argumentos:

  • O primeiro argumento especifica um ponteiro para um vetor do tipo tinyobj::attrib_t. Cada elemento desse vetor mantém os valores das posições, normais e coordenadas de textura do modelo em seus vetores vertices, normals e texcoords, respectivamente.
  • O segundo argumento especifica um ponteiro para um vetor do tipo tinyobj::shape_t. Cada elemento desse vetor representa um objeto separado e suas faces. Cada face consiste em um array de vértices e cada vértice contém os índices dos atributos de posição, normal e coordenada de textura no vetor attribs.
  • O terceiro argumento especifica um ponteiro para um vetor do tipo tinyobj::material_t. Cada elemento desse vetor mantém as propriedades do material para cada face do modelo. Ignoraremos esses atributos neste exemplo.
  • O quarto argumento especifica um ponteiro para uma string. É nesse argumento que a interface irá armazenar avisos que ocorreram durante o carregamento do arquivo, como, por exemplo, uma definição de material ausente.
  • O quinto argumento, também, especifica um ponteiro para uma string. Só que agora ele irá armazenar mensagens de erros que ocorreram durante o carregamento do arquivo. O carregamento realmente falha se a função LoadObj retornar false.
  • O sexto e ultimo argumento, especifica um array de caracteres contendo o caminho para o arquivo de modelo que será carregado.

Como mencionado acima, as faces nos arquivos OBJ podem ter qualquer número de vértices, enquanto nosso aplicativo pode renderizar apenas triângulos. Felizmente, a função tinyobj::LoadObj tem a capacidade de triangular as faces, que é habilitada por padrão.

Na função readOBJFile, também, vamos redimensionar o modelo para que possa ser exibido todo dentro na tela. Assim podemos visualizar modelos que tenham dimensões maiores que 1.0 × 1.0 × 1.0. Para isso ocorrer, definimos uma variável membro na estrutura Model, para armazenar a matriz de transformação para mudar a escala do modelo e centraliza-lo na tela:

#include <QVulkanFunctions>
#include <QMatrix4x4>
...
QVector<Vertex> vertices;
QMatrix4x4 transformation;

Em readOBJFile adicionamos o seguinte código para inicializar as variáveis locais que armazenarão os valores das dimensões máximas e mínimas do modelo:

const float fmin = std::numeric_limits<float>::lowest();
const float fmax = std::numeric_limits<float>::max();

QVector3D minDimension = QVector3D(fmax, fmax, fmax);
QVector3D maxDimension = QVector3D(fmin, fmin, fmin);

if (vertices.size()) {
	vertices.clear();
}

Também verificamos se o contêiner de vértices está vazio. Caso não esteja, limpamos ele com clear().

Vamos combinar todas as faces do arquivo em um único modelo, então apenas iteramos sobre todos os elementos de shapes:

for (const tinyobj::shape_t &shape : shapes) {

}

O recurso de triangulação já garantiu a existência de três vértices por face, para que agora possamos iterar diretamente sobre os vértices e armazená-los diretamente em nosso vetor de vértices:

for (const tinyobj::shape_t &shape : shapes) {
	for (const tinyobj::index_t &index : shape.mesh.indices) {
		Vertex vertex = {};

		size_t indexTemp;



		vertices.push_back(vertex);
	}
}

A variável index é do tipo tinyobj::index_t, que contém os membros vertex_index, normal_index e texcoord_index. Precisamos usar esses índices para procurar os valores de posição, normal e coordenada de textura do vértice no vetor attribs:

indexTemp = index.vertex_index * 3;
vertex.pos = {
	attribs.vertices[indexTemp],
	attribs.vertices[indexTemp + 1],
	attribs.vertices[indexTemp + 2]
};

indexTemp = index.texcoord_index * 2;
vertex.texCoord = {
	attribs.texcoords[indexTemp + 0],
	1.0f - attribs.texcoords[indexTemp + 1]
};

Precisamos multiplicar index.vertex_index por 3, pois o vetor attribs.vertices é um vetor de valores do tipo float em vez de algo como QVector3D. Pelo mesmo motivo, precisamos multiplicar index.texcoord_index por 2, pois existem dois componentes de coordenadas de textura por entrada. Os deslocamentos de 0, 1 e 2 são usados para acessar os componentes X, Y e Z ou U e V no caso de coordenadas de textura1.

1 Estamos assumindo aqui que o modelo carregado possuí coordenadas de textura para cada vértice.

vertex.color = {1.0f, 1.0f, 1.0f};

Ignoraremos o atributos de cores de cada vértice. Iremos definir o valor de cor (1.0f, 1.0f. 1.0f) para todos os vértices do modelo.

if (vertex.pos.x() < minDimension.x()) {
	minDimension.setX(vertex.pos.x());
}
if (vertex.pos.x() > maxDimension.x()) {
	maxDimension.setX(vertex.pos.x());
}

if (vertex.pos.y() < minDimension.y()) {
	minDimension.setY(vertex.pos.y());
}
if (vertex.pos.y() > maxDimension.y()) {
	maxDimension.setY(vertex.pos.y());
}

if (vertex.pos.z() < minDimension.z()) {
	minDimension.setZ(vertex.pos.z());
}
if (vertex.pos.z() > maxDimension.z()) {
	maxDimension.setZ(vertex.pos.z());
}

Conforme iteramos sobre todos os índices, verificamos se as dimensões de cada posição dos vértices são maiores ou menores do que aquelas que já foram carregados no contêiner de vértices. Caso sejam, atualizamos as variáveis maxDimension e minDimension com os novos valores maiores e menores, respectivamente.

float distance = qMax(
	maxDimension.x() - minDimension.x(),
	qMax(maxDimension.y() - minDimension.y(),
	maxDimension.z() - minDimension.z())
);

float sc = 1.0 / distance;
QVector3D center = (maxDimension + minDimension) / 2;

transformation.scale(sc);
transformation.translate(-center);

Depois que percorremos todos os vértices, utilizamos os valores máximos e mínimos das dimensões da malha para definir o tamanho do objeto com dimensões máximas de 1 × 1 × 1 e centralizá-lo.

Atualizamos o membro model do UBO na função updateUniformBuffer para utilizar essa matriz de transformação:

...
UniformBufferObject ubo = {};
ubo.model.setToIdentity();
ubo.model.rotate(time * 90.0, QVector3D(0.0f, 1.0f, 0.0));
ubo.model *= m_object->model->transformation;
...

Nosso programa agora está pronto para carregar e renderizar modelos 3D de arquivos OBJ.


Anterior Próximo