Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions application/application.pro
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ HEADERS += sources/flow_layout.h
SOURCES += sources/flow_layout.cc
HEADERS += sources/glb_file.h
SOURCES += sources/glb_file.cc
HEADERS += sources/gltf_file.h
SOURCES += sources/gltf_file.cc
HEADERS += sources/graphics_container_widget.h
SOURCES += sources/graphics_container_widget.cc
HEADERS += sources/horizontal_line_widget.h
Expand Down
35 changes: 35 additions & 0 deletions application/sources/document_window.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "float_number_widget.h"
#include "flow_layout.h"
#include "glb_file.h"
#include "gltf_file.h"
#include "horizontal_line_widget.h"
#include "image_forever.h"
#include "log_browser.h"
Expand Down Expand Up @@ -341,6 +342,10 @@ DocumentWindow::DocumentWindow()
connect(m_exportAsGlbAction, &QAction::triggered, this, &DocumentWindow::exportGlbResult, Qt::QueuedConnection);
m_fileMenu->addAction(m_exportAsGlbAction);

m_exportAsGltfAction = new QAction(tr("Export as glTF..."), this);
connect(m_exportAsGltfAction, &QAction::triggered, this, &DocumentWindow::exportGltfResult, Qt::QueuedConnection);
m_fileMenu->addAction(m_exportAsGltfAction);

#if defined(Q_OS_WASM)
#else
m_exportAsFbxAction = new QAction(tr("Export as FBX..."), this);
Expand Down Expand Up @@ -381,6 +386,7 @@ DocumentWindow::DocumentWindow()
connect(m_fileMenu, &QMenu::aboutToShow, [=]() {
m_exportAsObjAction->setEnabled(m_canvasGraphicsWidget->hasItems());
m_exportAsGlbAction->setEnabled(m_canvasGraphicsWidget->hasItems() && m_document->isExportReady());
m_exportAsGltfAction->setEnabled(m_canvasGraphicsWidget->hasItems() && m_document->isExportReady());
m_exportAsFbxAction->setEnabled(m_canvasGraphicsWidget->hasItems() && m_document->isExportReady());
});

Expand Down Expand Up @@ -1196,6 +1202,35 @@ void DocumentWindow::exportGlbToFilename(const QString& filename)
QApplication::restoreOverrideCursor();
}

void DocumentWindow::exportGltfResult()
{
QString filename = QFileDialog::getSaveFileName(this, QString(), QString(),
tr("glTF JSON Format (*.gltf)"));
if (filename.isEmpty()) {
return;
}
ensureFileExtension(&filename, ".gltf");
exportGltfToFilename(filename);
}

void DocumentWindow::exportGltfToFilename(const QString& filename)
{
if (!m_document->isExportReady()) {
qDebug() << "Export but document is not export ready";
return;
}
QApplication::setOverrideCursor(Qt::WaitCursor);
dust3d::Object skeletonResult = m_document->currentUvMappedObject();
QImage* textureMetalnessRoughnessAmbientOcclusionImage = UvMapGenerator::combineMetalnessRoughnessAmbientOcclusionImages(m_document->textureMetalnessImage,
m_document->textureRoughnessImage,
m_document->textureAmbientOcclusionImage);
GltfFileWriter gltfFileWriter(skeletonResult, filename,
m_document->textureImage, m_document->textureNormalImage, textureMetalnessRoughnessAmbientOcclusionImage);
gltfFileWriter.save();
delete textureMetalnessRoughnessAmbientOcclusionImage;
QApplication::restoreOverrideCursor();
}

void DocumentWindow::updateXlockButtonState()
{
if (m_document->xlocked)
Expand Down
3 changes: 3 additions & 0 deletions application/sources/document_window.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public slots:
void openPathAs(const QString& path, const QString& asName);
void exportObjResult();
void exportGlbResult();
void exportGltfResult();
void exportFbxResult();
void newWindow();
void newDocument();
Expand All @@ -82,6 +83,7 @@ public slots:
void exportObjToFilename(const QString& filename);
void exportFbxToFilename(const QString& filename);
void exportGlbToFilename(const QString& filename);
void exportGltfToFilename(const QString& filename);
void toggleRotation();
void generateComponentPreviewImages();
void componentPreviewImagesReady();
Expand Down Expand Up @@ -139,6 +141,7 @@ public slots:

QAction* m_exportAsObjAction = nullptr;
QAction* m_exportAsGlbAction = nullptr;
QAction* m_exportAsGltfAction = nullptr;
QAction* m_exportAsFbxAction = nullptr;

QMenu* m_viewMenu = nullptr;
Expand Down
269 changes: 269 additions & 0 deletions application/sources/gltf_file.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
#include "gltf_file.h"
#include "model_mesh.h"
#include "version.h"
#include <QByteArray>
#include <QDataStream>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QQuaternion>
#include <QTextStream>
#include <QtCore/qbuffer.h>

bool GltfFileWriter::m_enableComment = false;

GltfFileWriter::GltfFileWriter(dust3d::Object& object,
const QString& filename,
QImage* textureImage,
QImage* normalImage,
QImage* ormImage)
: m_filename(filename)
, m_baseName(QFileInfo(filename).baseName())
{
const std::vector<std::vector<dust3d::Vector3>>* triangleVertexNormals = object.triangleVertexNormals();
if (m_outputNormal) {
m_outputNormal = nullptr != triangleVertexNormals;
}

const std::vector<std::vector<dust3d::Vector2>>* triangleVertexUvs = object.triangleVertexUvs();
if (m_outputUv) {
m_outputUv = nullptr != triangleVertexUvs;
}

QDataStream binStream(&m_binByteArray, QIODevice::WriteOnly);
binStream.setFloatingPointPrecision(QDataStream::SinglePrecision);
binStream.setByteOrder(QDataStream::LittleEndian);

auto alignBin = [this, &binStream] {
while (0 != this->m_binByteArray.size() % 4) {
binStream << (quint8)0;
}
};

int bufferViewIndex = 0;
int bufferViewFromOffset;

m_json["asset"]["version"] = "2.0";
m_json["asset"]["generator"] = APP_NAME " " APP_HUMAN_VER;
m_json["scenes"][0]["nodes"] = { 0 };
m_json["scene"] = 0;
m_json["nodes"][0]["mesh"] = 0;

std::vector<dust3d::Vector3> triangleVertexPositions;
std::vector<size_t> triangleVertexOldIndices;
for (const auto& triangleIndices : object.triangles) {
for (size_t j = 0; j < 3; ++j) {
triangleVertexOldIndices.push_back(triangleIndices[j]);
triangleVertexPositions.push_back(object.vertices[triangleIndices[j]]);
}
}

int primitiveIndex = 0;
if (!triangleVertexPositions.empty()) {

m_json["meshes"][0]["primitives"][primitiveIndex]["indices"] = bufferViewIndex;
m_json["meshes"][0]["primitives"][primitiveIndex]["material"] = primitiveIndex;

int attributeIndex = 0;
m_json["meshes"][0]["primitives"][primitiveIndex]["attributes"]["POSITION"] = bufferViewIndex + (++attributeIndex);
if (m_outputNormal)
m_json["meshes"][0]["primitives"][primitiveIndex]["attributes"]["NORMAL"] = bufferViewIndex + (++attributeIndex);
if (m_outputUv)
m_json["meshes"][0]["primitives"][primitiveIndex]["attributes"]["TEXCOORD_0"] = bufferViewIndex + (++attributeIndex);
int textureIndex = 0;
m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["baseColorTexture"]["index"] = textureIndex++;
m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["metallicFactor"] = ModelMesh::m_defaultMetalness;
m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["roughnessFactor"] = ModelMesh::m_defaultRoughness;
if (object.alphaEnabled)
m_json["materials"][primitiveIndex]["alphaMode"] = "BLEND";
if (normalImage) {
m_json["materials"][primitiveIndex]["normalTexture"]["index"] = textureIndex++;
}
if (ormImage) {
m_json["materials"][primitiveIndex]["occlusionTexture"]["index"] = textureIndex;
m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["metallicRoughnessTexture"]["index"] = textureIndex;
m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["metallicFactor"] = 1.0;
m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["roughnessFactor"] = 1.0;
textureIndex++;
}

primitiveIndex++;

bufferViewFromOffset = (int)m_binByteArray.size();
for (size_t index = 0; index < triangleVertexPositions.size(); index += 3) {
binStream << (quint16)index << (quint16)(index + 1) << (quint16)(index + 2);
}
m_json["bufferViews"][bufferViewIndex]["buffer"] = 0;
m_json["bufferViews"][bufferViewIndex]["byteOffset"] = bufferViewFromOffset;
m_json["bufferViews"][bufferViewIndex]["byteLength"] = triangleVertexPositions.size() / 3 * 3 * sizeof(quint16);
m_json["bufferViews"][bufferViewIndex]["target"] = 34963;
alignBin();
if (m_enableComment)
m_json["accessors"][bufferViewIndex]["__comment"] = QString("/accessors/%1: triangle indices").arg(QString::number(bufferViewIndex)).toUtf8().constData();
m_json["accessors"][bufferViewIndex]["bufferView"] = bufferViewIndex;
m_json["accessors"][bufferViewIndex]["byteOffset"] = 0;
m_json["accessors"][bufferViewIndex]["componentType"] = 5123;
m_json["accessors"][bufferViewIndex]["count"] = triangleVertexPositions.size();
m_json["accessors"][bufferViewIndex]["type"] = "SCALAR";
bufferViewIndex++;

bufferViewFromOffset = (int)m_binByteArray.size();
m_json["bufferViews"][bufferViewIndex]["buffer"] = 0;
m_json["bufferViews"][bufferViewIndex]["byteOffset"] = bufferViewFromOffset;
float minX = 100;
float maxX = -100;
float minY = 100;
float maxY = -100;
float minZ = 100;
float maxZ = -100;
for (const auto& position : triangleVertexPositions) {
float x = (float)position.x();
float y = (float)position.y();
float z = (float)position.z();
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
if (y < minY)
minY = y;
if (y > maxY)
maxY = y;
if (z < minZ)
minZ = z;
if (z > maxZ)
maxZ = z;
binStream << x << y << z;
}
Q_ASSERT((int)triangleVertexPositions.size() * 3 * sizeof(float) == m_binByteArray.size() - bufferViewFromOffset);
m_json["bufferViews"][bufferViewIndex]["byteLength"] = triangleVertexPositions.size() * 3 * sizeof(float);
m_json["bufferViews"][bufferViewIndex]["target"] = 34962;
alignBin();
if (m_enableComment)
m_json["accessors"][bufferViewIndex]["__comment"] = QString("/accessors/%1: xyz").arg(QString::number(bufferViewIndex)).toUtf8().constData();
m_json["accessors"][bufferViewIndex]["bufferView"] = bufferViewIndex;
m_json["accessors"][bufferViewIndex]["byteOffset"] = 0;
m_json["accessors"][bufferViewIndex]["componentType"] = 5126;
m_json["accessors"][bufferViewIndex]["count"] = triangleVertexPositions.size();
m_json["accessors"][bufferViewIndex]["type"] = "VEC3";
m_json["accessors"][bufferViewIndex]["max"] = { maxX, maxY, maxZ };
m_json["accessors"][bufferViewIndex]["min"] = { minX, minY, minZ };
bufferViewIndex++;

if (m_outputNormal) {
bufferViewFromOffset = (int)m_binByteArray.size();
m_json["bufferViews"][bufferViewIndex]["buffer"] = 0;
m_json["bufferViews"][bufferViewIndex]["byteOffset"] = bufferViewFromOffset;
QStringList normalList;
for (const auto& normals : (*triangleVertexNormals)) {
for (const auto& it : normals) {
binStream << (float)it.x() << (float)it.y() << (float)it.z();
if (m_enableComment && m_outputNormal)
normalList.append(QString("<%1,%2,%3>").arg(QString::number(it.x())).arg(QString::number(it.y())).arg(QString::number(it.z())));
}
}
Q_ASSERT((int)triangleVertexNormals->size() * 3 * 3 * sizeof(float) == m_binByteArray.size() - bufferViewFromOffset);
m_json["bufferViews"][bufferViewIndex]["byteLength"] = triangleVertexNormals->size() * 3 * 3 * sizeof(float);
m_json["bufferViews"][bufferViewIndex]["target"] = 34962;
alignBin();
if (m_enableComment)
m_json["accessors"][bufferViewIndex]["__comment"] = QString("/accessors/%1: normal %2").arg(QString::number(bufferViewIndex)).arg(normalList.join(" ")).toUtf8().constData();
m_json["accessors"][bufferViewIndex]["bufferView"] = bufferViewIndex;
m_json["accessors"][bufferViewIndex]["byteOffset"] = 0;
m_json["accessors"][bufferViewIndex]["componentType"] = 5126;
m_json["accessors"][bufferViewIndex]["count"] = triangleVertexNormals->size() * 3;
m_json["accessors"][bufferViewIndex]["type"] = "VEC3";
bufferViewIndex++;
}

if (m_outputUv) {
bufferViewFromOffset = (int)m_binByteArray.size();
m_json["bufferViews"][bufferViewIndex]["buffer"] = 0;
m_json["bufferViews"][bufferViewIndex]["byteOffset"] = bufferViewFromOffset;
for (const auto& uvs : (*triangleVertexUvs)) {
for (const auto& it : uvs)
binStream << (float)it.x() << (float)it.y();
}
m_json["bufferViews"][bufferViewIndex]["byteLength"] = m_binByteArray.size() - bufferViewFromOffset;
alignBin();
if (m_enableComment)
m_json["accessors"][bufferViewIndex]["__comment"] = QString("/accessors/%1: uv").arg(QString::number(bufferViewIndex)).toUtf8().constData();
m_json["accessors"][bufferViewIndex]["bufferView"] = bufferViewIndex;
m_json["accessors"][bufferViewIndex]["byteOffset"] = 0;
m_json["accessors"][bufferViewIndex]["componentType"] = 5126;
m_json["accessors"][bufferViewIndex]["count"] = triangleVertexUvs->size() * 3;
m_json["accessors"][bufferViewIndex]["type"] = "VEC2";
bufferViewIndex++;
}
}

m_json["samplers"][0]["magFilter"] = 9729;
m_json["samplers"][0]["minFilter"] = 9987;
m_json["samplers"][0]["wrapS"] = 33648;
m_json["samplers"][0]["wrapT"] = 33648;

int imageIndex = 0;
int textureIndex = 0;

// Handle texture images (save as separate PNG files)
if (nullptr != textureImage) {
m_json["textures"][textureIndex]["sampler"] = 0;
m_json["textures"][textureIndex]["source"] = imageIndex;

QString textureFilename = m_baseName + "_baseColor.png";
textureImage->save(QFileInfo(m_filename).absolutePath() + "/" + textureFilename);
m_json["images"][imageIndex]["uri"] = textureFilename.toUtf8().constData();

imageIndex++;
textureIndex++;
}
if (nullptr != normalImage) {
m_json["textures"][textureIndex]["sampler"] = 0;
m_json["textures"][textureIndex]["source"] = imageIndex;

QString normalFilename = m_baseName + "_normal.png";
normalImage->save(QFileInfo(m_filename).absolutePath() + "/" + normalFilename);
m_json["images"][imageIndex]["uri"] = normalFilename.toUtf8().constData();

imageIndex++;
textureIndex++;
}
if (nullptr != ormImage) {
m_json["textures"][textureIndex]["sampler"] = 0;
m_json["textures"][textureIndex]["source"] = imageIndex;

QString ormFilename = m_baseName + "_orm.png";
ormImage->save(QFileInfo(m_filename).absolutePath() + "/" + ormFilename);
m_json["images"][imageIndex]["uri"] = ormFilename.toUtf8().constData();

imageIndex++;
textureIndex++;
}

m_json["buffers"][0]["byteLength"] = m_binByteArray.size();
m_json["buffers"][0]["uri"] = (m_baseName + ".bin").toUtf8().constData();
}

void GltfFileWriter::saveJsonFile()
{
QFile jsonFile(m_filename);
if (jsonFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream stream(&jsonFile);
stream << QString::fromStdString(m_json.dump(2));
}
}

void GltfFileWriter::saveBinFile()
{
QString binFilename = QFileInfo(m_filename).absolutePath() + "/" + m_baseName + ".bin";
QFile binFile(binFilename);
if (binFile.open(QIODevice::WriteOnly)) {
binFile.write(m_binByteArray);
}
}

bool GltfFileWriter::save()
{
saveJsonFile();
saveBinFile();
return true;
}
Loading