nodes init

This commit is contained in:
lachrymaLF 2024-05-26 16:05:18 -04:00
parent 2f7378f218
commit 0d713b6da6
7 changed files with 214 additions and 173 deletions

View file

@ -5,29 +5,27 @@ cmake_minimum_required (VERSION 3.12)
project("Main")
add_executable (Keishiki
"ext/imgui/imgui.cpp" "ext/imgui_impl_bgfx.cpp" "ext/imgui/backends/imgui_impl_sdl2.cpp"
"ext/imgui/imgui_demo.cpp" "ext/imgui/imgui_draw.cpp" "ext/imgui/imgui_tables.cpp" "ext/imgui/imgui_widgets.cpp"
"ext/imgui/misc/cpp/imgui_stdlib.cpp"
file(GLOB SRC_FILES ${PROJECT_SOURCE_DIR}/*.cpp)
file(GLOB IMGUI_SRC ${PROJECT_SOURCE_DIR}/ext/imgui/*.cpp)
"Keishiki.cpp" "Graphics.cpp" "UI.cpp" "ShaderGraph.cpp")
add_executable (Keishiki ${IMGUI_SRC}
"ext/imgui_impl_bgfx.cpp" "ext/imgui/backends/imgui_impl_sdl2.cpp" "ext/imgui/misc/cpp/imgui_stdlib.cpp" "ext/ImNodeFlow/src/ImNodeFlow.cpp"
${SRC_FILES})
set_property(TARGET Keishiki PROPERTY CXX_STANDARD 23)
include_directories ("include" "include/ext" "ext/imgui")
include_directories ("include" "include/ext" "ext/imgui" "ext/ImNodeFlow/include")
add_subdirectory("ext/freetype")
add_subdirectory("ext/bgfx")
#add_subdirectory("ext/ImNodeFlow")
add_compile_definitions(IMGUI_DEFINE_MATH_OPERATORS)
#target_link_libraries(Keishiki ImNodeFlow)
if (WIN32)
include_directories("include/windows")
target_link_libraries (Keishiki PRIVATE freetype bgfx bx ${PROJECT_SOURCE_DIR}/lib/x64/SDL2.lib ${PROJECT_SOURCE_DIR}/lib/x64/SDL2main.lib)
target_link_libraries (Keishiki PRIVATE freetype bgfx bx ${PROJECT_SOURCE_DIR}/lib/x64/SDL2.lib ${PROJECT_SOURCE_DIR}/lib/x64/SDL2main.lib imgui-sdl2)
elseif (APPLE)
find_package(SDL2 REQUIRED)
target_link_libraries (Keishiki PRIVATE freetype bgfx bx SDL2::SDL2)

View file

@ -75,13 +75,12 @@ namespace K {
}
bgfx::setDebug(BGFX_DEBUG_TEXT);
bgfx::setViewClear(K::Graphics::K_VIEW_BG, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x000000ff, 1.0f, 0);
bgfx::setViewRect(K::Graphics::K_VIEW_BG, 0, 0, window_width, window_height);
bgfx::setViewRect(K::Graphics::K_VIEW_LOGO, 0, 0, window_width, window_height);
float view[16];
bx::mtxTranslate(view, 0.f, 0.f, 1.0f);
float proj[16];
bx::mtxOrtho(proj, 0.0f, window_width, 0.0f, window_height, 0.1f, 100.0f, 0.f, bgfx::getCaps()->homogeneousDepth);
bgfx::setViewTransform(K::Graphics::K_VIEW_BG, view, proj);
bgfx::setViewTransform(K::Graphics::K_VIEW_LOGO, view, proj);
K::UI::Init(window);
@ -100,7 +99,7 @@ namespace K {
}
void Render() {
K::Graphics::DrawTextureImageAlpha(K::Graphics::K_VIEW_BG, *logo, window_width - logo->w + 10, 0, .7f);
K::Graphics::DrawTextureImageAlpha(K::Graphics::K_VIEW_LOGO, *logo, window_width - logo->w + 10, 0, 1.0f);
UI::Draw(frame, state);

View file

@ -12,11 +12,12 @@
#include <stb_image_write.h>
namespace {
// windows
// Panels
bool draw_viewport = true,
draw_plugboard = true,
draw_shader = true,
draw_comp = true;
draw_comp = true,
draw_interpolation = true;
const f32 row_height = 20.0f;
@ -131,6 +132,7 @@ namespace K::UI {
ImGui::Text("Composition Size: %ux%u", s.width, s.height);
ImGui::SameLine();
auto cp = save_called;
if (cp) ImGui::BeginDisabled();
@ -146,7 +148,7 @@ namespace K::UI {
}
void Composition(CompState& s) {
if (!ImGui::Begin("Composition", &draw_plugboard)) {
if (!ImGui::Begin("Composition", &draw_comp)) {
ImGui::End();
return;
}
@ -179,10 +181,11 @@ namespace K::UI {
l.track.uniforms["translate"].second = ShaderGraph::T_Map<ShaderGraph::T_XY>::type(.0f, .0f);
l.track.shader = "void main() {\n"
"\tfloat angle = -rot * M_PI / 180.0f;\n"
"\tvec2 uv = vec2(v_texcoord0.x, 1.0f - v_texcoord0.y) - .5f;\n"
"\tuv = uv - translate;\n"
"\tuv = uv * aspect_ratio;\n"
"\tuv = vec2(cos(rot)*uv.x - sin(rot)*uv.y, sin(rot)*uv.x + cos(rot)*uv.y);\n"
"\tuv = vec2(cos(angle)*uv.x - sin(angle)*uv.y, sin(angle)*uv.x + cos(angle)*uv.y);\n"
"\tuv = uv / scale;\n"
"\tuv = uv + .5f;\n\n"
"\tvec4 tx = texture2D(s_texColor, uv);\n"
@ -192,6 +195,7 @@ namespace K::UI {
s.layers.push_back(std::move(l));
}
ImGui::SameLine();
ImGui::SetNextItemWidth(-100.0f);
ImGui::InputText("##LayerName", &name);
ImGui::SameLine();
if (no_selection) ImGui::BeginDisabled();
@ -261,7 +265,7 @@ namespace K::UI {
auto source_flags = ImGuiDragDropFlags_SourceNoDisableHover | ImGuiDragDropFlags_SourceNoHoldToOpenOthers;
if (!no_selection) source_flags |= ImGuiDragDropFlags_SourceNoPreviewTooltip;
if ((no_selection || s.selected.contains(i)) && ImGui::BeginDragDropSource(source_flags)) {
if (s.selected.empty()) ImGui::Text("Moving From #%u %s", i, s.layers[i].name.c_str());
if (s.selected.empty()) ImGui::Text("Swap with #%u %s", i, s.layers[i].name.c_str());
ImGui::SetDragDropPayload("DND_DEMO_NAME", &i, sizeof(u32));
ImGui::EndDragDropSource();
}
@ -335,101 +339,91 @@ namespace K::UI {
}
void Plugboard(CompState& s) {
if (ImGui::Begin("Plugboard", &draw_plugboard)) {
if (ImGui::BeginTable("Source", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit, { 50.0f, -FLT_MIN })) {
if (!ImGui::Begin("Plugboard", &draw_plugboard)) {
ImGui::End();
return;
}
ImGuiIO& io = ImGui::GetIO();
ImGuiStyle style = ImGui::GetStyle();
static f32 view_scale = 1.0f;
static ImVec2 view_pos{}, old_view_pos{};
ImGui::PushStyleColor(ImGuiCol_ChildBg, 0xFF100500);
ImGui::BeginChild("view_port", ImGui::GetContentRegionAvail(), 0, ImGuiWindowFlags_NoMove);
ImGui::PopStyleColor();
if (ImGui::IsMouseClicked(0)) {
old_view_pos = view_pos;
}
if (ImGui::IsItemActive()) {
view_pos = old_view_pos + (io.MousePos - io.MouseClickedPos[0]);
}
static ImVec2 out{}, in{}, out2{}, in2{};
static ImVec2 pos{}, old_pos{};
ImGui::SetCursorPos(view_pos + pos);
static bool active{};
ImGui::PushID(1);
ImGui::PushStyleColor(ImGuiCol_ChildBg, 0xFF080813);
ImGui::BeginChild("sad", {}, ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Border);
ImGui::PopStyleColor();
if (ImGui::BeginTable("s", 3)) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Button("Asdf", {100.0f , 0.0f});
if (ImGui::IsItemClicked()) {
old_pos = pos;
}
if (ImGui::IsItemActive()) {
pos = old_pos + (io.MousePos - io.MouseClickedPos[0]);
}
ImGui::TableNextColumn();
ImGui::PushID(1);
ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height);
ImGui::TableNextColumn();
ImGui::Text("Time");
ImGui::TableNextColumn();
ImGui::Dummy({100.0f - ImGui::CalcTextSize("asd").x - style.ItemSpacing.x, 0.0f});
ImGui::SameLine();
ImGui::Text("qq");
ImGui::TableNextColumn();
ImGui::RadioButton("##asd", active);
out = ImGui::GetCursorScreenPos() + ImVec2{ ImGui::GetFrameHeight() / 2, -ImGui::GetFrameHeight() / 2 - style.ItemInnerSpacing.y };
ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height);
ImGui::TableNextColumn();
ImGui::RadioButton("##asd", active);
in2 = ImGui::GetCursorScreenPos() + ImVec2{ ImGui::GetFrameHeight() / 2, -ImGui::GetFrameHeight() / 2 - style.ItemInnerSpacing.y };
ImGui::TableNextColumn();
ImGui::Text("asd");
ImGui::TableNextColumn();
ImGui::PopID();
ImGui::EndTable();
}
ImGui::SameLine();
if (ImGui::InvisibleButton("Graph", ImVec2{200.0f, 200.0f})) {
Log("d");
}
}
ImGui::End();
}
void Nodegraph(CompState& s) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems) {
ImGui::End();
return;
}
else {
ImGuiIO& io = ImGui::GetIO();
ImGui::Text("Inspecting layer #%u \"%s\"", active_index, active->name.c_str());
const ImGuiStyle& style = ImGui::GetCurrentContext()->Style;
const ImGuiID id = window->GetID("Nodegraph");
const ImRect bbox(window->DC.CursorPos, window->DC.CursorPos + ImGui::GetContentRegionAvail());
ImGui::ItemSize(bbox, style.FramePadding.y);
if (!ImGui::ItemAdd(bbox, id)) {
ImGui::End();
return;
}
/*
if (ImGui::IsItemActive()) {
window->DrawList->AddLine(io.MouseClickedPos[0], io.MousePos, 0xFFAAAAAA, 2.0f);
}
static ImRect canvas_rect{ 0.0f, 0.0f, 100.0f, 100.0f };
static ImRect view_rect{ 0.0f, 0.0f, 10.0f, 10.0f };
struct OutToIn {
Node* node;
u32 index;
};
auto DrawNode = [&](Node& n) {
ImGui::SetCursorPos(n.pos);
ImGui::BeginGroup();
ImGui::Button(n.name.c_str());
static ImVec2 old_pos = n.pos;
if (ImGui::IsItemClicked()) {
old_pos = n.pos;
}
if (ImGui::IsItemActive()) {
n.pos = old_pos + (io.MousePos - io.MouseClickedPos[0]);
}
u32 i = 0;
for (u32 u = 0; u < n.out.size(); u++) {
auto& [type, name] = n.out[u];
ImGui::PushID(i);
ImGui::Dummy({100.0f - ImGui::CalcTextSize(name.c_str()).x, 0});
ImGui::SameLine();
ImGui::Text(name.c_str());
ImGui::SameLine();
bool active{};
ImGui::RadioButton("##", active);
if (ImGui::IsItemActive()) {
window->DrawList->AddLine(io.MouseClickedPos[0], io.MousePos, 0xFFAAAAAA, 2.0f);
}
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
OutToIn d{ &n, u };
ImGui::SetDragDropPayload("K_SHADER_OUT_TO_IN", &d, sizeof(d));
ImGui::Text("%s", Type_To_Str[type]);
ImGui::Text("asdf");
ImGui::EndDragDropSource();
}
//if (ImGui::BeginDragDropTarget()) {
// if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("K_SHADER_IN_TO_OUT")) {
// InSlot* in = *(InSlot**)payload->Data;
// }
// ImGui::EndDragDropTarget();
//}
ImGui::PopID();
i++;
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("K_SHADER_IN_TO_OUT")) {
InSlot* in = *(InSlot**)payload->Data;
}
for (InSlot& s : n.inputs) {
ImGui::EndDragDropTarget();
}
*/
/* for (InSlot& s : n.inputs) {
ImGui::PushID(i);
bool active = s.node != nullptr;
@ -459,24 +453,48 @@ namespace K::UI {
ImGui::PopID();
i++;
}
}*/
ImGui::EndGroup();
window->DrawList->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), 0xFFFFFFFF);
};
window->DrawList->AddRectFilled(bbox.Min, bbox.Max, ImGui::GetColorU32(ImGuiCol_FrameBg));
for (u32 i = 0; i < active->track.tree.nodes.size(); i++) {
ImGui::PushID(i);
DrawNode(active->track.tree.nodes[i]);
ImGui::EndChild();
ImGui::PopID();
}
static bool graph_hover{}, graph_held{};
ImGui::ButtonBehavior(bbox, id, &graph_hover, &graph_held);
ImGui::SetCursorPos({});
ImGui::PushStyleColor(ImGuiCol_ChildBg, 0x33FFFFFF);
ImGui::BeginChild("asdasd", {}, ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY );
ImGui::PopStyleColor();
if (ImGui::BeginTable("Source", 2)) {
ImGui::PushID(1);
ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height);
ImGui::TableNextColumn();
ImGui::Text("Time");
ImGui::TableNextColumn();
ImGui::RadioButton("##asd", active);
out2 = ImGui::GetCursorScreenPos() + ImVec2{ ImGui::GetFrameHeight() / 2, -ImGui::GetFrameHeight() / 2 - style.ItemInnerSpacing.y };
ImGui::PopID();
ImGui::EndTable();
}
ImGui::EndChild();
ImGui::SetCursorPos({ImGui::GetContentRegionAvail().x - 100.0f, 0.0f});
ImGui::PushStyleColor(ImGuiCol_ChildBg, 0x33FFFFFF);
ImGui::BeginChild("ert", {}, ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY);
ImGui::PopStyleColor();
if (ImGui::BeginTable("Source", 1)) {
ImGui::PushID(1);
ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height);
ImGui::TableNextColumn();
ImGui::RadioButton("asfnjklanf", active);
in = ImGui::GetCursorScreenPos() + ImVec2{ ImGui::GetFrameHeight() / 2, -ImGui::GetFrameHeight() / 2 - style.ItemInnerSpacing.y };
ImGui::PopID();
ImGui::EndTable();
}
ImGui::EndChild();
ImGui::GetCurrentWindow()->DrawList->AddLine(in, out, 0xFFFFFFFF, 2.0f);
ImGui::GetCurrentWindow()->DrawList->AddLine(in2, out2, 0xFFAAAAAA, 2.0f);
ImGui::EndChild();
ImGui::End();
}
@ -498,18 +516,19 @@ namespace K::UI {
(data->EventChar >= '0' && data->EventChar <= '9') ||
data->EventChar == '_'));
} };
ImGui::SetNextItemWidth(-80.0f);
ImGui::InputText("##UniformName", &name, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::VariableName);
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::Combo("Type", &type_current, ShaderGraph::Type_To_Str, ShaderGraph::T_Count);
ImGui::Combo("##Type", &type_current, ShaderGraph::Type_To_Str, ShaderGraph::T_Count);
if (ImGui::BeginTable("Uniforms", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoSort);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_NoSort);
ImGui::TableSetupColumn("Expose", ImGuiTableColumnFlags_NoSort);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_NoSort, ImGui::GetContentRegionAvail().x / 1.7f);
ImGui::TableSetupColumn("##del", ImGuiTableColumnFlags_NoSort);
if (ImGui::BeginTable("Uniforms", 5, ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Expose", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##del", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed);
ImGui::TableHeadersRow();
i32 i = 0;
for (auto it = s.layers[s.active].track.uniforms.begin(); it != s.layers[s.active].track.uniforms.end();) {
@ -529,13 +548,13 @@ namespace K::UI {
if constexpr (std::is_same_v<T, ShaderGraph::T_Map<ShaderGraph::T_Float>::type>)
ImGui::DragFloat("##", &arg, 0.005f);
else if constexpr (std::is_same_v<T, ShaderGraph::T_Map<ShaderGraph::T_Int>::type>)
ImGui::DragInt("##", &arg);
ImGui::DragInt("##", &arg, 0.005f);
else if constexpr (std::is_same_v<T, ShaderGraph::T_Map<ShaderGraph::T_RGBA>::type>)
ImGui::DragFloat4("##", &arg.r);
ImGui::DragFloat4("##", &arg.r, 0.005f);
else if constexpr (std::is_same_v<T, ShaderGraph::T_Map<ShaderGraph::T_XY>::type>)
ImGui::DragFloat2("##", &arg.x);
ImGui::DragFloat2("##", &arg.x, 0.005f);
else if constexpr (std::is_same_v<T, ShaderGraph::T_Map<ShaderGraph::T_XYZ>::type>)
ImGui::DragFloat3("##", &arg.x);
ImGui::DragFloat3("##", &arg.x, 0.005f);
}, it->second.second);
ImGui::TableNextColumn();
if (ImGui::Button("X")) {
@ -548,7 +567,7 @@ namespace K::UI {
ImGui::EndTable();
}
ImGui::Text("Editing Layer %u: %s", s.active, s.layers[s.active].name.c_str());
ImGui::InputTextMultiline("##source", &s.layers[s.active].track.shader, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 40), ImGuiInputTextFlags_AllowTabInput);
ImGui::InputTextMultiline("##source", &s.layers[s.active].track.shader, ImVec2(-FLT_MIN, ImGui::GetContentRegionAvail().y - 25.0f), ImGuiInputTextFlags_AllowTabInput);
if (ImGui::Button("Submit Shader"))
s.layers[s.active].track.compile();
}
@ -556,12 +575,20 @@ namespace K::UI {
ImGui::End();
}
void Interpolation(CompState& s) {
if (ImGui::Begin("Interpolation", &draw_interpolation)) {
}
ImGui::End();
}
void Draw(u32 frame, CompState& s) {
ImGui_ImplSDL2_NewFrame();
ImGui_Implbgfx_NewFrame();
ImGui::NewFrame();
ImGui::ShowDemoWindow();
ImGui::DockSpaceOverViewport(ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode);
// ImGui::ShowDemoWindow();
static ImGuiStyle& style = ImGui::GetStyle();
style.GrabRounding = style.FrameRounding = 5.0f;
MainMenuBar(s);
@ -569,6 +596,7 @@ namespace K::UI {
if (draw_shader) Shader(s);
if (draw_plugboard) Plugboard(s);
if (draw_comp) Composition(s);
if (draw_interpolation) Interpolation(s);
if (save_called && ready_frame == frame) {
stbi_write_png("frame.png", s.width, s.height, 4, save_buffer, s.width * 4);
@ -585,7 +613,7 @@ namespace K::UI {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// io.Fonts->AddFontFromFileTTF("SourceHanSans-Regular.ttc", 17);
ImGui_Implbgfx_Init(K::Graphics::K_VIEW_TOP);
ImGui_Implbgfx_Init(K::Graphics::K_VIEW_UI);
switch (bgfx::getRendererType()) {
case bgfx::RendererType::Noop:

View file

@ -7,11 +7,11 @@
namespace K::Graphics {
enum VIEW_ID {
K_VIEW_BG,
K_VIEW_COMP_COMPOSITE,
K_VIEW_COMP_SAVE,
K_VIEW_DRAW,
K_VIEW_TOP,
K_VIEW_UI,
K_VIEW_LOGO,
};
bool Init(u16 width, u16 height);
@ -72,7 +72,6 @@ namespace K::Graphics {
K_V_ColourBurn,
K_V_LinearBurn,
K_V_ColourDodge,
K_V_LinearDodge,
K_V_Hue,
K_V_Saturation,
@ -112,7 +111,6 @@ namespace K::Graphics {
"Colour Burn",
"Linear Burn",
"Colour Dodge",
"Linear Dodge",
"Hue",
"Saturation",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -1,6 +1,6 @@
<img src="Keishiki.webp" alt="Logo" width="300"/>
# Keishiki: Real-Time Compositor-Sequencer
# Keishiki: Real-Time Compositor-Sequencer Framework
## Libraries
- [bgfx](https://github.com/bkaradzic/bgfx)

26
TODO.md
View file

@ -1,17 +1,35 @@
# NOW
## Compositor
- Manage Samplers -- (Resources in general)
- Blender previews (important)
- Data model for comps
- Dump and read back state, (de)serialization!!!
- std::unordered_map -> std::flat_map pending compiler support
- Non-negotiables:
- Text (idea: index-based evaluation in plugboard)
- Shapes (idea: embed glisp :mmtroll:, need to inquire -- still a lot of friction for simple shapes if we don't also get the glisp tools)
- External data driving (json?)
- Layer Groups (jokes -- can be completely UI side)
## UI
- Sequencer timeline init
- Viewport Gizmos (https://github.com/CedricGuillemet/ImGuizmo)
- Shader nodes (https://github.com/CedricGuillemet/ImGuizmo)
- Interpolation Editor init
- Timeline/Dope sheet init
- Viewport gizmos (Layer-specific & nodes -- can separate into different tools?)
# Later
## Compositor
- std::unordered_map -> std::flat_map pending compiler support
- Simple 3D engine
## Audio
- Wait for SDL3!
- SDL_mixer will be able to do all of wav ogg flac mp3 opus, we live in good times
- output needs to be handled differently (if we care at all -- likely not)
## IO
- Video import (research opencv libavcodec etc)
- maybe https://superuser.com/a/1397578 -- no portable way to get stdout, this is messed up
- boost::process? idk, might as well just go through a file
- File dialogues pending SDL3
- https://wiki.libsdl.org/SDL3/CategoryDialog
- Down the road: OIIO output for more than PNG's
- don't care about video export -- leave it for ffmpeg