diff --git a/.gitignore b/.gitignore index 83fa3abba..a62b62cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ session_manager.mu # See maya_tools.mu.in maya_tools.mu # See rvnuke_mode.mu.in -rvnuke_mode.mu \ No newline at end of file +rvnuke_mode.musrc/plugins/rv-packages/session_manager/composite_ui.py +*_ui.py diff --git a/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-sixteen.md b/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-sixteen.md index 9b47b359a..f8b7e5174 100644 --- a/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-sixteen.md +++ b/docs/rv-manuals/rv-reference-manual/rv-reference-manual-chapter-sixteen.md @@ -610,7 +610,7 @@ The stack node is part of a stack group and handles control for settings like co | output.size | int[2] | 1 | The virtual output size of the stack. This may not match the input sizes. | | output.autoSize | int | 1 | Figure out a good size automatically from the input sizes if 1. Otherwise use output.size. | | output.chosenAudioInput | string | 1 | Name of input which becomes the audio output of the stack. If the value is .all. then all inputs are mixed. If the value is .first. then the first input is used. | -| composite.type | string | 1 | The compositing operation to perform on the inputs. Valid values are: over, add, difference, -difference, and replace | +| composite.type | string | 1 | The compositing operation to perform on the inputs. Valid values are: over, add, dissolve, difference, -difference, replace, and topmost | | mode.useCutInfo | int | 1 | Use cut information on the inputs to determine EDL timing. | | mode.strictFrameRanges | int | 1 | If 1 match the timeline frames to the source frames instead of retiming to frame 1. | | mode.alignStartFrames | int | 1 | If 1 offset all inputs so they start at same frame as the first input. | diff --git a/src/lib/ip/IPBaseNodes/IPBaseNodes/StackIPNode.h b/src/lib/ip/IPBaseNodes/IPBaseNodes/StackIPNode.h index ce7b320bd..965f9a121 100644 --- a/src/lib/ip/IPBaseNodes/IPBaseNodes/StackIPNode.h +++ b/src/lib/ip/IPBaseNodes/IPBaseNodes/StackIPNode.h @@ -70,6 +70,8 @@ namespace IPCore IntProperty* autoSizeProperty() const { return m_autoSize; } + // float dissolveAmount() const { return m_dissolveAmount->front(); } + virtual void propagateFlushToInputs(const FlushContext&); void invalidate(); @@ -112,6 +114,7 @@ namespace IPCore IntProperty* m_autoSize; IntProperty* m_interactiveSize; IntProperty* m_supportReversedOrderBlending; + FloatProperty* m_dissolveAmount; private: static std::string m_defaultCompType; diff --git a/src/lib/ip/IPBaseNodes/StackIPNode.cpp b/src/lib/ip/IPBaseNodes/StackIPNode.cpp index 1f2931a85..74f212431 100644 --- a/src/lib/ip/IPBaseNodes/StackIPNode.cpp +++ b/src/lib/ip/IPBaseNodes/StackIPNode.cpp @@ -87,6 +87,8 @@ namespace IPCore || this->name() == "defaultLayout_stack") ? m_defaultCompType : "over"); + + m_dissolveAmount = declareProperty("composite.dissolveAmount", 0.5f); } StackIPNode::~StackIPNode() { pthread_mutex_destroy(&m_lock); } @@ -175,6 +177,9 @@ namespace IPCore if (p == m_activeAudioInput) updateChosenAudioInput(); + + if (p == m_dissolveAmount) + invalidate(); } IPNode::propertyChanged(p); @@ -453,7 +458,16 @@ namespace IPCore inExpressions); root->appendChildren(images); - root->mergeExpr = newBlend(root, inExpressions, mode); + if (mode == IPImage::Dissolve) + { + float dissolveAmount = m_dissolveAmount ? m_dissolveAmount->front() : 0.5f; + root->mergeExpr = newDissolveBlend(root, inExpressions, mode, dissolveAmount); + } + else + { + root->mergeExpr = newBlend(root, inExpressions, mode); + } + root->shaderExpr = Shader::newSourceRGBA(root); root->recordResourceUsage(); @@ -504,10 +518,12 @@ namespace IPCore const IPImage::BlendMode mode = IPImage::getBlendModeFromString(comp); const bool topmostOnly = !strcmp(comp, "topmost"); + const bool dissolveOnly = !strcmp(comp, "dissolve"); const bool localUseMerge = useMerge - || (mode == IPImage::Replace && !topmostOnly && useMergeForReplace); + || (mode == IPImage::Replace && !topmostOnly && useMergeForReplace) + || (mode == IPImage::Dissolve); IPImage* root = 0; @@ -568,6 +584,12 @@ namespace IPCore haveOneImage = true; } + if (dissolveOnly && images.size() >= 2) + { + // For dissolve mode, only process the first two inputs + break; + } + Context c = context; c.fps = m_outputFPS->front(); c.frame = inputFrame(i, frame); @@ -726,6 +748,7 @@ namespace IPCore } const bool topmostOnly = (!strcmp(comp, "topmost")); + const bool dissolveOnly = (!strcmp(comp, "dissolve")); const bool strictFrameRanges = m_strictFrameRanges->front(); const bool useCutInfo = m_useCutInfo->front(); @@ -764,6 +787,12 @@ namespace IPCore haveOneImage = true; } + if (dissolveOnly && i >= 2) + { + // For dissolve mode, only process the first two inputs + break; + } + Context c = context; c.frame = inputFrame(i, frame); diff --git a/src/lib/ip/IPCore/CMakeLists.txt b/src/lib/ip/IPCore/CMakeLists.txt index 11a7b3cc5..4995186d9 100644 --- a/src/lib/ip/IPCore/CMakeLists.txt +++ b/src/lib/ip/IPCore/CMakeLists.txt @@ -155,6 +155,7 @@ SET(_shaders ReverseDifference2 ReverseDifference3 ReverseDifference4 + Dissolve2 InlineDissolve2 ColorCDL_SAT_noClamp ColorPremultLight diff --git a/src/lib/ip/IPCore/IPCore/ShaderCommon.h b/src/lib/ip/IPCore/IPCore/ShaderCommon.h index c3d2fbe5f..85ca4638e 100644 --- a/src/lib/ip/IPCore/IPCore/ShaderCommon.h +++ b/src/lib/ip/IPCore/IPCore/ShaderCommon.h @@ -238,6 +238,9 @@ namespace IPCore Expression* newBlend(const IPImage*, const std::vector&, const IPImage::BlendMode); + Expression* newDissolveBlend(const IPImage*, const std::vector&, + const IPImage::BlendMode, float dissolveAmount); + Expression* newHistogram(const IPImage*, const std::vector&); diff --git a/src/lib/ip/IPCore/IPImage.cpp b/src/lib/ip/IPCore/IPImage.cpp index 2ee2e721f..6380fa4b0 100644 --- a/src/lib/ip/IPCore/IPImage.cpp +++ b/src/lib/ip/IPCore/IPImage.cpp @@ -321,7 +321,6 @@ namespace IPCore blendMode = IPImage::Replace; else if (!strcmp(blendModeString, "topmost")) blendMode = IPImage::Replace; - return blendMode; } diff --git a/src/lib/ip/IPCore/ShaderCommon.cpp b/src/lib/ip/IPCore/ShaderCommon.cpp index 08a1992d9..cc02e99e7 100644 --- a/src/lib/ip/IPCore/ShaderCommon.cpp +++ b/src/lib/ip/IPCore/ShaderCommon.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -142,6 +143,7 @@ extern const char* Difference4_glsl; extern const char* ReverseDifference2_glsl; extern const char* ReverseDifference3_glsl; extern const char* ReverseDifference4_glsl; +extern const char* Dissolve2_glsl; extern const char* InlineDissolve2_glsl; extern const char* BoxFilter_glsl; extern const char* ConstantBG_glsl; @@ -844,6 +846,30 @@ namespace IPCore funcName, shaderCode, Shader::Function::Color, params, globals); } + Function* dissolveBlend(int no, const char* funcName, const char* shaderCode) + { + assert(no >= 2); + assert(no <= MAX_TEXTURE_PER_SHADER); + + SymbolVector params, globals; + + // Add input texture parameters (i0, i1, i2, i3) + for (int i = 0; i < no; ++i) + { + ostringstream str; + str << "i" << i; + params.push_back(new Symbol(Symbol::ParameterConstIn, str.str(), + Symbol::Vec4fType)); + } + + // Add dissolve amount parameter + params.push_back(new Symbol(Symbol::ParameterConstIn, "dissolveAmount", + Symbol::FloatType)); + + return new Shader::Function( + funcName, shaderCode, Shader::Function::Color, params, globals); + } + Function* colorQuantize() { if (!Shader_ColorQuantize) @@ -3036,6 +3062,7 @@ namespace IPCore const char* reverseDifferenceShaders[] = {ReverseDifference2_glsl, ReverseDifference3_glsl, ReverseDifference4_glsl}; + const char* dissolveShaders[] = {Dissolve2_glsl}; // generate a blend expr of a certain mode // with the input Expressions as input to the blend shaders (over, add, @@ -3080,7 +3107,6 @@ namespace IPCore F = blend(size, name, overShaders[size - 2]); //*2_glsl is at position 0 break; - // TODO: dissolve } ArgumentVector args(F->parameters().size()); @@ -3093,6 +3119,37 @@ namespace IPCore return new Expression(F, args, image); } + // Dissolve blend function for exactly 2 inputs with dissolveAmount parameter + Expression* newDissolveBlend(const IPImage* image, + const vector& FA1, + const IPImage::BlendMode mode, + float dissolveAmount) + { + if (mode == IPImage::Dissolve) + { + int size = FA1.size(); + assert(size == 2); // Dissolve only works with exactly 2 inputs + + // Create dissolve blend function for 2 inputs + Function* F = dissolveBlend(2, "main", dissolveShaders[0]); + ArgumentVector args(F->parameters().size()); + + // Bind the 2 input expressions + args[0] = new BoundExpression(F->parameters()[0], FA1[0]); + args[1] = new BoundExpression(F->parameters()[1], FA1[1]); + + // Bind the dissolveAmount parameter + args[2] = new BoundFloat(F->parameters()[2], dissolveAmount); + + return new Expression(F, args, image); + } + else + { + // For non-dissolve modes, use the original function + return newBlend(image, FA1, mode); + } + } + Expression* newHistogram(const IPImage* image, const std::vector& FA1) { diff --git a/src/lib/ip/IPCore/glsl/Dissolve2.glsl b/src/lib/ip/IPCore/glsl/Dissolve2.glsl new file mode 100644 index 000000000..19318afc8 --- /dev/null +++ b/src/lib/ip/IPCore/glsl/Dissolve2.glsl @@ -0,0 +1,20 @@ +// +// Copyright (C) 2023 Autodesk, Inc. All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Dissolve blend between two inputs with configurable amount + +vec4 main(const in vec4 i0, const in vec4 i1, const in float dissolveAmount) +{ + // Blend RGB channels using dissolveAmount (0.0 = all i1, 1.0 = all i0) + vec3 rgb = mix(i1.rgb, i0.rgb, dissolveAmount); + + // Ignore input alpha channels and output solid alpha of 1.0 + float alpha = 1.0; + + return vec4(clamp(rgb.r, 0.0, 1.0), + clamp(rgb.g, 0.0, 1.0), + clamp(rgb.b, 0.0, 1.0), + alpha); +} diff --git a/src/plugins/rv-packages/session_manager/Composite_edit_mode.mu b/src/plugins/rv-packages/session_manager/Composite_edit_mode.mu index 1c84af75a..a2d7e9559 100644 --- a/src/plugins/rv-packages/session_manager/Composite_edit_mode.mu +++ b/src/plugins/rv-packages/session_manager/Composite_edit_mode.mu @@ -17,12 +17,14 @@ use glyph; use app_utils; use io; use system; -use app_utils; class: CompositeEditMode : MinorMode { QWidget _ui; QComboBox _comboBox; + QLineEdit _dissolveLineEdit; + QLabel _dissolveLabel; + QSlider _dissolveSlider; method: auxFilePath (string; string name) { @@ -37,14 +39,18 @@ class: CompositeEditMode : MinorMode { 0 -> { name = "over"; } 1 -> { name = "add"; } - // 2 -> { name = "dissolve"; } - 2 -> { name = "difference"; } - 3 -> { name = "-difference"; } - 4 -> { name = "replace"; } - 5 -> { name = "topmost"; } + 2 -> { name = "dissolve"; } + 3 -> { name = "difference"; } + 4 -> { name = "-difference"; } + 5 -> { name = "replace"; } + 6 -> { name = "topmost"; } } set("#RVStack.composite.type", name); + + // Force UI update immediately after changing blend mode + updateUI(); + redraw(); } @@ -53,25 +59,109 @@ class: CompositeEditMode : MinorMode setOp(index); } + method: setDissolveAmount (void;) + { + let amountText = _dissolveLineEdit.text(); + + try + { + float amount = float(amountText); + // Clamp to valid range + if (amount < 0.0) amount = 0.0; + if (amount > 1.0) amount = 1.0; + + // Update slider to match (0.0-1.0 -> 0-100) + _dissolveSlider.setValue(int(amount * 100.0)); + + set("#RVStack.composite.dissolveAmount", float[] {amount}); + redraw(); + } + catch (...) + { + // If parsing fails, reset to default + _dissolveLineEdit.setText("0.5"); + _dissolveSlider.setValue(50); + set("#RVStack.composite.dissolveAmount", float[] {0.5}); + redraw(); + } + } + + method: setDissolveAmountFromSlider (void; int value) + { + // Convert slider value (0-100) to float (0.0-1.0) + float amount = float(value) / 100.0; + + // Update text field + _dissolveLineEdit.setText("%g" % amount); + + set("#RVStack.composite.dissolveAmount", float[] {amount}); + redraw(); + } + method: updateUI (void;) { if (_ui eq nil) return; int index = 0; + string currentType = getStringProperty("#RVStack.composite.type").front(); - case (getStringProperty("#RVStack.composite.type").front()) + case (currentType) { "over" -> { index = 0; } "add" -> { index = 1; } - // "dissolve" -> { index = 2; } - "difference" -> { index = 2; } - "-difference" -> { index = 3; } - "replace" -> { index = 4; } - "topmost" -> { index = 5; } - _ -> { index = 6; } + "dissolve" -> { index = 2; } + "difference" -> { index = 3; } + "-difference" -> { index = 4; } + "replace" -> { index = 5; } + "topmost" -> { index = 6; } + _ -> { index = 7; } } _comboBox.setCurrentIndex(index); + + // Show/hide dissolve amount controls based on mode + bool showDissolve = (currentType == "dissolve"); + _dissolveLineEdit.setVisible(showDissolve); + _dissolveLabel.setVisible(showDissolve); + _dissolveSlider.setVisible(showDissolve); + + // Resize UI to fit the visible controls + if (_ui neq nil) + { + _ui.adjustSize(); + _ui.updateGeometry(); + + // Force a repaint to ensure layout updates are visible + _ui.update(); + + // Try to trigger parent layout update + QWidget parent = _ui.parentWidget(); + if (parent neq nil) + { + parent.adjustSize(); + parent.update(); + } + } + + // Update dissolve amount value + if (showDissolve) + { + try + { + float[] amounts = getFloatProperty("#RVStack.composite.dissolveAmount"); + if (amounts.size() > 0) + { + float amount = amounts[0]; + _dissolveLineEdit.setText("%g" % amount); + _dissolveSlider.setValue(int(amount * 100.0)); + } + } + catch (...) + { + _dissolveLineEdit.setText("0.5"); // Default value + _dissolveSlider.setValue(50); + } + } } method: propertyChanged (void; Event event) @@ -89,6 +179,7 @@ class: CompositeEditMode : MinorMode if (comp == "composite") { if (name == "type") updateUI(); + else if (name == "dissolveAmount") updateUI(); } event.reject(); @@ -107,8 +198,19 @@ class: CompositeEditMode : MinorMode { _ui = loadUIFile(manager.auxFilePath("composite.ui"), m); _comboBox = _ui.findChild("comboBox"); + _dissolveLineEdit = _ui.findChild("dissolveLineEdit"); + _dissolveLabel = _ui.findChild("dissolveLabel"); + _dissolveSlider = _ui.findChild("dissolveSlider"); + + // Initially hide dissolve controls + _dissolveLineEdit.setVisible(false); + _dissolveLabel.setVisible(false); + _dissolveSlider.setVisible(false); + manager.addEditor("Composite Function", _ui); connect(_comboBox, QComboBox.currentIndexChanged, setOp); + connect(_dissolveLineEdit, QLineEdit.editingFinished, setDissolveAmount); + connect(_dissolveSlider, QSlider.valueChanged, setDissolveAmountFromSlider); } updateUI(); @@ -142,11 +244,11 @@ class: CompositeEditMode : MinorMode {"Composite Operation", nil, nil, inactiveState}, {" Over", setOpEvent(,0), nil, opState("over")}, {" Add", setOpEvent(,1), nil, opState("add")}, - //{" Dissolve", setOpEvent(,2), nil, opState("dissolve")}, - {" Difference", setOpEvent(,2), nil, opState("difference")}, - {" Inverted Difference", setOpEvent(,3), nil, opState("-difference")}, - {" Replace", setOpEvent(,4), nil, opState("replace")}, - {" Topmost", setOpEvent(,5), nil, opState("topmost")}, + {" Dissolve", setOpEvent(,2), nil, opState("dissolve")}, + {" Difference", setOpEvent(,3), nil, opState("difference")}, + {" Inverted Difference", setOpEvent(,4), nil, opState("-difference")}, + {" Replace", setOpEvent(,5), nil, opState("replace")}, + {" Topmost", setOpEvent(,6), nil, opState("topmost")}, {"_", nil, nil, nil}, {"Cycle Forward", cycleStackForward, nil, isStackMode}, {"Cycle Backward", cycleStackBackward, nil, isStackMode} diff --git a/src/plugins/rv-packages/session_manager/composite.ui b/src/plugins/rv-packages/session_manager/composite.ui index 00f53f3e6..549035abc 100644 --- a/src/plugins/rv-packages/session_manager/composite.ui +++ b/src/plugins/rv-packages/session_manager/composite.ui @@ -6,8 +6,8 @@ 0 0 - 207 - 83 + 272 + 140 @@ -19,6 +19,18 @@ + + + 0 + 0 + + + + + 0 + 22 + + Over @@ -29,6 +41,11 @@ Add + + + Dissolve + + Difference @@ -51,10 +68,59 @@ + + + + Dissolve Amount + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + Dissolve amount (0-100%) + + + 0 + + + 100 + + + 50 + + + Qt::Orientation::Horizontal + + + + + + + + 40 + 16777215 + + + + Dissolve amount (0.0 to 1.0) + + + 0.5 + + + + + + - Qt::Vertical + Qt::Orientation::Vertical @@ -70,7 +136,7 @@ Operation - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter