mirror of
https://github.com/overte-org/overte.git
synced 2025-04-20 03:44:02 +02:00
Merge pull request #4725 from howard-stearns/objReader
Textures for .obj reader:
This commit is contained in:
commit
719747ee2d
4 changed files with 409 additions and 237 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue