color: global LUT library, LUT error reporting, clamp + display fixes
- Add a global LUT library: user-configurable directories (new Preferences > LUT tab) scanned recursively for .cube/.3dl files; LUT node file pickers offer the library dirs as sidebar shortcuts via a 'lut_library' input property handled by the param view bridge - OCIOLutNode no longer fails silently: missing files, unsupported extensions and OCIO load errors are recorded in last_error() and surfaced in the status bar (input still passes through for rendering safety) - ColorDialog: re-enable the display -> reference conversion using ColorProcessor::kInverse with a validity guard, and re-enable the Display tab in ColorValuesWidget; covered by a round-trip regression test proving the old OCIO inverse crash no longer occurs - OCIOGradingTransformLinearNode: enforce the OCIO clampWhite > clampBlack invariant per frame in Value() so keyframed/connected values cannot produce invalid grading transforms, and constrain the white clamp UI minimum whenever the black clamp is static - Regression tests for LUT extension checks, direction switching, node error reporting, LUT library scanning, display inverse round-trip and clamp enforcement
This commit is contained in:
@@ -92,10 +92,10 @@ OCIOGradingTransformLinearNode::OCIOGradingTransformLinearNode()
|
||||
GetStandardValue(kClampWhiteEnableInput).toBool());
|
||||
SetInputProperty(kClampWhiteInput, QStringLiteral("base"), 0.01);
|
||||
|
||||
// FIXME: Temporarily disabled. This will break if "clamp black" is keyframed or connected to
|
||||
// something and there's currently no solution to remedy that. If there is in the future,
|
||||
// we can look into re-enabling this.
|
||||
//SetInputProperty(kClampWhiteInput, QStringLiteral("min"), GetStandardValue(kClampBlackInput).toDouble() + 0.000001);
|
||||
// Constrain the white clamp minimum to just above the (static) black clamp
|
||||
// as per OCIO::GradingPrimary::validate. When the black clamp is keyframed
|
||||
// or connected, Value() enforces the invariant per frame instead.
|
||||
UpdateClampWhiteMinimum();
|
||||
}
|
||||
|
||||
QString OCIOGradingTransformLinearNode::Name() const
|
||||
@@ -149,16 +149,49 @@ void OCIOGradingTransformLinearNode::InputValueChangedEvent(
|
||||
SetInputProperty(kClampBlackInput, QStringLiteral("enabled"),
|
||||
GetStandardValue(kClampBlackEnableInput).toBool());
|
||||
} else if (input == kClampBlackInput) {
|
||||
// Ensure the white clamp is always greater than the black clamp as per OCIO::GradingPrimary::validate
|
||||
// FIXME: Temporarily disabled. This will break if "clamp black" is keyframed or connected to
|
||||
// something and there's currently no solution to remedy that. If there is in the future,
|
||||
// we can look into re-enabling this.
|
||||
//SetInputProperty(kClampWhiteInput, QStringLiteral("min"), GetStandardValue(kClampBlackInput).toDouble() + 0.000001);
|
||||
// Ensure the white clamp is always greater than the black clamp as per
|
||||
// OCIO::GradingPrimary::validate
|
||||
UpdateClampWhiteMinimum();
|
||||
}
|
||||
|
||||
GenerateProcessor();
|
||||
}
|
||||
|
||||
void OCIOGradingTransformLinearNode::InputConnectedEvent(const QString &input,
|
||||
int element, Node *output)
|
||||
{
|
||||
super::InputConnectedEvent(input, element, output);
|
||||
|
||||
if (input == kClampBlackInput) {
|
||||
UpdateClampWhiteMinimum();
|
||||
}
|
||||
}
|
||||
|
||||
void OCIOGradingTransformLinearNode::InputDisconnectedEvent(const QString &input,
|
||||
int element,
|
||||
Node *output)
|
||||
{
|
||||
super::InputDisconnectedEvent(input, element, output);
|
||||
|
||||
if (input == kClampBlackInput) {
|
||||
UpdateClampWhiteMinimum();
|
||||
}
|
||||
}
|
||||
|
||||
void OCIOGradingTransformLinearNode::UpdateClampWhiteMinimum()
|
||||
{
|
||||
// A static UI minimum cannot follow an animated black clamp; for keyframed
|
||||
// or connected values the white>black invariant is enforced per frame in
|
||||
// Value() instead
|
||||
if (IsInputKeyframing(kClampBlackInput) ||
|
||||
IsInputConnected(kClampBlackInput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetInputProperty(kClampWhiteInput, QStringLiteral("min"),
|
||||
GetStandardValue(kClampBlackInput).toDouble() + 0.000001);
|
||||
}
|
||||
|
||||
void OCIOGradingTransformLinearNode::GenerateProcessor()
|
||||
{
|
||||
if (manager()) {
|
||||
@@ -241,6 +274,21 @@ void OCIOGradingTransformLinearNode::Value(const NodeValueRow &value,
|
||||
OCIO::GradingPrimary::NoClampWhite()));
|
||||
}
|
||||
|
||||
if (value[kClampBlackEnableInput].toBool() &&
|
||||
value[kClampWhiteEnableInput].toBool()) {
|
||||
// OCIO::GradingPrimary::validate requires the white clamp to be
|
||||
// greater than the black clamp. Keyframed or connected values
|
||||
// can violate this at arbitrary times, so enforce the invariant
|
||||
// per frame here.
|
||||
const double clamp_black = value[kClampBlackInput].toDouble();
|
||||
const double clamp_white = value[kClampWhiteInput].toDouble();
|
||||
if (clamp_white <= clamp_black) {
|
||||
job.Insert(kClampWhiteInput,
|
||||
NodeValue(NodeValue::kFloat,
|
||||
clamp_black + 0.000001));
|
||||
}
|
||||
}
|
||||
|
||||
table->Push(NodeValue::kTexture, tex->toJob(job), this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ public:
|
||||
virtual void Retranslate() override;
|
||||
virtual void InputValueChangedEvent(const QString &input,
|
||||
int element) override;
|
||||
virtual void InputConnectedEvent(const QString &input, int element,
|
||||
Node *output) override;
|
||||
virtual void InputDisconnectedEvent(const QString &input, int element,
|
||||
Node *output) override;
|
||||
void GenerateProcessor();
|
||||
|
||||
virtual void Value(const NodeValueRow &value, const NodeGlobals &globals,
|
||||
@@ -63,6 +67,16 @@ protected slots:
|
||||
|
||||
private:
|
||||
void SetVec4InputColors(const QString &input);
|
||||
|
||||
/**
|
||||
* @brief Constrains the white clamp UI minimum to just above the black
|
||||
* clamp, as required by OCIO::GradingPrimary::validate
|
||||
*
|
||||
* Only applies while the black clamp is a static value; when it is
|
||||
* keyframed or connected the invariant is enforced per frame in Value()
|
||||
* instead.
|
||||
*/
|
||||
void UpdateClampWhiteMinimum();
|
||||
};
|
||||
|
||||
} // olive
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#include "core.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "render/lutlibrary.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
@@ -40,12 +42,6 @@ const QString OCIOLutNode::kDirectionInput = QStringLiteral("lut_dir_in");
|
||||
namespace
|
||||
{
|
||||
|
||||
bool IsSupportedLutExtension(const QString &suffix)
|
||||
{
|
||||
const QString lower = suffix.toLower();
|
||||
return lower == QStringLiteral("cube") || lower == QStringLiteral("3dl");
|
||||
}
|
||||
|
||||
bool IsMainProcess()
|
||||
{
|
||||
return qobject_cast<QApplication *>(QCoreApplication::instance()) !=
|
||||
@@ -86,6 +82,8 @@ OCIOLutNode::OCIOLutNode()
|
||||
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"));
|
||||
// Allow the UI to offer the global LUT library for this input
|
||||
SetInputProperty(kFileInput, QStringLiteral("lut_library"), true);
|
||||
|
||||
AddInput(kDirectionInput, NodeValue::kCombo, 0,
|
||||
InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable));
|
||||
@@ -194,6 +192,21 @@ void OCIOLutNode::EnsureProcessor() const
|
||||
CreateProcessorFromInputs();
|
||||
}
|
||||
|
||||
void OCIOLutNode::SetLastError(const QString &error) const
|
||||
{
|
||||
if (last_error_ == error) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_error_ = error;
|
||||
|
||||
// Make the error visible to the user instead of failing silently, but only
|
||||
// from the main process (the render worker has no status bar)
|
||||
if (!error.isEmpty() && IsMainProcess() && Core::instance()) {
|
||||
Core::instance()->ShowStatusBarMessage(error, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
bool OCIOLutNode::CreateProcessorFromInputs() const
|
||||
{
|
||||
if (!manager()) {
|
||||
@@ -214,6 +227,7 @@ bool OCIOLutNode::CreateProcessorFromInputs() const
|
||||
last_path_.clear();
|
||||
last_direction_ = -1;
|
||||
processor_dirty_ = false;
|
||||
SetLastError(QString());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -231,17 +245,22 @@ bool OCIOLutNode::CreateProcessorFromInputs() const
|
||||
last_path_.clear();
|
||||
last_direction_ = -1;
|
||||
processor_dirty_ = false;
|
||||
SetLastError(tr("OCIO LUT: file does not exist: %1").arg(path));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString suffix = info.suffix();
|
||||
if (!IsSupportedLutExtension(suffix)) {
|
||||
if (!LUTLibrary::IsSupportedExtension(suffix)) {
|
||||
qWarning() << "Unsupported OCIO LUT file extension:" << path;
|
||||
const_cast<OCIOLutNode *>(this)->set_processor(nullptr);
|
||||
last_processor_.reset();
|
||||
last_path_.clear();
|
||||
last_direction_ = -1;
|
||||
processor_dirty_ = false;
|
||||
SetLastError(
|
||||
tr("OCIO LUT: unsupported LUT file extension (expected .cube or "
|
||||
".3dl): %1")
|
||||
.arg(path));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -267,6 +286,12 @@ bool OCIOLutNode::CreateProcessorFromInputs() const
|
||||
processor = nullptr;
|
||||
}
|
||||
|
||||
if (!processor) {
|
||||
SetLastError(tr("OCIO LUT: failed to load LUT file: %1").arg(path));
|
||||
} else {
|
||||
SetLastError(QString());
|
||||
}
|
||||
|
||||
last_path_ = path;
|
||||
last_direction_ = direction;
|
||||
last_processor_ = processor;
|
||||
|
||||
@@ -50,6 +50,18 @@ public:
|
||||
static const QString kFileInput;
|
||||
static const QString kDirectionInput;
|
||||
|
||||
/**
|
||||
* @brief Human-readable description of why no LUT processor is active
|
||||
*
|
||||
* Empty when a valid LUT processor is in use or no LUT file has been
|
||||
* selected yet. This allows the UI (and tests) to surface silent
|
||||
* passthrough states (missing file, unsupported extension, OCIO errors).
|
||||
*/
|
||||
const QString &last_error() const
|
||||
{
|
||||
return last_error_;
|
||||
}
|
||||
|
||||
protected slots:
|
||||
virtual void ConfigChanged() override;
|
||||
|
||||
@@ -58,11 +70,14 @@ private:
|
||||
void EnsureProcessor() const;
|
||||
bool CreateProcessorFromInputs() const;
|
||||
|
||||
void SetLastError(const QString &error) const;
|
||||
|
||||
mutable QMutex gen_mutex_;
|
||||
mutable bool processor_dirty_ = true;
|
||||
mutable QString last_path_;
|
||||
mutable int last_direction_ = -1;
|
||||
mutable ColorProcessorPtr last_processor_;
|
||||
mutable QString last_error_;
|
||||
};
|
||||
|
||||
} // namespace olive
|
||||
|
||||
Reference in New Issue
Block a user