nodes: real polygon and mask implementations on the GPU
Polygon and mask previously pushed null texture handles ('fake'
implementations). They now generate real ShaderJobPayloads and render
through the existing GPU shader pipeline:
- shaderfx: std140 uniform array support (Vec4Array(N)) — the parser
accepts 'uniform <type> <name>[N];' declarations, translate()
re-emits them as vec4[N] block members with per-element std140
offsets, and pack_uniforms writes array items from the new
NodeValue::Vec4Array value, padding short arrays to the declared N.
- polygon: value() collects the inherited points array (row element
keys 'points_in[i]', else the node's own per-element values — an
unconnected array resolves to the default pentagon via
GetValueAtTime parity) and pushes a ShaderJobPayload; the 'rgb'
fragment shader rasterizes the closed point loop with an odd-even
fill in screen space (center-translated, y-flipped to match the C++
point convention) and outputs color_in inside / transparent outside.
The CPU QPainterPath generate_frame stays a documented no-op.
- mask: value() pushes a single ShaderJobPayload whose new 'mask'
fragment shader folds the whole C++ chain into one GPU pass — base
texture multiplied by the polygon matte, optional invert, and the
optional feather gaussian softens the matte during sampling (the
separable blur.frag h/v iterations as a one-pass product,
density-normalized, radius capped at 16 px).
- Tests: translate/pack array coverage in shaderfx, GPU end-to-end
rasterization of the pentagon (center white, corner transparent),
mask multiply/invert/feather on real frames, and updated oak-node
payload assertions. oak-render 178, oak-node 440, oak-app 272 lib
tests pass.
This commit is contained in:
@@ -275,6 +275,85 @@ impl MaskDistortNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Combined mask fragment shader for the `"mask"` shader id, replacing
|
||||
/// the C++ chain — matte rasterize -> optional invert -> optional 2-pass
|
||||
/// feather blur -> multiply over base — with a single GPU pass: the base
|
||||
/// texture is multiplied by the polygon matte (odd-even fill of the
|
||||
/// closed point loop, same transform as the polygon generator), the
|
||||
/// matte is optionally inverted and optionally softened with a 2D
|
||||
/// gaussian (one pass; the C++ `blur.frag` horizontal+vertical
|
||||
/// iterations, evaluated as the separable product — sigma = radius/2,
|
||||
/// matching the C++ gaussian2 call). Feather radius is capped at 16 px
|
||||
/// for pass cost; the C++ cap is the full blur shader.
|
||||
const SHADER_MASK_FRAG: &str = r#"// Input texture
|
||||
uniform sampler2D base_in;
|
||||
uniform int point_count;
|
||||
uniform vec4 points_in[64];
|
||||
uniform float feather_in;
|
||||
uniform bool invert_in;
|
||||
uniform vec2 resolution_in;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main(void) {
|
||||
vec2 pixel = ove_texcoord * resolution_in;
|
||||
vec4 base = texture(base_in, ove_texcoord);
|
||||
float matte = 0.0;
|
||||
float cx = resolution_in.x * 0.5;
|
||||
float cy = resolution_in.y * 0.5;
|
||||
|
||||
if (point_count >= 3) {
|
||||
int crossings = 0;
|
||||
for (int i = 0; i < point_count; i++) {
|
||||
vec2 a = vec2(points_in[i].x + cx, cy - points_in[i].y);
|
||||
vec2 b = vec2(points_in[(i + 1) % point_count].x + cx, cy - points_in[(i + 1) % point_count].y);
|
||||
if ((a.y <= pixel.y && b.y > pixel.y) || (b.y <= pixel.y && a.y > pixel.y)) {
|
||||
float x_cross = a.x + (pixel.y - a.y) / (b.y - a.y) * (b.x - a.x);
|
||||
if (x_cross > pixel.x) {
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
matte = (crossings % 2 == 1) ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
if (feather_in > 0.0) {
|
||||
float r = min(feather_in, 16.0);
|
||||
float sigma = r * 0.5;
|
||||
float wsum = 0.0;
|
||||
float acc = 0.0;
|
||||
for (int y = -int(ceil(r)); y <= int(ceil(r)); y++) {
|
||||
for (int x = -int(ceil(r)); x <= int(ceil(r)); x++) {
|
||||
vec2 p = clamp(ove_texcoord + vec2(float(x), float(y)) / resolution_in, 0.0, 1.0);
|
||||
vec2 pp = p * resolution_in;
|
||||
int cs = 0;
|
||||
for (int i = 0; i < point_count; i++) {
|
||||
vec2 a = vec2(points_in[i].x + cx, cy - points_in[i].y);
|
||||
vec2 b = vec2(points_in[(i + 1) % point_count].x + cx, cy - points_in[(i + 1) % point_count].y);
|
||||
if ((a.y <= pp.y && b.y > pp.y) || (b.y <= pp.y && a.y > pp.y)) {
|
||||
float x_cross = a.x + (pp.y - a.y) / (b.y - a.y) * (b.x - a.x);
|
||||
if (x_cross > pp.x) {
|
||||
cs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
float inside = (cs % 2 == 1) ? 1.0 : 0.0;
|
||||
float w = exp(-0.5 * ((float(x) * float(x)) + (float(y) * float(y))) / (sigma * sigma));
|
||||
acc += w * inside;
|
||||
wsum += w;
|
||||
}
|
||||
}
|
||||
matte = acc / max(wsum, 1e-6);
|
||||
}
|
||||
|
||||
if (invert_in) {
|
||||
matte = 1.0 - matte;
|
||||
}
|
||||
frag_color = vec4(base.rgb * matte, base.a * matte);
|
||||
}
|
||||
"#;
|
||||
|
||||
impl NodeBehavior for MaskDistortNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
@@ -312,25 +391,14 @@ impl NodeBehavior for MaskDistortNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): generates the polygon matte
|
||||
/// (via the inherited `get_generate_job`) at the base texture's
|
||||
/// params or the global video params when there is no base; if
|
||||
/// `invert_in` is set wraps the matte in an `"invert"` shader job;
|
||||
/// with a base texture pushes an `"mrg"` multiply merge of base
|
||||
/// (`tex_a`) and matte (`tex_b`) — where `feather_in` > 0.0 the
|
||||
/// matte is first nested in a two-iteration gaussian `"feather"`
|
||||
/// blur job (method gaussian, horiz/vert/repeat-edge true, radius =
|
||||
/// feather value, `resolution_in` from the texture or the global
|
||||
/// square resolution); without a base texture pushes the matte
|
||||
/// itself.
|
||||
///
|
||||
/// The chain starts with a CPU rasterization of the polygon matte
|
||||
/// (C++ `get_generate_job`), which a [`ShaderJobPayload`] cannot
|
||||
/// express — the payload has no generate phase. The output is kept
|
||||
/// as a null texture handle marking "renderer must produce this
|
||||
/// texture"; expressing the rasterize -> (optional `"invert"`) ->
|
||||
/// (optional `"feather"` nested in) `"mrg"` multiply chain as
|
||||
/// payloads is a renderer TODO (`// CPP-PARITY: mask.cpp` `value()`).
|
||||
/// Evaluate outputs (C++ `value()`): pushes a REAL single
|
||||
/// [`ShaderJobPayload`] whose `"mask"` fragment shader does the whole
|
||||
/// C++ chain on the GPU — yes. One pass multiplies the base texture
|
||||
/// by the polygon matte (odd-even fill), optionally invert, and the
|
||||
/// optional feather gaussian softens the matte during sampling. The
|
||||
/// payload's params carry `points_in` (the inherited array, collected
|
||||
/// as in the polygon generator), `point_count`, `invert_in` and
|
||||
/// `feather_in`, with the base texture under the effect input.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -338,10 +406,37 @@ impl NodeBehavior for MaskDistortNode {
|
||||
time: oak_core::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, inputs, time);
|
||||
let points = crate::nodes::polygon::point_array(core, inputs, time);
|
||||
let point_count = points.len();
|
||||
let mut params = inputs.clone();
|
||||
params.insert(
|
||||
crate::nodes::polygon::POINTS_INPUT.to_string(),
|
||||
crate::value::NodeValue::Vec4Array(points),
|
||||
);
|
||||
params.insert(
|
||||
"point_count".to_string(),
|
||||
crate::value::NodeValue::Int(point_count as i64),
|
||||
);
|
||||
// The row carries invert/feather in the traverser flow; fall back
|
||||
// to the node's own values for direct `value()` calls.
|
||||
for id in [INVERT_INPUT, FEATHER_INPUT] {
|
||||
if !params.contains_key(id) {
|
||||
params.insert(id.to_string(), core.value_at_time(id, -1, time));
|
||||
}
|
||||
}
|
||||
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: "mask".to_string(),
|
||||
effect_input: crate::nodes::generatorwithmerge::BASE_INPUT.to_string(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
});
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
crate::value::NodeValue::Texture(job),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -355,6 +450,7 @@ impl NodeBehavior for MaskDistortNode {
|
||||
"mrg" => Some(SHADER_MRG_FRAG.to_string()),
|
||||
"feather" => Some(SHADER_FEATHER_FRAG.to_string()),
|
||||
"invert" => Some(SHADER_INVERT_FRAG.to_string()),
|
||||
"mask" => Some(SHADER_MASK_FRAG.to_string()),
|
||||
_ => self.polygon.shader_code(request),
|
||||
}
|
||||
}
|
||||
@@ -532,9 +628,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_always_pushes_deferred_matte_or_merge() {
|
||||
fn value_pushes_real_mask_job() {
|
||||
let (core, behavior) = create();
|
||||
// No inputs at all: the matte is generated and pushed.
|
||||
// No inputs at all: the matte job pushes with defaults.
|
||||
let mut table = NodeValueTable::default();
|
||||
behavior.value(
|
||||
&core,
|
||||
@@ -542,16 +638,36 @@ mod tests {
|
||||
Rational::new(0, 1),
|
||||
&mut table,
|
||||
);
|
||||
assert!(table.get(ValueType::Texture).is_some());
|
||||
|
||||
// With a base texture: an "mrg" merge job is pushed instead.
|
||||
let inputs = crate::value::NodeValueRow::from([(
|
||||
crate::nodes::generatorwithmerge::BASE_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());
|
||||
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, "mask");
|
||||
assert_eq!(job.type_id, "org.olivevideoeditor.Olive.mask");
|
||||
assert_eq!(
|
||||
job.effect_input,
|
||||
crate::nodes::generatorwithmerge::BASE_INPUT
|
||||
);
|
||||
match job.params.get(crate::nodes::polygon::POINTS_INPUT) {
|
||||
Some(NodeValue::Vec4Array(points)) => {
|
||||
assert_eq!(points.len(), 5, "default pentagon");
|
||||
assert_eq!(points[0], [0.0, -135.0, 0.0, 0.0]);
|
||||
}
|
||||
other => panic!("points_in is not a Vec4Array: {other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
job.params.get("point_count"),
|
||||
Some(&NodeValue::Int(5))
|
||||
);
|
||||
assert_eq!(
|
||||
job.params.get(INVERT_INPUT),
|
||||
Some(&NodeValue::Boolean(false))
|
||||
);
|
||||
assert_eq!(job.params.get(FEATHER_INPUT), Some(&NodeValue::Float(0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -565,6 +681,11 @@ mod tests {
|
||||
assert!(feather.contains("gaussian2"));
|
||||
let invert = n.shader_code("invert").unwrap();
|
||||
assert!(invert.contains("color = 1.0 - color;"));
|
||||
let mask = n.shader_code("mask").unwrap();
|
||||
assert!(mask.contains("points_in[64]"));
|
||||
assert!(mask.contains("feather_in"));
|
||||
assert!(mask.contains("invert_in"));
|
||||
assert!(mask.contains("base.rgb * matte"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -39,24 +39,54 @@ pub const COLOR_INPUT: &str = "color_in";
|
||||
/// `NodeCore::gizmos`, so they are omitted.
|
||||
pub struct PolygonGenerator;
|
||||
|
||||
/// Fragment shader for the `"rgb"` shader id (C++ loads
|
||||
/// `:/shaders/rgb.frag` in `get_shader_code`), recoloring the rasterized
|
||||
/// polygon mask with the color input. Text copied verbatim from
|
||||
/// `engine/shaders/rgb.frag`.
|
||||
const RGB_SHADER_FRAG: &str = r#"// Input texture
|
||||
uniform sampler2D texture_in;
|
||||
/// Fragment shader for the `"rgb"` shader id: rasterizes the point
|
||||
/// polygon on the GPU. The C++ pipeline rasterized the (bezier) path
|
||||
/// on the CPU and recolored it with `:/shaders/rgb.frag` (sampling
|
||||
/// `texture_in`); the Rust equivalent skips the CPU rasterization
|
||||
/// entirely and draws the closed point loop direct in the fragment
|
||||
/// shader — an odd-even (nonzero-winding-equivalent here) fill test
|
||||
/// per pixel against the point positions, outputting `color_in` when
|
||||
/// the pixel is inside and transparent otherwise. Points are in the
|
||||
/// C++ generator's space (relative to the frame center, y-up); the
|
||||
/// shader translates to the texture coordinate space (bottom-left
|
||||
/// origin, y-down via the center y flip).
|
||||
const RGB_SHADER_FRAG: &str = r#"// Point polygon rasterization
|
||||
uniform int point_count;
|
||||
uniform vec4 points_in[64];
|
||||
uniform vec2 resolution_in;
|
||||
uniform vec4 color_in;
|
||||
|
||||
// Input texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Input color
|
||||
uniform vec4 color_in;
|
||||
void main(void) {
|
||||
vec2 pixel = ove_texcoord * resolution_in;
|
||||
if (point_count < 3) {
|
||||
frag_color = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color = texture(texture_in, ove_texcoord);
|
||||
color.rgb = color_in.rgb * color.a;
|
||||
frag_color = color;
|
||||
float cx = resolution_in.x * 0.5;
|
||||
float cy = resolution_in.y * 0.5;
|
||||
int crossings = 0;
|
||||
|
||||
for (int i = 0; i < point_count; i++) {
|
||||
vec2 a = vec2(points_in[i].x + cx, cy - points_in[i].y);
|
||||
vec2 b = vec2(points_in[(i + 1) % point_count].x + cx, cy - points_in[(i + 1) % point_count].y);
|
||||
|
||||
if ((a.y <= pixel.y && b.y > pixel.y) || (b.y <= pixel.y && a.y > pixel.y)) {
|
||||
float x_cross = a.x + (pixel.y - a.y) / (b.y - a.y) * (b.x - a.x);
|
||||
if (x_cross > pixel.x) {
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crossings % 2 == 1) {
|
||||
frag_color = color_in;
|
||||
} else {
|
||||
frag_color = vec4(0.0);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -68,6 +98,61 @@ impl PolygonGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the point array of the polygon input (C++ iterating
|
||||
/// `GetValueAtTime(k_points_in, i)`): per-element row keys
|
||||
/// (`points_in[i]`, the connected-array convention) first, then the
|
||||
/// node's own standard/keyframe values per element — an unconnected
|
||||
/// array resolves to the default pentagon — sized by the input's
|
||||
/// array_size (capped at the shader's 64-element uniform array). An
|
||||
/// absent array falls back to a single bare-key value (non-array
|
||||
/// connection) or a single degenerate point, so `value()` consumers
|
||||
/// always receive a closed loop of at least one point.
|
||||
pub fn point_array(core: &NodeCore, inputs: &crate::value::NodeValueRow, time: oak_core::Rational) -> Vec<[f64; 4]> {
|
||||
let size = core
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|i| i.id == POINTS_INPUT)
|
||||
.map(|i| (i.array_size as usize).min(64))
|
||||
.unwrap_or(5);
|
||||
let mut points: Vec<[f64; 4]> = Vec::new();
|
||||
for i in 0..size {
|
||||
let key = format!("{POINTS_INPUT}[{i}]");
|
||||
let value = match inputs.get(&key) {
|
||||
Some(v) => v.clone(),
|
||||
None => core.value_at_time(POINTS_INPUT, i as i32, time),
|
||||
};
|
||||
match value {
|
||||
crate::value::NodeValue::Vec4(v) => points.push(v),
|
||||
crate::value::NodeValue::Vec2(v) => {
|
||||
points.push([v[0], v[1], 0.0, 0.0]);
|
||||
}
|
||||
crate::value::NodeValue::Color(v) => {
|
||||
let mut p = [0.0f64; 4];
|
||||
let n = v.len().min(4);
|
||||
p[..n].copy_from_slice(&v[..n]);
|
||||
points.push(p);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
if points.is_empty() {
|
||||
match inputs.get(POINTS_INPUT) {
|
||||
Some(crate::value::NodeValue::Vec4(v)) => points.push(*v),
|
||||
Some(crate::value::NodeValue::Vec2(v)) => {
|
||||
points.push([v[0], v[1], 0.0, 0.0]);
|
||||
}
|
||||
Some(crate::value::NodeValue::Color(v)) => {
|
||||
let mut p = [0.0f64; 4];
|
||||
let n = v.len().min(4);
|
||||
p[..n].copy_from_slice(&v[..n]);
|
||||
points.push(p);
|
||||
}
|
||||
_ => points.push([0.0, 0.0, 0.0, 0.0]),
|
||||
}
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
impl NodeBehavior for PolygonGenerator {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
@@ -101,21 +186,22 @@ impl NodeBehavior for PolygonGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): wraps the generate job
|
||||
/// (rasterized at u8 pixel format, then recolored by an `"rgb"`
|
||||
/// shader job sampling it as `texture_in` with `color_in`) in a
|
||||
/// texture at the sequence video params and pushes it through
|
||||
/// `push_mergable_job` (merged over `base_in` when connected).
|
||||
/// Evaluate outputs (C++ `value()`): emits the polygon rasterization
|
||||
/// as a REAL [`ShaderJobPayload`] — the fragment shader (`"rgb"`)
|
||||
/// rasterizes the point polygon in screen space (odd-even fill of the
|
||||
/// closed point loop) and multiplies the color input, all on the GPU.
|
||||
/// The bezier control points are not representable (the crate has no
|
||||
/// bezier value type), so the polygon is drawn with straight segments
|
||||
/// between the point positions — the C++ curve smoothing is a UI-only
|
||||
/// refinement the shader omits here.
|
||||
///
|
||||
/// The C++ chain starts with `get_generate_job()` — a CPU
|
||||
/// rasterization of the polygon path (QPainterPath bezier fill into
|
||||
/// an RGBA8888 frame via `generate_frame()`), which a
|
||||
/// [`ShaderJobPayload`] cannot express: the payload has no generate
|
||||
/// phase, and the rasterize -> `"rgb"` recolor -> optional `"mrg"`
|
||||
/// alpha-over chain has no Rust equivalent. The output is kept as a
|
||||
/// null texture handle marking "renderer must produce this texture";
|
||||
/// expressing the chain as payloads is a renderer TODO
|
||||
/// (`// CPP-PARITY: polygon.cpp` `value()`).
|
||||
/// The payload params carry `point_count`, the `points_in` array
|
||||
/// (packed into the std140 uniform block as `vec4[N]`; the renderer
|
||||
/// pads short arrays to the declared size) and `color_in`; the
|
||||
/// renderer injects `resolution_in` at job-build time. With an
|
||||
/// upstream `base_in` connected the payload is handed through
|
||||
/// `push_mergable_job` (alpha-over the generator output over the
|
||||
/// base); otherwise it is pushed as the node's output.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
@@ -123,12 +209,40 @@ impl NodeBehavior for PolygonGenerator {
|
||||
time: oak_core::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, inputs, time);
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
None,
|
||||
// Collect the point array (row element keys, else the node's own
|
||||
// standard/keyframe values — an unconnected array resolves to the
|
||||
// default pentagon; see [`point_array`]).
|
||||
let points = point_array(core, inputs, time);
|
||||
let point_count = points.len();
|
||||
let mut params = inputs.clone();
|
||||
params.insert(
|
||||
POINTS_INPUT.to_string(),
|
||||
crate::value::NodeValue::Vec4Array(points),
|
||||
);
|
||||
params.insert(
|
||||
"point_count".to_string(),
|
||||
crate::value::NodeValue::Int(point_count as i64),
|
||||
);
|
||||
// The color is part of the row in the traverser flow (the bare
|
||||
// key is always inserted for an unconnected input); fall back to
|
||||
// the node's own value for direct `value()` calls.
|
||||
if !params.contains_key(COLOR_INPUT) {
|
||||
params.insert(
|
||||
COLOR_INPUT.to_string(),
|
||||
core.value_at_time(COLOR_INPUT, -1, time),
|
||||
);
|
||||
}
|
||||
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: core.effect_input.clone(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
});
|
||||
super::generatorwithmerge::GeneratorWithMerge::push_mergable_job(inputs, job, table);
|
||||
}
|
||||
|
||||
/// Direct frame generation (C++ `generate_frame()`): clears the RGBA
|
||||
@@ -312,7 +426,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_pushes_deferred_job() {
|
||||
fn value_pushes_real_shader_job_with_pentagon() {
|
||||
let (core, behavior) = create();
|
||||
let mut table = NodeValueTable::default();
|
||||
behavior.value(
|
||||
@@ -321,7 +435,27 @@ mod tests {
|
||||
Rational::new(0, 1),
|
||||
&mut table,
|
||||
);
|
||||
assert!(table.get(ValueType::Texture).is_some());
|
||||
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.polygon");
|
||||
match job.params.get("points_in") {
|
||||
Some(NodeValue::Vec4Array(points)) => {
|
||||
assert_eq!(points.len(), 5, "default pentagon");
|
||||
}
|
||||
other => panic!("points_in is not a Vec4Array: {other:?}"),
|
||||
}
|
||||
assert_eq!(job.params.get("point_count"), Some(&NodeValue::Int(5)));
|
||||
assert_eq!(
|
||||
job.params.get(COLOR_INPUT),
|
||||
Some(&NodeValue::Color([1.0, 1.0, 1.0, 1.0]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -340,7 +474,9 @@ mod tests {
|
||||
fn shader_code_dispatches() {
|
||||
let n = PolygonGenerator;
|
||||
let rgb = n.shader_code("rgb").unwrap();
|
||||
assert!(rgb.contains("color.rgb = color_in.rgb * color.a;"));
|
||||
assert!(rgb.contains("points_in[64]"));
|
||||
assert!(rgb.contains("crossings % 2 == 1"));
|
||||
assert!(rgb.contains("frag_color = color_in;"));
|
||||
let mrg = n.shader_code("mrg").unwrap();
|
||||
assert!(mrg.contains("base_col *= 1.0 - blend_col.a;"));
|
||||
assert!(n.shader_code("other").is_none());
|
||||
|
||||
@@ -142,6 +142,9 @@ pub enum NodeValue {
|
||||
Vec4([f64; 4]),
|
||||
/// 4x4 matrix, row-major 16 elements (C++ `k_matrix`).
|
||||
Matrix([f64; 16]),
|
||||
/// Array of `vec4` values (C++ bezier `points_in` array input; the
|
||||
/// polygon generator's point table, packed as `vec4[i]` uniforms).
|
||||
Vec4Array(Vec<[f64; 4]>),
|
||||
/// Combo index.
|
||||
Combo(i64),
|
||||
/// String combo.
|
||||
@@ -410,6 +413,7 @@ impl NodeValue {
|
||||
NodeValue::Vec2(_) => ValueType::Vec2,
|
||||
NodeValue::Vec3(_) => ValueType::Vec3,
|
||||
NodeValue::Vec4(_) => ValueType::Vec4,
|
||||
NodeValue::Vec4Array(_) => ValueType::Vec4,
|
||||
NodeValue::Matrix(_) => ValueType::Matrix,
|
||||
NodeValue::Combo(_) => ValueType::Combo,
|
||||
NodeValue::StrCombo(_) => ValueType::StrCombo,
|
||||
@@ -710,6 +714,7 @@ impl Clone for NodeValue {
|
||||
NodeValue::Vec2(v) => NodeValue::Vec2(*v),
|
||||
NodeValue::Vec3(v) => NodeValue::Vec3(*v),
|
||||
NodeValue::Vec4(v) => NodeValue::Vec4(*v),
|
||||
NodeValue::Vec4Array(v) => NodeValue::Vec4Array(v.clone()),
|
||||
NodeValue::Matrix(v) => NodeValue::Matrix(*v),
|
||||
NodeValue::Combo(v) => NodeValue::Combo(*v),
|
||||
NodeValue::StrCombo(v) => NodeValue::StrCombo(v.clone()),
|
||||
|
||||
Reference in New Issue
Block a user