Maddie Slockbower 366774622b Implemented Shader Function
- Updated License to GPL v3.0
- Added New Math Nodes
- Prototype Rendering Code for Debugging Functions
2024-11-03 12:57:12 -05:00

580 lines
16 KiB
C++

// =====================================================================================================================
// 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 <https://www.gnu.org/licenses/>.
// =====================================================================================================================
#include <queue>
#include <numeric>
#include <Graph/Nodes/Shaders.h>
#include <imgui-docking/misc/cpp/imgui_stdlib.h>
#include "Core/Console.h"
#include "Core/Engine.h"
#include "Editor/EditorSystem.h"
#include "imgui-extras/imgui_extras.h"
#include "Renderer/Renderer.h"
using namespace OpenShaderDesigner;
using namespace OpenShaderDesigner::Nodes::Shaders;
RegisterAsset("Shaders/Function", Function, ".sf");
// =====================================================================================================================
// Shaders
// =====================================================================================================================
// Function ------------------------------------------------------------------------------------------------------------
Function::Function(const FileManager::Path& path, ShaderGraph& graph)
: Node(graph, { 500, 0 })
, ShaderAsset(path, graph)
, Shader_(nullptr)
, Inputs_(graph, { 0, 0 })
, DisplayVar_(0)
{
Info.Flags |= NodeFlags_Const;
Info.Alias = "Function";
Header.Title = HeaderMarker + "Outputs";
Header.Color = HeaderColor;
Header.HoveredColor = HeaderHoveredColor;
Header.ActiveColor = HeaderActiveColor;
ID_ = GetState().AddNode(this);
InputsID_ = GetState().AddNode(&Inputs_);
IO.Inputs.push_back({ "Out", PinType_Vector });
Inputs_.IO.Outputs.push_back({ "In", PinType_Vector });
MakeDirty();
}
Function::~Function()
{
delete Shader_;
}
Node* Function::Copy(ShaderGraph &graph) const
{
return nullptr; // Non Copyable
}
void Function::Inspect()
{
auto& Outputs = IO.Inputs;
for(int i = 0; i < Outputs.size(); ++i)
{
const float avail_x = ImGui::GetContentRegionAvail().x;
ImGui::PushItemWidth(avail_x * 0.625f);
ImGui::InputText(std::format("##{}_name", i).c_str(), &Outputs[i].Name, ImGuiInputTextFlags_AutoSelectAll);
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::Combo(std::format("##{}_type", i).c_str(), &Outputs[i].Type, Pin::TypeNames, PinType_Any);
ImGui::PopItemWidth();
}
if(ImGui::TextLink("\uEA11"))
{
PinType type = PinType_Vector;
if(not Outputs.empty()) type = Outputs.back().Type;
Outputs.push_back({
std::format("Out{}", Outputs.size()), type
});
}
}
void Function::Compile()
{
// Get Static Objects
ShaderGraph& Graph = *EditorSystem::Get<ShaderGraph>();
GraphState& State = GetState();
const auto& Inputs = Inputs_.IO.Outputs;
const auto& Outputs = IO.Inputs;
if(Outputs.size() < 1)
{
Console::Log(Console::Error, "Compilation Error; \"No Outputs Specified\"");
return;
}
// Generate node priorities
ocu::dynarray Priority(State.Nodes.capacity(), -1);
int p = 0;
std::deque VisitQueue { Outputs[0].Ptr };
// The goal is to generate a priority for each node so that we can compile them to glsl in the correct order
// Each branch has the potential to contain the same node as another branch
// For this reason, we use a map first so that we don't have to spend a lot of time updating a priority queue
while(not VisitQueue.empty())
{
ImPinPtr pin = VisitQueue.front(); VisitQueue.pop_front();
NodeId node = ImNodeGraph::GetUserID(pin.Node).Int;
Priority[node] = p++;
for(Pin& pin : State.Nodes[node]->IO.Inputs)
{
const ImVector<ImGuiID>& connections = ImNodeGraph::GetConnections(pin.Ptr);
if(connections.empty()) continue;
ImPinConnection connection = ImNodeGraph::GetConnection(connections[0]);
if(connection.A == pin.Ptr)
{
// Skip Literals
if(Graph.FindPin(connection.B).Flags & PinFlags_Literal) continue;
VisitQueue.push_back(connection.B);
}
else
{
// Skip Literals
if(Graph.FindPin(connection.A).Flags & PinFlags_Literal) continue;
VisitQueue.push_back(connection.A);
}
}
}
// Here the priority map dumps each node into an order array with a nodes priority being repsective to its id
// We use optional as a shortcut to prevent any "real" sorting operation being done
// This allows us to keep these operations at linear time complexity
ocu::dynarray<ocu::optional<NodeId>> Order(p);
for(NodeId node = 0; node < Priority.size(); ++node)
{
if(Priority[node] == -1) continue;
Order[Priority[node]] = node;
}
// Write out the code
std::stringstream Out;
// Function Signature
Out << std::format("void {}(", GetFile().path().stem().string());
for(int i = 0; i < Inputs.size(); ++i)
{
if(i > 0)
Out << ", ";
Out << std::format("in {} {}", Pin::TypeKeywords[Inputs[i].Type], Inputs[i].GetVarName());
}
for(int i = 0; i < Outputs.size(); ++i)
{
if(not Inputs.empty() || i > 0)
Out << ", ";
Out << std::format("out {} {}", Pin::TypeKeywords[Outputs[i].Type], Outputs[i].GetVarName());
}
Out << ")" << std::endl;
Out << "{" << std::endl;
// Function Code
for(int i = Order.size() - 1; i >= 0; --i)
{
if(not Order[i]()) continue;
NodeId id = Order[i];
if(id == InputsID_) continue;
Out << State.Nodes[id]->GetCode();
if(i > 0) Out << std::endl;
}
Out << "}" << std::endl;
Code = Out.str();
Console::Log(Console::Message, "{}", Code);
CompileDisplayShader_();
}
void Function::Open()
{
EditorSystem::Get<ShaderGraph>()->OpenShader(this);
EditorSystem::Get<Renderer>()->OpenShader(this);
}
FileManager::Asset* Function::Create(const FileManager::Path &path)
{
return new Function(path, *EditorSystem::Get<ShaderGraph>());
}
FileManager::Asset* Function::Load(const FileManager::Path &path)
{
return nullptr;
}
FileManager::Asset* Function::Import(const FileManager::Path &src, const FileManager::Path &dst)
{
return nullptr;
}
std::string Function::GetCode() const
{
std::stringstream Out;
const auto& Outputs = IO.Inputs;
for(int i = 0; i < Outputs.size(); ++i)
{
Out << std::format("{} = {};", Outputs[i].GetVarName(), Graph.GetValue(Outputs[0].Ptr)) << std::endl;
}
return Out.str();
}
void Function::View(HDRTexture::HandleType* Target)
{
DrawImage_(Target);
DrawInputs_();
}
void Function::DrawImage_(HDRTexture::HandleType* Target)
{
if(Target == nullptr) return;
ImGui::BeginChild("##view", { 0, 0 }, ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY);
ImVec2 reg = ImGui::GetContentRegionAvail();
glm::vec2 size = Target->size();
float min_r = glm::min(reg.x / size.x, reg.y / size.y);
size = size * min_r;
Target->resize({ size.x, size.y });
size = Target->size();
Render_(Target);
if(reg.x > reg.y)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (reg.x - size.x) / 2.0f);
ImGui::Image(reinterpret_cast<ImTextureID>(static_cast<intptr_t>(Target->handle())), { size.x, size.y });
ImGui::EndChild();
}
void Function::DrawInputs_()
{
auto& Inputs = Inputs_.IO.Outputs;
const auto& Outputs = IO.Inputs;
const float avail_x = ImGui::GetContentRegionAvail().x;
if(ImGui::TreeNodeEx("Inputs"))
{
for(int i = 0; i < Inputs.size(); ++i)
{
int flags = ImGuiInputTextFlags_EnterReturnsTrue;
auto& pin = Inputs[i];
const auto& items = InputTypes[Inputs[i].Type];
auto& value = InputValues_[i];
ImGui::Text(pin.Name.c_str());
ImGui::SameLine();
ImGui::PushItemWidth(avail_x * 0.25f);
if(ImGui::BeginCombo(std::format("##{}_var", pin.Name).c_str(), InputNames[value].c_str()))
{
for(int j = 0; j < items.size(); ++j)
{
glw::enum_t t = items[j];
if(ImGui::Selectable(InputNames[t].c_str(), t == value))
{
value = t;
CompileDisplayShader_();
}
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
if(value == FuncInput_Custom)
{
ImGui::SameLine();
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
bool changed = false;
switch(pin.Type)
{
case PinType_Int: changed |= ImGui::InputInt(std::format("##{}_val", pin.Name).c_str(), pin.Value); break;
case PinType_UInt: changed |= ImGui::InputUInt(std::format("##{}_val", pin.Name).c_str(), pin.Value); break;
default:
case PinType_Float: changed |= ImGui::InputFloat(std::format("##{}_val", pin.Name).c_str(), pin.Value); break;
case PinType_Vector:
ImGui::BeginGroup();
changed |= ImGui::ColorPicker3(
std::format("##{}_val", pin.Name).c_str(), &pin.Value.get<glm::vec3>().x
, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float
);
changed |= ImGui::ColorPreview3(
std::format("##vec{}_val", pin.Name).c_str(), &pin.Value.get<glm::vec3>().x
, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float
);
ImGui::EndGroup();
break;
}
ImGui::PopItemWidth();
if(changed) CompileDisplayShader_();
}
/*if(ImGui::InputText(Inputs[i].Name.c_str(), &InputValues_[i], flags))
{
CompileDisplayShader_();
}*/
}
ImGui::TreePop();
}
if(ImGui::BeginCombo("Output", Outputs.empty() ? "" : Outputs[DisplayVar_].Name.c_str()))
{
for(int i = 0; i < Outputs.size(); ++i)
{
if(ImGui::Selectable(Outputs[i].Name.c_str(), DisplayVar_ == i))
{
DisplayVar_ = i;
CompileDisplayShader_();
}
}
ImGui::EndCombo();
}
}
void Function::Render_(HDRTexture::HandleType *Target)
{
if(Shader_ == nullptr)
{
glm::vec4 fill(0, 0, 0, 1);
Target->clear(Target->size(), { 0, 0 }, 0, &fill, glw::rgba, glw::float32);
return;
}
Shader_->bind();
Shader_->operator[]("t") = static_cast<float>(Engine::Runtime);
Shader_->operator[]("dt") = static_cast<float>(Engine::Delta);
Target->bind_image(0, glw::write);
auto size = Target->size();
Shader_->dispatch(size.x, size.y, 1);
}
void Function::CompileDisplayShader_()
{
const auto& Inputs = Inputs_.IO.Outputs;
const auto& Outputs = IO.Inputs;
std::stringstream Out;
// Print Version String and Group Sizes
Out << VersionString << std::endl;
Out << glw::shader::group_size(8, 8, 1) << std::endl;
Out << std::endl;
Out << glw::shader::built_ins_compute << std::endl;
Out << std::endl;
Out << "uniform float t;" << std::endl;
Out << "uniform float dt;" << std::endl;
// Print Function Code
Out << Code << std::endl << std::endl;
Out << "layout (rgba16f, binding = 0) writeonly restrict uniform image2D out_Color0;" << std::endl;
// Print out driver
Out << "void main(void)" << std::endl
<< "{" << std::endl
<< std::format(" vec3 size = vec3({}.xy, 1);", glw::shader::glw_RequestedInvocations) << std::endl
<< std::format(" vec3 xy = vec3({}.xy, 0);", glw::shader::gl_GlobalInvocationID) << std::endl
<< std::format(" vec3 uv = xy / size;") << std::endl
<< " float x = xy.x, y = xy.y, u = uv.x, v = uv.y;" << std::endl;
// Output vars
for(int i = 0; i < Outputs.size(); ++i)
{
Out << std::format(" {} out_{};", Pin::TypeKeywords[Outputs[i].Type], Outputs[i].Name) << std::endl;
}
// Function Call
Out << std::format(" {}(", GetFile().path().stem().string());
for(int i = 0; i < Inputs.size(); ++i)
{
if(i > 0)
Out << ", ";
glw::enum_t value = InputValues_.get(i, FuncInput_Custom);
if(value == FuncInput_Custom)
{
switch(Inputs[i].Type)
{
case PinType_UInt: Out << Inputs[i].Value.get<glm::int32>(); break;
case PinType_Int: Out << Inputs[i].Value.get<glm::uint32>(); break;
default:
case PinType_Float: Out << Inputs[i].Value.get<glm::float32>(); break;
case PinType_Vector:
const auto& v = Inputs[i].Value.get<glm::vec3>();
Out << std::format("vec3({}, {}, {})", v.x, v.y, v.z);
break;
}
}
else
{
Out << InputVars[value];
}
}
for(int i = 0; i < Outputs.size(); ++i)
{
if(not Inputs.empty() || i > 0)
Out << ", ";
Out << "out_" << Outputs[i].Name;
}
Out << ");" << std::endl;
// Result
if(Outputs.empty())
{
Console::Log(Console::Alert, "Attempted to compile function with no outputs.");
return;
}
switch(Outputs[DisplayVar_].Type)
{
case PinType_Float:
case PinType_Int:
case PinType_UInt:
Out << std::format(" vec3 result = vec3(out_{});", IO.Inputs[DisplayVar_].Name) << std::endl; break;
case PinType_Vector:
default:
Out << std::format(" vec3 result = out_{};", IO.Inputs[DisplayVar_].Name) << std::endl; break;
}
// Set the pixel in the render target
Out << " imageStore(out_Color0, ivec2(xy), vec4(result, 1));" << std::endl;
Out << "}" << std::endl;
// Get the code string
DisplayCode_ = Out.str();
// Generate the shader
delete Shader_;
Shader_ = new glw::shader();
Console::Log(Console::Message, "Generated GLSL:\n{}", DisplayCode_);
if(not Shader_->attach_source(glw::compute, { DisplayCode_ }))
{
Console::Log(Console::Error, "{}", Shader_->get_error_string());
delete Shader_; Shader_ = nullptr;
return;
}
if(not Shader_->link())
{
Console::Log(Console::Error, "{}", Shader_->get_error_string());
delete Shader_; Shader_ = nullptr;
return;
}
}
// Function Inputs -----------------------------------------------------------------------------------------------------
FunctionInputs::FunctionInputs(ShaderGraph &graph, ImVec2 pos)
: Node(graph, pos)
{
Info.Flags |= NodeFlags_Const;
Info.Alias = "FunctionInputs";
Header.Title = HeaderMarker + "Inputs";
Header.Color = HeaderColor;
Header.HoveredColor = HeaderHoveredColor;
Header.ActiveColor = HeaderActiveColor;
}
Node* FunctionInputs::Copy(ShaderGraph &graph) const
{
return new FunctionInputs(graph, { 0, 0 });
}
void FunctionInputs::Inspect()
{
auto& Inputs = IO.Outputs;
for(int i = 0; i < Inputs.size(); ++i)
{
const float avail_x = ImGui::GetContentRegionAvail().x;
ImGui::PushItemWidth(avail_x * 0.625f);
ImGui::InputText(std::format("##{}_name", i).c_str(), &Inputs[i].Name, ImGuiInputTextFlags_AutoSelectAll);
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::Combo(std::format("##{}_type", i).c_str(), &Inputs[i].Type, Pin::TypeNames, PinType_Any);
ImGui::PopItemWidth();
}
if(ImGui::TextLink("\uEA11"))
{
PinType type = PinType_Vector;
if(not Inputs.empty()) type = Inputs.back().Type;
Inputs.push_back({
std::format("In{}", Inputs.size()), type
});
}
}
std::string FunctionInputs::GetCode() const
{
return "";
}