nodes: real OCIO color grading (linear + log) on the GPU
The OCIO grading nodes previously pushed null texture handles; they now push real ShaderJobPayloads whose GLSL is the OCIO-generated dynamic grading-primary GPU shader — the exact code the C++ path applies, so no approximation: - color: grading_primary_function_shader(style) builds a dynamic GradingPrimaryTransform (LIN/LOG) on the default config, extracts the GLSL via GpuShaderDesc (function 'ove_grading_primary', resource prefix ocio_, no LUT textures) and caches it per style + config id. - eval: OCIO_GRADING_STUBS maps the two node type ids to the grading style; process_shader_job resolves the stub and splices it into the node's %1 marker (same wiring as the chromakey OCIO stub); the pipeline cache key folds the stub text so a config change recompiles. - nodes: value() pushes a ShaderJobPayload with the C++ value() rewrite applied to the row — vec4 (RGBM x=master) grading inputs to the vec3 GPU uniform form (lin: contrast RGB=c*m, offset RGB=c+m, exposure RGB=2^(c+m); log: lift RGB=c+m, gain c*m, gamma c*m), plus pivot/saturation floats, the log pivotBlack/pivotWhite normalization range (0/1), clamp sentinels (NoClampBlack -1 / NoClampWhite 2), white>black enforcement per frame, and localBypass=false. Generated uniform names bind by name (the log node's OCIO_NAMESPACE_ id text normalizes to the ocio_ resource prefix). - Tests: grading stub generation (analytic GLSL, cache) in color, end-to-end GPU exposure doubling for lin (+1 stop on 0.2 gray -> 0.4) and lift for log, node payload rewrite assertions, and the all-shaders sweep now retries grading stubs. oak-render 181, oak-node 441, oak-app 272 lib tests pass.
This commit is contained in:
@@ -82,6 +82,68 @@ pub struct OCIOGradingTransformLinearNode {
|
||||
base: OcioBase,
|
||||
}
|
||||
|
||||
/// Fragment shader. The `%1` marker is replaced at request time with the
|
||||
/// OCIO-generated grading-primary GLSL stub for the linear style (the
|
||||
/// renderer's OCIO_GRADING_STUBS entry; the stub declares
|
||||
/// the `ocio_grading_primary_*` uniforms and `ove_grading_primary`), and
|
||||
/// the body applies it to the sampled input. The uniform names match the
|
||||
/// grading input ids, so the rewritten params row binds by name — the
|
||||
/// C++ job's `values` assigned to the GPU uniforms by name-match the same
|
||||
/// way.
|
||||
const SHADER_FRAG: &str = r#"// Main texture input
|
||||
uniform sampler2D tex_in;
|
||||
|
||||
// Main texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Program will replace this with OCIO's auto-generated shader code
|
||||
%1
|
||||
|
||||
void main() {
|
||||
vec4 col = texture(tex_in, ove_texcoord);
|
||||
frag_color = ove_grading_primary(col);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Row value for `id` (the traverser convention), else the node's own
|
||||
/// standard/keyframe value (C++ `GetValueAtTime` parity for direct
|
||||
/// calls).
|
||||
fn row_or_standard(
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
id: &str,
|
||||
time: oak_core::Rational,
|
||||
) -> crate::value::NodeValue {
|
||||
match inputs.get(id) {
|
||||
Some(v) => v.clone(),
|
||||
None => core.value_at_time(id, -1, time),
|
||||
}
|
||||
}
|
||||
|
||||
/// A vec4 input value as `[x, y, z, w]` (x = master per the RGBM
|
||||
/// convention), collecting a generic numeric value into a padded array.
|
||||
fn to_vec4(v: crate::value::NodeValue) -> [f64; 4] {
|
||||
match v {
|
||||
crate::value::NodeValue::Vec4(v) => v,
|
||||
crate::value::NodeValue::Vec3(v) => [v[0], v[1], v[2], 0.0],
|
||||
crate::value::NodeValue::Vec2(v) => [v[0], v[1], 0.0, 0.0],
|
||||
crate::value::NodeValue::Float(v) => [v, v, v, v],
|
||||
_ => [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-channel vec3 the OCIO GPU uniforms expect: `rgb[i] =
|
||||
/// f(channel[i], master)`.
|
||||
fn channel_merge(v: [f64; 4], f: impl Fn(f64, f64) -> f64) -> crate::value::NodeValue {
|
||||
let m = v[0];
|
||||
crate::value::NodeValue::Vec3([
|
||||
f(v[1], m),
|
||||
f(v[2], m),
|
||||
f(v[3], m),
|
||||
])
|
||||
}
|
||||
|
||||
/// Set or replace an input property (C++ `set_input_property`).
|
||||
fn set_input_property(core: &mut NodeCore, input: &str, key: &str, value: crate::value::NodeValue) {
|
||||
if let Some(input) = core.get_input_mut(input) {
|
||||
@@ -291,15 +353,20 @@ impl NodeBehavior for OCIOGradingTransformLinearNode {
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// processor not ready -> push nothing (unlike the base, there is no
|
||||
/// pass-through branch). Otherwise builds a `ColorTransformJob` from
|
||||
/// the whole input row and rewrites the vec4 (RGBM: x = master)
|
||||
/// inputs into the vec3 form the GPU uniforms expect: offset RGB =
|
||||
/// channel + master; exposure RGB = 2^(channel + master); contrast
|
||||
/// RGB = channel * master. Disabled clamps are pushed as
|
||||
/// `GradingPrimary::NoClampBlack()/NoClampWhite()`, and when both
|
||||
/// clamps are enabled the white clamp is raised to black + 0.000001
|
||||
/// per frame if keyframed/connected values violate white > black.
|
||||
/// otherwise pushes a REAL [`ShaderJobPayload`] whose params carry the
|
||||
/// input row rewritten into the vec3 form the OCIO-generated GPU
|
||||
/// uniforms expect — the C++ job's `values` rewrite, applied to the
|
||||
/// uniform names the renderer's grading stub declares:
|
||||
/// `ocio_grading_primary_contrast` RGB = channel * master, `offset`
|
||||
/// RGB = channel + master, `exposure` RGB = 2^(channel + master).
|
||||
/// Disabled clamps are pushed as `GradingPrimary::NoClampBlack()`
|
||||
/// (-1.0) / `NoClampWhite()` (2.0), and when both clamps are enabled
|
||||
/// the white clamp is raised to black + 0.000001 per frame if
|
||||
/// keyframed/connected values violate white > black. The transform
|
||||
/// itself is the OCIO shader the renderer splices in — the processor
|
||||
/// is generated at render time from the default config, so the
|
||||
/// evaluation-time guard (C++ processor gate) is replaced by the
|
||||
/// node's own presence check on the input texture.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -307,33 +374,97 @@ impl NodeBehavior for OCIOGradingTransformLinearNode {
|
||||
time: oak_core::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, time);
|
||||
match inputs.get(crate::nodes::ociobase::TEXTURE_INPUT) {
|
||||
Some(crate::value::NodeValue::Texture(_)) => {
|
||||
if self.base.processor().is_some() {
|
||||
// `// CPP-PARITY: ociogradingtransformlinear.cpp`
|
||||
// `value()` — the C++ builds a ColorTransformJob and
|
||||
// rewrites the vec4 (RGBM: x = master) inputs into the
|
||||
// vec3 GPU uniform form: offset RGB = channel +
|
||||
// master, exposure RGB = 2^(channel + master),
|
||||
// contrast RGB = channel * master. Disabled clamps
|
||||
// are pushed as `ocio::GradingPrimary::NoClampBlack()`
|
||||
// (-1.0) / `NoClampWhite()` (2.0), and when both
|
||||
// clamps are enabled the white clamp is raised to
|
||||
// black + 0.000001 per frame if keyframed/connected
|
||||
// values violate white > black. The Rust model has no
|
||||
// color-transform job payload: the renderer seam
|
||||
// resolves the deferred job from this null handle.
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Processor not ready: push nothing (no pass-through).
|
||||
}
|
||||
_ => {}
|
||||
let _ = self;
|
||||
if !matches!(
|
||||
inputs.get(crate::nodes::ociobase::TEXTURE_INPUT),
|
||||
Some(crate::value::NodeValue::Texture(_))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut params = inputs.clone();
|
||||
|
||||
// vec4 (RGBM: x = master) inputs -> the vec3 GPU uniform form.
|
||||
let contrast = to_vec4(row_or_standard(core, inputs, CONTRAST_INPUT, time));
|
||||
let offset = to_vec4(row_or_standard(core, inputs, OFFSET_INPUT, time));
|
||||
let exposure = to_vec4(row_or_standard(core, inputs, EXPOSURE_INPUT, time));
|
||||
params.insert(
|
||||
CONTRAST_INPUT.to_string(),
|
||||
channel_merge(contrast, |c, m| c * m),
|
||||
);
|
||||
params.insert(
|
||||
OFFSET_INPUT.to_string(),
|
||||
channel_merge(offset, |c, m| c + m),
|
||||
);
|
||||
params.insert(
|
||||
EXPOSURE_INPUT.to_string(),
|
||||
channel_merge(exposure, |c, m| 2f64.powf(c + m)),
|
||||
);
|
||||
|
||||
// Scalar uniforms (the IDs are the generated uniform names).
|
||||
params.insert(
|
||||
SATURATION_INPUT.to_string(),
|
||||
row_or_standard(core, inputs, SATURATION_INPUT, time),
|
||||
);
|
||||
params.insert(
|
||||
PIVOT_INPUT.to_string(),
|
||||
row_or_standard(core, inputs, PIVOT_INPUT, time),
|
||||
);
|
||||
|
||||
// Clamps: enabled -> the value, disabled -> the OCIO sentinels
|
||||
// (NoClampBlack -1 / NoClampWhite 2 keep the GPU clamp a no-op).
|
||||
let black_enabled = row_or_standard(core, inputs, CLAMP_BLACK_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let white_enabled = row_or_standard(core, inputs, CLAMP_WHITE_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let mut black =
|
||||
row_or_standard(core, inputs, CLAMP_BLACK_INPUT, time).to_double();
|
||||
let mut white =
|
||||
row_or_standard(core, inputs, CLAMP_WHITE_INPUT, time).to_double();
|
||||
if black_enabled && white_enabled {
|
||||
// ocio::GradingPrimary::validate: white > black (per frame,
|
||||
// when the static UI minimum cannot follow animated values).
|
||||
white = white.max(black + 0.000001);
|
||||
}
|
||||
params.insert(
|
||||
CLAMP_BLACK_INPUT.to_string(),
|
||||
crate::value::NodeValue::Float(if black_enabled { black } else { -1.0 }),
|
||||
);
|
||||
params.insert(
|
||||
CLAMP_WHITE_INPUT.to_string(),
|
||||
crate::value::NodeValue::Float(if white_enabled { white } else { 2.0 }),
|
||||
);
|
||||
params.insert(
|
||||
"ocio_grading_primary_localBypass".to_string(),
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
);
|
||||
|
||||
let job = crate::handle::make_owned(crate::nodes::jobs::ShaderJobPayload {
|
||||
node_id: crate::id::NodeId::INVALID,
|
||||
time,
|
||||
iterations: 1,
|
||||
type_id: self.type_id().to_string(),
|
||||
shader_id: "rgb".to_string(),
|
||||
effect_input: crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
});
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(job),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): reads the fragment
|
||||
/// shader and replaces every `%1` marker with `request` — the OCIO
|
||||
/// auto-generated grading-primary shader text (C++ `GetShaderCode`
|
||||
/// receives the stub from the renderer's color context; the Rust
|
||||
/// renderer resolves it from the `OCIO_GRADING_STUBS` table before
|
||||
/// calling, see [`crate::nodes::ociogradingtransformlog`]'s sibling
|
||||
/// wiring).
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
Some(SHADER_FRAG.replace("%1", request))
|
||||
}
|
||||
|
||||
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
|
||||
@@ -767,31 +898,116 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_without_processor_pushes_nothing() {
|
||||
// Unlike the OCIO base, grading has no pass-through branch.
|
||||
let core = NodeCore::new();
|
||||
let n = node();
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
fn value_texture_pushes_real_grading_job() {
|
||||
let (core, behavior) = create();
|
||||
let inputs = crate::value::NodeValueRow::from([
|
||||
(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
),
|
||||
(CONTRAST_INPUT.to_string(), NodeValue::Vec4([0.5, 0.25, 0.5, 0.75])),
|
||||
(OFFSET_INPUT.to_string(), NodeValue::Vec4([0.25, -0.125, 0.0, 0.125])),
|
||||
(EXPOSURE_INPUT.to_string(), NodeValue::Vec4([1.0, 0.0, 0.0, 0.0])),
|
||||
]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.is_empty());
|
||||
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
let tex = table.get(ValueType::Texture).expect("texture pushed");
|
||||
let NodeValue::Texture(handle) = tex else {
|
||||
panic!("pushed value is not a texture handle");
|
||||
};
|
||||
let job = unsafe {
|
||||
crate::handle::get_checked::<crate::nodes::jobs::ShaderJobPayload>(handle)
|
||||
}
|
||||
.expect("real shader job");
|
||||
assert_eq!(job.shader_id, "rgb");
|
||||
assert_eq!(
|
||||
job.type_id,
|
||||
"org.olivevideoeditor.Olive.ociogradingtransformlinear"
|
||||
);
|
||||
assert_eq!(job.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
|
||||
// vec4 (RGBM: x = master) rewrites: contrast RGB = c*m, offset
|
||||
// RGB = c+m, exposure RGB = 2^(c+m).
|
||||
assert_eq!(
|
||||
job.params.get(CONTRAST_INPUT),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(OFFSET_INPUT),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(EXPOSURE_INPUT),
|
||||
Some(&NodeValue::Vec3([2.0, 2.0, 2.0]))
|
||||
);
|
||||
// Scalar defaults from the node's standard values.
|
||||
assert_eq!(
|
||||
job.params.get(SATURATION_INPUT),
|
||||
Some(&NodeValue::Float(1.0))
|
||||
);
|
||||
assert_eq!(job.params.get(PIVOT_INPUT), Some(&NodeValue::Float(0.18)));
|
||||
// Clamps disabled -> OCIO sentinels (NoClampBlack/NoClampWhite).
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_BLACK_INPUT),
|
||||
Some(&NodeValue::Float(-1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_WHITE_INPUT),
|
||||
Some(&NodeValue::Float(2.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_localBypass"),
|
||||
Some(&NodeValue::Boolean(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_with_processor_pushes_deferred_job() {
|
||||
let core = NodeCore::new();
|
||||
let mut n = node();
|
||||
n.base.set_processor(Some(crate::handle::CHandle::null()));
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
fn value_enables_clamp_when_flagged_of_white_and_black() {
|
||||
let (core, behavior) = create();
|
||||
let inputs = crate::value::NodeValueRow::from([
|
||||
(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
),
|
||||
(
|
||||
CLAMP_BLACK_ENABLE_INPUT.to_string(),
|
||||
NodeValue::Boolean(true),
|
||||
),
|
||||
(
|
||||
CLAMP_WHITE_ENABLE_INPUT.to_string(),
|
||||
NodeValue::Boolean(true),
|
||||
),
|
||||
(CLAMP_BLACK_INPUT.to_string(), NodeValue::Float(0.5)),
|
||||
(CLAMP_WHITE_INPUT.to_string(), NodeValue::Float(0.4)),
|
||||
]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.get(ValueType::Texture).is_some());
|
||||
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
let tex = table.get(ValueType::Texture).expect("texture pushed");
|
||||
let NodeValue::Texture(handle) = tex else {
|
||||
panic!("pushed value is not a texture handle");
|
||||
};
|
||||
let job = unsafe {
|
||||
crate::handle::get_checked::<crate::nodes::jobs::ShaderJobPayload>(handle)
|
||||
}
|
||||
.expect("real shader job");
|
||||
// White <= black is raised to black + 0.000001.
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_BLACK_INPUT),
|
||||
Some(&NodeValue::Float(0.5))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(CLAMP_WHITE_INPUT),
|
||||
Some(&NodeValue::Float(0.500001))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shader_code_splices_stub_marker() {
|
||||
let n = node();
|
||||
let stub = "vec4 ove_grading_primary(vec4 c) { return c; }";
|
||||
let code = n.shader_code(stub).unwrap();
|
||||
assert!(!code.contains("%1"));
|
||||
assert!(code.contains("ove_grading_primary(col)"));
|
||||
assert!(code.contains(stub));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -93,6 +93,76 @@ pub struct OCIOGradingTransformLogNode {
|
||||
base: OcioBase,
|
||||
}
|
||||
|
||||
/// Fragment shader. The `%1` marker is replaced at request time with the
|
||||
/// OCIO-generated grading-primary GLSL stub for the log style (the
|
||||
/// renderer's OCIO_GRADING_STUBS entry; the stub declares
|
||||
/// the `ocio_grading_primary_*` uniforms and `ove_grading_primary`), and
|
||||
/// the body applies it to the sampled input.
|
||||
const SHADER_FRAG: &str = r#"// Main texture input
|
||||
uniform sampler2D tex_in;
|
||||
|
||||
// Main texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Program will replace this with OCIO's auto-generated shader code
|
||||
%1
|
||||
|
||||
void main() {
|
||||
vec4 col = texture(tex_in, ove_texcoord);
|
||||
frag_color = ove_grading_primary(col);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// The generated OCIO uniform name for an input id: the C++ log node's
|
||||
/// ids carry the literal `OCIO_NAMESPACE_` macro text while the
|
||||
/// generated GPU shader uses the `ocio_` resource prefix, so the prefix
|
||||
/// is normalized at params-build time.
|
||||
fn uniform_name(id: &str) -> String {
|
||||
match id.strip_prefix("OCIO_NAMESPACE_") {
|
||||
Some(rest) => format!("ocio_{rest}"),
|
||||
None => id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Row value for `id` (the traverser convention), else the node's own
|
||||
/// standard/keyframe value (C++ `GetValueAtTime` parity for direct
|
||||
/// calls).
|
||||
fn row_or_standard(
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
id: &str,
|
||||
time: oak_core::Rational,
|
||||
) -> crate::value::NodeValue {
|
||||
match inputs.get(id) {
|
||||
Some(v) => v.clone(),
|
||||
None => core.value_at_time(id, -1, time),
|
||||
}
|
||||
}
|
||||
|
||||
/// A vec4 input value as `[x, y, z, w]` (x = master per the RGBM
|
||||
/// convention), collecting a generic numeric value into a padded array.
|
||||
fn to_vec4(v: crate::value::NodeValue) -> [f64; 4] {
|
||||
match v {
|
||||
crate::value::NodeValue::Vec4(v) => v,
|
||||
crate::value::NodeValue::Vec3(v) => [v[0], v[1], v[2], 0.0],
|
||||
crate::value::NodeValue::Vec2(v) => [v[0], v[1], 0.0, 0.0],
|
||||
crate::value::NodeValue::Float(v) => [v, v, v, v],
|
||||
_ => [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-channel vec3 the OCIO GPU uniforms expect: `rgb[i] =
|
||||
/// f(channel[i], master)`.
|
||||
fn channel_merge(v: [f64; 4], f: impl Fn(f64, f64) -> f64) -> crate::value::NodeValue {
|
||||
let m = v[0];
|
||||
crate::value::NodeValue::Vec3([
|
||||
f(v[1], m),
|
||||
f(v[2], m),
|
||||
f(v[3], m),
|
||||
])
|
||||
}
|
||||
|
||||
/// Set or replace an input property (C++ `set_input_property`).
|
||||
fn set_input_property(core: &mut NodeCore, input: &str, key: &str, value: crate::value::NodeValue) {
|
||||
if let Some(input) = core.get_input_mut(input) {
|
||||
@@ -302,15 +372,21 @@ impl NodeBehavior for OCIOGradingTransformLogNode {
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// processor not ready -> push nothing (no pass-through branch).
|
||||
/// Otherwise builds a `ColorTransformJob` from the whole input row
|
||||
/// and rewrites the vec4 (RGBM: x = master) inputs into the vec3
|
||||
/// form the GPU uniforms expect: lift RGB = channel + master
|
||||
/// (additive); gain and gamma RGB = channel * master
|
||||
/// otherwise pushes a REAL [`ShaderJobPayload`] whose params carry the
|
||||
/// input row rewritten into the vec3 form the OCIO-generated GPU
|
||||
/// uniforms expect — under the generated uniform names (`ocio_`
|
||||
/// prefix, the input ids' `OCIO_NAMESPACE_` text normalized):
|
||||
/// lift/brightness RGB = channel + master (additive), gain/contrast
|
||||
/// RGB = channel * master, gamma RGB = channel * master
|
||||
/// (multiplicative). Disabled clamps are pushed as
|
||||
/// `GradingPrimary::NoClampBlack()/NoClampWhite()`, and when both
|
||||
/// clamps are enabled the white clamp is raised to black + 0.000001
|
||||
/// per frame if keyframed/connected values violate white > black.
|
||||
/// `GradingPrimary::NoClampBlack()` (-1.0) / `NoClampWhite()` (2.0),
|
||||
/// and when both clamps are enabled the white clamp is raised to
|
||||
/// black + 0.000001 per frame if keyframed/connected values violate
|
||||
/// white > black. The transform itself is the OCIO shader the
|
||||
/// renderer splices in — the processor is generated at render time
|
||||
/// from the default config, so the evaluation-time guard (C++
|
||||
/// processor gate) is replaced by the node's own presence check on
|
||||
/// the input texture.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -318,33 +394,107 @@ impl NodeBehavior for OCIOGradingTransformLogNode {
|
||||
time: oak_core::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, time);
|
||||
match inputs.get(crate::nodes::ociobase::TEXTURE_INPUT) {
|
||||
Some(crate::value::NodeValue::Texture(_)) => {
|
||||
if self.base.processor().is_some() {
|
||||
// `// CPP-PARITY: ociogradingtransformlog.cpp`
|
||||
// `value()` — the C++ builds a ColorTransformJob and
|
||||
// rewrites the vec4 (RGBM: x = master) inputs into the
|
||||
// vec3 GPU uniform form: lift RGB = channel + master
|
||||
// (additive); gain and gamma RGB = channel * master
|
||||
// (multiplicative). Disabled clamps are pushed as
|
||||
// `OCIO_NAMESPACE::GradingPrimary::NoClampBlack()`
|
||||
// (-1.0) / `NoClampWhite()` (2.0), and when both
|
||||
// clamps are enabled the white clamp is raised to
|
||||
// black + 0.000001 per frame if keyframed/connected
|
||||
// values violate white > black. The Rust model has no
|
||||
// color-transform job payload: the renderer seam
|
||||
// resolves the deferred job from this null handle.
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Processor not ready: push nothing (no pass-through).
|
||||
}
|
||||
_ => {}
|
||||
let _ = self;
|
||||
if !matches!(
|
||||
inputs.get(crate::nodes::ociobase::TEXTURE_INPUT),
|
||||
Some(crate::value::NodeValue::Texture(_))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut params = inputs.clone();
|
||||
|
||||
// vec4 (RGBM: x = master) inputs -> the vec3 GPU uniform form
|
||||
// (normalized to the generated `ocio_` names).
|
||||
let lift = to_vec4(row_or_standard(core, inputs, LIFT_INPUT, time));
|
||||
let gain = to_vec4(row_or_standard(core, inputs, GAIN_INPUT, time));
|
||||
let gamma = to_vec4(row_or_standard(core, inputs, GAMMA_INPUT, time));
|
||||
params.insert(
|
||||
uniform_name(LIFT_INPUT),
|
||||
channel_merge(lift, |c, m| c + m),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(GAIN_INPUT),
|
||||
channel_merge(gain, |c, m| c * m),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(GAMMA_INPUT),
|
||||
channel_merge(gamma, |c, m| c * m),
|
||||
);
|
||||
|
||||
// Scalar uniforms. pivotBlack/pivotWhite are the log-log-scaled
|
||||
// normalization range (identity at gamma = 1 with the defaults
|
||||
// 0/1); the pivot default (-0.2) is the GRADING_LOG pivot.
|
||||
params.insert(
|
||||
uniform_name(SATURATION_INPUT),
|
||||
row_or_standard(core, inputs, SATURATION_INPUT, time),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(PIVOT_INPUT),
|
||||
row_or_standard(core, inputs, PIVOT_INPUT, time),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name("OCIO_NAMESPACE_grading_primary_pivotBlack"),
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name("OCIO_NAMESPACE_grading_primary_pivotWhite"),
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
);
|
||||
|
||||
// Clamps: enabled -> the value, disabled -> the OCIO sentinels
|
||||
// (NoClampBlack -1 / NoClampWhite 2 keep the GPU clamp a no-op).
|
||||
let black_enabled = row_or_standard(core, inputs, CLAMP_BLACK_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let white_enabled = row_or_standard(core, inputs, CLAMP_WHITE_ENABLE_INPUT, time)
|
||||
.to_double() != 0.0;
|
||||
let mut black =
|
||||
row_or_standard(core, inputs, CLAMP_BLACK_INPUT, time).to_double();
|
||||
let mut white =
|
||||
row_or_standard(core, inputs, CLAMP_WHITE_INPUT, time).to_double();
|
||||
if black_enabled && white_enabled {
|
||||
// ocio::GradingPrimary::validate: white > black (per frame,
|
||||
// when the static UI minimum cannot follow animated values).
|
||||
white = white.max(black + 0.000001);
|
||||
}
|
||||
params.insert(
|
||||
uniform_name(CLAMP_BLACK_INPUT),
|
||||
crate::value::NodeValue::Float(if black_enabled { black } else { -1.0 }),
|
||||
);
|
||||
params.insert(
|
||||
uniform_name(CLAMP_WHITE_INPUT),
|
||||
crate::value::NodeValue::Float(if white_enabled { white } else { 2.0 }),
|
||||
);
|
||||
params.insert(
|
||||
"ocio_grading_primary_localBypass".to_string(),
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
);
|
||||
|
||||
let job = crate::handle::make_owned(crate::nodes::jobs::ShaderJobPayload {
|
||||
node_id: crate::id::NodeId::INVALID,
|
||||
time,
|
||||
iterations: 1,
|
||||
type_id: self.type_id().to_string(),
|
||||
shader_id: "rgb".to_string(),
|
||||
effect_input: crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
});
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(job),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): reads the fragment
|
||||
/// shader and replaces every `%1` marker with `request` — the OCIO
|
||||
/// auto-generated grading-primary shader text (the renderer resolves
|
||||
/// it from the OCIO_GRADING_STUBS table before calling; this node's
|
||||
/// type id matches the C++ dtype, which carries the unexpanded
|
||||
/// OCIO_NAMESPACE macro text).
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
Some(SHADER_FRAG.replace("%1", request))
|
||||
}
|
||||
|
||||
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
|
||||
@@ -735,30 +885,88 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_without_processor_pushes_nothing() {
|
||||
let core = NodeCore::new();
|
||||
let n = node();
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
fn value_texture_pushes_real_grading_job() {
|
||||
let (core, behavior) = create();
|
||||
let inputs = crate::value::NodeValueRow::from([
|
||||
(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
),
|
||||
(LIFT_INPUT.to_string(), NodeValue::Vec4([0.25, -0.125, 0.0, 0.125])),
|
||||
(GAIN_INPUT.to_string(), NodeValue::Vec4([0.5, 0.25, 0.5, 0.75])),
|
||||
(GAMMA_INPUT.to_string(), NodeValue::Vec4([2.0, 1.0, 1.5, 2.0])),
|
||||
]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.is_empty());
|
||||
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
let tex = table.get(ValueType::Texture).expect("texture pushed");
|
||||
let NodeValue::Texture(handle) = tex else {
|
||||
panic!("pushed value is not a texture handle");
|
||||
};
|
||||
let job = unsafe {
|
||||
crate::handle::get_checked::<crate::nodes::jobs::ShaderJobPayload>(handle)
|
||||
}
|
||||
.expect("real shader job");
|
||||
assert_eq!(job.shader_id, "rgb");
|
||||
assert_eq!(
|
||||
job.type_id,
|
||||
"org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog"
|
||||
);
|
||||
assert_eq!(job.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
|
||||
// vec4 (RGBM: x = master) rewrites under the generated `ocio_`
|
||||
// uniform names: lift RGB = c+m, gain/gamma RGB = c*m.
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_brightness"),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_contrast"),
|
||||
Some(&NodeValue::Vec3([0.125, 0.25, 0.375]))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_gamma"),
|
||||
Some(&NodeValue::Vec3([2.0, 3.0, 4.0]))
|
||||
);
|
||||
// Scalar defaults from the node's standard values (pivot -0.2 for
|
||||
// the log style) plus the log normalization range.
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_saturation"),
|
||||
Some(&NodeValue::Float(1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_pivot"),
|
||||
Some(&NodeValue::Float(-0.2))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_pivotBlack"),
|
||||
Some(&NodeValue::Float(0.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_pivotWhite"),
|
||||
Some(&NodeValue::Float(1.0))
|
||||
);
|
||||
// Clamps disabled -> OCIO sentinels (NoClampBlack/NoClampWhite).
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_clampBlack"),
|
||||
Some(&NodeValue::Float(-1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_clampWhite"),
|
||||
Some(&NodeValue::Float(2.0))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get("ocio_grading_primary_localBypass"),
|
||||
Some(&NodeValue::Boolean(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_with_processor_pushes_deferred_job() {
|
||||
let core = NodeCore::new();
|
||||
let mut n = node();
|
||||
n.base.set_processor(Some(crate::handle::CHandle::null()));
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
||||
NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
)]);
|
||||
let mut table = NodeValueTable::default();
|
||||
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
||||
assert!(table.get(ValueType::Texture).is_some());
|
||||
fn shader_code_splices_stub_marker() {
|
||||
let n = node();
|
||||
let stub = "vec4 ove_grading_primary(vec4 c) { return c; }";
|
||||
let code = n.shader_code(stub).unwrap();
|
||||
assert!(!code.contains("%1"));
|
||||
assert!(code.contains("ove_grading_primary(col)"));
|
||||
assert!(code.contains(stub));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user