1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "materialadapter.h"
5
6#include <QtQuick3D/private/qquick3dshaderutils_p.h>
7#include <QtQuick3D/private/qquick3dobject_p.h>
8
9#include <QtQuick3DRuntimeRender/private/qssgrendershadercache_p.h>
10#include <QtQuick3DRuntimeRender/private/qssgrendercustommaterial_p.h>
11
12#include <QtCore/qdebug.h>
13#include <QtCore/qdir.h>
14#include <QtCore/QDataStream>
15
16#include <QtCore/qbuffer.h>
17
18#include <QtQuick3DAssetUtils/private/qssgrtutilities_p.h>
19#include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
20
21#include <QtGui/QImageReader>
22
23#include "uniformmodel.h"
24
25QT_BEGIN_NAMESPACE
26
27using namespace Qt::StringLiterals;
28
29namespace {
30class CustomMaterialExposed : public QQuick3DCustomMaterial
31{
32public:
33 using Dirty = QQuick3DCustomMaterial::Dirty;
34 using QQuick3DCustomMaterial::markDirty;
35 CustomMaterialExposed() = delete;
36};
37}
38
39enum class ShaderType
40{
41 Vertex,
42 Fragment
43};
44
45static QString getScheme() { return u"q3dres"_s; }
46static QString getUserType() { return u"material"_s; }
47static constexpr QStringView fileSuffix(ShaderType type) { return (type == ShaderType::Vertex) ? u".vert" : u".frag"; }
48
49static QUrl defaultShaderUrl(ShaderType type)
50{
51 return QUrl(getScheme() + "://material@editor" + fileSuffix(type).toString());
52}
53
54using BuilderPtr = QPointer<MaterialAdapter>;
55Q_GLOBAL_STATIC(BuilderPtr, builderInstance);
56
57[[nodiscard]] static BuildMessage parseErrorMessage(const QString &errorMsg)
58{
59 // "ERROR: :1: '' : syntax error, unexpected IDENTIFIER"
60 const QString head = QString::fromLatin1(ba: "ERROR:");
61 qint32 lineNr = -1;
62 qint32 columnNr = -1;
63 QString identifier;
64 auto msg = errorMsg;
65 if (errorMsg.startsWith(s: head)) {
66 auto pos = head.size();
67 auto idx = errorMsg.indexOf(c: u':', from: pos);
68 if (idx > pos && idx < pos + 16 /* sanity check */) {
69 pos = idx;
70 idx = errorMsg.indexOf(c: u':', from: pos + 1);
71 // Line nr
72 if (idx > pos && idx < pos + 6 /* sanity check */) {
73 auto mid = errorMsg.mid(position: pos + 1, n: idx - pos - 1);
74 bool ok = false;
75 auto v = mid.toInt(ok: &ok);
76 if (ok) {
77 lineNr = v;
78 pos = idx;
79 }
80
81 // check if we have a symbol (this might be empty)
82 idx = errorMsg.indexOf(c: u'\'', from: pos + 1);
83 if (idx > pos) {
84 pos = idx;
85 idx = errorMsg.indexOf(c: u'\'', from: pos + 1);
86 if (idx > pos && idx > pos + 1)
87 identifier = errorMsg.mid(position: pos + 1, n: idx - pos - 1);
88 }
89
90 // Find the message
91 idx = errorMsg.indexOf(c: u':', from: pos + 1);
92 if (idx > pos)
93 msg = errorMsg.mid(position: idx + 1).trimmed();
94 }
95 }
96 }
97 return BuildMessage{ .message: msg, .identifier: identifier, .line: lineNr, .column: columnNr, .status: BuildMessage::Status::Error };
98}
99
100// NOTE: We're being called from the render thread here...
101void MaterialAdapter::bakerStatusCallback(const QByteArray &descKey, QtQuick3DEditorHelpers::ShaderBaker::Status status, const QString &err, QShader::Stage stage)
102{
103 (void)descKey;
104 if (auto that = (*builderInstance)) {
105 using namespace QtQuick3DEditorHelpers::ShaderBaker;
106 if (status == Status::Success) {
107 if (stage == QShader::Stage::VertexStage) {
108 auto fileName = (!that->m_vertexUrl.isEmpty()) ? that->m_vertexUrl.path() : QLatin1String("<VERT_BUFFER>");
109 that->m_vertexMsg= { BuildMessage{}, fileName, ShaderBuildMessage::Stage::Vertex };
110 Q_EMIT that->vertexStatusChanged();
111 } else {
112 auto fileName = (!that->m_fragUrl.isEmpty()) ? that->m_fragUrl.path() : QLatin1String("<FRAG_BUFFER>");
113 that->m_fragmentMsg = { BuildMessage{}, fileName, ShaderBuildMessage::Stage::Fragment };
114 Q_EMIT that->fragmentStatusChanged();
115 }
116 } else if (status == Status::Error) {
117 const auto errList = err.split(sep: u'\n');
118 if (errList.size() > 0) {
119 auto statusMessage = parseErrorMessage(errorMsg: errList.first());
120 if (stage == QShader::Stage::VertexStage) {
121 auto fileName = (!that->m_vertexUrl.isEmpty()) ? that->m_vertexUrl.path() : QLatin1String("<VERT_BUFFER>");
122 that->m_vertexMsg = { statusMessage, fileName, ShaderBuildMessage::Stage::Vertex };
123 Q_EMIT that->vertexStatusChanged();
124 } else {
125 auto fileName = (!that->m_fragUrl.isEmpty()) ? that->m_fragUrl.path() : QLatin1String("<FRAG_BUFFER>");
126 that->m_fragmentMsg = { statusMessage, fileName, ShaderBuildMessage::Stage::Fragment };
127 Q_EMIT that->fragmentStatusChanged();
128 }
129 #if 0
130 const auto shaderUrl = (stage == QShader::Stage::VertexStage) ? that->m_vertexUrl : that->m_fragUrl;
131 qDebug() << shaderUrl.host() << "=>" << that->m_error;
132 #endif
133 }
134 } else {
135 Q_UNREACHABLE();
136 }
137 }
138}
139
140// NOTE: Called from the sync phase.
141static bool resolveShader(const QUrl &url, const QQmlContext *context, QByteArray &shaderData, QByteArray &shaderPathKey)
142{
143 Q_UNUSED(context);
144 Q_UNUSED(shaderPathKey);
145 if (auto that = (*builderInstance)) {
146 if (url.scheme() == getScheme() && url.userInfo() == getUserType()) {
147 const auto filenName = url.host();
148 if (filenName.endsWith(s: fileSuffix(type: ShaderType::Fragment))) {
149 shaderData = that->fragmentShader().toUtf8();
150 return true;
151 }
152
153 if (filenName.endsWith(s: fileSuffix(type: ShaderType::Vertex))) {
154 shaderData = that->vertexShader().toUtf8();
155 return true;
156 }
157 } else {
158 const QUrl loadUrl = context ? context->resolvedUrl(url) : url;
159 const auto path = (loadUrl.scheme() == u"qrc") ? QDir::currentPath() + loadUrl.path()
160 : loadUrl.path();
161 QFile f(path);
162 if (f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
163 shaderData = f.readAll();
164 if (path.endsWith(s: fileSuffix(type: ShaderType::Vertex)))
165 that->setVertexShader(shaderData);
166 else if (path.endsWith(s: fileSuffix(type: ShaderType::Fragment)))
167 that->setFragmentShader(shaderData);
168 return true;
169 }
170 }
171 }
172
173 return false;
174}
175
176void MaterialAdapter::updateShader(QQuick3DMaterial &target)
177{
178 if (QQuick3DObjectPrivate::get(item: &target)->type == QQuick3DObjectPrivate::Type::CustomMaterial) {
179 QQuick3DCustomMaterial &material = static_cast<QQuick3DCustomMaterial &>(target);
180 // We mark the material as dirty, this will trigger the material to reload the
181 // shader source file, which then again will trigger our resolveShader() function.
182 CustomMaterialExposed::markDirty(that&: material, type: CustomMaterialExposed::Dirty::ShaderSettingsDirty);
183 CustomMaterialExposed::markDirty(that&: material, type: CustomMaterialExposed::Dirty::DynamicPropertiesDirty);
184 }
185}
186
187void MaterialAdapter::updateMaterialDescription(CustomMaterial::Shaders shaders)
188{
189 // TODO: We might need to make some more clean-up of textures and front-end nodes
190 // that are now replaced, but leaving as-is for now.
191 auto oldMaterial = m_material;
192 if (m_rootNode != nullptr) {
193 if (auto v = m_materialDescr.create(parent&: *m_rootNode, uniforms: uniformTable, properties: m_properties, shaders)) {
194 m_material = v;
195 CustomMaterialExposed::markDirty(that&: *m_material, type: CustomMaterialExposed::Dirty::ShaderSettingsDirty);
196 CustomMaterialExposed::markDirty(that&: *m_material, type: CustomMaterialExposed::Dirty::DynamicPropertiesDirty);
197 Q_EMIT materialChanged();
198 }
199 }
200}
201
202void MaterialAdapter::updateMaterialDescription()
203{
204 updateMaterialDescription(shaders: { .vert: defaultShaderUrl(type: ShaderType::Vertex), .frag: defaultShaderUrl(type: ShaderType::Fragment) });
205}
206
207MaterialAdapter::MaterialAdapter(QObject *parent)
208 : QObject(parent)
209{
210 // NOTE (todo?): As-is this means there can only be one ShaderAdapter per process.
211 Q_ASSERT((*builderInstance).isNull());
212 (*builderInstance) = this;
213 QSSGShaderUtils::setResolveFunction(&resolveShader);
214 QtQuick3DEditorHelpers::ShaderBaker::setStatusCallback(&bakerStatusCallback);
215}
216
217QQuick3DCustomMaterial *MaterialAdapter::material() const
218{
219 return m_material;
220}
221
222QString MaterialAdapter::fragmentShader() const
223{
224 return m_fragmentShader;
225}
226
227void MaterialAdapter::setFragmentShader(const QString &newFragmentShader)
228{
229 if (m_fragmentShader == newFragmentShader)
230 return;
231
232 m_fragmentShader = newFragmentShader;
233 emit fragmentShaderChanged();
234 setUnsavedChanges(true);
235
236 if (m_material)
237 updateShader(target&: *m_material);
238}
239
240QString MaterialAdapter::vertexShader() const
241{
242 return m_vertexShader;
243}
244
245void MaterialAdapter::setVertexShader(const QString &newVertexShader)
246{
247 if (m_vertexShader == newVertexShader)
248 return;
249
250 m_vertexShader = newVertexShader;
251 emit vertexShaderChanged();
252 setUnsavedChanges(true);
253
254 if (m_material)
255 updateShader(target&: *m_material);
256}
257
258ShaderBuildMessage MaterialAdapter::vertexStatus() const
259{
260 return m_vertexMsg;
261}
262
263ShaderBuildMessage MaterialAdapter::fragmentStatus() const
264{
265 return m_fragmentMsg;
266}
267
268QString MaterialAdapter::importShader(const QUrl &shaderFile)
269{
270 QString shaderContents;
271 QFile file = resolveFileFromUrl(fileUrl: shaderFile);
272 if (file.open(flags: QIODevice::ReadOnly | QIODevice::Text))
273 shaderContents = file.readAll();
274 else
275 qWarning() << "Could not open shader file: " << file.fileName();
276
277
278 return shaderContents;
279}
280
281QFile MaterialAdapter::resolveFileFromUrl(const QUrl &fileUrl)
282{
283 const QQmlContext *context = qmlContext(this);
284 const auto resolvedUrl = context ? context->resolvedUrl(fileUrl) : fileUrl;
285 const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
286
287 QFileInfo fileInfo(qmlSource);
288 QString filePath = fileInfo.canonicalFilePath();
289 if (filePath.isEmpty())
290 filePath = fileInfo.absoluteFilePath();
291 return QFile(filePath);
292}
293
294void MaterialAdapter::importFragmentShader(const QUrl &shaderFile)
295{
296 setFragmentShader(importShader(shaderFile));
297}
298
299void MaterialAdapter::importVertexShader(const QUrl &shaderFile)
300{
301 setVertexShader(importShader(shaderFile));
302}
303
304bool MaterialAdapter::save()
305{
306 if (!m_materialSaveFile.isEmpty())
307 return saveMaterial(materialFile: m_materialSaveFile);
308 return false;
309}
310
311static const quint32 MATERIAL_MAGIC = 3365961549;
312static const quint32 MATERIAL_VERSION = 1;
313
314bool MaterialAdapter::saveMaterial(const QUrl &materialFile)
315{
316 auto saveFile = resolveFileFromUrl(fileUrl: materialFile);
317 if (saveFile.open(flags: QIODevice::WriteOnly)) {
318 QDataStream out(&saveFile);
319 out.setByteOrder(QDataStream::LittleEndian);
320 out.setFloatingPointPrecision(QDataStream::SinglePrecision);
321 out.setVersion(QDataStream::Qt_6_3);
322 out << MATERIAL_MAGIC << MATERIAL_VERSION;
323 out << m_vertexShader;
324 out << m_fragmentShader;
325 out << int(m_material->srcBlend());
326 out << int(m_material->dstBlend());
327 out << int(m_material->cullMode());
328 out << int(m_material->depthDrawMode());
329 out << int(m_material->shadingMode());
330 // Uniforms
331 out << uniformTable.size();
332 for (const auto &uniform : std::as_const(t&: uniformTable))
333 out << uniform;
334 } else {
335 emit errorOccurred();
336 return false;
337 }
338
339 setUnsavedChanges(false);
340 setMaterialSaveFile(materialFile);
341 emit postMaterialSaved();
342 return true;
343}
344
345bool MaterialAdapter::loadMaterial(const QUrl &materialFile)
346{
347 auto loadFile = resolveFileFromUrl(fileUrl: materialFile);
348 if (loadFile.open(flags: QIODevice::ReadOnly)) {
349 QDataStream in(&loadFile);
350 in.setByteOrder(QDataStream::LittleEndian);
351 in.setFloatingPointPrecision(QDataStream::SinglePrecision);
352 in.setVersion(QDataStream::Qt_6_3);
353 quint32 magic = 0;
354 quint32 version = 0;
355 in >> magic >> version;
356 if (magic != MATERIAL_MAGIC && version < MATERIAL_VERSION)
357 return false;
358 QString vertexShader;
359 QString fragmentShader;
360 in >> vertexShader >> fragmentShader;
361 setVertexShader(vertexShader);
362 setFragmentShader(fragmentShader);
363 int sourceBlend;
364 int destBlend;
365 int cullMode;
366 int depthDrawMode;
367 int shadingMode;
368 in >> sourceBlend >> destBlend >> cullMode >> depthDrawMode >> shadingMode;
369 m_material->setSrcBlend(QQuick3DCustomMaterial::BlendMode(sourceBlend));
370 m_material->setDstBlend(QQuick3DCustomMaterial::BlendMode(destBlend));
371 m_material->setCullMode(QQuick3DMaterial::CullMode(cullMode));
372 m_material->setDepthDrawMode(QQuick3DMaterial::DepthDrawMode(depthDrawMode));
373 m_material->setShadingMode(QQuick3DCustomMaterial::ShadingMode(shadingMode));
374 // Uniforms
375 qsizetype uniformsCount = 0;
376 in >> uniformsCount;
377 uniformTable.clear();
378 for (qsizetype i = 0; i < uniformsCount; ++i) {
379 CustomMaterial::Uniform uniform = { };
380 in >> uniform;
381 uniformTable.append(t: uniform);
382 }
383 // We have a new table, so update the table model
384 if (m_uniformModel) {
385 m_uniformModel->setModelData(&uniformTable);
386 updateMaterialDescription();
387 }
388 // Set filename to loaded one
389 setMaterialSaveFile(materialFile);
390 } else {
391 return false;
392 }
393
394 setUnsavedChanges(false);
395
396 return true;
397}
398
399bool MaterialAdapter::exportQmlComponent(const QUrl &componentFile, const QString &vertName, const QString &fragName)
400{
401 QFileInfo fi(componentFile.path());
402 auto filename = fi.fileName();
403 if (filename.isEmpty())
404 return false;
405
406 // Some sanity checks
407 // Ensure the component starts with an upper-case letter.
408 const auto firstLetter = filename.at(i: 0);
409 if (!firstLetter.isLetter()) {
410 qWarning() << "Component name needs to start with an upper-case letter!";
411 return false;
412 }
413
414 // Assume this is what the user wanted and fix it now.
415 if (firstLetter.isLower()) {
416 qWarning() << "Component name needs to start with an upper-case letter!";
417 filename[0] = firstLetter.toUpper();
418 }
419
420 static const auto saveShader = [](const QDir &dir, const QString &filename, const QString &text) {
421 const QString savePath = dir.path() + QDir::separator() + filename;
422 auto saveFile = QFile(savePath);
423 bool ret = false;
424 if (saveFile.open(flags: QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
425 QTextStream out(&saveFile);
426 out << text;
427 ret = true;
428 } else {
429 qWarning(msg: "Unable to open \'%s\' for writing", qPrintable(savePath));
430 }
431
432 return ret;
433 };
434
435 static const auto relShaderUrl = [](const QString &name, ShaderType type) {
436 QString relPath;
437 if (name.size() > 0) {
438 auto suffix = fileSuffix(type);
439 if (!name.endsWith(s: suffix))
440 relPath = name + suffix.toString();
441 else
442 relPath = name;
443 }
444
445 return QUrl(relPath);
446 };
447
448 bool ret = false;
449 const auto &dir = fi.dir();
450 auto dirPath = dir.path();
451 if (!dirPath.isEmpty()) {
452 if (m_materialDescr.isValid()) {
453 // NOTE: Relative paths. The shaders are exported with the component and we assume they live in the same location.
454 CustomMaterial::Shaders shaders = { .vert: relShaderUrl(vertName, ShaderType::Vertex), .frag: relShaderUrl(fragName, ShaderType::Fragment) };
455 const bool vertShaderOk = (m_vertexShader.size() > 0) ? saveShader(dir, shaders.vert.fileName(), m_vertexShader) : true;
456 const bool fragShaderOk = (m_fragmentShader.size() > 0) ? saveShader(dir, shaders.frag.fileName(), m_fragmentShader) : true;
457 if (vertShaderOk && fragShaderOk) {
458 updateMaterialDescription(shaders);
459 auto saveFile = QFile(dirPath + QDir::separator() + filename);
460 if (saveFile.open(flags: QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
461 const auto orgPath = QDir::current().path();
462 QDir::setCurrent(dirPath);
463 QTextStream out(&saveFile);
464 out << m_materialDescr;
465 QDir::setCurrent(orgPath);
466 ret = true;
467 }
468 } else {
469 emit errorOccurred();
470 ret = false;
471 }
472
473 // Re-set the shader urls
474 updateMaterialDescription();
475 }
476 }
477
478 return ret;
479}
480
481void MaterialAdapter::reset()
482{
483 m_properties = CustomMaterial::Properties{};
484
485 if (m_material == nullptr)
486 return;
487
488 delete m_material;
489
490 uniformTable = {};
491
492 if (m_uniformModel)
493 m_uniformModel->setModelData(&uniformTable);
494
495 // Set Default Shader Templates
496 setFragmentShader(QString());
497 setVertexShader(QString());
498
499 updateMaterialDescription();
500}
501
502UniformModel *MaterialAdapter::uniformModel() const
503{
504 return m_uniformModel;
505}
506
507bool MaterialAdapter::unsavedChanges() const
508{
509 return m_unsavedChanges;
510}
511
512void MaterialAdapter::setUnsavedChanges(bool newUnsavedChanges)
513{
514 if (m_unsavedChanges == newUnsavedChanges)
515 return;
516 m_unsavedChanges = newUnsavedChanges;
517 emit unsavedChangesChanged();
518}
519
520const QUrl &MaterialAdapter::materialSaveFile() const
521{
522 return m_materialSaveFile;
523}
524
525void MaterialAdapter::setMaterialSaveFile(const QUrl &newMaterialSaveFile)
526{
527 if (m_materialSaveFile == newMaterialSaveFile)
528 return;
529 m_materialSaveFile = newMaterialSaveFile;
530 emit materialSaveFileChanged();
531}
532
533QQuick3DNode *MaterialAdapter::rootNode() const
534{
535 return m_rootNode;
536}
537
538void MaterialAdapter::setRootNode(QQuick3DNode *newResourceNode)
539{
540 if (m_rootNode == newResourceNode)
541 return;
542 m_rootNode = newResourceNode;
543 emit rootNodeChanged();
544
545 updateMaterialDescription();
546}
547
548MaterialAdapter::CullMode MaterialAdapter::cullMode() const
549{
550 return m_properties.cullMode;
551}
552
553void MaterialAdapter::setCullMode(CullMode newCullMode)
554{
555 if (m_properties.cullMode == newCullMode)
556 return;
557 m_properties.cullMode = newCullMode;
558 emit cullModeChanged();
559
560 updateMaterialDescription();
561}
562
563MaterialAdapter::DepthDrawMode MaterialAdapter::depthDrawMode() const
564{
565 return m_properties.depthDrawMode;
566}
567
568void MaterialAdapter::setDepthDrawMode(DepthDrawMode newDepthDrawMode)
569{
570 if (m_properties.depthDrawMode == newDepthDrawMode)
571 return;
572 m_properties.depthDrawMode = newDepthDrawMode;
573 emit depthDrawModeChanged();
574
575 updateMaterialDescription();
576}
577
578MaterialAdapter::ShadingMode MaterialAdapter::shadingMode() const
579{
580 return m_properties.shadingMode;
581}
582
583void MaterialAdapter::setShadingMode(ShadingMode newShadingMode)
584{
585 if (m_properties.shadingMode == newShadingMode)
586 return;
587 m_properties.shadingMode = newShadingMode;
588 emit shadingModeChanged();
589
590 updateMaterialDescription();
591}
592
593MaterialAdapter::BlendMode MaterialAdapter::srcBlend() const
594{
595 return m_properties.sourceBlend;
596}
597
598void MaterialAdapter::setSrcBlend(BlendMode newSourceBlend)
599{
600 if (m_properties.sourceBlend == newSourceBlend)
601 return;
602 m_properties.sourceBlend = newSourceBlend;
603 emit srcBlendChanged();
604
605 updateMaterialDescription();
606}
607
608MaterialAdapter::BlendMode MaterialAdapter::dstBlend() const
609{
610 return m_properties.destinationBlend;
611}
612
613void MaterialAdapter::setDstBlend(BlendMode newDestinationBlend)
614{
615 if (m_properties.destinationBlend == newDestinationBlend)
616 return;
617 m_properties.destinationBlend = newDestinationBlend;
618 emit dstBlendChanged();
619
620 updateMaterialDescription();
621}
622
623void MaterialAdapter::setUniformModel(UniformModel *newUniformModel)
624{
625 m_uniformModel = newUniformModel;
626 if (m_uniformModel) {
627 m_uniformModel->setModelData(&uniformTable);
628 connect(sender: m_uniformModel, signal: &UniformModel::dataChanged, slot: [this]() {
629 updateMaterialDescription();
630 });
631 }
632 emit uniformModelChanged();
633}
634
635QString MaterialAdapter::getSupportedImageFormatsFilter() const
636{
637 auto formats = QImageReader::supportedImageFormats();
638 QString imageFilter = QStringLiteral("Image files (");
639 for (const auto &format : std::as_const(t&: formats))
640 imageFilter += QStringLiteral("*.") + format + QStringLiteral(" ");
641 imageFilter += QStringLiteral(")");
642 return imageFilter;
643}
644
645
646QT_END_NAMESPACE
647

source code of qtquick3d/tools/materialeditor/materialadapter.cpp