// ===================================================================================================================== // OpenShaderDesigner, an open source software utility to create materials and shaders. // Copyright (C) 2024 Medusa Slockbower // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // ===================================================================================================================== #include #include #include #include #include #include #include "imgui-extras/imgui_extras.h" using namespace OpenShaderDesigner; static ShaderGraph* GCurrentGraph = nullptr; static bool ValidateConnection(ImPinPtr a, ImPinPtr b) { ShaderGraph& Graph = *GCurrentGraph; bool result = false; result |= Graph.FindNode(a)->CheckConnection(&Graph.FindPin(a), &Graph.FindPin(b)); result |= Graph.FindNode(b)->CheckConnection(&Graph.FindPin(b), &Graph.FindPin(a)); return result; } ImColor operator*(const ImColor& c, float f) { return ImVec4(c.Value.x * f, c.Value.y * f, c.Value.z * f, c.Value.w); } GraphState::GraphState(ShaderGraph& parent) : Parent(parent) { } GraphState::GraphState(const GraphState& other) : Parent(other.Parent) , Nodes(other.Nodes) { for(Node*& node : Nodes) { node = node->Copy(Parent); } } GraphState::~GraphState() { for(Node* node : Nodes) { if(node) delete node; } } GraphState& GraphState::operator=(const GraphState& other) { Nodes = other.Nodes; for(Node*& node : Nodes) if(node) node = node->Copy(Parent); return *this; } Node::Node(ShaderGraph& graph, ImVec2 pos) : Graph(graph) , Position(pos) , Header { .Title = "Node" , .Color = ImColor(0xA7, 0x62, 0x53) , .HoveredColor = ImColor(0xC5, 0x79, 0x67) , .ActiveColor = ImColor(0x82, 0x4C, 0x40) , .Enabled = true } , IO { } , Info { .Flags = NodeFlags_None } { } void Node::DrawPin(int id, Pin& pin, ImPinDirection direction) { ImPinFlags flags = 0; if(pin.Flags & PinFlags_NoPadding) flags |= ImPinFlags_NoPadding; bool res = ImNodeGraph::BeginPin(id, pin.Type, direction, flags); pin.Ptr = ImNodeGraph::GetPinPtr(); if(res) { const ImVector& connections = ImNodeGraph::GetConnections(); const ImVector& new_conns = ImNodeGraph::GetNewConnections(); if(pin.Flags & PinFlags_Ambiguous) { if(connections.size() == new_conns.size() && new_conns.size() > 0) { Pin& first = Graph.FindPin(new_conns.front()); if(first.Type != PinType_Any) pin.Type = first.Type; } if(connections.size() == 0) pin.Type = PinType_Any; } ValidateConnections(); } const bool connected = ImNodeGraph::IsPinConnected(); const bool any = pin.Type == PinType_Any; const bool force_collapse = pin.Flags & PinFlags_AlwaysCollapse; const bool no_collapse = pin.Flags & PinFlags_NoCollapse; if((connected || any || direction || force_collapse) && !no_collapse) { ImGui::Text(pin.Name.c_str()); } else { switch (pin.Type) { case PinType_Int: ImNodeGraph::PushItemWidth(200.0f); ImGui::InputInt(std::format("##in{}{}", pin.Name, id).c_str(), pin.Value); break; case PinType_UInt: ImNodeGraph::PushItemWidth(200.0f); ImGui::InputUInt(std::format("##in{}{}", pin.Name, id).c_str(), pin.Value); break; case PinType_Float: ImNodeGraph::PushItemWidth(100.0f); ImGui::InputFloat(std::format("##in{}{}", pin.Name, id).c_str(), pin.Value); break; case PinType_Vector: ImGui::BeginGroup(); // Color Picker ImNodeGraph::PushItemWidth(150.0f); ImGui::ColorPicker3( std::format("##in{}{}", pin.Name, id).c_str(), &pin.Value.get().x , ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float ); ImNodeGraph::PushItemWidth(150.0f); ImGui::ColorPreview3( std::format("##invec{}{}", pin.Name, id).c_str(), &pin.Value.get().x , ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float ); ImGui::EndGroup(); break; } } ImNodeGraph::EndPin(); } void Node::Draw(ImGuiID id) { ImNodeGraph::BeginNode(id, Position); if(Header.Enabled) { ImNodeGraph::BeginNodeHeader(id, Header.Color, Header.HoveredColor, Header.ActiveColor); ImGui::Text(Header.Title.c_str()); ImNodeGraph::EndNodeHeader(); } ImGuiID pid = 0; for(Pin& pin : IO.Inputs) DrawPin(++pid, pin, ImPinDirection_Input); ImVec2 cursor = ImGui::GetCursorPos(); pid = 0; for(Pin& pin : IO.Outputs) DrawPin(--pid, pin, ImPinDirection_Output); ImGui::SetCursorPos(cursor); if(Info.Flags & NodeFlags_DynamicInputs) { ImGui::Text("\uEA11"); } ImNodeGraph::EndNode(); } ShaderGraph::ShaderGraph() : EditorWindow("\uED46 Shader Graph", ImGuiWindowFlags_MenuBar) , GrabFocus_(false) , Shader_(nullptr) { } ShaderGraph::~ShaderGraph() { } void ShaderGraph::OnOpen() { EditorSystem::Open()->Graph = this; GrabFocus_ = true; } void ShaderGraph::DrawMenu() { if(ImGui::MenuItem("\uf455 Compile")) { ImNodeGraph::BeginGraphPostOp("ShaderGraph"); Shader_->Compile(); ImNodeGraph::EndGraphPostOp(); } } void ShaderGraph::DrawWindow() { GCurrentGraph = this; if(Shader_ == nullptr) return; ImNodeGraph::BeginGraph("ShaderGraph"); ImNodeGraph::SetPinColors(Pin::Colors); ImNodeGraph::SetGraphValidation(ValidateConnection); if(GrabFocus_) { GrabFocus_ = false; ImGui::SetWindowFocus(); ImGui::SetNavWindow(ImGui::GetCurrentWindow()); } GraphState& State = Shader_->GetState(); for(ImGuiID id = 0; id < State.Nodes.size(); ++id) { if(State.Nodes(id) == false) continue; State.Nodes[id]->Draw(id); } DrawContextMenu(); ImNodeGraph::EndGraph(); ImNodeGraph::BeginGraphPostOp("ShaderGraph"); if(ImGui::IsKeyPressed(ImGuiKey_Delete)) Erase(); ImGuiIO& io = ImGui::GetIO(); if(!(io.KeyMods & ~ImGuiMod_Ctrl)) { if(ImGui::IsKeyPressed(ImGuiKey_C)) Copy(); if(ImGui::IsKeyPressed(ImGuiKey_P)) Paste(ImGui::GetMousePos()); if(ImGui::IsKeyPressed(ImGuiKey_X)) { Copy(); Erase(); } } const ImSet& Selected = *ImNodeGraph::GetSelected(); Selected_.reset(); if(Selected.Size == 1) { Selected_ = ImNodeGraph::GetUserID(*Selected.cbegin()).Int; } ImNodeGraph::EndGraphPostOp(); } void ShaderGraph::DrawContextMenu() { ContextMenuHierarchy& ContextMenu = ShaderGraph::ContextMenu(); if(ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { ContextMenuPosition_ = ImNodeGraph::ScreenToGrid(ImGui::GetMousePos()); } if(ImGui::BeginPopupContextWindow("graph_context")) { if(ImGui::MenuItem("Copy", "Ctrl+C", false, false)) Copy(); if(ImGui::MenuItem("Cut", "Ctrl+X", false, false)) { Copy(); Erase(); } if(ImGui::MenuItem("Paste", "Ctrl+V", false, false)) Paste(ContextMenuPosition_); ImGui::Separator(); ImGui::Text("Create"); ImGui::Separator(); // Create Nodes ImVec2 position = ContextMenuPosition_; struct Visitor { bool operator()(ContextMenuItem& item, ContextID id) { ContextMenuHierarchy& ContextMenu = ShaderGraph::ContextMenu(); const auto depth = ContextMenu.depth(id); if(depth > Context.size()) return false; while(depth < Context.size()) { Context.pop(); ImGui::EndMenu(); } if(Context.top() != ContextMenu.parent(id)) return false; std::string name = std::format("{}##{}", item.Name, id); if(item.Constructor) { if(ImGui::MenuItem(item.Name.c_str())) { Graph.Shader_->GetState().AddNode(item.Constructor(Graph, Location)); } } else { if(ImGui::BeginMenu(item.Name.c_str())) { Context.push(id); } else { return false; } } return false; } ShaderGraph& Graph; const ImVec2 Location; std::stack Context; } MenuVisitor { .Graph = *this , .Location = position }; MenuVisitor.Context.push(0); ContextMenu.traverse(MenuVisitor); MenuVisitor.Context.pop(); while(MenuVisitor.Context.empty() == false) { ImGui::EndMenu(); MenuVisitor.Context.pop(); } ImGui::EndPopup(); } } void ShaderGraph::Copy() { } void ShaderGraph::Erase() { GraphState& State = Shader_->GetState(); ImSet& Selected = *ImNodeGraph::GetSelected(); for(ImGuiID node : Selected) { State.RemoveNode(ImNodeGraph::GetUserID(node).Int); } Selected.Clear(); } void ShaderGraph::Paste(ImVec2) { } void ShaderGraph::Clear() { } Node* ShaderGraph::FindNode(ImPinPtr ptr) { return Shader_->GetState().Nodes[ImNodeGraph::GetUserID("ShaderGraph", ptr.Node).Int]; } Node* ShaderGraph::FindNode(ImGuiID id) { return Shader_->GetState().Nodes[ImNodeGraph::GetUserID("ShaderGraph", id).Int]; } Pin& ShaderGraph::FindPin(ImPinPtr ptr) { Node* node = Shader_->GetState().Nodes[ImNodeGraph::GetUserID(ptr.Node).Int]; auto& pins = ptr.Direction ? node->IO.Outputs : node->IO.Inputs; int idx = ImNodeGraph::GetUserID(ptr).Int; if(ptr.Direction) idx *= -1; idx -= 1; return pins[idx]; } std::string ShaderGraph::GetValue(ImPinPtr ptr) { if(ptr.Direction == ImPinDirection_Output) { NodeId node_id = ImNodeGraph::GetUserID(ptr.Node).Int; Node* node = Shader_->GetState().Nodes[node_id]; NodeId pin_id = ImNodeGraph::GetUserID(ptr).Int; Pin& pin = FindPin(ptr); if(pin.Flags & PinFlags_Literal) { switch(pin.Type) { case PinType_UInt: return std::to_string(pin.Value.get()); case PinType_Int: return std::to_string(pin.Value.get()); case PinType_Float: return std::to_string(pin.Value.get()); case PinType_Vector: { const glm::vec3& val = pin.Value.get(); return std::format("vec3({},{},{})", val.x, val.y, val.z); } default: return "0"; } } return pin.GetVarName(); } else { const ImVector& connections = ImNodeGraph::GetConnections(ptr); Pin& pin = FindPin(ptr); // L-Value if(connections.empty()) { switch(pin.Type) { case PinType_UInt: return std::to_string(pin.Value.get()); case PinType_Int: return std::to_string(pin.Value.get()); case PinType_Float: return std::to_string(pin.Value.get()); case PinType_Vector: { const glm::vec3& val = pin.Value.get(); return std::format("vec3({},{},{})", val.x, val.y, val.z); } default: return "0"; } } // Variable ImPinConnection connection = ImNodeGraph::GetConnection(connections[0]); if(connection.A == ptr) return GetValue(connection.B); return GetValue(connection.A); } } void ShaderGraph::Register(const std::filesystem::path& path, ConstructorPtr constructor) { const std::string name = path.filename().string(); ContextMenuHierarchy& ContextMenu = ShaderGraph::ContextMenu(); ContextID node = 0; for(auto it = path.begin(); it != path.end();) { ContextID child = ContextMenu.first_child(node); while(child) { if(ContextMenu[child].Name == it->string()) { node = child; ++it; break; } child = ContextMenu.next_sibling(child); } if(node == 0 || node != child) { node = ContextMenu.insert({ it->string(), nullptr }, node); ++it; } } ContextMenu[node].Constructor = constructor; } Inspector::Inspector() : EditorWindow("Inspector", ImGuiWindowFlags_None) , Graph(nullptr) { } void Inspector::DrawWindow() { if(Graph->Shader_ == nullptr) return; if(Graph->Selected_()) { Graph->Shader_->GetState().Nodes[Graph->Selected_]->Inspect(); } }