From 0c68c266a48bd18ed2dfa3acd3919e60c0a9d2a7 Mon Sep 17 00:00:00 2001 From: nigel sumner Date: Thu, 2 Oct 2025 11:21:06 +1300 Subject: [PATCH] Add dissolve blend composite mode with UI controls This commit implements a new dissolve blend composite mode for OpenRV's stack compositor with full UI integration and real-time controls. Features: - Dissolve blend mode in StackIPNode for 2-input compositing - GPU shader implementation (Dissolve2.glsl) with configurable dissolve amount - UI slider and text input controls in Session Manager - Real-time dissolve amount adjustment (0.0 to 1.0 range) - Immediate UI updates when switching between composite modes Technical Implementation: - StackIPNode.cpp: Added dissolve mode support with m_dissolveAmount property - ShaderCommon.cpp: Implemented newDissolveBlend() function for GPU shader management - Dissolve2.glsl: GLSL shader for hardware-accelerated dissolve blending - composite.ui: Added QSlider and QLineEdit controls for dissolve amount - Composite_edit_mode.mu: Enhanced with slider integration and UI synchronization - CMakeLists.txt: Updated build configuration for dissolve shader compilation Tested on macOS with existing OpenRV build system. UI controls properly show/hide when switching composite modes, and dissolve blending works correctly with 2-input stacks. Signed-off-by: nigel sumner --- .gitignore | 3 +- .../rv-reference-manual-chapter-sixteen.md | 2 +- .../ip/IPBaseNodes/IPBaseNodes/StackIPNode.h | 3 + src/lib/ip/IPBaseNodes/StackIPNode.cpp | 33 ++++- src/lib/ip/IPCore/CMakeLists.txt | 1 + src/lib/ip/IPCore/IPCore/ShaderCommon.h | 3 + src/lib/ip/IPCore/IPImage.cpp | 1 - src/lib/ip/IPCore/ShaderCommon.cpp | 59 +++++++- src/lib/ip/IPCore/glsl/Dissolve2.glsl | 20 +++ .../session_manager/Composite_edit_mode.mu | 138 +++++++++++++++--- .../rv-packages/session_manager/composite.ui | 74 +++++++++- 11 files changed, 309 insertions(+), 28 deletions(-) create mode 100644 src/lib/ip/IPCore/glsl/Dissolve2.glsl 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 68633af3d..7d127735b 100644 --- a/src/lib/ip/IPCore/CMakeLists.txt +++ b/src/lib/ip/IPCore/CMakeLists.txt @@ -151,6 +151,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 97e114478..af8bac06f 100644 --- a/src/lib/ip/IPCore/IPCore/ShaderCommon.h +++ b/src/lib/ip/IPCore/IPCore/ShaderCommon.h @@ -234,6 +234,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 1dcb37764..d63362be9 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 @@ -138,6 +139,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; @@ -836,6 +838,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) @@ -2971,6 +2997,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, @@ -3015,7 +3042,6 @@ namespace IPCore F = blend(size, name, overShaders[size - 2]); //*2_glsl is at position 0 break; - // TODO: dissolve } ArgumentVector args(F->parameters().size()); @@ -3028,6 +3054,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