diff --git a/src/ShaderNode/DataModels/InputValue.cpp b/src/ShaderNode/DataModels/InputValue.cpp index f6b6bf2e4..3f8d2fbdc 100644 --- a/src/ShaderNode/DataModels/InputValue.cpp +++ b/src/ShaderNode/DataModels/InputValue.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -11,7 +12,10 @@ ShaderNode(graph) m_onInputUpdateSlot.Connect(GetGraph().OnInputUpdate, [&](ShaderGraph*, std::size_t inputIndex) { if (m_currentInputIndex == inputIndex) + { UpdatePreview(); + Q_EMIT dataUpdated(0); + } }); if (graph.GetInputCount() > 0) @@ -172,3 +176,19 @@ QString InputValue::validationMessage() const return QString(); } + +void InputValue::restore(const QJsonObject& data) +{ + m_currentInputText = data["input"].toString().toStdString(); + OnInputListUpdate(); + + ShaderNode::restore(data); +} + +QJsonObject InputValue::save() const +{ + QJsonObject data = ShaderNode::save(); + data["input"] = QString::fromStdString(m_currentInputText); + + return data; +} diff --git a/src/ShaderNode/DataModels/InputValue.hpp b/src/ShaderNode/DataModels/InputValue.hpp index 4b4c1b31c..681a85e8d 100644 --- a/src/ShaderNode/DataModels/InputValue.hpp +++ b/src/ShaderNode/DataModels/InputValue.hpp @@ -37,6 +37,9 @@ class InputValue : public ShaderNode bool ComputePreview(QPixmap& pixmap) override; void OnInputListUpdate(); + void restore(const QJsonObject& data) override; + QJsonObject save() const override; + NazaraSlot(ShaderGraph, OnInputListUpdate, m_onInputListUpdateSlot); NazaraSlot(ShaderGraph, OnInputUpdate, m_onInputUpdateSlot); diff --git a/src/ShaderNode/DataModels/OutputValue.cpp b/src/ShaderNode/DataModels/OutputValue.cpp index 04eeafd48..538884706 100644 --- a/src/ShaderNode/DataModels/OutputValue.cpp +++ b/src/ShaderNode/DataModels/OutputValue.cpp @@ -188,3 +188,19 @@ void OutputValue::OnOutputListUpdate() inputIndex++; } } + +void OutputValue::restore(const QJsonObject& data) +{ + m_currentOutputText = data["input"].toString().toStdString(); + OnOutputListUpdate(); + + ShaderNode::restore(data); +} + +QJsonObject OutputValue::save() const +{ + QJsonObject data = ShaderNode::save(); + data["input"] = QString::fromStdString(m_currentOutputText); + + return data; +} diff --git a/src/ShaderNode/DataModels/OutputValue.hpp b/src/ShaderNode/DataModels/OutputValue.hpp index 1e7055da5..4cc523b66 100644 --- a/src/ShaderNode/DataModels/OutputValue.hpp +++ b/src/ShaderNode/DataModels/OutputValue.hpp @@ -36,6 +36,9 @@ class OutputValue : public ShaderNode bool ComputePreview(QPixmap& pixmap) override; void OnOutputListUpdate(); + void restore(const QJsonObject& data) override; + QJsonObject save() const override; + NazaraSlot(ShaderGraph, OnOutputListUpdate, m_onOutputListUpdateSlot); NazaraSlot(ShaderGraph, OnOutputUpdate, m_onOutputUpdateSlot); diff --git a/src/ShaderNode/DataModels/SampleTexture.cpp b/src/ShaderNode/DataModels/SampleTexture.cpp index 970e8a965..3f0036519 100644 --- a/src/ShaderNode/DataModels/SampleTexture.cpp +++ b/src/ShaderNode/DataModels/SampleTexture.cpp @@ -54,14 +54,25 @@ void SampleTexture::UpdateOutput() float u = float(uvPtr[0]) / 255; float v = float(uvPtr[1]) / 255; - int texX = std::clamp(int(u * textureWidth), 0, textureWidth - 1); - int texY = std::clamp(int(v * textureHeight), 0, textureHeight - 1); - int texPixel = (texY * textureWidth + texX) * 4; + if (textureWidth > 0 && textureHeight > 0) + { + int texX = std::clamp(int(u * textureWidth), 0, textureWidth - 1); + int texY = std::clamp(int(v * textureHeight), 0, textureHeight - 1); + int texPixel = (texY * textureWidth + texX) * 4; + + *outputPtr++ = texturePtr[texPixel + 0]; + *outputPtr++ = texturePtr[texPixel + 1]; + *outputPtr++ = texturePtr[texPixel + 2]; + *outputPtr++ = texturePtr[texPixel + 3]; + } + else + { + *outputPtr++ = 0; + *outputPtr++ = 0; + *outputPtr++ = 0; + *outputPtr++ = 0xFF; + } - *outputPtr++ = texturePtr[texPixel + 0]; - *outputPtr++ = texturePtr[texPixel + 1]; - *outputPtr++ = texturePtr[texPixel + 2]; - *outputPtr++ = texturePtr[texPixel + 3]; uvPtr += 4; } } diff --git a/src/ShaderNode/DataModels/ShaderNode.cpp b/src/ShaderNode/DataModels/ShaderNode.cpp index b955c3c42..a9b06adf8 100644 --- a/src/ShaderNode/DataModels/ShaderNode.cpp +++ b/src/ShaderNode/DataModels/ShaderNode.cpp @@ -81,8 +81,6 @@ void ShaderNode::EnablePreview(bool enable) m_pixmapLabel->clear(); m_pixmap.reset(); } - - embeddedWidgetSizeUpdated(); } } @@ -95,6 +93,29 @@ void ShaderNode::setInData(std::shared_ptr, int) { } +void ShaderNode::restore(const QJsonObject& data) +{ + NodeDataModel::restore(data); + + bool isPreviewEnabled = data["preview_enabled"].toBool(m_isPreviewEnabled); + m_previewSize.x = data["preview_width"].toInt(m_previewSize.x); + m_previewSize.y = data["preview_height"].toInt(m_previewSize.y); + m_variableName = data["variable_name"].toString().toStdString(); + + EnablePreview(isPreviewEnabled); +} + +QJsonObject ShaderNode::save() const +{ + QJsonObject data = NodeDataModel::save(); + data["preview_enabled"] = m_isPreviewEnabled; + data["preview_width"] = m_previewSize.x; + data["preview_height"] = m_previewSize.y; + data["variable_name"] = QString::fromStdString(m_variableName); + + return data; +} + bool ShaderNode::ComputePreview(QPixmap& /*pixmap*/) { return false; @@ -103,7 +124,10 @@ bool ShaderNode::ComputePreview(QPixmap& /*pixmap*/) void ShaderNode::UpdatePreview() { if (!m_pixmap) + { + embeddedWidgetSizeUpdated(); return; + } QPixmap& pixmap = *m_pixmap; @@ -116,4 +140,6 @@ void ShaderNode::UpdatePreview() pixmap = pixmap.scaled(m_previewSize.x, m_previewSize.y); m_pixmapLabel->setPixmap(pixmap); + + embeddedWidgetSizeUpdated(); } diff --git a/src/ShaderNode/DataModels/ShaderNode.hpp b/src/ShaderNode/DataModels/ShaderNode.hpp index 91f6dc0d3..e7ac130c9 100644 --- a/src/ShaderNode/DataModels/ShaderNode.hpp +++ b/src/ShaderNode/DataModels/ShaderNode.hpp @@ -40,6 +40,9 @@ class ShaderNode : public QtNodes::NodeDataModel inline void EnableCustomVariableName(bool enable = true); void UpdatePreview(); + void restore(const QJsonObject& data) override; + QJsonObject save() const override; + private: virtual bool ComputePreview(QPixmap& pixmap); diff --git a/src/ShaderNode/DataModels/TextureValue.cpp b/src/ShaderNode/DataModels/TextureValue.cpp index f78a610a9..78f7df761 100644 --- a/src/ShaderNode/DataModels/TextureValue.cpp +++ b/src/ShaderNode/DataModels/TextureValue.cpp @@ -12,7 +12,10 @@ ShaderNode(graph) m_onTexturePreviewUpdateSlot.Connect(GetGraph().OnTexturePreviewUpdate, [&](ShaderGraph*, std::size_t textureIndex) { if (m_currentTextureIndex == textureIndex) + { UpdatePreview(); + Q_EMIT dataUpdated(0); + } }); if (graph.GetTextureCount() > 0) @@ -161,3 +164,19 @@ QString TextureValue::validationMessage() const return QString(); } + +void TextureValue::restore(const QJsonObject& data) +{ + m_currentTextureText = data["texture"].toString().toStdString(); + OnTextureListUpdate(); + + ShaderNode::restore(data); +} + +QJsonObject TextureValue::save() const +{ + QJsonObject data = ShaderNode::save(); + data["texture"] = QString::fromStdString(m_currentTextureText); + + return data; +} diff --git a/src/ShaderNode/DataModels/TextureValue.hpp b/src/ShaderNode/DataModels/TextureValue.hpp index fdf355828..4b46be4d6 100644 --- a/src/ShaderNode/DataModels/TextureValue.hpp +++ b/src/ShaderNode/DataModels/TextureValue.hpp @@ -36,6 +36,9 @@ class TextureValue : public ShaderNode bool ComputePreview(QPixmap& pixmap) override; void OnTextureListUpdate(); + void restore(const QJsonObject& data) override; + QJsonObject save() const override; + NazaraSlot(ShaderGraph, OnTextureListUpdate, m_onTextureListUpdateSlot); NazaraSlot(ShaderGraph, OnTexturePreviewUpdate, m_onTexturePreviewUpdateSlot); diff --git a/src/ShaderNode/DataModels/VecValue.hpp b/src/ShaderNode/DataModels/VecValue.hpp index b115477e3..fe94e67cb 100644 --- a/src/ShaderNode/DataModels/VecValue.hpp +++ b/src/ShaderNode/DataModels/VecValue.hpp @@ -33,9 +33,11 @@ class VecValue : public ShaderNode private: bool ComputePreview(QPixmap& pixmap) override; - QColor ToColor() const; + void restore(const QJsonObject& data) override; + QJsonObject save() const override; + VecType m_value; }; diff --git a/src/ShaderNode/DataModels/VecValue.inl b/src/ShaderNode/DataModels/VecValue.inl index fd6d31be1..a35a95686 100644 --- a/src/ShaderNode/DataModels/VecValue.inl +++ b/src/ShaderNode/DataModels/VecValue.inl @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -116,3 +117,27 @@ QColor VecValue::ToColor() const return QColor::fromRgbF(values[0], values[1], values[2], values[3]); } + +template +void VecValue::restore(const QJsonObject& data) +{ + QJsonArray vecValues = data["value"].toArray(); + for (std::size_t i = 0; i < ComponentCount; ++i) + m_value[i] = vecValues[int(i)].toInt(m_value[i]); + + ShaderNode::restore(data); +} + +template +QJsonObject VecValue::save() const +{ + QJsonObject data = ShaderNode::save(); + + QJsonArray vecValues; + for (std::size_t i = 0; i < ComponentCount; ++i) + vecValues.push_back(m_value[i]); + + data["value"] = vecValues; + + return data; +} diff --git a/src/ShaderNode/Enums.hpp b/src/ShaderNode/Enums.hpp index f67623a3f..72a5443f7 100644 --- a/src/ShaderNode/Enums.hpp +++ b/src/ShaderNode/Enums.hpp @@ -4,6 +4,8 @@ #define NAZARA_SHADERNODES_ENUMS_HPP #include +#include +#include enum class InputRole { @@ -40,10 +42,11 @@ enum class TextureType constexpr std::size_t TextureTypeCount = static_cast(TextureType::Max) + 1; -std::size_t GetComponentCount(InOutType type); +template std::optional DecodeEnum(const std::string_view& str); const char* EnumToString(InputRole role); const char* EnumToString(InOutType input); const char* EnumToString(TextureType textureType); +std::size_t GetComponentCount(InOutType type); #include diff --git a/src/ShaderNode/Enums.inl b/src/ShaderNode/Enums.inl index 243d12205..f54d0801f 100644 --- a/src/ShaderNode/Enums.inl +++ b/src/ShaderNode/Enums.inl @@ -1 +1,15 @@ #include + +template +std::optional DecodeEnum(const std::string_view& str) +{ + constexpr std::size_t ValueCount = static_cast(T::Max) + 1; + for (std::size_t i = 0; i < ValueCount; ++i) + { + T value = static_cast(i); + if (str == EnumToString(value)) + return value; + } + + return {}; +} diff --git a/src/ShaderNode/ShaderGraph.cpp b/src/ShaderNode/ShaderGraph.cpp index 38d1a8dcb..6fc2fe6aa 100644 --- a/src/ShaderNode/ShaderGraph.cpp +++ b/src/ShaderNode/ShaderGraph.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +115,136 @@ std::size_t ShaderGraph::AddTexture(std::string name, TextureType type) return index; } +void ShaderGraph::Clear() +{ + m_flowScene.clearScene(); + m_flowScene.clear(); + + m_inputs.clear(); + m_outputs.clear(); + m_textures.clear(); + + OnInputListUpdate(this); + OnOutputListUpdate(this); + OnTextureListUpdate(this); +} + +void ShaderGraph::Load(const QJsonObject& data) +{ + Clear(); + + QJsonArray inputArray = data["inputs"].toArray(); + for (const auto& inputDocRef : inputArray) + { + QJsonObject inputDoc = inputDocRef.toObject(); + + InputEntry& input = m_inputs.emplace_back(); + input.name = inputDoc["name"].toString().toStdString(); + input.role = DecodeEnum(inputDoc["role"].toString().toStdString()).value(); + input.roleIndex = static_cast(inputDoc["roleIndex"].toInt(0)); + input.type = DecodeEnum(inputDoc["type"].toString().toStdString()).value(); + } + + OnInputListUpdate(this); + + QJsonArray outputArray = data["outputs"].toArray(); + for (const auto& outputDocRef : outputArray) + { + QJsonObject outputDoc = outputDocRef.toObject(); + + OutputEntry& output = m_outputs.emplace_back(); + output.name = outputDoc["name"].toString().toStdString(); + output.type = DecodeEnum(outputDoc["type"].toString().toStdString()).value(); + } + + OnOutputListUpdate(this); + + QJsonArray textureArray = data["textures"].toArray(); + for (const auto& textureDocRef : textureArray) + { + QJsonObject textureDoc = textureDocRef.toObject(); + + TextureEntry& texture = m_textures.emplace_back(); + texture.name = textureDoc["name"].toString().toStdString(); + texture.type = DecodeEnum(textureDoc["type"].toString().toStdString()).value(); + } + + OnTextureListUpdate(this); + + for (QJsonValueRef node : data["nodes"].toArray()) + m_flowScene.restoreNode(node.toObject()); + + for (QJsonValueRef connection : data["connections"].toArray()) + m_flowScene.restoreConnection(connection.toObject()); +} + +QJsonObject ShaderGraph::Save() +{ + QJsonObject sceneJson; + + QJsonArray inputArray; + { + for (const auto& input : m_inputs) + { + QJsonObject inputDoc; + inputDoc["name"] = QString::fromStdString(input.name); + inputDoc["role"] = QString(EnumToString(input.role)); + inputDoc["roleIndex"] = int(input.roleIndex); + inputDoc["type"] = QString(EnumToString(input.type)); + + inputArray.append(inputDoc); + } + } + sceneJson["inputs"] = inputArray; + + QJsonArray outputArray; + { + for (const auto& output : m_outputs) + { + QJsonObject outputDoc; + outputDoc["name"] = QString::fromStdString(output.name); + outputDoc["type"] = QString(EnumToString(output.type)); + + outputArray.append(outputDoc); + } + } + sceneJson["outputs"] = outputArray; + + QJsonArray textureArray; + { + for (const auto& texture : m_textures) + { + QJsonObject textureDoc; + textureDoc["name"] = QString::fromStdString(texture.name); + textureDoc["type"] = QString(EnumToString(texture.type)); + + textureArray.append(textureDoc); + } + } + sceneJson["textures"] = textureArray; + + QJsonArray nodesJsonArray; + { + for (auto&& [uuid, node] : m_flowScene.nodes()) + nodesJsonArray.append(node->save()); + } + sceneJson["nodes"] = nodesJsonArray; + + QJsonArray connectionJsonArray; + { + for (auto&& [uuid, connection] : m_flowScene.connections()) + { + QJsonObject connectionJson = connection->save(); + + if (!connectionJson.isEmpty()) + connectionJsonArray.append(connectionJson); + } + } + sceneJson["connections"] = connectionJsonArray; + + return sceneJson; +} + Nz::ShaderAst::StatementPtr ShaderGraph::ToAst() { std::vector statements; diff --git a/src/ShaderNode/ShaderGraph.hpp b/src/ShaderNode/ShaderGraph.hpp index 854d5cfb7..536a01dcc 100644 --- a/src/ShaderNode/ShaderGraph.hpp +++ b/src/ShaderNode/ShaderGraph.hpp @@ -27,6 +27,8 @@ class ShaderGraph std::size_t AddOutput(std::string name, InOutType type); std::size_t AddTexture(std::string name, TextureType type); + void Clear(); + inline const InputEntry& GetInput(std::size_t inputIndex) const; inline std::size_t GetInputCount() const; inline const std::vector& GetInputs() const; @@ -39,6 +41,9 @@ class ShaderGraph inline std::size_t GetTextureCount() const; inline const std::vector& GetTextures() const; + void Load(const QJsonObject& data); + QJsonObject Save(); + Nz::ShaderAst::StatementPtr ToAst(); void UpdateInput(std::size_t inputIndex, std::string name, InOutType type, InputRole role, std::size_t roleIndex); diff --git a/src/ShaderNode/Widgets/MainWindow.cpp b/src/ShaderNode/Widgets/MainWindow.cpp index 505c1f285..c30007f14 100644 --- a/src/ShaderNode/Widgets/MainWindow.cpp +++ b/src/ShaderNode/Widgets/MainWindow.cpp @@ -6,7 +6,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -77,7 +80,19 @@ m_shaderGraph(graph) void MainWindow::BuildMenu() { - QMenu* compileMenu = menuBar()->addMenu(tr("&Compilation")); + QMenuBar* menu = menuBar(); + + QMenu* shader = menu->addMenu(tr("&Shader")); + { + QtNodes::FlowScene* scene = &m_shaderGraph.GetScene(); + + QAction* loadShader = shader->addAction(tr("Load...")); + QObject::connect(loadShader, &QAction::triggered, this, &MainWindow::OnLoad); + QAction* saveShader = shader->addAction(tr("Save...")); + QObject::connect(saveShader, &QAction::triggered, this, &MainWindow::OnSave); + } + + QMenu* compileMenu = menu->addMenu(tr("&Compilation")); QAction* compileToGlsl = compileMenu->addAction(tr("GLSL")); connect(compileToGlsl, &QAction::triggered, [&](bool) { OnCompileToGLSL(); }); } @@ -103,3 +118,48 @@ void MainWindow::OnCompileToGLSL() QMessageBox::critical(this, tr("Compilation failed"), QString("Compilation failed: ") + e.what()); } } + +void MainWindow::OnLoad() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Open shader flow"), QDir::homePath(), tr("Shader Flow Files (*.shaderflow)")); + if (fileName.isEmpty()) + return; + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) + { + QMessageBox::critical(this, tr("Failed to open file"), QString("Failed to open shader flow file: ") + file.errorString()); + return; + } + + QJsonObject jsonDocument = QJsonDocument::fromJson(file.readAll()).object(); + if (jsonDocument.isEmpty()) + { + QMessageBox::critical(this, tr("Invalid file"), tr("Invalid shader flow file")); + return; + } + + try + { + m_shaderGraph.Load(jsonDocument); + } + catch (const std::exception& e) + { + QMessageBox::critical(this, tr("Invalid file"), tr("Invalid shader flow file: ") + e.what()); + return; + } +} + +void MainWindow::OnSave() +{ + QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Open shader flow"), QDir::homePath(), tr("Shader Flow Files (*.shaderflow)")); + if (fileName.isEmpty()) + return; + + if (!fileName.endsWith("flow", Qt::CaseInsensitive)) + fileName += ".shaderflow"; + + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) + file.write(QJsonDocument(m_shaderGraph.Save()).toJson()); +} diff --git a/src/ShaderNode/Widgets/MainWindow.hpp b/src/ShaderNode/Widgets/MainWindow.hpp index 777c3b92b..ff8b307c1 100644 --- a/src/ShaderNode/Widgets/MainWindow.hpp +++ b/src/ShaderNode/Widgets/MainWindow.hpp @@ -18,6 +18,8 @@ class MainWindow : public QMainWindow private: void BuildMenu(); void OnCompileToGLSL(); + void OnLoad(); + void OnSave(); NazaraSlot(ShaderGraph, OnSelectedNodeUpdate, m_onSelectedNodeUpdate);