Merge pull request #4725 from howard-stearns/objReader

Textures for .obj reader:
This commit is contained in:
Seth Alves 2015-05-06 14:30:29 -07:00
commit 719747ee2d
4 changed files with 409 additions and 237 deletions

View file

@ -15,7 +15,12 @@
#include <QBuffer>
#include <QIODevice>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QEventLoop>
#include <ctype.h> // .obj files are not locale-specific. The C/ASCII charset applies.
#include <NetworkAccessManager.h>
#include "FBXReader.h"
#include "OBJReader.h"
#include "Shape.h"
@ -25,35 +30,14 @@
QHash<QString, float> COMMENT_SCALE_HINTS = {{"This file uses centimeters as units", 1.0f / 100.0f},
{"This file uses millimeters as units", 1.0f / 1000.0f}};
class OBJTokenizer {
public:
OBJTokenizer(QIODevice* device);
enum SpecialToken {
NO_TOKEN = -1,
NO_PUSHBACKED_TOKEN = -1,
DATUM_TOKEN = 0x100,
COMMENT_TOKEN = 0x101
};
int nextToken();
const QByteArray& getDatum() const { return _datum; }
bool isNextTokenFloat();
void skipLine() { _device->readLine(); }
void pushBackToken(int token) { _pushedBackToken = token; }
void ungetChar(char ch) { _device->ungetChar(ch); }
const QString getComment() const { return _comment; }
private:
QIODevice* _device;
QByteArray _datum;
int _pushedBackToken;
QString _comment;
};
const QString SMART_DEFAULT_MATERIAL_NAME = "High Fidelity smart default material name";
OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) {
}
const QByteArray OBJTokenizer::getLineAsDatum() {
return _device->readLine().trimmed();
}
int OBJTokenizer::nextToken() {
if (_pushedBackToken != NO_PUSHBACKED_TOKEN) {
@ -116,14 +100,35 @@ bool OBJTokenizer::isNextTokenFloat() {
return ok;
}
void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) {
glm::vec3 OBJTokenizer::getVec3() {
auto x = getFloat(); // N.B.: getFloat() has side-effect
auto y = getFloat(); // And order of arguments is different on Windows/Linux.
auto z = getFloat();
auto v = glm::vec3(x, y, z);
while (isNextTokenFloat()) {
// the spec(s) get(s) vague here. might be w, might be a color... chop it off.
nextToken();
}
return v;
}
glm::vec2 OBJTokenizer::getVec2() {
auto v = glm::vec2(getFloat(), 1.0f - getFloat()); // OBJ has an odd sense of u, v. Also N.B.: getFloat() has side-effect
while (isNextTokenFloat()) {
// there can be a w, but we don't handle that
nextToken();
}
return v;
}
void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) {
meshPart.diffuseColor = glm::vec3(1, 1, 1);
meshPart.specularColor = glm::vec3(1, 1, 1);
meshPart.emissiveColor = glm::vec3(0, 0, 0);
meshPart.emissiveParams = glm::vec2(0, 1);
meshPart.shininess = 40;
meshPart.opacity = 1;
meshPart.materialID = materialID;
meshPart.opacity = 1.0;
meshPart._material = model::MaterialPointer(new model::Material());
@ -134,14 +139,146 @@ void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) {
meshPart._material->setEmissive(glm::vec3(0.0, 0.0, 0.0));
}
bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping,
FBXGeometry &geometry, QVector<glm::vec3>& faceNormals, QVector<int>& faceNormalIndexes,
float& scaleGuess) {
FBXMesh &mesh = geometry.meshes[0];
// OBJFace
bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector<glm::vec3>& vertices) {
bool ok;
int index = vertexIndex.toInt(&ok);
if (!ok) {
return false;
}
vertexIndices.append(index - 1);
if (!textureIndex.isEmpty()) {
index = textureIndex.toInt(&ok);
if (!ok) {
return false;
}
if (index < 0) { // Count backwards from the last one added.
index = vertices.count() + 1 + index;
}
textureUVIndices.append(index - 1);
}
if (!normalIndex.isEmpty()) {
index = normalIndex.toInt(&ok);
if (!ok) {
return false;
}
normalIndices.append(index - 1);
}
return true;
}
QVector<OBJFace> OBJFace::triangulate() {
QVector<OBJFace> newFaces;
const int nVerticesInATriangle = 3;
if (vertexIndices.count() == nVerticesInATriangle) {
newFaces.append(*this);
} else {
for (int i = 1; i < vertexIndices.count() - 1; i++) {
OBJFace newFace;
newFace.addFrom(this, 0);
newFace.addFrom(this, i);
newFace.addFrom(this, i + 1);
newFace.groupName = groupName;
newFace.materialName = materialName;
newFaces.append(newFace);
}
}
return newFaces;
}
void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f at index i
vertexIndices.append(face->vertexIndices[index]);
if (face->textureUVIndices.count() > 0) { // Any at all. Runtime error if not consistent.
textureUVIndices.append(face->textureUVIndices[index]);
}
if (face->normalIndices.count() > 0) {
normalIndices.append(face->normalIndices[index]);
}
}
bool OBJReader::isValidTexture(const QByteArray &filename) {
QUrl candidateUrl = url->resolved(QUrl(filename));
QNetworkReply *netReply = request(candidateUrl, true);
bool isValid = netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200);
netReply->deleteLater();
return isValid;
}
void OBJReader::parseMaterialLibrary(QIODevice* device) {
OBJTokenizer tokenizer(device);
QString matName = SMART_DEFAULT_MATERIAL_NAME;
OBJMaterial& currentMaterial = materials[matName];
while (true) {
switch (tokenizer.nextToken()) {
case OBJTokenizer::COMMENT_TOKEN:
qCDebug(modelformat) << "OBJ Reader MTLLIB comment:" << tokenizer.getComment();
break;
case OBJTokenizer::DATUM_TOKEN:
break;
default:
materials[matName] = currentMaterial;
qCDebug(modelformat) << "OBJ Reader Last material shininess:" << currentMaterial.shininess << " opacity:" << currentMaterial.opacity << " diffuse color:" << currentMaterial.diffuseColor << " specular color:" << currentMaterial.specularColor << " diffuse texture:" << currentMaterial.diffuseTextureFilename << " specular texture:" << currentMaterial.specularTextureFilename;
return;
}
QByteArray token = tokenizer.getDatum();
if (token == "newmtl") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
return;
}
materials[matName] = currentMaterial;
matName = tokenizer.getDatum();
currentMaterial = materials[matName];
currentMaterial.diffuseTextureFilename = "test";
qCDebug(modelformat) << "OBJ Reader Starting new material definition " << matName;
currentMaterial.diffuseTextureFilename = "";
} else if (token == "Ns") {
currentMaterial.shininess = tokenizer.getFloat();
} else if ((token == "d") || (token == "Tr")) {
currentMaterial.opacity = tokenizer.getFloat();
} else if (token == "Ka") {
qCDebug(modelformat) << "OBJ Reader Ignoring material Ka " << tokenizer.getVec3();
} else if (token == "Kd") {
currentMaterial.diffuseColor = tokenizer.getVec3();
} else if (token == "Ks") {
currentMaterial.specularColor = tokenizer.getVec3();
} else if ((token == "map_Kd") || (token == "map_Ks")) {
QByteArray filename = QUrl(tokenizer.getLineAsDatum()).fileName().toUtf8();
if (filename.endsWith(".tga")) {
qCDebug(modelformat) << "OBJ Reader WARNING: currently ignoring tga texture " << filename << " in " << url;
break;
}
if (isValidTexture(filename)) {
if (token == "map_Kd") {
currentMaterial.diffuseTextureFilename = filename;
} else {
currentMaterial.specularTextureFilename = filename;
}
} else {
qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " ignoring missing texture " << filename;
}
}
}
}
QNetworkReply* OBJReader::request(QUrl& url, bool isTest) {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest netRequest(url);
QNetworkReply* netReply = isTest ? networkAccessManager.head(netRequest) : networkAccessManager.get(netRequest);
QEventLoop loop; // Create an event loop that will quit when we get the finished signal
QObject::connect(netReply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec(); // Nothing is going to happen on this whole run thread until we get this
netReply->waitForReadyRead(-1); // so we might as well block this thread waiting for the response, rather than
return netReply; // trying to sync later on.
}
bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess) {
FaceGroup faces;
FBXMesh& mesh = geometry.meshes[0];
mesh.parts.append(FBXMeshPart());
FBXMeshPart &meshPart = mesh.parts.last();
FBXMeshPart& meshPart = mesh.parts.last();
bool sawG = false;
bool result = true;
int originalFaceCountForDebugging = 0;
QString currentGroup;
setMeshPartDefaults(meshPart, QString("dontknow") + QString::number(mesh.parts.count()));
@ -165,6 +302,7 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping,
break;
}
QByteArray token = tokenizer.getDatum();
//qCDebug(modelformat) << token;
if (token == "g") {
if (sawG) {
// we've encountered the beginning of the next group.
@ -176,54 +314,43 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping,
break;
}
QByteArray groupName = tokenizer.getDatum();
meshPart.materialID = groupName;
currentGroup = groupName;
//qCDebug(modelformat) << "new group:" << groupName;
} else if (token == "mtllib") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
QByteArray libraryName = tokenizer.getDatum();
if (librariesSeen.contains(libraryName)) {
break; // Some files use mtllib over and over again for the same libraryName
}
librariesSeen[libraryName] = true;
QUrl libraryUrl = url->resolved(QUrl(libraryName).fileName()); // Throw away any path part of libraryName, and merge against original url.
qCDebug(modelformat) << "OBJ Reader new library:" << libraryName << " at:" << libraryUrl;
QNetworkReply* netReply = request(libraryUrl, false);
if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) {
parseMaterialLibrary(netReply);
} else {
qCDebug(modelformat) << "OBJ Reader " << libraryName << " did not answer. Got " << netReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
}
netReply->deleteLater();
} else if (token == "usemtl") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
currentMaterialName = tokenizer.getDatum();
qCDebug(modelformat) << "OBJ Reader new current material:" << currentMaterialName;
} else if (token == "v") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
float x = std::stof(tokenizer.getDatum().data());
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
float y = std::stof(tokenizer.getDatum().data());
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
float z = std::stof(tokenizer.getDatum().data());
while (tokenizer.isNextTokenFloat()) {
// the spec(s) get(s) vague here. might be w, might be a color... chop it off.
tokenizer.nextToken();
}
mesh.vertices.append(glm::vec3(x, y, z));
vertices.append(tokenizer.getVec3());
} else if (token == "vn") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
float x = std::stof(tokenizer.getDatum().data());
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
float y = std::stof(tokenizer.getDatum().data());
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
float z = std::stof(tokenizer.getDatum().data());
while (tokenizer.isNextTokenFloat()) {
// the spec gets vague here. might be w
tokenizer.nextToken();
}
faceNormals.append(glm::vec3(x, y, z));
normals.append(tokenizer.getVec3());
} else if (token == "vt") {
textureUVs.append(tokenizer.getVec2());
} else if (token == "f") {
// a face can have 3 or more vertices
QVector<int> indices;
QVector<int> normalIndices;
OBJFace face;
while (true) {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
if (indices.count() == 0) {
if (face.vertexIndices.count() == 0) {
// nonsense, bail out.
goto done;
}
@ -233,69 +360,22 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping,
// vertex-index
// vertex-index/texture-index
// vertex-index/texture-index/surface-normal-index
QByteArray token = tokenizer.getDatum();
QList<QByteArray> parts = token.split('/');
assert(parts.count() >= 1);
assert(parts.count() <= 3);
QByteArray vertIndexBA = parts[ 0 ];
bool ok;
int vertexIndex = vertIndexBA.toInt(&ok);
if (!ok) {
// it wasn't #/#/#, put it back and exit this loop.
if (!isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done;
tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN);
break;
}
// if (parts.count() > 1) {
// QByteArray textureIndexBA = parts[ 1 ];
// }
if (parts.count() > 2) {
QByteArray normalIndexBA = parts[ 2 ];
bool ok;
int normalIndex = normalIndexBA.toInt(&ok);
if (ok) {
normalIndices.append(normalIndex - 1);
}
}
// negative indexes count backward from the current end of the vertex list
vertexIndex = (vertexIndex >= 0 ? vertexIndex : mesh.vertices.count() + vertexIndex + 1);
// obj index is 1 based
assert(vertexIndex >= 1);
indices.append(vertexIndex - 1);
QList<QByteArray> parts = token.split('/');
assert(parts.count() >= 1);
assert(parts.count() <= 3);
const QByteArray noData {};
face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices);
face.groupName = currentGroup;
face.materialName = currentMaterialName;
}
if (indices.count() == 3) {
meshPart.triangleIndices.append(indices[0]);
meshPart.triangleIndices.append(indices[1]);
meshPart.triangleIndices.append(indices[2]);
if (normalIndices.count() == 3) {
faceNormalIndexes.append(normalIndices[0]);
faceNormalIndexes.append(normalIndices[1]);
faceNormalIndexes.append(normalIndices[2]);
} else {
// hmm.
}
} else if (indices.count() == 4) {
meshPart.quadIndices << indices;
} else {
// some obj writers (maya) will write a face with lots of points.
for (int i = 1; i < indices.count() - 1; i++) {
// break the face into triangles
meshPart.triangleIndices.append(indices[0]);
meshPart.triangleIndices.append(indices[i]);
meshPart.triangleIndices.append(indices[i+1]);
}
if (indices.count() == normalIndices.count()) {
for (int i = 1; i < normalIndices.count() - 1; i++) {
faceNormalIndexes.append(normalIndices[0]);
faceNormalIndexes.append(normalIndices[i]);
faceNormalIndexes.append(normalIndices[i+1]);
}
}
originalFaceCountForDebugging++;
foreach(OBJFace face, face.triangulate()) {
faces.append(face);
}
} else {
// something we don't (yet) care about
@ -303,61 +383,40 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping,
tokenizer.skipLine();
}
}
done:
if (meshPart.triangleIndices.size() == 0 && meshPart.quadIndices.size() == 0) {
// empty mesh?
done:
if (faces.count() == 0) { // empty mesh
mesh.parts.pop_back();
}
faceGroups.append(faces); // We're done with this group. Add the faces.
//qCDebug(modelformat) << "end group:" << meshPart.materialID << " original faces:" << originalFaceCountForDebugging << " triangles:" << faces.count() << " keep going:" << result;
return result;
}
FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping) {
FBXGeometry OBJReader::readOBJ(const QByteArray& model, const QVariantHash& mapping) {
QBuffer buffer(const_cast<QByteArray*>(&model));
buffer.open(QIODevice::ReadOnly);
return readOBJ(&buffer, mapping);
return readOBJ(&buffer, mapping, nullptr);
}
FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping) {
FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, QUrl* url) {
FBXGeometry geometry;
OBJTokenizer tokenizer(device);
QVector<int> faceNormalIndexes;
QVector<glm::vec3> faceNormals;
float scaleGuess = 1.0f;
faceNormalIndexes.clear();
this->url = url;
geometry.meshExtents.reset();
geometry.meshes.append(FBXMesh());
try {
// call parseOBJGroup as long as it's returning true. Each successful call will
// add a new meshPart to the geometry's single mesh.
bool success = true;
while (success) {
success = parseOBJGroup(tokenizer, mapping, geometry, faceNormals, faceNormalIndexes, scaleGuess);
}
while (parseOBJGroup(tokenizer, mapping, geometry, scaleGuess)) {}
FBXMesh &mesh = geometry.meshes[0];
FBXMesh& mesh = geometry.meshes[0];
mesh.meshIndex = 0;
// if we got a hint about units, scale all the points
if (scaleGuess != 1.0f) {
for (int i = 0; i < mesh.vertices.size(); i++) {
mesh.vertices[i] *= scaleGuess;
}
}
mesh.meshExtents.reset();
foreach (const glm::vec3& vertex, mesh.vertices) {
mesh.meshExtents.addPoint(vertex);
geometry.meshExtents.addPoint(vertex);
}
geometry.joints.resize(1);
geometry.joints[0].isFree = false;
geometry.joints[0].parentIndex = -1;
@ -380,63 +439,96 @@ FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping) {
0, 0, 1, 0,
0, 0, 0, 1);
mesh.clusters.append(cluster);
// The OBJ format has normals for faces. The FBXGeometry structure has normals for points.
// run through all the faces, look-up (or determine) a normal and set the normal for the points
// that make up each face.
QVector<glm::vec3> pointNormalsSums;
mesh.normals.fill(glm::vec3(0,0,0), mesh.vertices.count());
pointNormalsSums.fill(glm::vec3(0,0,0), mesh.vertices.count());
foreach (FBXMeshPart meshPart, mesh.parts) {
int triCount = meshPart.triangleIndices.count() / 3;
for (int i = 0; i < triCount; i++) {
int p0Index = meshPart.triangleIndices[i*3];
int p1Index = meshPart.triangleIndices[i*3+1];
int p2Index = meshPart.triangleIndices[i*3+2];
assert(p0Index < mesh.vertices.count());
assert(p1Index < mesh.vertices.count());
assert(p2Index < mesh.vertices.count());
glm::vec3 n0, n1, n2;
if (i < faceNormalIndexes.count()) {
int n0Index = faceNormalIndexes[i*3];
int n1Index = faceNormalIndexes[i*3+1];
int n2Index = faceNormalIndexes[i*3+2];
n0 = faceNormals[n0Index];
n1 = faceNormals[n1Index];
n2 = faceNormals[n2Index];
} else {
// We didn't read normals, add bogus normal data for this face
glm::vec3 p0 = mesh.vertices[p0Index];
glm::vec3 p1 = mesh.vertices[p1Index];
glm::vec3 p2 = mesh.vertices[p2Index];
n0 = glm::cross(p1 - p0, p2 - p0);
n1 = n0;
n2 = n0;
}
// we sum up the normal for each point and then divide by the count to get an average
pointNormalsSums[p0Index] += n0;
pointNormalsSums[p1Index] += n1;
pointNormalsSums[p2Index] += n2;
// Some .obj files use the convention that a group with uv coordinates that doesn't define a material, should use a texture with the same basename as the .obj file.
QString filename = url->fileName();
int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail
QString basename = filename.remove(extIndex + 1, sizeof("obj"));
OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME];
preDefinedMaterial.diffuseColor = glm::vec3(1.0f);
QVector<QByteArray> extensions = {"jpg", "jpeg", "png", "tga"};
QByteArray base = basename.toUtf8(), textName = "";
for (int i = 0; i < extensions.count(); i++) {
QByteArray candidateString = base + extensions[i];
if (isValidTexture(candidateString)) {
textName = candidateString;
break;
}
int vertCount = mesh.vertices.count();
for (int i = 0; i < vertCount; i++) {
float length = glm::length(pointNormalsSums[i]);
if (length > FLT_EPSILON) {
mesh.normals[i] = glm::normalize(pointNormalsSums[i]);
}
}
// XXX do same normal calculation for quadCount
}
}
catch(const std::exception& e) {
qCDebug(modelformat) << "something went wrong in OBJ reader";
if (!textName.isEmpty()) {
preDefinedMaterial.diffuseTextureFilename = textName;
}
materials[SMART_DEFAULT_MATERIAL_NAME] = preDefinedMaterial;
for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) {
FBXMeshPart& meshPart = mesh.parts[i];
FaceGroup faceGroup = faceGroups[meshPartCount];
OBJFace leadFace = faceGroup[0]; // All the faces in the same group will have the same name and material.
QString groupMaterialName = leadFace.materialName;
if (groupMaterialName.isEmpty() && (leadFace.textureUVIndices.count() > 0)) {
qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " needs a texture that isn't specified. Using default mechanism.";
groupMaterialName = SMART_DEFAULT_MATERIAL_NAME;
} else if (!groupMaterialName.isEmpty() && !materials.contains(groupMaterialName)) {
qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " specifies a material " << groupMaterialName << " that is not defined. Using default mechanism.";
groupMaterialName = SMART_DEFAULT_MATERIAL_NAME;
}
if (!groupMaterialName.isEmpty()) {
OBJMaterial* material = &materials[groupMaterialName];
// The code behind this is in transition. Some things are set directly in the FXBMeshPart...
meshPart.materialID = groupMaterialName;
meshPart.diffuseTexture.filename = material->diffuseTextureFilename;
meshPart.specularTexture.filename = material->specularTextureFilename;
// ... and some things are set in the underlying material.
meshPart._material->setDiffuse(material->diffuseColor);
meshPart._material->setSpecular(material->specularColor);
meshPart._material->setShininess(material->shininess);
meshPart._material->setOpacity(material->opacity);
}
qCDebug(modelformat) << "OBJ Reader part:" << meshPartCount << "name:" << leadFace.groupName << "material:" << groupMaterialName << "diffuse:" << meshPart._material->getDiffuse() << "faces:" << faceGroup.count() << "triangle indices will start with:" << mesh.vertices.count();
foreach(OBJFace face, faceGroup) {
glm::vec3 v0 = vertices[face.vertexIndices[0]];
glm::vec3 v1 = vertices[face.vertexIndices[1]];
glm::vec3 v2 = vertices[face.vertexIndices[2]];
meshPart.triangleIndices.append(mesh.vertices.count()); // not face.vertexIndices into vertices
mesh.vertices << v0;
meshPart.triangleIndices.append(mesh.vertices.count());
mesh.vertices << v1;
meshPart.triangleIndices.append(mesh.vertices.count());
mesh.vertices << v2;
glm::vec3 n0, n1, n2;
if (face.normalIndices.count()) {
n0 = normals[face.normalIndices[0]];
n1 = normals[face.normalIndices[1]];
n2 = normals[face.normalIndices[2]];
} else { // generate normals from triangle plane if not provided
n0 = n1 = n2 = glm::cross(v1 - v0, v2 - v0);
}
mesh.normals << n0 << n1 << n2;
if (face.textureUVIndices.count()) {
mesh.texCoords
<< textureUVs[face.textureUVIndices[0]]
<< textureUVs[face.textureUVIndices[1]]
<< textureUVs[face.textureUVIndices[2]];
}
}
}
// if we got a hint about units, scale all the points
if (scaleGuess != 1.0f) {
for (int i = 0; i < mesh.vertices.size(); i++) {
mesh.vertices[i] *= scaleGuess;
}
}
mesh.meshExtents.reset();
foreach (const glm::vec3& vertex, mesh.vertices) {
mesh.meshExtents.addPoint(vertex);
geometry.meshExtents.addPoint(vertex);
}
fbxDebugDump(geometry);
} catch(const std::exception& e) {
qCDebug(modelformat) << "OBJ reader fail: " << e.what();
}
return geometry;
@ -453,11 +545,11 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) {
foreach (FBXMesh mesh, fbxgeo.meshes) {
qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count();
qCDebug(modelformat) << " normals.count() =" << mesh.normals.count();
if (mesh.normals.count() == mesh.vertices.count()) {
/*if (mesh.normals.count() == mesh.vertices.count()) {
for (int i = 0; i < mesh.normals.count(); i++) {
qCDebug(modelformat) << " " << mesh.vertices[ i ] << mesh.normals[ i ];
}
}
}*/
qCDebug(modelformat) << " tangents.count() =" << mesh.tangents.count();
qCDebug(modelformat) << " colors.count() =" << mesh.colors.count();
qCDebug(modelformat) << " texCoords.count() =" << mesh.texCoords.count();
@ -470,13 +562,15 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) {
foreach (FBXMeshPart meshPart, mesh.parts) {
qCDebug(modelformat) << " quadIndices.count() =" << meshPart.quadIndices.count();
qCDebug(modelformat) << " triangleIndices.count() =" << meshPart.triangleIndices.count();
qCDebug(modelformat) << " diffuseColor =" << meshPart.diffuseColor;
qCDebug(modelformat) << " specularColor =" << meshPart.specularColor;
qCDebug(modelformat) << " emissiveColor =" << meshPart.emissiveColor;
qCDebug(modelformat) << " diffuseColor =" << meshPart.diffuseColor << "mat =" << meshPart._material->getDiffuse();
qCDebug(modelformat) << " specularColor =" << meshPart.specularColor << "mat =" << meshPart._material->getSpecular();
qCDebug(modelformat) << " emissiveColor =" << meshPart.emissiveColor << "mat =" << meshPart._material->getEmissive();
qCDebug(modelformat) << " emissiveParams =" << meshPart.emissiveParams;
qCDebug(modelformat) << " shininess =" << meshPart.shininess;
qCDebug(modelformat) << " opacity =" << meshPart.opacity;
qCDebug(modelformat) << " shininess =" << meshPart.shininess << "mat =" << meshPart._material->getShininess();
qCDebug(modelformat) << " opacity =" << meshPart.opacity << "mat =" << meshPart._material->getOpacity();
qCDebug(modelformat) << " materialID =" << meshPart.materialID;
qCDebug(modelformat) << " diffuse texture =" << meshPart.diffuseTexture.filename;
qCDebug(modelformat) << " specular texture =" << meshPart.specularTexture.filename;
}
qCDebug(modelformat) << " clusters.count() =" << mesh.clusters.count();
foreach (FBXCluster cluster, mesh.clusters) {

View file

@ -1,8 +1,86 @@
#include <QtNetwork/QNetworkReply>
#include "FBXReader.h"
FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping);
FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping);
void fbxDebugDump(const FBXGeometry& fbxgeo);
void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID);
class OBJTokenizer {
public:
OBJTokenizer(QIODevice* device);
enum SpecialToken {
NO_TOKEN = -1,
NO_PUSHBACKED_TOKEN = -1,
DATUM_TOKEN = 0x100,
COMMENT_TOKEN = 0x101
};
int nextToken();
const QByteArray& getDatum() const { return _datum; }
bool isNextTokenFloat();
const QByteArray getLineAsDatum(); // some "filenames" have spaces in them
void skipLine() { _device->readLine(); }
void pushBackToken(int token) { _pushedBackToken = token; }
void ungetChar(char ch) { _device->ungetChar(ch); }
const QString getComment() const { return _comment; }
glm::vec3 getVec3();
glm::vec2 getVec2();
float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); }
private:
QIODevice* _device;
QByteArray _datum;
int _pushedBackToken;
QString _comment;
};
class OBJFace { // A single face, with three or more planar vertices. But see triangulate().
public:
QVector<int> vertexIndices;
QVector<int> textureUVIndices;
QVector<int> normalIndices;
QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use.
QString materialName;
// Add one more set of vertex data. Answers true if successful
bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector<glm::vec3>& vertices);
// Return a set of one or more OBJFaces from this one, in which each is just a triangle.
// Even though FBXMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles.
QVector<OBJFace> triangulate();
private:
void addFrom(const OBJFace* face, int index);
};
// Materials and references to material names can come in any order, and different mesh parts can refer to the same material.
// Therefore it would get pretty hacky to try to use FBXMeshPart to store these as we traverse the files.
class OBJMaterial {
public:
float shininess;
float opacity;
glm::vec3 diffuseColor;
glm::vec3 specularColor;
QByteArray diffuseTextureFilename;
QByteArray specularTextureFilename;
OBJMaterial() : opacity(1.0f) {}
};
class OBJReader: public QObject { // QObject so we can make network requests.
Q_OBJECT
public:
typedef QVector<OBJFace> FaceGroup;
QVector<glm::vec3> vertices; // all that we ever encounter while reading
QVector<glm::vec2> textureUVs;
QVector<glm::vec3> normals;
QVector<FaceGroup> faceGroups;
QString currentMaterialName;
QHash<QString, OBJMaterial> materials;
QUrl* url;
QNetworkReply* request(QUrl& url, bool isTest);
FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping);
FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping, QUrl* url);
private:
QHash<QByteArray, bool> librariesSeen;
bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess);
void parseMaterialLibrary(QIODevice* device);
bool isValidTexture(const QByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format.
};
// What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility.
void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID);
void fbxDebugDump(const FBXGeometry& fbxgeo);

View file

@ -1797,8 +1797,8 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer<NetworkGe
if (url.isEmpty()) {
// make the minimal amount of dummy geometry to satisfy Model
FBXJoint joint = { false, QVector<int>(), -1, 0.0f, 0.0f, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(),
glm::quat(), glm::mat4(), glm::mat4(), glm::vec3(), glm::vec3(), glm::quat(), glm::quat(),
glm::mat4(), QString(""), glm::vec3(), glm::quat(), SHAPE_TYPE_NONE, false};
glm::quat(), glm::mat4(), glm::mat4(), glm::vec3(), glm::vec3(), glm::quat(), glm::quat(),
glm::mat4(), QString(""), glm::vec3(), glm::quat(), SHAPE_TYPE_NONE, false};
_geometry.joints.append(joint);
_geometry.leftEyeJointIndex = -1;
_geometry.rightEyeJointIndex = -1;
@ -2107,7 +2107,7 @@ void GeometryReader::run() {
}
fbxgeo = readFBX(_reply, _mapping, grabLightmaps, lightmapLevel);
} else if (_url.path().toLower().endsWith(".obj")) {
fbxgeo = readOBJ(_reply, _mapping);
fbxgeo = OBJReader().readOBJ(_reply, _mapping, &_url);
}
QMetaObject::invokeMethod(geometry.data(), "setGeometry", Q_ARG(const FBXGeometry&, fbxgeo));
} else {

View file

@ -36,7 +36,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) {
QByteArray fbxContents = fbx.readAll();
if (filename.toLower().endsWith(".obj")) {
result = readOBJ(fbxContents, QVariantHash());
result = OBJReader().readOBJ(fbxContents, QVariantHash());
} else if (filename.toLower().endsWith(".fbx")) {
result = readFBX(fbxContents, QVariantHash());
} else {