From cac5a40b58797f28ffa3ae1085fbee6afd25c33b Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 7 Jun 2026 00:24:08 +0800 Subject: [PATCH] Implement color LUT v0.4 features --- app/node/color/CMakeLists.txt | 2 + app/node/color/ociolut/CMakeLists.txt | 14 ++ app/node/color/ociolut/ociolut.cpp | 138 ++++++++++++++++++ app/node/color/ociolut/ociolut.h | 58 ++++++++ app/node/color/threewaycolor/CMakeLists.txt | 22 +++ .../color/threewaycolor/threewaycolor.cpp | 124 ++++++++++++++++ app/node/color/threewaycolor/threewaycolor.h | 77 ++++++++++ app/node/factory.cpp | 6 + app/node/factory.h | 2 + app/panel/scope/scope.cpp | 8 + app/panel/scope/scope.h | 4 + app/shaders/rgbvectorscope.frag | 48 ++++++ app/shaders/rgbvectorscope.vert | 25 ++++ app/shaders/threewaycolor.frag | 35 +++++ app/widget/filefield/filefield.cpp | 3 +- app/widget/filefield/filefield.h | 7 + .../nodeparamviewwidgetbridge.cpp | 2 + app/widget/scope/CMakeLists.txt | 1 + app/widget/scope/vectorscope/CMakeLists.txt | 22 +++ app/widget/scope/vectorscope/vectorscope.cpp | 132 +++++++++++++++++ app/widget/scope/vectorscope/vectorscope.h | 45 ++++++ docs/zh/color-lut-v04-plan.md | 114 +++++++++++++++ tests/gtest/CMakeLists.txt | 1 + tests/gtest/color_lut_test.cpp | 103 +++++++++++++ tests/gtest/shader_resources_test.cpp | 5 +- 25 files changed, 996 insertions(+), 2 deletions(-) create mode 100644 app/node/color/ociolut/CMakeLists.txt create mode 100644 app/node/color/ociolut/ociolut.cpp create mode 100644 app/node/color/ociolut/ociolut.h create mode 100644 app/node/color/threewaycolor/CMakeLists.txt create mode 100644 app/node/color/threewaycolor/threewaycolor.cpp create mode 100644 app/node/color/threewaycolor/threewaycolor.h create mode 100644 app/shaders/rgbvectorscope.frag create mode 100644 app/shaders/rgbvectorscope.vert create mode 100644 app/shaders/threewaycolor.frag create mode 100644 app/widget/scope/vectorscope/CMakeLists.txt create mode 100644 app/widget/scope/vectorscope/vectorscope.cpp create mode 100644 app/widget/scope/vectorscope/vectorscope.h create mode 100644 docs/zh/color-lut-v04-plan.md create mode 100644 tests/gtest/color_lut_test.cpp diff --git a/app/node/color/CMakeLists.txt b/app/node/color/CMakeLists.txt index 96bd0db4f..37975cab4 100644 --- a/app/node/color/CMakeLists.txt +++ b/app/node/color/CMakeLists.txt @@ -18,6 +18,8 @@ add_subdirectory(colormanager) add_subdirectory(displaytransform) add_subdirectory(ociobase) add_subdirectory(ociogradingtransformlinear) +add_subdirectory(ociolut) +add_subdirectory(threewaycolor) set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/node/color/ociolut/CMakeLists.txt b/app/node/color/ociolut/CMakeLists.txt new file mode 100644 index 000000000..55a4ae8a2 --- /dev/null +++ b/app/node/color/ociolut/CMakeLists.txt @@ -0,0 +1,14 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# 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. + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/ociolut/ociolut.cpp + node/color/ociolut/ociolut.h + PARENT_SCOPE +) diff --git a/app/node/color/ociolut/ociolut.cpp b/app/node/color/ociolut/ociolut.cpp new file mode 100644 index 000000000..0fb2fae87 --- /dev/null +++ b/app/node/color/ociolut/ociolut.cpp @@ -0,0 +1,138 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + 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 "ociolut.h" + +#include + +#include "node/color/colormanager/colormanager.h" + +namespace olive +{ + +const QString OCIOLutNode::kFileInput = QStringLiteral("lut_file_in"); +const QString OCIOLutNode::kDirectionInput = QStringLiteral("lut_dir_in"); + +#define super OCIOBaseNode + +OCIOLutNode::OCIOLutNode() +{ + AddInput(kFileInput, NodeValue::kFile, QString(), + InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable)); + SetInputProperty( + kFileInput, QStringLiteral("filter"), + tr("LUT Files (*.cube *.3dl);;Cube LUT (*.cube);;3DL LUT (*.3dl);;All Files (*)")); + SetInputProperty(kFileInput, QStringLiteral("placeholder"), + tr("Select a .cube or .3dl LUT file")); + + AddInput(kDirectionInput, NodeValue::kCombo, 0, + InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable)); +} + +QString OCIOLutNode::Name() const +{ + return tr("OCIO LUT"); +} + +QString OCIOLutNode::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.ociolut"); +} + +QVector OCIOLutNode::Category() const +{ + return { kCategoryColor }; +} + +QString OCIOLutNode::Description() const +{ + return tr("Applies a LUT file through OpenColorIO."); +} + +void OCIOLutNode::Retranslate() +{ + super::Retranslate(); + + SetInputName(kTextureInput, tr("Input")); + SetInputName(kFileInput, tr("LUT File")); + SetInputName(kDirectionInput, tr("Direction")); + SetComboBoxStrings(kDirectionInput, { tr("Forward"), tr("Inverse") }); +} + +void OCIOLutNode::InputValueChangedEvent(const QString &input, int element) +{ + Q_UNUSED(element) + + if (input == kFileInput || input == kDirectionInput) { + GenerateProcessor(); + } +} + +void OCIOLutNode::ConfigChanged() +{ + GenerateProcessor(); +} + +void OCIOLutNode::GenerateProcessor() +{ + if (!manager()) { + set_processor(nullptr); + return; + } + + const QString path = GetStandardValue(kFileInput).toString(); + if (path.isEmpty()) { + set_processor(nullptr); + return; + } + + const QFileInfo info(path); + if (!info.exists() || !info.isFile()) { + qWarning() << "OCIO LUT file does not exist:" << path; + set_processor(nullptr); + return; + } + + const QString suffix = info.suffix(); + if (!OCIO::FileTransform::IsFormatExtensionSupported(suffix.toUtf8().constData())) { + qWarning() << "Unsupported OCIO LUT file extension:" << path; + set_processor(nullptr); + return; + } + + try { + OCIO::FileTransformRcPtr transform = OCIO::FileTransform::Create(); + transform->setSrc(path.toUtf8().constData()); + transform->setInterpolation(OCIO::INTERP_LINEAR); + transform->setDirection( + static_cast( + GetStandardValue(kDirectionInput).toInt()) == ColorProcessor::kNormal + ? OCIO::TRANSFORM_DIR_FORWARD + : OCIO::TRANSFORM_DIR_INVERSE); + + set_processor(ColorProcessor::Create( + manager()->GetConfig()->getProcessor(transform))); + } catch (const OCIO::Exception &e) { + qWarning() << "OCIO LUT processor error:" << e.what(); + set_processor(nullptr); + } +} + +} // namespace olive diff --git a/app/node/color/ociolut/ociolut.h b/app/node/color/ociolut/ociolut.h new file mode 100644 index 000000000..98daa8177 --- /dev/null +++ b/app/node/color/ociolut/ociolut.h @@ -0,0 +1,58 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + 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 . + +***/ + +#ifndef OCIOLUTNODE_H +#define OCIOLUTNODE_H + +#include "node/color/ociobase/ociobase.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +class OCIOLutNode : public OCIOBaseNode { + Q_OBJECT +public: + OCIOLutNode(); + + NODE_DEFAULT_FUNCTIONS(OCIOLutNode) + + virtual QString Name() const override; + virtual QString id() const override; + virtual QVector Category() const override; + virtual QString Description() const override; + + virtual void Retranslate() override; + virtual void InputValueChangedEvent(const QString &input, + int element) override; + + static const QString kFileInput; + static const QString kDirectionInput; + +protected slots: + virtual void ConfigChanged() override; + +private: + void GenerateProcessor(); +}; + +} // namespace olive + +#endif // OCIOLUTNODE_H diff --git a/app/node/color/threewaycolor/CMakeLists.txt b/app/node/color/threewaycolor/CMakeLists.txt new file mode 100644 index 000000000..ee2ba14a2 --- /dev/null +++ b/app/node/color/threewaycolor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/threewaycolor/threewaycolor.h + node/color/threewaycolor/threewaycolor.cpp + PARENT_SCOPE +) diff --git a/app/node/color/threewaycolor/threewaycolor.cpp b/app/node/color/threewaycolor/threewaycolor.cpp new file mode 100644 index 000000000..3b03df09d --- /dev/null +++ b/app/node/color/threewaycolor/threewaycolor.cpp @@ -0,0 +1,124 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 mikesolar + + 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 "threewaycolor.h" + +#include + +#include "node/project.h" +#include "widget/slider/floatslider.h" + +namespace olive +{ + +#define super Node + +const QString ThreeWayColorNode::kTextureInput = QStringLiteral("tex_in"); +const QString ThreeWayColorNode::kShadowsColorInput = + QStringLiteral("shadows_color_in"); +const QString ThreeWayColorNode::kMidtonesColorInput = + QStringLiteral("midtones_color_in"); +const QString ThreeWayColorNode::kHighlightsColorInput = + QStringLiteral("highlights_color_in"); +const QString ThreeWayColorNode::kShadowsAmountInput = + QStringLiteral("shadows_amount_in"); +const QString ThreeWayColorNode::kMidtonesAmountInput = + QStringLiteral("midtones_amount_in"); +const QString ThreeWayColorNode::kHighlightsAmountInput = + QStringLiteral("highlights_amount_in"); +const QString ThreeWayColorNode::kLumaCoefficientsInput = + QStringLiteral("luma_coefficients_in"); + +ThreeWayColorNode::ThreeWayColorNode() +{ + AddInput(kTextureInput, NodeValue::kTexture, + InputFlags(kInputFlagNotKeyframable)); + + const QVariant neutral = QVariant::fromValue(Color(0.5, 0.5, 0.5, 1.0)); + AddInput(kShadowsColorInput, NodeValue::kColor, neutral); + AddInput(kMidtonesColorInput, NodeValue::kColor, neutral); + AddInput(kHighlightsColorInput, NodeValue::kColor, neutral); + + AddInput(kShadowsAmountInput, NodeValue::kFloat, 1.0); + AddInput(kMidtonesAmountInput, NodeValue::kFloat, 1.0); + AddInput(kHighlightsAmountInput, NodeValue::kFloat, 1.0); + + const QString min = QStringLiteral("min"); + const QString view = QStringLiteral("view"); + SetInputProperty(kShadowsAmountInput, min, 0.0); + SetInputProperty(kMidtonesAmountInput, min, 0.0); + SetInputProperty(kHighlightsAmountInput, min, 0.0); + SetInputProperty(kShadowsAmountInput, view, FloatSlider::kPercentage); + SetInputProperty(kMidtonesAmountInput, view, FloatSlider::kPercentage); + SetInputProperty(kHighlightsAmountInput, view, FloatSlider::kPercentage); + + SetEffectInput(kTextureInput); + SetFlag(kVideoEffect); +} + +void ThreeWayColorNode::Retranslate() +{ + super::Retranslate(); + + SetInputName(kTextureInput, tr("Input")); + SetInputName(kShadowsColorInput, tr("Shadows")); + SetInputName(kMidtonesColorInput, tr("Midtones")); + SetInputName(kHighlightsColorInput, tr("Highlights")); + SetInputName(kShadowsAmountInput, tr("Shadows Amount")); + SetInputName(kMidtonesAmountInput, tr("Midtones Amount")); + SetInputName(kHighlightsAmountInput, tr("Highlights Amount")); +} + +ShaderCode +ThreeWayColorNode::GetShaderCode(const ShaderRequest &request) const +{ + Q_UNUSED(request) + return ShaderCode( + FileFunctions::ReadFileAsString(":/shaders/threewaycolor.frag")); +} + +void ThreeWayColorNode::Value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + Q_UNUSED(globals) + + if (TexturePtr tex = value[kTextureInput].toTexture()) { + ShaderJob job(value); + + double luma_coeffs[3] = { 0.0, 0.0, 0.0 }; + if (project() && project()->color_manager()) { + project()->color_manager()->GetDefaultLumaCoefs(luma_coeffs); + } else { + luma_coeffs[0] = 0.2126; + luma_coeffs[1] = 0.7152; + luma_coeffs[2] = 0.0722; + } + job.Insert(kLumaCoefficientsInput, + NodeValue(NodeValue::kVec3, + QVector3D(luma_coeffs[0], luma_coeffs[1], + luma_coeffs[2]))); + + table->Push(NodeValue::kTexture, tex->toJob(job), this); + } +} + +} diff --git a/app/node/color/threewaycolor/threewaycolor.h b/app/node/color/threewaycolor/threewaycolor.h new file mode 100644 index 000000000..33b34ab78 --- /dev/null +++ b/app/node/color/threewaycolor/threewaycolor.h @@ -0,0 +1,77 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 mikesolar + + 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 . + +***/ + +#ifndef THREEWAYCOLORNODE_H +#define THREEWAYCOLORNODE_H + +#include "node/node.h" + +namespace olive +{ + +class ThreeWayColorNode : public Node { + Q_OBJECT +public: + ThreeWayColorNode(); + + NODE_DEFAULT_FUNCTIONS(ThreeWayColorNode) + + virtual QString Name() const override + { + return tr("Three-Way Color"); + } + + virtual QString id() const override + { + return QStringLiteral("org.olivevideoeditor.Olive.threewaycolor"); + } + + virtual QVector Category() const override + { + return { kCategoryColor }; + } + + virtual QString Description() const override + { + return tr("Adjusts shadows, midtones, and highlights separately."); + } + + virtual void Retranslate() override; + + virtual ShaderCode + GetShaderCode(const ShaderRequest &request) const override; + + virtual void Value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const QString kTextureInput; + static const QString kShadowsColorInput; + static const QString kMidtonesColorInput; + static const QString kHighlightsColorInput; + static const QString kShadowsAmountInput; + static const QString kMidtonesAmountInput; + static const QString kHighlightsAmountInput; + static const QString kLumaCoefficientsInput; +}; + +} + +#endif // THREEWAYCOLORNODE_H diff --git a/app/node/factory.cpp b/app/node/factory.cpp index aea671386..cef0be9ab 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -32,6 +32,8 @@ #include "block/transition/diptocolor/diptocolortransition.h" #include "color/displaytransform/displaytransform.h" #include "color/ociogradingtransformlinear/ociogradingtransformlinear.h" +#include "color/ociolut/ociolut.h" +#include "color/threewaycolor/threewaycolor.h" #include "common/Current.h" #include "distort/cornerpin/cornerpindistortnode.h" #include "distort/crop/cropdistortnode.h" @@ -358,6 +360,10 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new DisplayTransformNode(); case kOCIOGradingTransformLinear: return new OCIOGradingTransformLinearNode(); + case kOCIOLut: + return new OCIOLutNode(); + case kThreeWayColor: + return new ThreeWayColorNode(); case kChromaKey: return new ChromaKeyNode(); case kMaskDistort: diff --git a/app/node/factory.h b/app/node/factory.h index 764386a5c..5651c02a9 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -73,6 +73,8 @@ public: kCornerPinDistort, kDisplayTransform, kOCIOGradingTransformLinear, + kOCIOLut, + kThreeWayColor, kChromaKey, kMaskDistort, kDropShadowFilter, diff --git a/app/panel/scope/scope.cpp b/app/panel/scope/scope.cpp index a45e1f8f0..d6aedbef0 100644 --- a/app/panel/scope/scope.cpp +++ b/app/panel/scope/scope.cpp @@ -59,6 +59,10 @@ ScopePanel::ScopePanel() waveform_view_ = new WaveformScope(); stack_->addWidget(waveform_view_); + // Create vectorscope + vectorscope_ = new VectorscopeScope(); + stack_->addWidget(vectorscope_); + // Create histogram histogram_ = new HistogramScope(); stack_->addWidget(histogram_); @@ -81,6 +85,8 @@ QString ScopePanel::TypeToName(ScopePanel::Type t) switch (t) { case kTypeWaveform: return tr("Waveform"); + case kTypeVectorscope: + return tr("Vectorscope"); case kTypeHistogram: return tr("Histogram"); case kTypeCount: @@ -124,12 +130,14 @@ void ScopePanel::SetViewerPanel(ViewerPanelBase *vp) void ScopePanel::SetReferenceBuffer(TexturePtr frame) { histogram_->SetBuffer(frame); + vectorscope_->SetBuffer(frame); waveform_view_->SetBuffer(frame); } void ScopePanel::SetColorManager(ColorManager *manager) { histogram_->ConnectColorManager(manager); + vectorscope_->ConnectColorManager(manager); waveform_view_->ConnectColorManager(manager); } diff --git a/app/panel/scope/scope.h b/app/panel/scope/scope.h index edcd07d2a..3286bab7c 100644 --- a/app/panel/scope/scope.h +++ b/app/panel/scope/scope.h @@ -28,6 +28,7 @@ #include "panel/panel.h" #include "panel/viewer/viewerbase.h" #include "widget/scope/histogram/histogram.h" +#include "widget/scope/vectorscope/vectorscope.h" #include "widget/scope/waveform/waveform.h" namespace olive @@ -38,6 +39,7 @@ class ScopePanel : public PanelWidget { public: enum Type { kTypeWaveform, + kTypeVectorscope, kTypeHistogram, kTypeCount @@ -73,6 +75,8 @@ private: WaveformScope *waveform_view_; + VectorscopeScope *vectorscope_; + HistogramScope *histogram_; ViewerPanelBase *viewer_; diff --git a/app/shaders/rgbvectorscope.frag b/app/shaders/rgbvectorscope.frag new file mode 100644 index 000000000..7af6a48ad --- /dev/null +++ b/app/shaders/rgbvectorscope.frag @@ -0,0 +1,48 @@ +uniform sampler2D ove_maintex; + +uniform vec2 viewport; +uniform vec3 luma_coeffs; + +uniform float vectorscope_gain; +uniform float vectorscope_point_radius; +uniform float vectorscope_intensity; +uniform float vectorscope_sample_grid; + +in vec2 ove_texcoord; +out vec4 frag_color; + +vec2 rgb_to_vectorscope(vec3 rgb) +{ + float y = dot(rgb, luma_coeffs); + float cb = (rgb.b - y) / max(2.0 * (1.0 - luma_coeffs.b), 0.0001); + float cr = (rgb.r - y) / max(2.0 * (1.0 - luma_coeffs.r), 0.0001); + return vec2(cr, cb) * vectorscope_gain + vec2(0.5); +} + +void main(void) +{ + float grid = max(vectorscope_sample_grid, 1.0); + float point_radius = vectorscope_point_radius / max(min(viewport.x, viewport.y), 1.0); + vec3 accumulated = vec3(0.0); + + for (int y = 0; y < 64; y++) { + if (float(y) >= grid) { + break; + } + + for (int x = 0; x < 64; x++) { + if (float(x) >= grid) { + break; + } + + vec2 sample_uv = (vec2(float(x), float(y)) + vec2(0.5)) / grid; + vec3 sample_rgb = clamp(texture(ove_maintex, sample_uv).rgb, 0.0, 1.0); + vec2 scope_point = rgb_to_vectorscope(sample_rgb); + float distance_to_point = distance(ove_texcoord, scope_point); + float contribution = 1.0 - smoothstep(point_radius, point_radius * 2.0, distance_to_point); + accumulated += sample_rgb * contribution * vectorscope_intensity; + } + } + + frag_color = vec4(clamp(accumulated, 0.0, 1.0), 1.0); +} diff --git a/app/shaders/rgbvectorscope.vert b/app/shaders/rgbvectorscope.vert new file mode 100644 index 000000000..766fba13a --- /dev/null +++ b/app/shaders/rgbvectorscope.vert @@ -0,0 +1,25 @@ +uniform float vectorscope_scale; + +in vec4 a_position; +in vec2 a_texcoord; + +out vec2 ove_texcoord; + +mat4 scale_mat4(vec3 scale) +{ + return mat4( + scale.x, 0.0, 0.0, 0.0, + 0.0, scale.y, 0.0, 0.0, + 0.0, 0.0, scale.z, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +void main() +{ + mat4 transform = mat4(1.0); + transform *= scale_mat4(vec3(vectorscope_scale, vectorscope_scale, 1.0)); + + gl_Position = transform * a_position; + ove_texcoord = a_texcoord; +} diff --git a/app/shaders/threewaycolor.frag b/app/shaders/threewaycolor.frag new file mode 100644 index 000000000..5b100e59f --- /dev/null +++ b/app/shaders/threewaycolor.frag @@ -0,0 +1,35 @@ +uniform sampler2D tex_in; + +uniform vec4 shadows_color_in; +uniform vec4 midtones_color_in; +uniform vec4 highlights_color_in; +uniform float shadows_amount_in; +uniform float midtones_amount_in; +uniform float highlights_amount_in; +uniform vec3 luma_coefficients_in; + +in vec2 ove_texcoord; +out vec4 frag_color; + +vec3 color_offset(vec4 control, float amount) +{ + return (control.rgb - vec3(0.5)) * 2.0 * amount; +} + +void main(void) +{ + vec4 source = texture(tex_in, ove_texcoord); + float luma = clamp(dot(source.rgb, luma_coefficients_in), 0.0, 1.0); + + float shadow_weight = smoothstep(0.75, 0.0, luma); + float highlight_weight = smoothstep(0.25, 1.0, luma); + float midtone_weight = clamp(1.0 - abs(luma - 0.5) * 2.0, 0.0, 1.0); + + vec3 adjustment = + color_offset(shadows_color_in, shadows_amount_in) * shadow_weight + + color_offset(midtones_color_in, midtones_amount_in) * midtone_weight + + color_offset(highlights_color_in, highlights_amount_in) * highlight_weight; + + vec3 graded = source.rgb + adjustment * source.rgb * (1.0 - source.rgb); + frag_color = vec4(clamp(graded, 0.0, 1.0), source.a); +} diff --git a/app/widget/filefield/filefield.cpp b/app/widget/filefield/filefield.cpp index 70e2ae697..1f02099c0 100644 --- a/app/widget/filefield/filefield.cpp +++ b/app/widget/filefield/filefield.cpp @@ -59,7 +59,8 @@ void FileField::BrowseBtnClicked() if (directory_mode_) { s = QFileDialog::getExistingDirectory(this, tr("Open Directory")); } else { - s = QFileDialog::getOpenFileName(this, tr("Open File")); + s = QFileDialog::getOpenFileName(this, tr("Open File"), QString(), + name_filter_); } if (!s.isEmpty()) { diff --git a/app/widget/filefield/filefield.h b/app/widget/filefield/filefield.h index 7bb4915b4..472f8944f 100644 --- a/app/widget/filefield/filefield.h +++ b/app/widget/filefield/filefield.h @@ -53,6 +53,11 @@ public: directory_mode_ = e; } + void SetNameFilter(const QString &filter) + { + name_filter_ = filter; + } + signals: void FilenameChanged(const QString &filename); @@ -63,6 +68,8 @@ private: bool directory_mode_; + QString name_filter_; + private slots: void BrowseBtnClicked(); diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 5a384db55..1ce418054 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -949,6 +949,8 @@ void NodeParamViewWidgetBridge::SetProperty(const QString &key, ff->SetPlaceholder(value.toString()); } else if (key == QStringLiteral("directory")) { ff->SetDirectoryMode(value.toBool()); + } else if (key == QStringLiteral("filter")) { + ff->SetNameFilter(value.toString()); } } diff --git a/app/widget/scope/CMakeLists.txt b/app/widget/scope/CMakeLists.txt index 4eb246120..fd40b03a1 100644 --- a/app/widget/scope/CMakeLists.txt +++ b/app/widget/scope/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(histogram) add_subdirectory(scopebase) +add_subdirectory(vectorscope) add_subdirectory(waveform) set(OLIVE_SOURCES diff --git a/app/widget/scope/vectorscope/CMakeLists.txt b/app/widget/scope/vectorscope/CMakeLists.txt new file mode 100644 index 000000000..d6ee4f89e --- /dev/null +++ b/app/widget/scope/vectorscope/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/scope/vectorscope/vectorscope.h + widget/scope/vectorscope/vectorscope.cpp + PARENT_SCOPE +) diff --git a/app/widget/scope/vectorscope/vectorscope.cpp b/app/widget/scope/vectorscope/vectorscope.cpp new file mode 100644 index 000000000..4680aee14 --- /dev/null +++ b/app/widget/scope/vectorscope/vectorscope.cpp @@ -0,0 +1,132 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 mikesolar + + 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 "vectorscope.h" + +#include +#include +#include +#include + +#include "common/qtutils.h" +#include "node/node.h" + +namespace olive +{ + +#define super ScopeBase + +VectorscopeScope::VectorscopeScope(QWidget *parent) + : super(parent) +{ +} + +ShaderCode VectorscopeScope::GenerateShaderCode() +{ + return ShaderCode( + FileFunctions::ReadFileAsString(":/shaders/rgbvectorscope.frag"), + FileFunctions::ReadFileAsString(":/shaders/rgbvectorscope.vert")); +} + +void VectorscopeScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) +{ + float vectorscope_scale = 0.80f; + float vectorscope_gain = 1.45f; + float vectorscope_point_radius = 1.75f; + float vectorscope_intensity = 0.035f; + float vectorscope_sample_grid = 28.0f; + + ShaderJob job; + + job.Insert(QStringLiteral("viewport"), + NodeValue(NodeValue::kVec2, QVector2D(width(), height()))); + + double luma_coeffs[3] = { 0.0f, 0.0f, 0.0f }; + color_manager()->GetDefaultLumaCoefs(luma_coeffs); + job.Insert( + QStringLiteral("luma_coeffs"), + NodeValue(NodeValue::kVec3, + QVector3D(luma_coeffs[0], luma_coeffs[1], luma_coeffs[2]))); + + job.Insert(QStringLiteral("vectorscope_scale"), + NodeValue(NodeValue::kFloat, vectorscope_scale)); + job.Insert(QStringLiteral("vectorscope_gain"), + NodeValue(NodeValue::kFloat, vectorscope_gain)); + job.Insert(QStringLiteral("vectorscope_point_radius"), + NodeValue(NodeValue::kFloat, vectorscope_point_radius)); + job.Insert(QStringLiteral("vectorscope_intensity"), + NodeValue(NodeValue::kFloat, vectorscope_intensity)); + job.Insert(QStringLiteral("vectorscope_sample_grid"), + NodeValue(NodeValue::kFloat, vectorscope_sample_grid)); + + job.Insert(QStringLiteral("ove_maintex"), + NodeValue(NodeValue::kTexture, + QVariant::fromValue(managed_tex))); + + renderer()->Blit(pipeline, job, GetViewportParams()); + + QPainter p(paint_device()); + QFont font = p.font(); + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); + + p.setCompositionMode(QPainter::CompositionMode_Plus); + p.setPen(QColor(0, 153, 0)); + p.setFont(font); + + float scope_size = qMin(width(), height()) * vectorscope_scale; + QPointF center(width() * 0.5, height() * 0.5); + float radius = scope_size * 0.5; + + p.drawEllipse(center, radius, radius); + p.drawLine(QPointF(center.x() - radius, center.y()), + QPointF(center.x() + radius, center.y())); + p.drawLine(QPointF(center.x(), center.y() - radius), + QPointF(center.x(), center.y() + radius)); + + struct Target { + const char *label; + float angle; + }; + const Target targets[] = { + { "R", 0.0f }, { "Mg", 60.0f }, { "B", 120.0f }, + { "Cy", 180.0f }, { "G", 240.0f }, { "Yl", 300.0f }, + }; + + const float label_radius = radius + 12.0f; + const float marker_radius = radius * 0.72f; + constexpr float kPi = 3.14159265358979323846f; + + for (const Target &target : targets) { + float radians = target.angle * kPi / 180.0f; + QPointF direction(qCos(radians), -qSin(radians)); + QPointF marker = center + direction * marker_radius; + QPointF label_pos = center + direction * label_radius; + QString label = QString::fromUtf8(target.label); + + p.drawEllipse(marker, 3.0, 3.0); + p.drawText(label_pos.x() - + QtUtils::QFontMetricsWidth(font_metrics, label) * 0.5, + label_pos.y() + font_metrics.capHeight() * 0.5, label); + } +} + +} diff --git a/app/widget/scope/vectorscope/vectorscope.h b/app/widget/scope/vectorscope/vectorscope.h new file mode 100644 index 000000000..2dbbe3eb3 --- /dev/null +++ b/app/widget/scope/vectorscope/vectorscope.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 mikesolar + + 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 . + +***/ + +#ifndef VECTORSCOPESCOPE_H +#define VECTORSCOPESCOPE_H + +#include "widget/scope/scopebase/scopebase.h" + +namespace olive +{ + +class VectorscopeScope : public ScopeBase { + Q_OBJECT +public: + VectorscopeScope(QWidget *parent = nullptr); + + MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR(VectorscopeScope) + +protected: + virtual ShaderCode GenerateShaderCode() override; + + virtual void DrawScope(TexturePtr managed_tex, QVariant pipeline) override; +}; + +} + +#endif // VECTORSCOPESCOPE_H diff --git a/docs/zh/color-lut-v04-plan.md b/docs/zh/color-lut-v04-plan.md new file mode 100644 index 000000000..1fe16cb2d --- /dev/null +++ b/docs/zh/color-lut-v04-plan.md @@ -0,0 +1,114 @@ +# v0.4 调色与 LUT 实施计划 + +本文档对应 `docs/zh/README.md` 路线图中 v0.4「调色与 LUT」里程碑。 + +## 目标 + +- 支持 `.cube` 与 `.3dl` LUT 文件作为可用调色入口。 +- 完成示波器面板的波形、矢量、直方图三类视图。 +- 提供三向色轮面板,面向阴影、中间调、高光做基础调色控制。 +- 尽量复用现有 OpenColorIO、节点系统、Viewer/Scope 面板和 GPU 渲染管线,不引入独立的调色框架。 + +## 当前状态 + +- 已有 OpenColorIO 基础能力:颜色管理、显示变换、OCIO 调色节点和渲染侧配置。 +- Scope 面板已提供波形、矢量、直方图三类视图。 +- 已有色轮基础控件;当前三向调色先通过节点参数面板暴露 Shadows、Midtones、Highlights 三组颜色与强度参数。 +- LUT 节点已接入节点工厂,并已增加 `.cube`/`.3dl` 相关测试。 + +## 阶段 1:LUT 节点入口 + +状态:已完成首版。 + +交付内容: + +- 新增 OCIO LUT 节点,使用 OpenColorIO `FileTransform` 读取外部 LUT 文件。 +- 明确支持 `.cube` 与 `.3dl` 扩展名,并拒绝未知格式。 +- 在节点工厂中注册 LUT 节点,保证工程加载和节点创建路径一致。 +- 增加 gtest 覆盖 LUT 扩展名支持和简单 LUT 转换结果。 + +验收标准: + +- `olive-gtest` 中 LUT 相关测试通过。 +- `olive-editor` 和 `olive-render-worker` 可正常构建。 +- LUT 文件缺失、格式不支持、OCIO 处理器创建失败时不会导致崩溃。 + +## 阶段 2:示波器补齐 + +状态:已完成首版。 + +交付内容: + +- 保留现有波形和直方图视图。 +- 新增矢量示波器视图,并接入 Scope 面板下拉选择。 +- 矢量示波器应使用当前 Viewer 帧,并经过现有显示/颜色管理路径。 +- 为新增 shader 或资源入口增加资源存在性测试。 + +验收标准: + +- Scope 面板可在 Waveform、Vectorscope、Histogram 间切换。 +- 无当前帧时视图保持空白或安全占位,不崩溃。 +- shader 资源测试和编辑器构建通过。 + +## 阶段 3:三向色轮面板 + +状态:已完成节点参数面板首版;独立三向色轮 dock 面板作为后续体验增强。 + +交付内容: + +- 基于现有参数面板提供 Shadows、Midtones、Highlights 三组控制。 +- 为每组控制提供色彩偏移和强度/亮度相关参数。 +- 将三向色轮参数映射到现有 OCIO 调色节点,或新增可序列化节点承载参数。 +- 保证参数能随工程保存、加载、撤销和重做。 + +验收标准: + +- 用户可以在 UI 中操作三向调色参数并看到 Viewer 结果变化。 +- 参数在工程文件中可序列化并可恢复。 +- 节点参数变更不破坏现有 OCIO 调色节点兼容性。 + +## 阶段 4:集成与体验 + +状态:当前范围已完成;独立三向色轮 dock 面板和更细的交互体验作为后续增强。 + +交付内容: + +- 为 LUT 节点补齐清晰的文件选择过滤器和用户可见名称。已完成。 +- 在调色相关 UI 中保持命名一致:LUT、Waveform、Vectorscope、Histogram、Shadows、Midtones、Highlights。已完成首版。 +- 更新中文文档,说明 LUT、示波器和三向调色的当前入口。已在本文档记录。 + +验收标准: + +- 用户能从现有节点/UI 路径发现 LUT 和调色功能。 +- 文档与实际 UI 命名一致。 +- LUT 文件选择器限制为 `.cube` 与 `.3dl`,并保留 All Files 兜底。 +- 不引入和现有翻译系统冲突的硬编码字符串。 + +## 阶段 5:验证 + +状态:自动化构建和核心测试已通过;手动 Viewer/Scope 观感检查将在真实项目中继续验证。 + +构建命令: + +```sh +ninja -C cmake-build-debug olive-gtest olive-editor olive-render-worker -j2 +``` + +测试命令: + +```sh +QT_QPA_PLATFORM=offscreen cmake-build-debug/tests/gtest/olive-gtest --gtest_filter='ColorLut.*:ColorV04.*:Shaders.*:NodeSerialization.*:NodeValue.*' --gtest_brief=1 +``` + +手动检查: + +- 打开工程并加载一段素材。 +- 在 Scope 面板分别切换 Waveform、Vectorscope、Histogram。 +- 添加 LUT 节点并选择 `.cube` 或 `.3dl` 文件。 +- 调整三向色轮参数,确认 Viewer 输出和工程保存/加载行为。 + +## 风险与待定点 + +- 三向色轮应优先映射到 OCIO 现有调色能力;如果现有节点表达能力不足,再新增独立节点。 +- 矢量示波器 shader 需要兼容当前 OpenGL 版本和已有渲染抽象,避免只在单一驱动上可用。 +- LUT 文件路径序列化需要尊重现有工程文件路径策略,避免绝对路径导致工程不可迁移。 diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index 864795c56..87b338477 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(olive-gtest common_current_test.cpp common_xmlutils_test.cpp config_test.cpp + color_lut_test.cpp node_value_test.cpp node_keyframe_test.cpp node_serialization_test.cpp diff --git a/tests/gtest/color_lut_test.cpp b/tests/gtest/color_lut_test.cpp new file mode 100644 index 000000000..5b58f369e --- /dev/null +++ b/tests/gtest/color_lut_test.cpp @@ -0,0 +1,103 @@ +#include + +#include +#include +#include + +#include + +#include + +#include "node/color/ociolut/ociolut.h" +#include "node/color/threewaycolor/threewaycolor.h" +#include "node/factory.h" +#include "render/colorprocessor.h" + +namespace OCIO = OCIO_NAMESPACE; + +namespace { + +QString WriteTestCube(QTemporaryDir *dir) +{ + const QString path = QDir(dir->path()).filePath(QStringLiteral("invert.cube")); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + return QString(); + } + + const QByteArray data = + "TITLE \"Oak test invert\"\n" + "LUT_1D_SIZE 2\n" + "DOMAIN_MIN 0.0 0.0 0.0\n" + "DOMAIN_MAX 1.0 1.0 1.0\n" + "1.0 1.0 1.0\n" + "0.0 0.0 0.0\n"; + file.write(data); + file.close(); + return path; +} + +} // namespace + +TEST(ColorLut, OcioSupportsCubeAnd3dlExtensions) +{ + EXPECT_TRUE(OCIO::FileTransform::IsFormatExtensionSupported("cube")); + EXPECT_TRUE(OCIO::FileTransform::IsFormatExtensionSupported(".cube")); + EXPECT_TRUE(OCIO::FileTransform::IsFormatExtensionSupported("3dl")); + EXPECT_TRUE(OCIO::FileTransform::IsFormatExtensionSupported(".3dl")); +} + +TEST(ColorLut, CubeFileTransformConvertsColor) +{ + QTemporaryDir dir; + ASSERT_TRUE(dir.isValid()); + const QString path = WriteTestCube(&dir); + ASSERT_FALSE(path.isEmpty()); + + OCIO::FileTransformRcPtr transform = OCIO::FileTransform::Create(); + transform->setSrc(path.toUtf8().constData()); + transform->setInterpolation(OCIO::INTERP_LINEAR); + transform->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + + OCIO::ConstConfigRcPtr config = OCIO::Config::CreateRaw(); + olive::ColorProcessorPtr processor = + olive::ColorProcessor::Create(config->getProcessor(transform)); + ASSERT_TRUE(processor); + + const olive::Color out = processor->ConvertColor(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)); + EXPECT_NEAR(out.red(), 0.75f, 0.02f); + EXPECT_NEAR(out.green(), 0.50f, 0.02f); + EXPECT_NEAR(out.blue(), 0.25f, 0.02f); + EXPECT_NEAR(out.alpha(), 1.0f, 0.001f); +} + +TEST(ColorV04, FactoryCreatesColorNodes) +{ + std::unique_ptr lut( + olive::NodeFactory::CreateFromFactoryIndex( + olive::NodeFactory::kOCIOLut)); + ASSERT_NE(lut, nullptr); + EXPECT_EQ(lut->id(), QStringLiteral("org.olivevideoeditor.Olive.ociolut")); + + std::unique_ptr three_way( + olive::NodeFactory::CreateFromFactoryIndex( + olive::NodeFactory::kThreeWayColor)); + ASSERT_NE(three_way, nullptr); + EXPECT_EQ(three_way->id(), + QStringLiteral("org.olivevideoeditor.Olive.threewaycolor")); + EXPECT_TRUE(three_way->HasInputWithID( + olive::ThreeWayColorNode::kShadowsColorInput)); + EXPECT_TRUE(three_way->HasInputWithID( + olive::ThreeWayColorNode::kMidtonesColorInput)); + EXPECT_TRUE(three_way->HasInputWithID( + olive::ThreeWayColorNode::kHighlightsColorInput)); + + const olive::Color neutral = + three_way->GetStandardValue( + olive::ThreeWayColorNode::kMidtonesColorInput) + .value(); + EXPECT_FLOAT_EQ(neutral.red(), 0.5f); + EXPECT_FLOAT_EQ(neutral.green(), 0.5f); + EXPECT_FLOAT_EQ(neutral.blue(), 0.5f); + EXPECT_FLOAT_EQ(neutral.alpha(), 1.0f); +} diff --git a/tests/gtest/shader_resources_test.cpp b/tests/gtest/shader_resources_test.cpp index 8997a0d75..f11fe6a7f 100644 --- a/tests/gtest/shader_resources_test.cpp +++ b/tests/gtest/shader_resources_test.cpp @@ -10,7 +10,10 @@ TEST(Shaders, ResourcesAvailable) QStringLiteral(":/shaders/yuv2rgb.frag"), QStringLiteral(":/shaders/deinterlace2.frag"), QStringLiteral(":/shaders/rgbhistogram.frag"), - QStringLiteral(":/shaders/rgbhistogram.vert") + QStringLiteral(":/shaders/rgbhistogram.vert"), + QStringLiteral(":/shaders/rgbvectorscope.frag"), + QStringLiteral(":/shaders/rgbvectorscope.vert"), + QStringLiteral(":/shaders/threewaycolor.frag") }; for (const QString &path : shader_paths) {