- oakengine_sequence_move_clip implemented for real (oaktimeline TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple trim bugs the stub was hiding); same-track via the frozen C ABI, cross-track supported by the module command - oaknode clip blocks now declare a tex_in texture input and set effect_input to it, so timeline clips can host effect chains; facade test covers effect insert/remove on a real clip - oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a Homebrew upgrade left the system ffmpeg .pc pointing at a deleted dav1d Cellar path, breaking links); reads a git-ignored workspace .env for IDEs that cannot inject env vars (RustRover); links the C++ stdlib for C++ codec libs (svt-av1) - oakengine re-exports oaknode so tests share one crate instance; it_node uses the direct instance's value type where it calls the module FFI (the --workspace dev-dependency feature split builds oaknode twice)
391 lines
12 KiB
Rust
391 lines
12 KiB
Rust
// Oak Video Editor - 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 <http://www.gnu.org/licenses/>.
|
|
|
|
//! Opacity effect (C++ `src/node/src/effect/opacity/opacityeffect.{h,cpp}`,
|
|
//! `olive::OpacityEffect`).
|
|
|
|
use crate::factory::NodeMeta;
|
|
use crate::node::{Category, NodeBehavior, NodeCore};
|
|
|
|
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
|
/// not-keyframable; this is the node's effect input.
|
|
pub const TEXTURE_INPUT: &str = "tex_in";
|
|
|
|
/// Opacity input id (C++ `k_value_input`). Type: float; default `1.0`;
|
|
/// properties: `view = percentage`, `min = 0.0`, `max = 1.0`.
|
|
pub const VALUE_INPUT: &str = "opacity_in";
|
|
|
|
/// Opacity effect node. Multiplies a texture's alpha by a 0..1 factor.
|
|
pub struct OpacityEffect {
|
|
/// Owned child math node configured to `multiply` (C++ `math_`,
|
|
/// formerly a QObject child).
|
|
math: Box<super::math::MathNode>,
|
|
}
|
|
|
|
/// Fragment shader for the plain opacity path (C++ loads the
|
|
/// `:/shaders/opacity.frag` resource in `get_shader_code`).
|
|
/// Text copied verbatim from `engine/shaders/opacity.frag`.
|
|
const SHADER_FRAG: &str = r#"// Inputs
|
|
uniform sampler2D tex_in;
|
|
uniform float opacity_in;
|
|
|
|
// Input texture coordinate
|
|
in vec2 ove_texcoord;
|
|
out vec4 frag_color;
|
|
|
|
void main() {
|
|
frag_color = texture(tex_in, ove_texcoord) * opacity_in;
|
|
}
|
|
"#;
|
|
|
|
/// Fragment shader for the `rgbmult` shader id (C++
|
|
/// `:/shaders/opacity_rgb.frag`), used when the opacity input is
|
|
/// itself a texture. Text copied verbatim from
|
|
/// `engine/shaders/opacity_rgb.frag`.
|
|
const SHADER_RGB_MULT_FRAG: &str = r#"// Inputs
|
|
uniform sampler2D tex_in;
|
|
uniform sampler2D opacity_in;
|
|
|
|
// Input texture coordinate
|
|
in vec2 ove_texcoord;
|
|
out vec4 frag_color;
|
|
|
|
vec3 rgb2hsv(vec3 c)
|
|
{
|
|
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
|
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
|
|
|
float d = q.x - min(q.w, q.y);
|
|
float e = 1.0e-10;
|
|
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
|
}
|
|
|
|
void main() {
|
|
vec4 value = texture(opacity_in, ove_texcoord);
|
|
float v = rgb2hsv(value.rgb).b;
|
|
|
|
vec4 c = texture(tex_in, ove_texcoord);
|
|
c *= v;
|
|
frag_color = c;
|
|
}
|
|
"#;
|
|
|
|
impl OpacityEffect {
|
|
/// Fragment shader for the plain opacity path (C++
|
|
/// `get_shader_code()` default branch).
|
|
fn shader_frag() -> &'static str {
|
|
SHADER_FRAG
|
|
}
|
|
|
|
/// Fragment shader for the `rgbmult` request (C++
|
|
/// `get_shader_code()` `"rgbmult"` branch).
|
|
fn shader_rgb_mult_frag() -> &'static str {
|
|
SHADER_RGB_MULT_FRAG
|
|
}
|
|
}
|
|
|
|
impl NodeBehavior for OpacityEffect {
|
|
/// Human-readable name (C++ `name()`).
|
|
fn name(&self) -> &str {
|
|
"Opacity"
|
|
}
|
|
|
|
/// Stable type id (C++ `id()`).
|
|
fn type_id(&self) -> &str {
|
|
"org.olivevideoeditor.Olive.opacity"
|
|
}
|
|
|
|
/// Categories (C++ `category()`; note: C++ files Opacity under
|
|
/// `k_category_filter` even though the sources live in `effect/`).
|
|
fn categories(&self) -> &[Category] {
|
|
&[Category::Filter]
|
|
}
|
|
|
|
/// Description (C++ `description()`).
|
|
fn description(&self) -> &str {
|
|
"Alter a video's opacity.\n\nThis is equivalent to multiplying a video by a number between 0.0 and 1.0."
|
|
}
|
|
|
|
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
|
/// "Texture", `opacity_in` -> "Opacity".
|
|
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
|
match id {
|
|
TEXTURE_INPUT => "Texture",
|
|
VALUE_INPUT => "Opacity",
|
|
_ => id,
|
|
}
|
|
}
|
|
|
|
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
|
/// texture opacity input -> `rgbmult` shader job; scalar opacity
|
|
/// != 1.0 -> plain shader job; opacity == 1.0 -> pass-through push
|
|
/// of the input texture unchanged.
|
|
///
|
|
/// The Rust model has no shader-job payload: the two job cases push
|
|
/// a null texture handle marking a renderer-deferred job resolved
|
|
/// via [`Self::shader_code`] (`// CPP-PARITY: opacityeffect.cpp`
|
|
/// `value()`).
|
|
fn value(
|
|
&self,
|
|
core: &NodeCore,
|
|
inputs: &crate::value::NodeValueRow,
|
|
time: oakcore_rs::Rational,
|
|
table: &mut crate::value::NodeValueTable,
|
|
) {
|
|
let _ = (core, time);
|
|
let tex = match inputs.get(TEXTURE_INPUT) {
|
|
Some(tex @ crate::value::NodeValue::Texture(_)) => tex.clone(),
|
|
_ => return,
|
|
};
|
|
|
|
match inputs.get(VALUE_INPUT) {
|
|
Some(crate::value::NodeValue::Texture(_)) => {
|
|
// Texture opacity input: rgbmult shader job.
|
|
table.push(
|
|
crate::value::ValueType::Texture,
|
|
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
|
None,
|
|
);
|
|
}
|
|
Some(v) => {
|
|
let opacity = v.to_double();
|
|
// Same semantics as `!qFuzzyCompare(opacity, 1.0)`
|
|
// (double overload).
|
|
if (opacity - 1.0).abs() * 1e12 > opacity.abs().min(1.0) {
|
|
table.push(
|
|
crate::value::ValueType::Texture,
|
|
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
|
None,
|
|
);
|
|
} else {
|
|
table.push(crate::value::ValueType::Texture, tex, None);
|
|
}
|
|
}
|
|
None => {
|
|
let opacity = core.value_at_time(VALUE_INPUT, -1, time).to_double();
|
|
if (opacity - 1.0).abs() * 1e12 > opacity.abs().min(1.0) {
|
|
table.push(
|
|
crate::value::ValueType::Texture,
|
|
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
|
None,
|
|
);
|
|
} else {
|
|
table.push(crate::value::ValueType::Texture, tex, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shader code request (C++ `get_shader_code()`): dispatch on
|
|
/// request id (`"rgbmult"` vs default) between the two fragment
|
|
/// shaders above.
|
|
fn shader_code(&self, request: &str) -> Option<String> {
|
|
if request == "rgbmult" {
|
|
Some(SHADER_RGB_MULT_FRAG.to_string())
|
|
} else {
|
|
Some(SHADER_FRAG.to_string())
|
|
}
|
|
}
|
|
|
|
/// Deep copy (C++ `copy()`); clones the owned child math node too.
|
|
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
|
Some(Box::new(OpacityEffect {
|
|
math: super::math::MathNode::new(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// Constructor (C++ `OpacityEffect::OpacityEffect()`): builds the child
|
|
/// math node, adds `tex_in`/`opacity_in` with the defaults and
|
|
/// properties documented on the constants, sets the video-effect flag
|
|
/// and the effect input.
|
|
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
|
let mut core = NodeCore::new();
|
|
|
|
let mut tex = crate::input::Input::new(
|
|
TEXTURE_INPUT,
|
|
crate::value::ValueType::Texture,
|
|
crate::value::NodeValue::None,
|
|
);
|
|
tex.flags |= crate::input::flags::NOT_KEYFRAMABLE;
|
|
core.add_input(tex);
|
|
|
|
let mut opacity = crate::input::Input::new(
|
|
VALUE_INPUT,
|
|
crate::value::ValueType::Float,
|
|
crate::value::NodeValue::Float(1.0),
|
|
);
|
|
opacity.properties = vec![
|
|
(
|
|
"view".to_string(),
|
|
crate::value::NodeValue::Text("percentage".into()),
|
|
),
|
|
("min".to_string(), crate::value::NodeValue::Float(0.0)),
|
|
("max".to_string(), crate::value::NodeValue::Float(1.0)),
|
|
];
|
|
core.add_input(opacity);
|
|
|
|
core.flags |= crate::node::flags::VIDEO_EFFECT;
|
|
core.effect_input = TEXTURE_INPUT.to_string();
|
|
|
|
(
|
|
core,
|
|
Box::new(OpacityEffect {
|
|
math: super::math::MathNode::new(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::node::NodeBehavior;
|
|
use crate::value::{NodeValue, NodeValueTable, ValueType};
|
|
use oakcore_rs::Rational;
|
|
|
|
#[test]
|
|
fn input_names() {
|
|
let n = OpacityEffect {
|
|
math: super::super::math::MathNode::new(),
|
|
};
|
|
assert_eq!(n.input_name(TEXTURE_INPUT), "Texture");
|
|
assert_eq!(n.input_name(VALUE_INPUT), "Opacity");
|
|
}
|
|
|
|
#[test]
|
|
fn create_wires_inputs_and_flags() {
|
|
let (core, behavior) = create();
|
|
assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.opacity");
|
|
assert_eq!(
|
|
core.get_input(VALUE_INPUT).unwrap().default,
|
|
NodeValue::Float(1.0)
|
|
);
|
|
assert_eq!(core.effect_input, TEXTURE_INPUT);
|
|
assert_ne!(core.flags & crate::node::flags::VIDEO_EFFECT, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn value_no_texture_pushes_nothing() {
|
|
let (core, behavior) = create();
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(
|
|
&core,
|
|
&crate::value::NodeValueRow::default(),
|
|
Rational::new(0, 1),
|
|
&mut table,
|
|
);
|
|
assert!(table.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn value_opacity_unity_passes_texture_through() {
|
|
let (mut core, behavior) = create();
|
|
core.set_standard_value(VALUE_INPUT, -1, NodeValue::Float(1.0));
|
|
let tex = NodeValue::Texture(crate::handle::CHandle::null());
|
|
let inputs = crate::value::NodeValueRow::from([(TEXTURE_INPUT.to_string(), tex.clone())]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert_eq!(table.get(ValueType::Texture), Some(&tex));
|
|
}
|
|
|
|
#[test]
|
|
fn value_opacity_scaled_pushes_job_placeholder() {
|
|
let (mut core, behavior) = create();
|
|
core.set_standard_value(VALUE_INPUT, -1, NodeValue::Float(0.5));
|
|
let inputs = crate::value::NodeValueRow::from([(
|
|
TEXTURE_INPUT.to_string(),
|
|
NodeValue::Texture(crate::handle::CHandle::null()),
|
|
)]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert!(table.get(ValueType::Texture).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn value_opacity_in_row_scaled_pushes_job_placeholder() {
|
|
let (core, behavior) = create();
|
|
let inputs = crate::value::NodeValueRow::from([
|
|
(
|
|
TEXTURE_INPUT.to_string(),
|
|
NodeValue::Texture(crate::handle::CHandle::null()),
|
|
),
|
|
(VALUE_INPUT.to_string(), NodeValue::Float(0.5)),
|
|
]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert!(table.get(ValueType::Texture).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn value_opacity_in_row_unity_passes_through() {
|
|
let (core, behavior) = create();
|
|
let tex = NodeValue::Texture(crate::handle::CHandle::null());
|
|
let inputs = crate::value::NodeValueRow::from([
|
|
(TEXTURE_INPUT.to_string(), tex.clone()),
|
|
(VALUE_INPUT.to_string(), NodeValue::Float(1.0)),
|
|
]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert_eq!(table.get(ValueType::Texture), Some(&tex));
|
|
}
|
|
|
|
#[test]
|
|
fn value_texture_opacity_pushes_rgbmult_placeholder() {
|
|
let (core, behavior) = create();
|
|
let inputs = crate::value::NodeValueRow::from([
|
|
(
|
|
TEXTURE_INPUT.to_string(),
|
|
NodeValue::Texture(crate::handle::CHandle::null()),
|
|
),
|
|
(
|
|
VALUE_INPUT.to_string(),
|
|
NodeValue::Texture(crate::handle::CHandle::null()),
|
|
),
|
|
]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert!(table.get(ValueType::Texture).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn shader_code_dispatches() {
|
|
let n = OpacityEffect {
|
|
math: super::super::math::MathNode::new(),
|
|
};
|
|
assert!(n.shader_code("rgbmult").unwrap().contains("rgb2hsv"));
|
|
assert!(n.shader_code("other").unwrap().contains("opacity_in"));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_clones_behavior() {
|
|
let (core, behavior) = create();
|
|
let dup = behavior.duplicate(&core).unwrap();
|
|
assert_eq!(dup.name(), "Opacity");
|
|
}
|
|
}
|
|
|
|
/// Register this node type (C++ `k_opacity_effect` in
|
|
/// `factory.cpp::create_from_factory_index`).
|
|
pub fn register(meta: &mut Vec<NodeMeta>) {
|
|
meta.push(NodeMeta {
|
|
type_id: "org.olivevideoeditor.Olive.opacity",
|
|
name: "Opacity",
|
|
categories: &[Category::Filter],
|
|
create,
|
|
});
|
|
}
|