forked from qt-creator/qt-creator
With proper specifics sheet, the effect will show the available properties nicely in property view. Also introduced a new concept of FooBarSpecificsDynamic.qml to provide property specifics. The difference to regular FooBarSpecifics.qml file is that the dynamic version will be reloaded always, which allows us to change it at runtime. Fixes: QDS-11995 Change-Id: I744124182428c5c4607856e7970700d9a5e9972e Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io> Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io> Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
1922 lines
67 KiB
C++
1922 lines
67 KiB
C++
// Copyright (C) 2023 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
#include "effectcomposermodel.h"
|
|
|
|
#include "compositionnode.h"
|
|
#include "effectutils.h"
|
|
#include "propertyhandler.h"
|
|
#include "syntaxhighlighterdata.h"
|
|
#include "uniform.h"
|
|
|
|
#include <projectexplorer/projecttree.h>
|
|
#include <projectexplorer/target.h>
|
|
|
|
#include <qtsupport/qtkitaspect.h>
|
|
|
|
#include <utils/qtcassert.h>
|
|
#include <utils/process.h>
|
|
|
|
#include <modelnodeoperations.h>
|
|
|
|
#include <QByteArrayView>
|
|
#include <QLibraryInfo>
|
|
#include <QVector2D>
|
|
|
|
namespace EffectComposer {
|
|
|
|
enum class FileType
|
|
{
|
|
Binary,
|
|
Text
|
|
};
|
|
|
|
static bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType)
|
|
{
|
|
QDir().mkpath(QFileInfo(filename).path());
|
|
QFile f(filename);
|
|
QIODevice::OpenMode flags = QIODevice::WriteOnly | QIODevice::Truncate;
|
|
if (fileType == FileType::Text)
|
|
flags |= QIODevice::Text;
|
|
if (!f.open(flags)) {
|
|
qWarning() << "Failed to open file for writing:" << filename;
|
|
return false;
|
|
}
|
|
f.write(buf);
|
|
return true;
|
|
}
|
|
|
|
EffectComposerModel::EffectComposerModel(QObject *parent)
|
|
: QAbstractListModel{parent}
|
|
{
|
|
m_rebakeTimer.setSingleShot(true);
|
|
connect(&m_rebakeTimer, &QTimer::timeout, this, &EffectComposerModel::bakeShaders);
|
|
}
|
|
|
|
QHash<int, QByteArray> EffectComposerModel::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
roles[NameRole] = "nodeName";
|
|
roles[EnabledRole] = "nodeEnabled";
|
|
roles[UniformsRole] = "nodeUniformsModel";
|
|
roles[Dependency] = "isDependency";
|
|
return roles;
|
|
}
|
|
|
|
int EffectComposerModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent)
|
|
|
|
return m_nodes.count();
|
|
}
|
|
|
|
QVariant EffectComposerModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
QTC_ASSERT(index.isValid() && index.row() < m_nodes.size(), return {});
|
|
QTC_ASSERT(roleNames().contains(role), return {});
|
|
|
|
return m_nodes.at(index.row())->property(roleNames().value(role));
|
|
}
|
|
|
|
bool EffectComposerModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
{
|
|
if (!index.isValid() || !roleNames().contains(role))
|
|
return false;
|
|
|
|
if (role == EnabledRole) {
|
|
m_nodes.at(index.row())->setIsEnabled(value.toBool());
|
|
bakeShaders();
|
|
setHasUnsavedChanges(true);
|
|
|
|
emit dataChanged(index, index, {role});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void EffectComposerModel::setIsEmpty(bool val)
|
|
{
|
|
if (m_isEmpty != val) {
|
|
m_isEmpty = val;
|
|
emit isEmptyChanged();
|
|
|
|
if (m_isEmpty)
|
|
bakeShaders();
|
|
}
|
|
}
|
|
|
|
void EffectComposerModel::addNode(const QString &nodeQenPath)
|
|
{
|
|
beginResetModel();
|
|
auto *node = new CompositionNode({}, nodeQenPath);
|
|
connectCompositionNode(node);
|
|
|
|
const QList<QString> requiredNodes = node->requiredNodes();
|
|
if (requiredNodes.size() > 0) {
|
|
for (const QString &requiredId : requiredNodes) {
|
|
if (auto reqNode = findNodeById(requiredId)) {
|
|
reqNode->incRefCount();
|
|
continue;
|
|
}
|
|
|
|
const QString path = EffectUtils::nodesSourcesPath() + "/common/" + requiredId + ".qen";
|
|
auto requiredNode = new CompositionNode({}, path);
|
|
connectCompositionNode(requiredNode);
|
|
requiredNode->setRefCount(1);
|
|
m_nodes.prepend(requiredNode);
|
|
}
|
|
}
|
|
m_nodes.append(node);
|
|
endResetModel();
|
|
|
|
setIsEmpty(false);
|
|
|
|
bakeShaders();
|
|
setHasUnsavedChanges(true);
|
|
|
|
emit nodesChanged();
|
|
}
|
|
|
|
CompositionNode *EffectComposerModel::findNodeById(const QString &id) const
|
|
{
|
|
for (CompositionNode *node : std::as_const(m_nodes)) {
|
|
if (node->id() == id)
|
|
return node;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void EffectComposerModel::moveNode(int fromIdx, int toIdx)
|
|
{
|
|
if (fromIdx == toIdx)
|
|
return;
|
|
|
|
int toIdxAdjusted = fromIdx < toIdx ? toIdx + 1 : toIdx; // otherwise beginMoveRows() crashes
|
|
beginMoveRows({}, fromIdx, fromIdx, {}, toIdxAdjusted);
|
|
m_nodes.move(fromIdx, toIdx);
|
|
endMoveRows();
|
|
|
|
setHasUnsavedChanges(true);
|
|
bakeShaders();
|
|
}
|
|
|
|
void EffectComposerModel::removeNode(int idx)
|
|
{
|
|
beginResetModel();
|
|
m_rebakeTimer.stop();
|
|
CompositionNode *node = m_nodes.takeAt(idx);
|
|
|
|
const QStringList reqNodes = node->requiredNodes();
|
|
for (const QString &reqId : reqNodes) {
|
|
CompositionNode *depNode = findNodeById(reqId);
|
|
if (depNode && depNode->decRefCount() <= 0) {
|
|
m_nodes.removeOne(depNode);
|
|
delete depNode;
|
|
}
|
|
}
|
|
|
|
delete node;
|
|
endResetModel();
|
|
|
|
if (m_nodes.isEmpty())
|
|
setIsEmpty(true);
|
|
else
|
|
bakeShaders();
|
|
|
|
setHasUnsavedChanges(true);
|
|
emit nodesChanged();
|
|
}
|
|
|
|
void EffectComposerModel::clear(bool clearName)
|
|
{
|
|
beginResetModel();
|
|
m_rebakeTimer.stop();
|
|
qDeleteAll(m_nodes);
|
|
m_nodes.clear();
|
|
endResetModel();
|
|
|
|
if (clearName)
|
|
setCurrentComposition("");
|
|
|
|
setHasUnsavedChanges(!m_currentComposition.isEmpty());
|
|
|
|
setIsEmpty(true);
|
|
emit nodesChanged();
|
|
}
|
|
|
|
void EffectComposerModel::assignToSelected()
|
|
{
|
|
const QString effectsAssetsDir = QmlDesigner::ModelNodeOperations::getEffectsDefaultDirectory();
|
|
const QString path = effectsAssetsDir + QDir::separator() + m_currentComposition + ".qep";
|
|
emit assignToSelectedTriggered(path);
|
|
}
|
|
|
|
QString EffectComposerModel::getUniqueEffectName() const
|
|
{
|
|
const QString effectsDir = QmlDesigner::ModelNodeOperations::getEffectsDefaultDirectory();
|
|
const QString path = effectsDir + QDir::separator() + "Effect%1.qep";
|
|
|
|
int num = 0;
|
|
|
|
while (QFile::exists(path.arg(++num, 2, 10, QChar('0'))))
|
|
; // empty body
|
|
|
|
return QString("Effect%1").arg(num, 2, 10, QChar('0'));
|
|
}
|
|
|
|
QString EffectComposerModel::fragmentShader() const
|
|
{
|
|
return m_fragmentShader;
|
|
}
|
|
|
|
void EffectComposerModel::setFragmentShader(const QString &newFragmentShader)
|
|
{
|
|
if (m_fragmentShader == newFragmentShader)
|
|
return;
|
|
|
|
m_fragmentShader = newFragmentShader;
|
|
}
|
|
|
|
QString EffectComposerModel::vertexShader() const
|
|
{
|
|
return m_vertexShader;
|
|
}
|
|
|
|
void EffectComposerModel::setVertexShader(const QString &newVertexShader)
|
|
{
|
|
if (m_vertexShader == newVertexShader)
|
|
return;
|
|
|
|
m_vertexShader = newVertexShader;
|
|
}
|
|
|
|
QString EffectComposerModel::qmlComponentString() const
|
|
{
|
|
return m_qmlComponentString;
|
|
}
|
|
|
|
const QList<Uniform *> EffectComposerModel::allUniforms() const
|
|
{
|
|
QList<Uniform *> uniforms = {};
|
|
for (const auto &node : std::as_const(m_nodes))
|
|
uniforms.append(static_cast<EffectComposerUniformsModel *>(node->uniformsModel())->uniforms());
|
|
return uniforms;
|
|
}
|
|
|
|
const QString EffectComposerModel::getBufUniform()
|
|
{
|
|
QList<Uniform *> uniforms = allUniforms();
|
|
QString s;
|
|
s += "layout(std140, binding = 0) uniform buf {\n";
|
|
s += " mat4 qt_Matrix;\n";
|
|
s += " float qt_Opacity;\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time))
|
|
s += " float iTime;\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Frame))
|
|
s += " int iFrame;\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Resolution))
|
|
s += " vec3 iResolution;\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Mouse))
|
|
s += " vec4 iMouse;\n";
|
|
for (const auto uniform : uniforms) {
|
|
// TODO: Check if uniform is already added.
|
|
if (uniform->type() != Uniform::Type::Sampler && uniform->type() != Uniform::Type::Define) {
|
|
QString type = Uniform::stringFromType(uniform->type(), true);
|
|
QString props = " " + type + " " + uniform->name() + ";\n";
|
|
s += props;
|
|
}
|
|
}
|
|
s += "};\n";
|
|
return s;
|
|
}
|
|
|
|
const QString EffectComposerModel::getVSUniforms()
|
|
{
|
|
QString s;
|
|
s += "#version 440\n";
|
|
s += '\n';
|
|
s += "layout(location = 0) in vec4 qt_Vertex;\n";
|
|
s += "layout(location = 1) in vec2 qt_MultiTexCoord0;\n";
|
|
s += "layout(location = 0) out vec2 texCoord;\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::FragCoord))
|
|
s += "layout(location = 1) out vec2 fragCoord;\n";
|
|
s += '\n';
|
|
s += getBufUniform();
|
|
s += '\n';
|
|
s += "out gl_PerVertex { vec4 gl_Position; };\n";
|
|
s += '\n';
|
|
return s;
|
|
}
|
|
|
|
const QString EffectComposerModel::getFSUniforms()
|
|
{
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
QString s;
|
|
s += "#version 440\n";
|
|
s += '\n';
|
|
s += "layout(location = 0) in vec2 texCoord;\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::FragCoord))
|
|
s += "layout(location = 1) in vec2 fragCoord;\n";
|
|
s += "layout(location = 0) out vec4 fragColor;\n";
|
|
s += '\n';
|
|
s += getBufUniform();
|
|
s += '\n';
|
|
|
|
bool usesSource = m_shaderFeatures.enabled(ShaderFeatures::Source);
|
|
if (usesSource)
|
|
s += "layout(binding = 1) uniform sampler2D iSource;\n";
|
|
|
|
// Add sampler uniforms
|
|
int bindingIndex = usesSource ? 2 : 1;
|
|
for (const auto uniform : uniforms) {
|
|
// TODO: Check if uniform is already added.
|
|
if (uniform->type() == Uniform::Type::Sampler) {
|
|
// Start index from 2, 1 is source item
|
|
QString props = QString("layout(binding = %1) uniform sampler2D %2")
|
|
.arg(bindingIndex).arg(uniform->name());
|
|
s += props + ";\n";
|
|
bindingIndex++;
|
|
}
|
|
}
|
|
s += '\n';
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
|
|
const int blurItems = 5;
|
|
for (int i = 1; i <= blurItems; i++) {
|
|
QString props = QString("layout(binding = %1) uniform sampler2D iSourceBlur%2")
|
|
.arg(bindingIndex).arg(QString::number(i));
|
|
s += props + ";\n";
|
|
bindingIndex++;
|
|
}
|
|
s += '\n';
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Detects common GLSL error messages and returns potential
|
|
// additional error information related to them.
|
|
QString EffectComposerModel::detectErrorMessage(const QString &errorMessage)
|
|
{
|
|
static QHash<QString, QString> nodeErrors {
|
|
{ "'BLUR_HELPER_MAX_LEVEL' : undeclared identifier", "BlurHelper"},
|
|
{ "'iSourceBlur1' : undeclared identifier", "BlurHelper"},
|
|
{ "'hash23' : no matching overloaded function found", "NoiseHelper" },
|
|
{ "'HASH_BOX_SIZE' : undeclared identifier", "NoiseHelper" },
|
|
{ "'pseudo3dNoise' : no matching overloaded function found", "NoiseHelper" }
|
|
};
|
|
|
|
QString missingNodeError = QStringLiteral("Are you missing a %1 node?\n");
|
|
QHash<QString, QString>::const_iterator i = nodeErrors.constBegin();
|
|
while (i != nodeErrors.constEnd()) {
|
|
if (errorMessage.contains(i.key()))
|
|
return missingNodeError.arg(i.value());
|
|
++i;
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
// Return first error message (if any)
|
|
EffectError EffectComposerModel::effectError() const
|
|
{
|
|
for (const EffectError &e : std::as_const(m_effectErrors)) {
|
|
if (!e.m_message.isEmpty())
|
|
return e;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// Set the effect error message with optional type and lineNumber.
|
|
// Type comes from ErrorTypes, defaulting to common errors (-1).
|
|
// Note that type must match with UI editor tab index.
|
|
void EffectComposerModel::setEffectError(const QString &errorMessage, int type, int lineNumber)
|
|
{
|
|
EffectError error;
|
|
error.m_type = type;
|
|
if (type == 1 || type == 2) {
|
|
// For shaders, get the line number from baker output.
|
|
// Which is something like "ERROR: :15: message"
|
|
int glslErrorLineNumber = -1;
|
|
QStringList errorStringList = errorMessage.split(m_spaceReg, Qt::SkipEmptyParts);
|
|
if (errorStringList.size() >= 2) {
|
|
QString lineString = errorStringList.at(1).trimmed();
|
|
if (lineString.size() >= 3) {
|
|
// String is ":[linenumber]:", get only the number.
|
|
glslErrorLineNumber = lineString.sliced(1, lineString.size() - 2).toInt();
|
|
}
|
|
}
|
|
error.m_line = glslErrorLineNumber;
|
|
} else {
|
|
// For QML (and others) use given linenumber
|
|
error.m_line = lineNumber;
|
|
}
|
|
|
|
QString additionalErrorInfo = detectErrorMessage(errorMessage);
|
|
error.m_message = additionalErrorInfo + errorMessage;
|
|
m_effectErrors.insert(type, error);
|
|
qWarning() << QString("Effect error (line: %2): %1").arg(error.m_message, error.m_line);
|
|
Q_EMIT effectErrorChanged();
|
|
}
|
|
|
|
QString variantAsDataString(const Uniform::Type type, const Uniform::Type controlType, const QVariant &variant)
|
|
{
|
|
QString s;
|
|
switch (type) {
|
|
case Uniform::Type::Bool:
|
|
s = variant.toBool() ? QString("true") : QString("false");
|
|
break;
|
|
case Uniform::Type::Int:
|
|
s = QString::number(variant.toInt());
|
|
break;
|
|
case Uniform::Type::Float:
|
|
s = QString::number(variant.toDouble());
|
|
break;
|
|
case Uniform::Type::Vec2: {
|
|
QStringList list;
|
|
QVector2D v2 = variant.value<QVector2D>();
|
|
list << QString::number(v2.x());
|
|
list << QString::number(v2.y());
|
|
s = list.join(", ");
|
|
break;
|
|
}
|
|
case Uniform::Type::Vec3: {
|
|
QStringList list;
|
|
QVector3D v3 = variant.value<QVector3D>();
|
|
list << QString::number(v3.x());
|
|
list << QString::number(v3.y());
|
|
list << QString::number(v3.z());
|
|
s = list.join(", ");
|
|
break;
|
|
}
|
|
case Uniform::Type::Vec4: {
|
|
QStringList list;
|
|
QVector4D v4 = variant.value<QVector4D>();
|
|
list << QString::number(v4.x());
|
|
list << QString::number(v4.y());
|
|
list << QString::number(v4.z());
|
|
list << QString::number(v4.w());
|
|
s = list.join(", ");
|
|
break;
|
|
}
|
|
case Uniform::Type::Color: {
|
|
QStringList list;
|
|
QColor c = variant.value<QColor>();
|
|
list << QString::number(c.redF(), 'g', 3);
|
|
list << QString::number(c.greenF(), 'g', 3);
|
|
list << QString::number(c.blueF(), 'g', 3);
|
|
list << QString::number(c.alphaF(), 'g', 3);
|
|
s = list.join(", ");
|
|
break;
|
|
}
|
|
case Uniform::Type::Sampler:
|
|
case Uniform::Type::Define: {
|
|
if (controlType == Uniform::Type::Int)
|
|
s = QString::number(variant.toInt());
|
|
else if (controlType == Uniform::Type::Bool)
|
|
s = variant.toBool() ? QString("true") : QString("false");
|
|
else
|
|
s = variant.toString();
|
|
break;
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
QJsonObject nodeToJson(const CompositionNode &node)
|
|
{
|
|
QJsonObject nodeObject;
|
|
nodeObject.insert("name", node.name());
|
|
if (!node.description().isEmpty())
|
|
nodeObject.insert("description", node.description());
|
|
nodeObject.insert("enabled", node.isEnabled());
|
|
nodeObject.insert("version", 1);
|
|
nodeObject.insert("id", node.id());
|
|
|
|
// Add properties
|
|
QJsonArray propertiesArray;
|
|
const QList<Uniform *> uniforms = node.uniforms();
|
|
for (const Uniform *uniform : uniforms) {
|
|
QJsonObject uniformObject;
|
|
uniformObject.insert("name", QString(uniform->name()));
|
|
QString type = Uniform::stringFromType(uniform->type());
|
|
uniformObject.insert("type", type);
|
|
if (uniform->type() == Uniform::Type::Define) {
|
|
QString controlType = Uniform::stringFromType(uniform->controlType());
|
|
if (controlType != type)
|
|
uniformObject.insert("controlType", controlType);
|
|
}
|
|
|
|
if (!uniform->displayName().isEmpty())
|
|
uniformObject.insert("displayName", QString(uniform->displayName()));
|
|
|
|
QString value = variantAsDataString(uniform->type(), uniform->controlType(),
|
|
uniform->value());
|
|
if (uniform->type() == Uniform::Type::Sampler)
|
|
value = QFileInfo(value).fileName();
|
|
uniformObject.insert("value", value);
|
|
|
|
QString defaultValue = variantAsDataString(uniform->type(), uniform->controlType(),
|
|
uniform->defaultValue());
|
|
if (uniform->type() == Uniform::Type::Sampler) {
|
|
defaultValue = QFileInfo(value).fileName();
|
|
if (uniform->enableMipmap())
|
|
uniformObject.insert("enableMipmap", uniform->enableMipmap());
|
|
}
|
|
uniformObject.insert("defaultValue", defaultValue);
|
|
if (!uniform->description().isEmpty())
|
|
uniformObject.insert("description", uniform->description());
|
|
if (uniform->type() == Uniform::Type::Float
|
|
|| uniform->type() == Uniform::Type::Int
|
|
|| uniform->type() == Uniform::Type::Vec2
|
|
|| uniform->type() == Uniform::Type::Vec3
|
|
|| uniform->type() == Uniform::Type::Vec4
|
|
|| uniform->controlType() == Uniform::Type::Int) {
|
|
uniformObject.insert("minValue", variantAsDataString(uniform->type(),
|
|
uniform->controlType(),
|
|
uniform->minValue()));
|
|
uniformObject.insert("maxValue", variantAsDataString(uniform->type(),
|
|
uniform->controlType(),
|
|
uniform->maxValue()));
|
|
}
|
|
if (!uniform->customValue().isEmpty())
|
|
uniformObject.insert("customValue", uniform->customValue());
|
|
if (uniform->useCustomValue())
|
|
uniformObject.insert("useCustomValue", true);
|
|
|
|
propertiesArray.append(uniformObject);
|
|
}
|
|
if (!propertiesArray.isEmpty())
|
|
nodeObject.insert("properties", propertiesArray);
|
|
|
|
// Add shaders
|
|
if (!node.fragmentCode().trimmed().isEmpty()) {
|
|
QJsonArray fragmentCodeArray;
|
|
const QStringList fsLines = node.fragmentCode().split('\n');
|
|
for (const QString &line : fsLines)
|
|
fragmentCodeArray.append(line);
|
|
|
|
if (!fragmentCodeArray.isEmpty())
|
|
nodeObject.insert("fragmentCode", fragmentCodeArray);
|
|
}
|
|
if (!node.vertexCode().trimmed().isEmpty()) {
|
|
QJsonArray vertexCodeArray;
|
|
const QStringList vsLines = node.vertexCode().split('\n');
|
|
for (const QString &line : vsLines)
|
|
vertexCodeArray.append(line);
|
|
|
|
if (!vertexCodeArray.isEmpty())
|
|
nodeObject.insert("vertexCode", vertexCodeArray);
|
|
}
|
|
|
|
return nodeObject;
|
|
}
|
|
|
|
QString EffectComposerModel::getGeneratedMessage() const
|
|
{
|
|
QString s;
|
|
|
|
QString header {
|
|
R"(
|
|
// Created with Qt Design Studio (version %1), %2
|
|
// Do not manually edit this file, it will be overwritten if effect is modified in Qt Design Studio.
|
|
)"
|
|
};
|
|
|
|
s += header.arg(qApp->applicationVersion(), QDateTime::currentDateTime().toString());
|
|
return s;
|
|
}
|
|
|
|
QString EffectComposerModel::getDesignerSpecifics(const QString &name) const
|
|
{
|
|
QString s;
|
|
|
|
s += getGeneratedMessage();
|
|
|
|
s +=
|
|
R"(
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import HelperWidgets
|
|
import StudioTheme as StudioTheme
|
|
|
|
Column {
|
|
)";
|
|
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time)
|
|
|| m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
|
|
QString animSec =
|
|
R"(
|
|
Section {
|
|
caption: "%1"
|
|
width: parent.width
|
|
|
|
SectionLayout {
|
|
PropertyLabel {
|
|
text: "%2"
|
|
tooltip: "%3"
|
|
}
|
|
|
|
SecondColumnLayout {
|
|
CheckBox {
|
|
text: backendValues.timeRunning.valueToString
|
|
backendValue: backendValues.timeRunning
|
|
implicitWidth: StudioTheme.Values.twoControlColumnWidth
|
|
+ StudioTheme.Values.actionIndicatorWidth
|
|
}
|
|
ExpandingSpacer {}
|
|
}
|
|
)";
|
|
s += animSec.arg(tr("Animation"), tr("Running"), tr("Set this property to animate the effect."));
|
|
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time)) {
|
|
QString timeProp =
|
|
R"(
|
|
PropertyLabel {
|
|
text: "%1"
|
|
tooltip: "%2"
|
|
}
|
|
|
|
SecondColumnLayout {
|
|
SpinBox {
|
|
minimumValue: 0
|
|
maximumValue: 9999999
|
|
decimals: 2
|
|
stepSize: .01
|
|
backendValue: backendValues.animatedTime
|
|
implicitWidth: StudioTheme.Values.singleControlColumnWidth
|
|
+ StudioTheme.Values.actionIndicatorWidth
|
|
}
|
|
ExpandingSpacer {}
|
|
}
|
|
)";
|
|
s += timeProp.arg(tr("Time"), tr("This property allows explicit control of current animation time."));
|
|
}
|
|
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
|
|
QString frameProp =
|
|
R"(
|
|
PropertyLabel {
|
|
text: "%1"
|
|
tooltip: "%2"
|
|
}
|
|
|
|
SecondColumnLayout {
|
|
SpinBox {
|
|
minimumValue: 0
|
|
maximumValue: 99999999
|
|
decimals: 0
|
|
stepSize: 1
|
|
backendValue: backendValues.animatedFrame
|
|
implicitWidth: StudioTheme.Values.singleControlColumnWidth
|
|
+ StudioTheme.Values.actionIndicatorWidth
|
|
}
|
|
ExpandingSpacer {}
|
|
}
|
|
)";
|
|
s += frameProp.arg(tr("Frame"), tr("This property allows explicit control of current animation frame."));
|
|
}
|
|
s += " }\n";
|
|
s += " }\n";
|
|
}
|
|
|
|
for (const auto &node : std::as_const(m_nodes)) {
|
|
const QList<Uniform *> uniforms = static_cast<EffectComposerUniformsModel *>(
|
|
node->uniformsModel())->uniforms();
|
|
QString secStr =
|
|
R"(
|
|
Section {
|
|
caption: "%1"
|
|
width: parent.width
|
|
|
|
SectionLayout {
|
|
)";
|
|
secStr = secStr.arg(node->name());
|
|
|
|
const QString oldSecStr = secStr;
|
|
|
|
for (Uniform *uniform : uniforms)
|
|
secStr += uniform->getDesignerSpecifics();
|
|
|
|
// Only add the section if it has actual content
|
|
if (oldSecStr != secStr) {
|
|
secStr += " }\n";
|
|
secStr += " }\n";
|
|
s += secStr;
|
|
}
|
|
}
|
|
|
|
s += "}\n";
|
|
|
|
return s;
|
|
}
|
|
|
|
QString EffectComposerModel::getQmlEffectString()
|
|
{
|
|
QString s;
|
|
|
|
s += getGeneratedMessage();
|
|
|
|
// _isEffectItem is type var to hide it from property view
|
|
QString header {
|
|
R"(
|
|
import QtQuick
|
|
|
|
Item {
|
|
id: rootItem
|
|
|
|
// Use visible property to show and hide the effect.
|
|
visible: true
|
|
|
|
// This is an internal property used by tooling to identify effect items. Do not modify.
|
|
property bool _isEffectItem
|
|
|
|
// This is an internal property used to manage the effect. Do not modify.
|
|
property Item _oldParent: null
|
|
)"
|
|
};
|
|
|
|
s += header;
|
|
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Source)) {
|
|
s += " // This is the main source for the effect. Set internally to the current parent item. Do not modify.\n";
|
|
s += " property Item source: null\n";
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time)
|
|
|| m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
|
|
s += " // Enable this to animate iTime property\n";
|
|
s += " property bool timeRunning: true\n";
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time)) {
|
|
s += " // When timeRunning is false, this can be used to control iTime manually\n";
|
|
s += " property real animatedTime: frameAnimation.elapsedTime\n";
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
|
|
s += " // When timeRunning is false, this can be used to control iFrame manually\n";
|
|
s += " property int animatedFrame: frameAnimation.currentFrame\n";
|
|
}
|
|
|
|
QString parentChanged{
|
|
R"(
|
|
onParentChanged: {
|
|
if (_oldParent && _oldParent !== parent) {
|
|
_oldParent.layer.enabled = false
|
|
_oldParent.layer.effect = null
|
|
%2
|
|
_oldParent.update()
|
|
_oldParent = null
|
|
}
|
|
if (parent) {
|
|
_oldParent = parent
|
|
if (visible) {
|
|
parent.layer.enabled = true
|
|
parent.layer.effect = effectComponent
|
|
}
|
|
%1
|
|
}
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
parent.layer.enabled = true
|
|
parent.layer.effect = effectComponent
|
|
source = parent
|
|
} else {
|
|
parent.layer.enabled = false
|
|
parent.layer.effect = null
|
|
source = null
|
|
}
|
|
parent.update()
|
|
}
|
|
)"
|
|
};
|
|
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Mipmap)) {
|
|
QString mipmap1{
|
|
R"(parent.layer.smooth = true
|
|
parent.layer.mipmap = true
|
|
%1)"
|
|
};
|
|
QString mipmap2{
|
|
R"(_oldParent.layer.smooth = false
|
|
_oldParent.layer.mipmap = false
|
|
%2)"
|
|
};
|
|
parentChanged = parentChanged.arg(mipmap1, mipmap2);
|
|
}
|
|
|
|
parentChanged = parentChanged.arg(m_shaderFeatures.enabled(ShaderFeatures::Source)
|
|
? QString("source = parent") : QString(),
|
|
m_shaderFeatures.enabled(ShaderFeatures::Source)
|
|
? QString("source = null") : QString());
|
|
s += parentChanged;
|
|
|
|
// Custom properties
|
|
if (!m_exportedRootPropertiesString.isEmpty()) {
|
|
s += m_exportedRootPropertiesString;
|
|
s += '\n';
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time)
|
|
|| m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
|
|
s += " FrameAnimation {\n";
|
|
s += " id: frameAnimation\n";
|
|
s += " running: rootItem.timeRunning\n";
|
|
s += " }\n";
|
|
s += '\n';
|
|
}
|
|
|
|
QString customImagesString = getQmlImagesString(true);
|
|
if (!customImagesString.isEmpty())
|
|
s += customImagesString;
|
|
|
|
s += " Component {\n";
|
|
s += " id: effectComponent\n";
|
|
s += getQmlComponentString(true);
|
|
s += " }\n";
|
|
s += "}\n";
|
|
return s;
|
|
}
|
|
|
|
void EffectComposerModel::saveComposition(const QString &name)
|
|
{
|
|
if (name.isEmpty() || name.size() < 3 || name[0].isLower()) {
|
|
QString error = QString("Error: Couldn't save composition '%1', name is invalid").arg(name);
|
|
qWarning() << error;
|
|
return;
|
|
}
|
|
|
|
const QString effectsAssetsDir = QmlDesigner::ModelNodeOperations::getEffectsDefaultDirectory();
|
|
const QString path = effectsAssetsDir + QDir::separator() + name + ".qep";
|
|
auto saveFile = QFile(path);
|
|
if (!saveFile.open(QIODevice::WriteOnly)) {
|
|
QString error = QString("Error: Couldn't save composition file: '%1'").arg(path);
|
|
qWarning() << error;
|
|
return;
|
|
}
|
|
|
|
QJsonObject json;
|
|
// File format version
|
|
json.insert("version", 1);
|
|
|
|
// Add nodes
|
|
QJsonArray nodesArray;
|
|
for (const CompositionNode *node : std::as_const(m_nodes)) {
|
|
QJsonObject nodeObject = nodeToJson(*node);
|
|
nodesArray.append(nodeObject);
|
|
}
|
|
|
|
if (!nodesArray.isEmpty())
|
|
json.insert("nodes", nodesArray);
|
|
|
|
QJsonObject rootJson;
|
|
rootJson.insert("QEP", json);
|
|
QJsonDocument jsonDoc(rootJson);
|
|
|
|
saveFile.write(jsonDoc.toJson());
|
|
saveFile.close();
|
|
setCurrentComposition(name);
|
|
|
|
saveResources(name);
|
|
setHasUnsavedChanges(false);
|
|
}
|
|
|
|
void EffectComposerModel::openComposition(const QString &path)
|
|
{
|
|
clear(true);
|
|
|
|
const QString effectName = QFileInfo(path).baseName();
|
|
setCurrentComposition(effectName);
|
|
|
|
QFile compFile(path);
|
|
if (!compFile.open(QIODevice::ReadOnly)) {
|
|
QString error = QString("Couldn't open composition file: '%1'").arg(path);
|
|
qWarning() << qPrintable(error);
|
|
setEffectError(error);
|
|
return;
|
|
}
|
|
|
|
QByteArray data = compFile.readAll();
|
|
|
|
if (data.isEmpty())
|
|
return;
|
|
|
|
QJsonParseError parseError;
|
|
QJsonDocument jsonDoc(QJsonDocument::fromJson(data, &parseError));
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
QString error = QString("Error parsing the project file: %1").arg(parseError.errorString());
|
|
qWarning() << error;
|
|
setEffectError(error);
|
|
return;
|
|
}
|
|
QJsonObject rootJson = jsonDoc.object();
|
|
if (!rootJson.contains("QEP")) {
|
|
QString error = QStringLiteral("Error: Invalid project file");
|
|
qWarning() << error;
|
|
setEffectError(error);
|
|
return;
|
|
}
|
|
|
|
QJsonObject json = rootJson["QEP"].toObject();
|
|
|
|
int version = -1;
|
|
if (json.contains("version"))
|
|
version = json["version"].toInt(-1);
|
|
|
|
if (version != 1) {
|
|
QString error = QString("Error: Unknown project version (%1)").arg(version);
|
|
qWarning() << error;
|
|
setEffectError(error);
|
|
return;
|
|
}
|
|
|
|
if (json.contains("nodes") && json["nodes"].isArray()) {
|
|
beginResetModel();
|
|
QHash<QString, int> refCounts;
|
|
const QJsonArray nodesArray = json["nodes"].toArray();
|
|
|
|
for (const auto &nodeElement : nodesArray) {
|
|
auto *node = new CompositionNode(effectName, {}, nodeElement.toObject());
|
|
connectCompositionNode(node);
|
|
m_nodes.append(node);
|
|
const QStringList reqIds = node->requiredNodes();
|
|
for (const QString &reqId : reqIds)
|
|
++refCounts[reqId];
|
|
}
|
|
|
|
for (auto it = refCounts.cbegin(), end = refCounts.cend(); it != end; ++it) {
|
|
CompositionNode *depNode = findNodeById(it.key());
|
|
if (depNode)
|
|
depNode->setRefCount(it.value());
|
|
}
|
|
|
|
endResetModel();
|
|
|
|
setIsEmpty(m_nodes.isEmpty());
|
|
bakeShaders();
|
|
}
|
|
|
|
setHasUnsavedChanges(false);
|
|
emit nodesChanged();
|
|
}
|
|
|
|
void EffectComposerModel::saveResources(const QString &name)
|
|
{
|
|
// Make sure that uniforms are up-to-date
|
|
updateCustomUniforms();
|
|
|
|
QString qmlFilename = name + ".qml";
|
|
QString vsFilename = name + ".vert.qsb";
|
|
QString fsFilename = name + ".frag.qsb";
|
|
|
|
// Shaders should be all lowercase
|
|
vsFilename = vsFilename.toLower();
|
|
fsFilename = fsFilename.toLower();
|
|
|
|
// Get effects dir
|
|
const Utils::FilePath effectsResDir = QmlDesigner::ModelNodeOperations::getEffectsImportDirectory();
|
|
const QString effectsResPath = effectsResDir.pathAppended(name).toString() + QDir::separator();
|
|
Utils::FilePath effectPath = Utils::FilePath::fromString(effectsResPath);
|
|
|
|
// Create the qmldir for effects
|
|
QString qmldirFileName("qmldir");
|
|
Utils::FilePath qmldirPath = effectsResDir.resolvePath(qmldirFileName);
|
|
QString qmldirContent = QString::fromUtf8(qmldirPath.fileContents().value_or(QByteArray()));
|
|
if (qmldirContent.isEmpty()) {
|
|
qmldirContent.append("module Effects\n");
|
|
qmldirPath.writeFileContents(qmldirContent.toUtf8());
|
|
}
|
|
|
|
Utils::FilePaths oldFiles;
|
|
QStringList newFileNames;
|
|
|
|
// Create effect folder if not created
|
|
if (!effectPath.exists())
|
|
effectPath.createDir();
|
|
else
|
|
oldFiles = effectPath.dirEntries(QDir::Files);
|
|
|
|
// Create effect qmldir
|
|
newFileNames.append(qmldirFileName);
|
|
qmldirPath = effectPath.resolvePath(qmldirFileName);
|
|
qmldirContent = QString::fromUtf8(qmldirPath.fileContents().value_or(QByteArray()));
|
|
if (qmldirContent.isEmpty()) {
|
|
qmldirContent.append("module Effects.");
|
|
qmldirContent.append(name);
|
|
qmldirContent.append('\n');
|
|
qmldirContent.append(name);
|
|
qmldirContent.append(" 1.0 ");
|
|
qmldirContent.append(name);
|
|
qmldirContent.append(".qml\n");
|
|
qmldirPath.writeFileContents(qmldirContent.toUtf8());
|
|
}
|
|
|
|
// Create designer folder if not created
|
|
const Utils::FilePath designerPath = effectPath.pathAppended("designer");
|
|
if (!designerPath.exists())
|
|
designerPath.createDir();
|
|
|
|
// Create designer property sheet
|
|
// Since this is in subdir, no need to add it to newFileNames
|
|
QString specContent = getDesignerSpecifics(name);
|
|
QString specFileName("%1SpecificsDynamic.qml");
|
|
specFileName = specFileName.arg(name);
|
|
Utils::FilePath specPath = designerPath.resolvePath(specFileName);
|
|
specPath.writeFileContents(specContent.toUtf8());
|
|
|
|
// Create the qml file
|
|
QString qmlComponentString = getQmlEffectString();
|
|
QStringList qmlStringList = qmlComponentString.split('\n');
|
|
|
|
// Replace shaders with local versions
|
|
for (int i = 1; i < qmlStringList.size(); i++) {
|
|
QString line = qmlStringList.at(i).trimmed();
|
|
if (line.startsWith("vertexShader")) {
|
|
QString vsLine = " vertexShader: '" + vsFilename + "'";
|
|
qmlStringList[i] = vsLine;
|
|
} else if (line.startsWith("fragmentShader")) {
|
|
QString fsLine = " fragmentShader: '" + fsFilename + "'";
|
|
qmlStringList[i] = fsLine;
|
|
}
|
|
}
|
|
|
|
const QString qmlString = qmlStringList.join('\n');
|
|
QString qmlFilePath = effectsResPath + qmlFilename;
|
|
writeToFile(qmlString.toUtf8(), qmlFilePath, FileType::Text);
|
|
newFileNames.append(qmlFilename);
|
|
|
|
// Save shaders and images
|
|
QStringList sources = {m_vertexShaderFilename, m_fragmentShaderFilename};
|
|
QStringList dests = {vsFilename, fsFilename};
|
|
|
|
QHash<QString, Uniform *> fileNameToUniformHash;
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
bool hasSampler = false;
|
|
for (Uniform *uniform : uniforms) {
|
|
if (uniform->type() == Uniform::Type::Sampler && !uniform->value().toString().isEmpty()) {
|
|
QString imagePath = uniform->value().toString();
|
|
QFileInfo fi(imagePath);
|
|
QString imageFilename = fi.fileName();
|
|
if (imagePath.startsWith("file:")) {
|
|
QUrl url(imagePath);
|
|
imagePath = url.toLocalFile();
|
|
}
|
|
sources.append(imagePath);
|
|
dests.append(imageFilename);
|
|
fileNameToUniformHash.insert(imageFilename, uniform);
|
|
hasSampler = true;
|
|
}
|
|
}
|
|
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
|
|
QString blurHelperFilename("BlurHelper.qml");
|
|
QString blurFsFilename("bluritems.frag.qsb");
|
|
QString blurVsFilename("bluritems.vert.qsb");
|
|
QString blurHelperPath(EffectUtils::nodesSourcesPath() + "/common/");
|
|
sources.append(blurHelperPath + blurHelperFilename);
|
|
sources.append(blurHelperPath + blurFsFilename);
|
|
sources.append(blurHelperPath + blurVsFilename);
|
|
dests.append(blurHelperFilename);
|
|
dests.append(blurFsFilename);
|
|
dests.append(blurVsFilename);
|
|
}
|
|
|
|
for (int i = 0; i < sources.count(); ++i) {
|
|
Utils::FilePath source = Utils::FilePath::fromString(sources[i]);
|
|
Utils::FilePath target = Utils::FilePath::fromString(effectsResPath + dests[i]);
|
|
newFileNames.append(target.fileName());
|
|
if (target.exists() && source.fileName() != target.fileName())
|
|
target.removeFile(); // Remove existing file for update
|
|
if (!source.copyFile(target))
|
|
qWarning() << __FUNCTION__ << " Failed to copy file: " << source;
|
|
|
|
if (fileNameToUniformHash.contains(dests[i])) {
|
|
Uniform *uniform = fileNameToUniformHash[dests[i]];
|
|
const QVariant newValue = target.toString();
|
|
uniform->setDefaultValue(newValue);
|
|
uniform->setValue(newValue);
|
|
}
|
|
}
|
|
|
|
// Delete old content that was not overwritten
|
|
// We ignore subdirectories, as currently subdirs only contain fixed content
|
|
for (const Utils::FilePath &oldFile : oldFiles) {
|
|
if (!newFileNames.contains(oldFile.fileName()))
|
|
oldFile.removeFile();
|
|
}
|
|
|
|
// Refresh UI to update sampler UrlChoosers
|
|
if (hasSampler) {
|
|
beginResetModel();
|
|
endResetModel();
|
|
}
|
|
|
|
emit resourcesSaved(QString("Effects.%1.%1").arg(name).toUtf8(), effectPath);
|
|
}
|
|
|
|
void EffectComposerModel::resetEffectError(int type)
|
|
{
|
|
if (m_effectErrors.contains(type)) {
|
|
m_effectErrors.remove(type);
|
|
Q_EMIT effectErrorChanged();
|
|
}
|
|
}
|
|
|
|
// Get value in QML format that used for exports
|
|
QString EffectComposerModel::valueAsString(const Uniform &uniform)
|
|
{
|
|
if (uniform.type() == Uniform::Type::Bool) {
|
|
return uniform.value().toBool() ? QString("true") : QString("false");
|
|
} else if (uniform.type() == Uniform::Type::Int) {
|
|
return QString::number(uniform.value().toInt());
|
|
} else if (uniform.type() == Uniform::Type::Float) {
|
|
return QString::number(uniform.value().toDouble());
|
|
} else if (uniform.type() == Uniform::Type::Vec2) {
|
|
QVector2D v2 = uniform.value().value<QVector2D>();
|
|
return QString("Qt.point(%1, %2)").arg(v2.x()).arg(v2.y());
|
|
} else if (uniform.type() == Uniform::Type::Vec3) {
|
|
QVector3D v3 = uniform.value().value<QVector3D>();
|
|
return QString("Qt.vector3d(%1, %2, %3)").arg(v3.x()).arg(v3.y()).arg(v3.z());
|
|
} else if (uniform.type() == Uniform::Type::Vec4) {
|
|
QVector4D v4 = uniform.value().value<QVector4D>();
|
|
return QString("Qt.vector4d(%1, %2, %3, %4)").arg(v4.x()).arg(v4.y()).arg(v4.z()).arg(v4.w());
|
|
} else if (uniform.type() == Uniform::Type::Sampler) {
|
|
return getImageElementName(uniform, true);
|
|
} else if (uniform.type() == Uniform::Type::Color) {
|
|
return QString("\"%1\"").arg(uniform.value().toString());
|
|
} else if (uniform.type() == Uniform::Type::Define) {
|
|
if (uniform.controlType() == Uniform::Type::Int)
|
|
return QString::number(uniform.value().toInt());
|
|
else if (uniform.controlType() == Uniform::Type::Bool)
|
|
return uniform.value().toBool() ? QString("1") : QString("0");
|
|
return uniform.value().toString();
|
|
} else {
|
|
qWarning() << QString("Unhandled const variable type: %1").arg(int(uniform.type())).toLatin1();
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
// Get value in QML binding that used for previews
|
|
QString EffectComposerModel::valueAsBinding(const Uniform &uniform)
|
|
{
|
|
if (uniform.type() == Uniform::Type::Bool
|
|
|| uniform.type() == Uniform::Type::Int
|
|
|| uniform.type() == Uniform::Type::Float
|
|
|| uniform.type() == Uniform::Type::Color
|
|
|| uniform.type() == Uniform::Type::Define) {
|
|
return "g_propertyData." + uniform.name();
|
|
} else if (uniform.type() == Uniform::Type::Vec2) {
|
|
QString sx = QString("g_propertyData.%1.x").arg(uniform.name());
|
|
QString sy = QString("g_propertyData.%1.y").arg(uniform.name());
|
|
return QString("Qt.point(%1, %2)").arg(sx, sy);
|
|
} else if (uniform.type() == Uniform::Type::Vec3) {
|
|
QString sx = QString("g_propertyData.%1.x").arg(uniform.name());
|
|
QString sy = QString("g_propertyData.%1.y").arg(uniform.name());
|
|
QString sz = QString("g_propertyData.%1.z").arg(uniform.name());
|
|
return QString("Qt.vector3d(%1, %2, %3)").arg(sx, sy, sz);
|
|
} else if (uniform.type() == Uniform::Type::Vec4) {
|
|
QString sx = QString("g_propertyData.%1.x").arg(uniform.name());
|
|
QString sy = QString("g_propertyData.%1.y").arg(uniform.name());
|
|
QString sz = QString("g_propertyData.%1.z").arg(uniform.name());
|
|
QString sw = QString("g_propertyData.%1.w").arg(uniform.name());
|
|
return QString("Qt.vector4d(%1, %2, %3, %4)").arg(sx, sy, sz, sw);
|
|
} else if (uniform.type() == Uniform::Type::Sampler) {
|
|
return getImageElementName(uniform, false);
|
|
} else {
|
|
qWarning() << QString("Unhandled const variable type: %1").arg(int(uniform.type())).toLatin1();
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
// Get value in GLSL format that is used for non-exported const properties
|
|
QString EffectComposerModel::valueAsVariable(const Uniform &uniform)
|
|
{
|
|
if (uniform.type() == Uniform::Type::Bool) {
|
|
return uniform.value().toBool() ? QString("true") : QString("false");
|
|
} else if (uniform.type() == Uniform::Type::Int) {
|
|
return QString::number(uniform.value().toInt());
|
|
} else if (uniform.type() == Uniform::Type::Float) {
|
|
return QString::number(uniform.value().toDouble());
|
|
} else if (uniform.type() == Uniform::Type::Vec2) {
|
|
QVector2D v2 = uniform.value().value<QVector2D>();
|
|
return QString("vec2(%1, %2)").arg(v2.x(), v2.y());
|
|
} else if (uniform.type() == Uniform::Type::Vec3) {
|
|
QVector3D v3 = uniform.value().value<QVector3D>();
|
|
return QString("vec3(%1, %2, %3)").arg(v3.x(), v3.y(), v3.z());
|
|
} else if (uniform.type() == Uniform::Type::Vec4) {
|
|
QVector4D v4 = uniform.value().value<QVector4D>();
|
|
return QString("vec4(%1, %2, %3, %4)").arg(v4.x(), v4.y(), v4.z(), v4.w());
|
|
} else if (uniform.type() == Uniform::Type::Color) {
|
|
QColor c = uniform.value().value<QColor>();
|
|
return QString("vec4(%1, %2, %3, %4)").arg(c.redF(), c.greenF(), c.blueF(), c.alphaF());
|
|
} else {
|
|
qWarning() << QString("Unhandled const variable type: %1").arg(int(uniform.type())).toLatin1();
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
// Return name for the image property Image element
|
|
QString EffectComposerModel::getImageElementName(const Uniform &uniform, bool localFiles)
|
|
{
|
|
if (localFiles && uniform.value().toString().isEmpty())
|
|
return QStringLiteral("null");
|
|
QString simplifiedName = uniform.name().simplified();
|
|
simplifiedName = simplifiedName.remove(' ');
|
|
return QStringLiteral("imageItem") + simplifiedName;
|
|
}
|
|
|
|
const QString EffectComposerModel::getConstVariables()
|
|
{
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
QString s;
|
|
for (Uniform *uniform : uniforms) {
|
|
// TODO: Check if uniform is already added.
|
|
QString constValue = valueAsVariable(*uniform);
|
|
QString type = Uniform::stringFromType(uniform->type(), true);
|
|
s += QString("const %1 %2 = %3;\n").arg(type, uniform->name(), constValue);
|
|
}
|
|
if (!s.isEmpty())
|
|
s += '\n';
|
|
|
|
return s;
|
|
}
|
|
|
|
const QString EffectComposerModel::getDefineProperties()
|
|
{
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
QString s;
|
|
for (Uniform *uniform : uniforms) {
|
|
// TODO: Check if uniform is already added.
|
|
if (uniform->type() == Uniform::Type::Define)
|
|
s += QString("#define %1 %2\n").arg(uniform->name(), valueAsString(*uniform));
|
|
}
|
|
if (!s.isEmpty())
|
|
s += '\n';
|
|
|
|
return s;
|
|
}
|
|
|
|
int EffectComposerModel::getTagIndex(const QStringList &code, const QString &tag)
|
|
{
|
|
int index = -1;
|
|
int line = 0;
|
|
const QString tagString = QString("@%1").arg(tag);
|
|
for (const QString &s : code) {
|
|
auto st = s.trimmed();
|
|
// Check if line or first non-space content of the line matches to tag
|
|
static auto spaceReg = QRegularExpression("\\s");
|
|
auto firstSpace = st.indexOf(spaceReg);
|
|
QString firstWord = st;
|
|
if (firstSpace > 0)
|
|
firstWord = st.sliced(0, firstSpace);
|
|
if (firstWord == tagString) {
|
|
index = line;
|
|
break;
|
|
}
|
|
line++;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
QString EffectComposerModel::processVertexRootLine(const QString &line)
|
|
{
|
|
QString output;
|
|
QStringList lineList = line.split(m_spaceReg, Qt::SkipEmptyParts);
|
|
if (lineList.length() > 1 && lineList.at(0) == QStringLiteral("out")) {
|
|
lineList.removeFirst();
|
|
QString outLine = lineList.join(' ');
|
|
m_shaderVaryingVariables << outLine;
|
|
} else {
|
|
output = line + '\n';
|
|
}
|
|
return output;
|
|
}
|
|
|
|
QString EffectComposerModel::processFragmentRootLine(const QString &line)
|
|
{
|
|
QString output;
|
|
QStringList lineList = line.split(m_spaceReg, Qt::SkipEmptyParts);
|
|
// Just skip all "in" variables. It is enough to have "out" variable in vertex.
|
|
if (lineList.length() > 1 && lineList.at(0) == QStringLiteral("in"))
|
|
return QString();
|
|
output = line + '\n';
|
|
return output;
|
|
}
|
|
|
|
QStringList EffectComposerModel::getDefaultRootVertexShader()
|
|
{
|
|
if (m_defaultRootVertexShader.isEmpty()) {
|
|
m_defaultRootVertexShader << "void main() {";
|
|
m_defaultRootVertexShader << " texCoord = qt_MultiTexCoord0;";
|
|
m_defaultRootVertexShader << " fragCoord = qt_Vertex.xy;";
|
|
m_defaultRootVertexShader << " vec2 vertCoord = qt_Vertex.xy;";
|
|
m_defaultRootVertexShader << " @nodes";
|
|
m_defaultRootVertexShader << " gl_Position = qt_Matrix * vec4(vertCoord, 0.0, 1.0);";
|
|
m_defaultRootVertexShader << "}";
|
|
}
|
|
return m_defaultRootVertexShader;
|
|
}
|
|
|
|
QStringList EffectComposerModel::getDefaultRootFragmentShader()
|
|
{
|
|
if (m_defaultRootFragmentShader.isEmpty()) {
|
|
m_defaultRootFragmentShader << "void main() {";
|
|
m_defaultRootFragmentShader << " fragColor = texture(iSource, texCoord);";
|
|
m_defaultRootFragmentShader << " @nodes";
|
|
m_defaultRootFragmentShader << " fragColor = fragColor * qt_Opacity;";
|
|
m_defaultRootFragmentShader << "}";
|
|
}
|
|
return m_defaultRootFragmentShader;
|
|
}
|
|
|
|
// Remove all post-processing tags ("@tag") from the code.
|
|
// Except "@nodes" tag as that is handled later.
|
|
QStringList EffectComposerModel::removeTagsFromCode(const QStringList &codeLines)
|
|
{
|
|
QStringList s;
|
|
for (const QString &line : codeLines) {
|
|
const auto trimmedLine = line.trimmed();
|
|
if (!trimmedLine.startsWith('@') || trimmedLine.startsWith("@nodes")) {
|
|
s << line;
|
|
} else {
|
|
// Check if the tag is known
|
|
bool validTag = false;
|
|
const QList<QByteArrayView> tags = SyntaxHighlighterData::reservedTagNames();
|
|
QString firstWord = trimmedLine.split(m_spaceReg, Qt::SkipEmptyParts).first();
|
|
for (const QByteArrayView &tag : tags) {
|
|
if (firstWord == QString::fromUtf8(tag)) {
|
|
validTag = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!validTag)
|
|
setEffectError(QString("Unknown tag: %1").arg(trimmedLine), ErrorPreprocessor);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
QString EffectComposerModel::removeTagsFromCode(const QString &code)
|
|
{
|
|
QStringList codeLines = removeTagsFromCode(code.split('\n'));
|
|
return codeLines.join('\n');
|
|
}
|
|
|
|
QString EffectComposerModel::getCustomShaderVaryings(bool outState)
|
|
{
|
|
QString output;
|
|
QString direction = outState ? QStringLiteral("out") : QStringLiteral("in");
|
|
int varLocation = m_shaderFeatures.enabled(ShaderFeatures::FragCoord) ? 2 : 1;
|
|
for (const QString &var : std::as_const(m_shaderVaryingVariables)) {
|
|
output += QString("layout(location = %1) %2 %3\n").arg(QString::number(varLocation), direction, var);
|
|
varLocation++;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
QString EffectComposerModel::generateVertexShader(bool includeUniforms)
|
|
{
|
|
QString s;
|
|
|
|
if (includeUniforms)
|
|
s += getVSUniforms();
|
|
|
|
// Remove tags when not generating for features check
|
|
const bool removeTags = includeUniforms;
|
|
|
|
s += getDefineProperties();
|
|
// s += getConstVariables(); // Not sure yet, will check on this later
|
|
|
|
// When the node is complete, add shader code in correct nodes order
|
|
// split to root and main parts
|
|
QString s_root;
|
|
QString s_main;
|
|
QStringList s_sourceCode;
|
|
m_shaderVaryingVariables.clear();
|
|
for (const CompositionNode *n : std::as_const(m_nodes)) {
|
|
if (!n->vertexCode().isEmpty() && n->isEnabled()) {
|
|
const QStringList vertexCode = n->vertexCode().split('\n');
|
|
int mainIndex = getTagIndex(vertexCode, "main");
|
|
int line = 0;
|
|
for (const QString &ss : vertexCode) {
|
|
if (mainIndex == -1 || line > mainIndex)
|
|
s_main += " " + ss + '\n';
|
|
else if (line < mainIndex)
|
|
s_root += processVertexRootLine(ss);
|
|
line++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (s_sourceCode.isEmpty()) {
|
|
// If source nodes doesn't contain any code, use default one
|
|
s_sourceCode << getDefaultRootVertexShader();
|
|
}
|
|
|
|
if (removeTags) {
|
|
s_sourceCode = removeTagsFromCode(s_sourceCode);
|
|
s_root = removeTagsFromCode(s_root);
|
|
s_main = removeTagsFromCode(s_main);
|
|
}
|
|
|
|
s += getCustomShaderVaryings(true);
|
|
s += s_root + '\n';
|
|
|
|
int nodesIndex = getTagIndex(s_sourceCode, QStringLiteral("nodes"));
|
|
int line = 0;
|
|
for (const QString &ss : std::as_const(s_sourceCode))
|
|
s += (line++ == nodesIndex) ? s_main : ss + '\n';
|
|
|
|
return s;
|
|
}
|
|
|
|
QString EffectComposerModel::generateFragmentShader(bool includeUniforms)
|
|
{
|
|
QString s;
|
|
|
|
if (includeUniforms)
|
|
s += getFSUniforms();
|
|
|
|
// Remove tags when not generating for features check
|
|
const bool removeTags = includeUniforms;
|
|
|
|
s += getDefineProperties();
|
|
// s += getConstVariables(); // Not sure yet, will check on this later
|
|
|
|
// When the node is complete, add shader code in correct nodes order
|
|
// split to root and main parts
|
|
QString s_root;
|
|
QString s_main;
|
|
QStringList s_sourceCode;
|
|
for (const CompositionNode *n : std::as_const(m_nodes)) {
|
|
if (!n->fragmentCode().isEmpty() && n->isEnabled()) {
|
|
const QStringList fragmentCode = n->fragmentCode().split('\n');
|
|
int mainIndex = getTagIndex(fragmentCode, QStringLiteral("main"));
|
|
int line = 0;
|
|
for (const QString &ss : fragmentCode) {
|
|
if (mainIndex == -1 || line > mainIndex)
|
|
s_main += QStringLiteral(" ") + ss + '\n';
|
|
else if (line < mainIndex)
|
|
s_root += processFragmentRootLine(ss);
|
|
line++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (s_sourceCode.isEmpty()) {
|
|
// If source nodes doesn't contain any code, use default one
|
|
s_sourceCode << getDefaultRootFragmentShader();
|
|
}
|
|
|
|
if (removeTags) {
|
|
s_sourceCode = removeTagsFromCode(s_sourceCode);
|
|
s_root = removeTagsFromCode(s_root);
|
|
s_main = removeTagsFromCode(s_main);
|
|
}
|
|
|
|
s += getCustomShaderVaryings(false);
|
|
s += s_root + '\n';
|
|
|
|
int nodesIndex = getTagIndex(s_sourceCode, QStringLiteral("nodes"));
|
|
int line = 0;
|
|
for (const QString &ss : std::as_const(s_sourceCode))
|
|
s += (line++ == nodesIndex) ? s_main : ss + '\n';
|
|
|
|
return s;
|
|
}
|
|
|
|
void EffectComposerModel::handleQsbProcessExit(Utils::Process *qsbProcess, const QString &shader, bool preview)
|
|
{
|
|
--m_remainingQsbTargets;
|
|
|
|
const QString errStr = qsbProcess->errorString();
|
|
const QByteArray errStd = qsbProcess->readAllRawStandardError();
|
|
QString previewStr;
|
|
if (preview)
|
|
previewStr = QStringLiteral("preview");
|
|
|
|
if (!errStr.isEmpty()) {
|
|
qWarning() << QString("Failed to generate %3 QSB file for: %1 %2")
|
|
.arg(shader, errStr, previewStr);
|
|
}
|
|
|
|
if (!errStd.isEmpty()) {
|
|
qWarning() << QString("Failed to generate %3 QSB file for: %1 %2")
|
|
.arg(shader, QString::fromUtf8(errStd), previewStr);
|
|
}
|
|
|
|
if (m_remainingQsbTargets <= 0) {
|
|
Q_EMIT shadersBaked();
|
|
setShadersUpToDate(true);
|
|
|
|
// TODO: Mark shaders as baked, required by export later
|
|
}
|
|
|
|
qsbProcess->deleteLater();
|
|
}
|
|
|
|
// Generates string of the custom properties (uniforms) into ShaderEffect component
|
|
// Also generates QML images elements for samplers.
|
|
void EffectComposerModel::updateCustomUniforms()
|
|
{
|
|
QString exportedRootPropertiesString;
|
|
QString previewEffectPropertiesString;
|
|
QString exportedEffectPropertiesString;
|
|
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
for (Uniform *uniform : uniforms) {
|
|
// TODO: Check if uniform is already added.
|
|
const bool isDefine = uniform->type() == Uniform::Type::Define;
|
|
QString propertyType = Uniform::typeToProperty(uniform->type());
|
|
QString value = valueAsString(*uniform);
|
|
QString bindedValue = valueAsBinding(*uniform);
|
|
// When user has set custom uniform value, use it as-is
|
|
if (uniform->useCustomValue()) {
|
|
value = uniform->customValue();
|
|
bindedValue = value;
|
|
}
|
|
// Note: Define type properties appear also as QML properties (in preview) in case QML side
|
|
// needs to use them. This is used at least by BlurHelper BLUR_HELPER_MAX_LEVEL.
|
|
QString propertyName = isDefine ? uniform->name().toLower() : uniform->name();
|
|
if (!uniform->useCustomValue() && !isDefine && !uniform->description().isEmpty()) {
|
|
// When exporting, add API documentation for properties
|
|
const QStringList descriptionLines = uniform->description().split('\n');
|
|
for (const QString &line : descriptionLines) {
|
|
if (line.trimmed().isEmpty())
|
|
exportedRootPropertiesString += QStringLiteral(" //\n");
|
|
else
|
|
exportedRootPropertiesString += QStringLiteral(" // ") + line + '\n';
|
|
}
|
|
}
|
|
QString valueString = value.isEmpty() ? QString() : QString(": %1").arg(value);
|
|
QString boundValueString = bindedValue.isEmpty() ? QString() : QString(": %1").arg(bindedValue);
|
|
// Custom values are not readonly, others inside the effect can be
|
|
QString readOnly = uniform->useCustomValue() ? QString() : QStringLiteral("readonly ");
|
|
previewEffectPropertiesString += " " + readOnly + "property " + propertyType + " "
|
|
+ propertyName + boundValueString + '\n';
|
|
// Define type properties are not added into exports
|
|
if (!isDefine) {
|
|
if (uniform->useCustomValue()) {
|
|
// Custom values are only inside the effect, with description comments
|
|
if (!uniform->description().isEmpty()) {
|
|
const QStringList descriptionLines = uniform->description().split('\n');
|
|
for (const QString &line : descriptionLines)
|
|
exportedEffectPropertiesString += QStringLiteral(" // ") + line + '\n';
|
|
}
|
|
exportedEffectPropertiesString += QStringLiteral(" ") + readOnly
|
|
+ "property " + propertyType + " " + propertyName
|
|
+ boundValueString + '\n';
|
|
} else {
|
|
// Custom values are not added into root
|
|
exportedRootPropertiesString += " property " + propertyType + " " + propertyName
|
|
+ valueString + '\n';
|
|
exportedEffectPropertiesString += QStringLiteral(" ")
|
|
+ readOnly + "property " + propertyType + " " + propertyName
|
|
+ ": rootItem." + uniform->name() + '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
// See if any of the properties changed
|
|
m_exportedRootPropertiesString = exportedRootPropertiesString;
|
|
m_previewEffectPropertiesString = previewEffectPropertiesString;
|
|
m_exportedEffectPropertiesString = exportedEffectPropertiesString;
|
|
}
|
|
|
|
void EffectComposerModel::createFiles()
|
|
{
|
|
if (QFileInfo::exists(m_vertexShaderFilename))
|
|
QFile(m_vertexShaderFilename).remove();
|
|
if (QFileInfo::exists(m_fragmentShaderFilename))
|
|
QFile(m_fragmentShaderFilename).remove();
|
|
if (QFileInfo::exists(m_vertexShaderPreviewFilename))
|
|
QFile(m_vertexShaderPreviewFilename).remove();
|
|
if (QFileInfo::exists(m_fragmentShaderPreviewFilename))
|
|
QFile(m_fragmentShaderPreviewFilename).remove();
|
|
|
|
auto vertexShaderFile = QTemporaryFile(QDir::tempPath() + "/dsem_XXXXXX.vert.qsb");
|
|
auto fragmentShaderFile = QTemporaryFile(QDir::tempPath() + "/dsem_XXXXXX.frag.qsb");
|
|
auto vertexShaderPreviewFile = QTemporaryFile(QDir::tempPath() + "/dsem_prev_XXXXXX.vert.qsb");
|
|
auto fragmentShaderPreviewFile = QTemporaryFile(QDir::tempPath() + "/dsem_prev_XXXXXX.frag.qsb");
|
|
|
|
m_vertexSourceFile.setFileTemplate(QDir::tempPath() + "/dsem_XXXXXX.vert");
|
|
m_fragmentSourceFile.setFileTemplate(QDir::tempPath() + "/dsem_XXXXXX.frag");
|
|
|
|
if (!m_vertexSourceFile.open() || !m_fragmentSourceFile.open()
|
|
|| !vertexShaderFile.open() || !fragmentShaderFile.open()
|
|
|| !vertexShaderPreviewFile.open() || !fragmentShaderPreviewFile.open()) {
|
|
qWarning() << "Unable to open temporary files";
|
|
} else {
|
|
m_vertexSourceFilename = m_vertexSourceFile.fileName();
|
|
m_fragmentSourceFilename = m_fragmentSourceFile.fileName();
|
|
m_vertexShaderFilename = vertexShaderFile.fileName();
|
|
m_fragmentShaderFilename = fragmentShaderFile.fileName();
|
|
m_vertexShaderPreviewFilename = vertexShaderPreviewFile.fileName();
|
|
m_fragmentShaderPreviewFilename = fragmentShaderPreviewFile.fileName();
|
|
}
|
|
}
|
|
|
|
void EffectComposerModel::bakeShaders()
|
|
{
|
|
const QString failMessage = "Shader baking failed: ";
|
|
|
|
const ProjectExplorer::Target *target = ProjectExplorer::ProjectTree::currentTarget();
|
|
if (!target) {
|
|
qWarning() << failMessage << "Target not found";
|
|
return;
|
|
}
|
|
|
|
createFiles();
|
|
|
|
resetEffectError(ErrorPreprocessor);
|
|
if (m_vertexShader == generateVertexShader() && m_fragmentShader == generateFragmentShader()) {
|
|
setShadersUpToDate(true);
|
|
return;
|
|
}
|
|
|
|
setShadersUpToDate(false);
|
|
|
|
// First update the features based on shader content
|
|
// This will make sure that next calls to "generate" will produce correct uniforms.
|
|
m_shaderFeatures.update(generateVertexShader(false), generateFragmentShader(false),
|
|
m_previewEffectPropertiesString);
|
|
|
|
updateCustomUniforms();
|
|
|
|
setVertexShader(generateVertexShader());
|
|
QString vs = m_vertexShader;
|
|
writeToFile(vs.toUtf8(), m_vertexSourceFile.fileName(), FileType::Text);
|
|
|
|
setFragmentShader(generateFragmentShader());
|
|
QString fs = m_fragmentShader;
|
|
writeToFile(fs.toUtf8(), m_fragmentSourceFile.fileName(), FileType::Text);
|
|
|
|
QtSupport::QtVersion *qtVer = QtSupport::QtKitAspect::qtVersion(target->kit());
|
|
if (!qtVer) {
|
|
qWarning() << failMessage << "Qt version not found";
|
|
return;
|
|
}
|
|
|
|
Utils::FilePath qsbPath = qtVer->binPath().pathAppended("qsb").withExecutableSuffix();
|
|
if (!qsbPath.exists()) {
|
|
qWarning() << failMessage << "QSB tool for target kit not found";
|
|
return;
|
|
}
|
|
|
|
Utils::FilePath binPath = Utils::FilePath::fromString(
|
|
QLibraryInfo::path(QLibraryInfo::BinariesPath));
|
|
Utils::FilePath qsbPrevPath = binPath.pathAppended("qsb").withExecutableSuffix();
|
|
if (!qsbPrevPath.exists()) {
|
|
qWarning() << failMessage << "QSB tool for preview shaders not found";
|
|
return;
|
|
}
|
|
|
|
m_remainingQsbTargets = 2; // We only have 2 shaders
|
|
const QStringList srcPaths = {m_vertexSourceFilename, m_fragmentSourceFilename};
|
|
const QStringList outPaths = {m_vertexShaderFilename, m_fragmentShaderFilename};
|
|
const QStringList outPrevPaths = {m_vertexShaderPreviewFilename, m_fragmentShaderPreviewFilename};
|
|
|
|
auto runQsb = [this, srcPaths](const Utils::FilePath &qsbPath, const QStringList &outPaths, bool preview) {
|
|
for (int i = 0; i < 2; ++i) {
|
|
const auto workDir = Utils::FilePath::fromString(outPaths[i]);
|
|
// TODO: Optional legacy glsl support like standalone effect maker needs to add "100es,120"
|
|
QStringList args = {"-s", "--glsl", "300es,140,330,410", "--hlsl", "50", "--msl", "12"};
|
|
args << "-o" << outPaths[i] << srcPaths[i];
|
|
|
|
auto qsbProcess = new Utils::Process(this);
|
|
connect(qsbProcess, &Utils::Process::done, this,
|
|
[this, qsbProcess, path = srcPaths[i], preview] {
|
|
handleQsbProcessExit(qsbProcess, path, preview);
|
|
});
|
|
qsbProcess->setWorkingDirectory(workDir.absolutePath());
|
|
qsbProcess->setCommand({qsbPath, args});
|
|
qsbProcess->start();
|
|
}
|
|
};
|
|
|
|
runQsb(qsbPath, outPaths, false);
|
|
runQsb(qsbPrevPath, outPrevPaths, true);
|
|
|
|
}
|
|
|
|
bool EffectComposerModel::shadersUpToDate() const
|
|
{
|
|
return m_shadersUpToDate;
|
|
}
|
|
|
|
void EffectComposerModel::setShadersUpToDate(bool UpToDate)
|
|
{
|
|
if (m_shadersUpToDate == UpToDate)
|
|
return;
|
|
m_shadersUpToDate = UpToDate;
|
|
emit shadersUpToDateChanged();
|
|
}
|
|
|
|
bool EffectComposerModel::isEnabled() const
|
|
{
|
|
return m_isEnabled;
|
|
}
|
|
|
|
void EffectComposerModel::setIsEnabled(bool enabled)
|
|
{
|
|
if (m_isEnabled == enabled)
|
|
return;
|
|
m_isEnabled = enabled;
|
|
emit isEnabledChanged();
|
|
}
|
|
|
|
QString EffectComposerModel::getQmlImagesString(bool localFiles)
|
|
{
|
|
QString imagesString;
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
for (Uniform *uniform : uniforms) {
|
|
if (uniform->type() == Uniform::Type::Sampler) {
|
|
QString imagePath = uniform->value().toString();
|
|
// For preview, generate image element even if path is empty, as changing uniform values
|
|
// will not trigger qml code regeneration
|
|
if (localFiles) {
|
|
if (imagePath.isEmpty())
|
|
continue;
|
|
QFileInfo fi(imagePath);
|
|
imagePath = fi.fileName();
|
|
imagesString += QString(" property url %1Url: \"%2\"\n")
|
|
.arg(uniform->name(), imagePath);
|
|
}
|
|
imagesString += " Image {\n";
|
|
QString simplifiedName = getImageElementName(*uniform, localFiles);
|
|
imagesString += QString(" id: %1\n").arg(simplifiedName);
|
|
imagesString += " anchors.fill: parent\n";
|
|
// File paths are absolute, return as local when requested
|
|
if (localFiles) {
|
|
imagesString += QString(" source: rootItem.%1Url\n").arg(uniform->name());
|
|
} else {
|
|
imagesString += QString(" source: g_propertyData.%1\n").arg(uniform->name());
|
|
|
|
if (uniform->enableMipmap())
|
|
imagesString += " mipmap: true\n";
|
|
}
|
|
|
|
imagesString += " visible: false\n";
|
|
imagesString += " }\n";
|
|
}
|
|
}
|
|
return imagesString;
|
|
}
|
|
|
|
QString EffectComposerModel::getQmlComponentString(bool localFiles)
|
|
{
|
|
auto addProperty = [localFiles](const QString &name, const QString &var,
|
|
const QString &type, bool blurHelper = false)
|
|
{
|
|
if (localFiles) {
|
|
const QString parent = blurHelper ? QString("blurHelper.") : QString("rootItem.");
|
|
return QString("readonly property %1 %2: %3%4\n").arg(type, name, parent, var);
|
|
} else {
|
|
const QString parent = blurHelper ? "blurHelper." : QString();
|
|
return QString("readonly property %1 %2: %3%4\n").arg(type, name, parent, var);
|
|
}
|
|
};
|
|
|
|
QString s;
|
|
QString l1 = localFiles ? QStringLiteral(" ") : QStringLiteral("");
|
|
QString l2 = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
|
|
QString l3 = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
|
|
|
|
if (!localFiles)
|
|
s += "import QtQuick\n";
|
|
s += l1 + "ShaderEffect {\n";
|
|
|
|
if (localFiles) {
|
|
// Explicit "source" property is required for render puppet to detect effect correctly
|
|
s += l2 + "property Item source: null\n";
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Source))
|
|
s += l2 + addProperty("iSource", "source", "Item");
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Time))
|
|
s += l2 + addProperty("iTime", "animatedTime", "real");
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Frame))
|
|
s += l2 + addProperty("iFrame", "animatedFrame", "int");
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Resolution)) {
|
|
// Note: Pixel ratio is currently always 1.0
|
|
s += l2 + "readonly property vector3d iResolution: Qt.vector3d(width, height, 1.0)\n";
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::Mouse)) { // Do we need interactive effects?
|
|
s += l2 + "readonly property vector4d iMouse: Qt.vector4d(rootItem._effectMouseX, rootItem._effectMouseY,\n";
|
|
s += l2 + " rootItem._effectMouseZ, rootItem._effectMouseW)\n";
|
|
}
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
|
|
s += l2 + addProperty("iSourceBlur1", "blurSrc1", "Item", true);
|
|
s += l2 + addProperty("iSourceBlur2", "blurSrc2", "Item", true);
|
|
s += l2 + addProperty("iSourceBlur3", "blurSrc3", "Item", true);
|
|
s += l2 + addProperty("iSourceBlur4", "blurSrc4", "Item", true);
|
|
s += l2 + addProperty("iSourceBlur5", "blurSrc5", "Item", true);
|
|
}
|
|
// When used in preview component, we need property with value
|
|
// and when in exported component, property with binding to root value.
|
|
s += localFiles ? m_exportedEffectPropertiesString : m_previewEffectPropertiesString;
|
|
|
|
if (!localFiles) {
|
|
QString customImagesString = getQmlImagesString(false);
|
|
if (!customImagesString.isEmpty())
|
|
s += '\n' + customImagesString;
|
|
}
|
|
|
|
s += '\n';
|
|
const QString vertFile = localFiles ? m_vertexShaderFilename : m_vertexShaderPreviewFilename;
|
|
const QString fragFile = localFiles ? m_fragmentShaderFilename : m_fragmentShaderPreviewFilename;
|
|
s += l2 + "vertexShader: 'file:///" + vertFile + "'\n";
|
|
s += l2 + "fragmentShader: 'file:///" + fragFile + "'\n";
|
|
s += l2 + "anchors.fill: " + (localFiles ? "rootItem.source" : "parent") + "\n";
|
|
if (m_shaderFeatures.enabled(ShaderFeatures::GridMesh)) {
|
|
QString gridSize = QString("%1, %2").arg(m_shaderFeatures.gridMeshWidth())
|
|
.arg(m_shaderFeatures.gridMeshHeight());
|
|
s += l2 + "mesh: GridMesh {\n";
|
|
s += l3 + QString("resolution: Qt.size(%1)\n").arg(gridSize);
|
|
s += l2 + "}\n";
|
|
}
|
|
if (localFiles && m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
|
|
s += l2 + "BlurHelper {\n";
|
|
s += l3 + "id: blurHelper\n";
|
|
s += l3 + "source: rootItem.source\n";
|
|
int blurMax = 32;
|
|
if (g_propertyData.contains("BLUR_HELPER_MAX_LEVEL"))
|
|
blurMax = g_propertyData["BLUR_HELPER_MAX_LEVEL"].toInt();
|
|
s += l3 + QString("property int blurMax: %1\n").arg(blurMax);
|
|
s += l3 + "property real blurMultiplier: rootItem.blurMultiplier\n";
|
|
s += l2 + "}\n";
|
|
}
|
|
|
|
s += l1 + "}\n";
|
|
return s;
|
|
}
|
|
|
|
void EffectComposerModel::connectCompositionNode(CompositionNode *node)
|
|
{
|
|
connect(qobject_cast<EffectComposerUniformsModel *>(node->uniformsModel()),
|
|
&EffectComposerUniformsModel::dataChanged, this, [this] {
|
|
setHasUnsavedChanges(true);
|
|
});
|
|
connect(node, &CompositionNode::rebakeRequested, this, [this] {
|
|
// This can come multiple times in a row in response to property changes, so let's buffer it
|
|
m_rebakeTimer.start(200);
|
|
});
|
|
}
|
|
|
|
QString EffectComposerModel::currentComposition() const
|
|
{
|
|
return m_currentComposition;
|
|
}
|
|
|
|
void EffectComposerModel::setCurrentComposition(const QString &newCurrentComposition)
|
|
{
|
|
if (m_currentComposition == newCurrentComposition)
|
|
return;
|
|
|
|
m_currentComposition = newCurrentComposition;
|
|
emit currentCompositionChanged();
|
|
}
|
|
|
|
bool EffectComposerModel::hasUnsavedChanges() const
|
|
{
|
|
return m_hasUnsavedChanges;
|
|
}
|
|
|
|
void EffectComposerModel::setHasUnsavedChanges(bool val)
|
|
{
|
|
if (m_hasUnsavedChanges == val)
|
|
return;
|
|
|
|
m_hasUnsavedChanges = val;
|
|
emit hasUnsavedChangesChanged();
|
|
}
|
|
|
|
QStringList EffectComposerModel::uniformNames() const
|
|
{
|
|
QStringList usedList;
|
|
const QList<Uniform *> uniforms = allUniforms();
|
|
for (const auto uniform : uniforms)
|
|
usedList.append(uniform->name());
|
|
return usedList;
|
|
}
|
|
|
|
bool EffectComposerModel::isDependencyNode(int index) const
|
|
{
|
|
if (m_nodes.size() > index)
|
|
return m_nodes[index]->isDependency();
|
|
return false;
|
|
}
|
|
|
|
void EffectComposerModel::updateQmlComponent()
|
|
{
|
|
// Clear possible QML runtime errors
|
|
resetEffectError(ErrorQMLRuntime);
|
|
m_qmlComponentString = getQmlComponentString(false);
|
|
}
|
|
|
|
// Removes "file:" from the URL path.
|
|
// So e.g. "file:///C:/myimages/steel1.jpg" -> "C:/myimages/steel1.jpg"
|
|
QString EffectComposerModel::stripFileFromURL(const QString &urlString) const
|
|
{
|
|
QUrl url(urlString);
|
|
QString filePath = (url.scheme() == QStringLiteral("file")) ? url.toLocalFile() : url.toString();
|
|
return filePath;
|
|
}
|
|
|
|
} // namespace EffectComposer
|