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()),
|
||||
|
||||
@@ -56,6 +56,9 @@ pub enum UniformType {
|
||||
Vec4,
|
||||
/// `mat4`.
|
||||
Mat4,
|
||||
/// `vec4[N]` (an array uniform; the polygon generator's bezier point
|
||||
/// table is the only consumer, indexed by `[i]` in the shader body).
|
||||
Vec4Array(usize),
|
||||
}
|
||||
|
||||
impl UniformType {
|
||||
@@ -74,7 +77,9 @@ impl UniformType {
|
||||
})
|
||||
}
|
||||
|
||||
/// The GLSL keyword back (block re-emission).
|
||||
/// The GLSL keyword back (block re-emission). Array types have their
|
||||
/// count appended (`vec4{name}[{count}]`), matching C++ uniform-array
|
||||
/// declarations.
|
||||
fn keyword(self) -> &'static str {
|
||||
match self {
|
||||
UniformType::Float => "float",
|
||||
@@ -84,6 +89,7 @@ impl UniformType {
|
||||
UniformType::Vec3 => "vec3",
|
||||
UniformType::Vec4 => "vec4",
|
||||
UniformType::Mat4 => "mat4",
|
||||
UniformType::Vec4Array(_) => "vec4",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +99,7 @@ impl UniformType {
|
||||
UniformType::Float | UniformType::Int | UniformType::Bool => 4,
|
||||
UniformType::Vec2 => 8,
|
||||
UniformType::Vec3 | UniformType::Vec4 | UniformType::Mat4 => 16,
|
||||
UniformType::Vec4Array(_) => 16,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +111,7 @@ impl UniformType {
|
||||
UniformType::Vec3 => 12,
|
||||
UniformType::Vec4 => 16,
|
||||
UniformType::Mat4 => 64,
|
||||
UniformType::Vec4Array(n) => 16 * n.max(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,6 +376,17 @@ pub fn pack_uniforms(
|
||||
(UniformType::Vec4, NodeValue::Vec4(v) | NodeValue::Color(v)) => {
|
||||
v.iter().map(|x| *x as f32).collect()
|
||||
}
|
||||
(UniformType::Vec4Array(n), NodeValue::Vec4Array(v)) => {
|
||||
let mut out = Vec::with_capacity(n * 4);
|
||||
for el in v.iter().take(n) {
|
||||
out.extend(el.iter().map(|x| *x as f32));
|
||||
}
|
||||
// Pad to the declared count (each element is 16 bytes; the
|
||||
// buffer is sized n*16 regardless).
|
||||
let have = out.len();
|
||||
out.resize((n * 4).max(have), 0.0f32);
|
||||
out
|
||||
}
|
||||
// Matrices: GLSL mat4 is column-major; the NodeValue comment
|
||||
// marks the layout row-major, so transpose on the way in.
|
||||
(UniformType::Mat4, NodeValue::Matrix(m)) => {
|
||||
@@ -405,9 +424,11 @@ fn is_sampler_type(kw: &str) -> bool {
|
||||
|
||||
/// Parse a `uniform <type> <name>;` declaration line (the constrained
|
||||
/// style of the node shader corpus: one declaration per line, no layout
|
||||
/// qualifiers, no initializers, no arrays — arrays are reported as
|
||||
/// unsupported, matching the C++ Blit). Returns `(type, name)`.
|
||||
fn parse_uniform_line(line: &str) -> Option<(&str, &str)> {
|
||||
/// qualifiers, no initializers). Arrays ARE supported:
|
||||
/// `uniform <type> <name>[<count>];` (the polygon generator's point
|
||||
/// table). `None` for any other line (samplers are handled by the
|
||||
/// caller). Returns `(base keyword, name, array count)`.
|
||||
fn parse_uniform_line(line: &str) -> Option<(&str, &str, usize)> {
|
||||
let t = line.trim_start();
|
||||
let rest = t.strip_prefix("uniform")?;
|
||||
if !rest.starts_with(char::is_whitespace) {
|
||||
@@ -415,11 +436,19 @@ fn parse_uniform_line(line: &str) -> Option<(&str, &str)> {
|
||||
}
|
||||
let rest = rest.trim_start();
|
||||
let (ty, rest) = rest.split_once(char::is_whitespace)?;
|
||||
let name = rest.trim().strip_suffix(';')?.trim();
|
||||
let rest = rest.trim_start().trim_end_matches(';');
|
||||
let (name, count) = if let Some(idx) = rest.find('[') {
|
||||
let name = rest[..idx].trim();
|
||||
let end = rest[idx..].find(']')?;
|
||||
let count: usize = rest[idx + 1..idx + end].trim().parse().ok()?;
|
||||
(name, count.max(1))
|
||||
} else {
|
||||
(rest.trim(), 1)
|
||||
};
|
||||
if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
||||
return None;
|
||||
}
|
||||
Some((ty, name))
|
||||
Some((ty, name, count))
|
||||
}
|
||||
|
||||
/// Translate one GLSL fragment shader to WGSL (naga glsl-in → wgsl-out).
|
||||
@@ -427,26 +456,29 @@ fn parse_uniform_line(line: &str) -> Option<(&str, &str)> {
|
||||
/// docs for the rewrite steps.
|
||||
pub fn translate(glsl: &str) -> Result<TranslatedShader> {
|
||||
let mut body_lines: Vec<String> = Vec::new();
|
||||
let mut uniforms: Vec<(UniformType, String)> = Vec::new();
|
||||
let mut uniforms: Vec<(UniformType, String, usize)> = Vec::new();
|
||||
let mut textures: Vec<String> = Vec::new();
|
||||
|
||||
for line in glsl.lines() {
|
||||
if let Some((ty, name)) = parse_uniform_line(line) {
|
||||
if let Some((ty, name, count)) = parse_uniform_line(line) {
|
||||
if is_sampler_type(ty) {
|
||||
textures.push(name.to_string());
|
||||
for _ in 0..count {
|
||||
textures.push(name.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match UniformType::from_keyword(ty) {
|
||||
Some(t) => {
|
||||
uniforms.push((t, name.to_string()));
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
let base = UniformType::from_keyword(ty);
|
||||
let uniform = match (base, count) {
|
||||
(Some(UniformType::Vec4), n) if n > 1 => UniformType::Vec4Array(n),
|
||||
(Some(t), n) if n == 1 => t,
|
||||
_ => {
|
||||
return Err(Error::Failed(format!(
|
||||
"unsupported uniform type in shader: {ty} {name}"
|
||||
"unsupported array uniform type in shader: {ty} {name}[{count}]"
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
uniforms.push((uniform, name.to_string(), count));
|
||||
continue;
|
||||
}
|
||||
body_lines.push(line.to_string());
|
||||
}
|
||||
@@ -465,7 +497,7 @@ pub fn translate(glsl: &str) -> Result<TranslatedShader> {
|
||||
src.push_str("layout(std140, set = 0, binding = ");
|
||||
src.push_str(&UNIFORM_BLOCK_BINDING.to_string());
|
||||
src.push_str(") uniform OakParams {\n");
|
||||
for (ty, name) in &uniforms {
|
||||
for (ty, name, count) in &uniforms {
|
||||
// bools are declared as int (WGSL has no host-shareable
|
||||
// bool); the body's uses were rewritten to `bool(x)`.
|
||||
let kw = if *ty == UniformType::Bool {
|
||||
@@ -473,7 +505,11 @@ pub fn translate(glsl: &str) -> Result<TranslatedShader> {
|
||||
} else {
|
||||
ty.keyword()
|
||||
};
|
||||
src.push_str(&format!(" {kw} {name};\n"));
|
||||
if *count > 1 {
|
||||
src.push_str(&format!(" {kw} {name}[{}];\n", count));
|
||||
} else {
|
||||
src.push_str(&format!(" {kw} {name};\n"));
|
||||
}
|
||||
}
|
||||
src.push_str("};\n");
|
||||
}
|
||||
@@ -513,7 +549,7 @@ pub fn translate(glsl: &str) -> Result<TranslatedShader> {
|
||||
}
|
||||
// WGSL has no host-shareable bool: bool uniforms live in the
|
||||
// block as `int`, so their uses become `bool(x)` (nonzero test).
|
||||
for (ty, name) in &uniforms {
|
||||
for (ty, name, _count) in &uniforms {
|
||||
if *ty == UniformType::Bool {
|
||||
l = replace_ident(&l, name, &format!("bool({name})"));
|
||||
}
|
||||
@@ -556,7 +592,7 @@ pub fn translate(glsl: &str) -> Result<TranslatedShader> {
|
||||
// std140 offsets (declaration order; the block tail pads to 16).
|
||||
let mut offset = 0usize;
|
||||
let mut decls = Vec::with_capacity(uniforms.len());
|
||||
for (ty, name) in uniforms {
|
||||
for (ty, name, _count) in uniforms {
|
||||
let align = ty.align();
|
||||
offset = offset.next_multiple_of(align);
|
||||
decls.push(UniformDecl { name, ty, offset });
|
||||
@@ -642,14 +678,65 @@ void main() {
|
||||
assert_eq!(out.uniform_block_bytes, 48);
|
||||
}
|
||||
|
||||
/// Array uniforms are unsupported (C++ Blit skipped them too).
|
||||
/// Array uniforms translate into the block as packed std140 arrays
|
||||
/// (polygon's `points_in[64]`; the declaration order determines the
|
||||
/// offsets: `int` @0, the vec4 array aligned to 16 @16, the trailing
|
||||
/// vec2 aligned to 8).
|
||||
#[test]
|
||||
fn array_uniforms_are_rejected() {
|
||||
let glsl = "uniform float taps_in[8];\nvoid main() {}\n";
|
||||
// The array syntax is not a parseable `uniform <type> <name>;`
|
||||
// line for our parser, so the declaration is left in the body and
|
||||
// naga sees it — either way the translation must not panic.
|
||||
let _ = translate(glsl);
|
||||
fn array_uniforms_translate_and_pack() {
|
||||
use oak_node::value::{NodeValue, NodeValueRow};
|
||||
let glsl = r#"
|
||||
uniform int point_count;
|
||||
uniform vec4 points_in[64];
|
||||
uniform vec2 resolution_in;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() { frag_color = vec4(0.0); }
|
||||
"#;
|
||||
let out = translate(glsl).expect("translate array uniforms");
|
||||
let decls: Vec<(&str, &UniformType, usize)> = out
|
||||
.uniforms
|
||||
.iter()
|
||||
.map(|u| (u.name.as_str(), &u.ty, u.offset))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
decls,
|
||||
vec![
|
||||
("point_count", &UniformType::Int, 0),
|
||||
("points_in", &UniformType::Vec4Array(64), 16),
|
||||
("resolution_in", &UniformType::Vec2, 16 + 64 * 16),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
out.uniform_block_bytes,
|
||||
(16 + 64 * 16 + 8usize).next_multiple_of(16)
|
||||
);
|
||||
|
||||
let mut row = NodeValueRow::new();
|
||||
row.insert("point_count".into(), NodeValue::Int(3));
|
||||
row.insert(
|
||||
"points_in".into(),
|
||||
NodeValue::Vec4Array(vec![[1.0, 2.0, 0.0, 0.0], [3.0, 4.0, 0.0, 0.0]]),
|
||||
);
|
||||
let buf = pack_uniforms(&out, &row);
|
||||
assert_eq!(buf.len(), out.uniform_block_bytes);
|
||||
let count = out.uniforms.iter().find(|u| u.name == "point_count").unwrap();
|
||||
assert_eq!(
|
||||
i32::from_le_bytes(buf[count.offset..count.offset + 4].try_into().unwrap()),
|
||||
3
|
||||
);
|
||||
let points = out.uniforms.iter().find(|u| u.name == "points_in").unwrap();
|
||||
let at = |i: usize, c: usize| points.offset + i * 16 + c * 4;
|
||||
assert_eq!(f32::from_le_bytes(buf[at(0, 0)..at(0, 1)].try_into().unwrap()), 1.0);
|
||||
assert_eq!(f32::from_le_bytes(buf[at(0, 1)..at(0, 2)].try_into().unwrap()), 2.0);
|
||||
assert_eq!(f32::from_le_bytes(buf[at(1, 0)..at(1, 1)].try_into().unwrap()), 3.0);
|
||||
// Short arrays pad the remaining slots to zero.
|
||||
assert_eq!(
|
||||
f32::from_le_bytes(buf[at(63, 0)..at(63, 1)].try_into().unwrap()),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Uniform packing follows the declared types and std140 offsets.
|
||||
@@ -817,6 +904,165 @@ void main() { frag_color = texture(tex_in, ove_texcoord); }
|
||||
ctx.destroy_texture(src);
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
/// The polygon generator's shader rasterizes the default pentagon on
|
||||
/// the GPU: the center texel inside the closed point loop is opaque
|
||||
/// white, the frame corner is transparent, and the 1-point fallback
|
||||
/// renders nothing.
|
||||
#[test]
|
||||
fn gpu_polygon_rasterizes_pentagon() {
|
||||
let Some(ctx) = gpu() else {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
};
|
||||
let (_core, behavior) = oak_node::factory::Factory::global()
|
||||
.create_any("org.olivevideoeditor.Olive.polygon")
|
||||
.unwrap();
|
||||
let glsl = behavior.shader_code("rgb").unwrap();
|
||||
let effect = compile_effect(&ctx, "test/polygon", &glsl, false).unwrap();
|
||||
|
||||
let dst = ctx.create_texture(512, 512).unwrap();
|
||||
let mut row = oak_node::value::NodeValueRow::new();
|
||||
row.insert(
|
||||
"points_in".into(),
|
||||
oak_node::value::NodeValue::Vec4Array(vec![
|
||||
[0.0, -135.0, 0.0, 0.0],
|
||||
[135.0, -45.0, 0.0, 0.0],
|
||||
[90.0, 120.0, 0.0, 0.0],
|
||||
[-90.0, 120.0, 0.0, 0.0],
|
||||
[-135.0, -45.0, 0.0, 0.0],
|
||||
]),
|
||||
);
|
||||
row.insert("point_count".into(), oak_node::value::NodeValue::Int(5));
|
||||
row.insert(
|
||||
"color_in".into(),
|
||||
oak_node::value::NodeValue::Color([1.0, 1.0, 1.0, 1.0]),
|
||||
);
|
||||
run_effect(&ctx, &effect, &row, &[], dst, (512, 512), 1).unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
|
||||
let center = pixel(&out, 256 * 512 + 256);
|
||||
assert_eq!(center, [1.0, 1.0, 1.0, 1.0], "center is inside the pentagon");
|
||||
let corner = pixel(&out, 0);
|
||||
assert_eq!(corner, [0.0, 0.0, 0.0, 0.0], "corner is outside");
|
||||
|
||||
// Degenerate: a single point draws nothing.
|
||||
row.insert("point_count".into(), oak_node::value::NodeValue::Int(1));
|
||||
run_effect(&ctx, &effect, &row, &[], dst, (512, 512), 1).unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
assert_eq!(
|
||||
pixel(&out, 256 * 512 + 256),
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
"single point draws nothing"
|
||||
);
|
||||
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
|
||||
/// The mask effect's shader multiplies the base texture by the
|
||||
/// pentagon matte on the GPU: the center texel keeps the base value,
|
||||
/// the corner is cleared, and with `invert_in` the result flips.
|
||||
#[test]
|
||||
fn gpu_mask_multiplies_base_by_pentagon() {
|
||||
let Some(ctx) = gpu() else {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
};
|
||||
let (_core, behavior) = oak_node::factory::Factory::global()
|
||||
.create_any("org.olivevideoeditor.Olive.mask")
|
||||
.unwrap();
|
||||
let glsl = behavior.shader_code("mask").unwrap();
|
||||
let effect = compile_effect(&ctx, "test/mask", &glsl, false).unwrap();
|
||||
|
||||
let src = ctx.create_texture(512, 512).unwrap();
|
||||
let dst = ctx.create_texture(512, 512).unwrap();
|
||||
let gray = crate::shaderfx::tests::f32_frame(512, 512, |_| {
|
||||
[0.5, 0.5, 0.5, 1.0]
|
||||
});
|
||||
ctx.upload(src, &gray).unwrap();
|
||||
|
||||
let mut row = oak_node::value::NodeValueRow::new();
|
||||
row.insert(
|
||||
"points_in".into(),
|
||||
oak_node::value::NodeValue::Vec4Array(vec![
|
||||
[0.0, -135.0, 0.0, 0.0],
|
||||
[135.0, -45.0, 0.0, 0.0],
|
||||
[90.0, 120.0, 0.0, 0.0],
|
||||
[-90.0, 120.0, 0.0, 0.0],
|
||||
[-135.0, -45.0, 0.0, 0.0],
|
||||
]),
|
||||
);
|
||||
row.insert("point_count".into(), oak_node::value::NodeValue::Int(5));
|
||||
row.insert("feather_in".into(), oak_node::value::NodeValue::Float(0.0));
|
||||
row.insert("invert_in".into(), oak_node::value::NodeValue::Boolean(false));
|
||||
run_effect(
|
||||
&ctx,
|
||||
&effect,
|
||||
&row,
|
||||
&[("base_in".to_string(), src)],
|
||||
dst,
|
||||
(512, 512),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
let center = pixel(&out, 256 * 512 + 256);
|
||||
assert_eq!(center, [0.5, 0.5, 0.5, 1.0], "center keeps the base");
|
||||
assert_eq!(pixel(&out, 0), [0.0, 0.0, 0.0, 0.0], "corner is masked out");
|
||||
|
||||
// Inverted: the corner keeps the base, the center is cleared.
|
||||
row.insert("invert_in".into(), oak_node::value::NodeValue::Boolean(true));
|
||||
run_effect(
|
||||
&ctx,
|
||||
&effect,
|
||||
&row,
|
||||
&[("base_in".to_string(), src)],
|
||||
dst,
|
||||
(512, 512),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
assert_eq!(pixel(&out, 0), [0.5, 0.5, 0.5, 1.0], "inverted corner keeps base");
|
||||
assert_eq!(
|
||||
pixel(&out, 256 * 512 + 256),
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
"inverted center cleared"
|
||||
);
|
||||
|
||||
// Feather: a square polygon with radius 4 softens the edge — the
|
||||
// pixel right outside the crisp edge becomes partially visible.
|
||||
row.insert("invert_in".into(), oak_node::value::NodeValue::Boolean(false));
|
||||
row.insert("feather_in".into(), oak_node::value::NodeValue::Float(4.0));
|
||||
row.insert(
|
||||
"points_in".into(),
|
||||
oak_node::value::NodeValue::Vec4Array(vec![
|
||||
[-150.0, -150.0, 0.0, 0.0],
|
||||
[150.0, -150.0, 0.0, 0.0],
|
||||
[150.0, 150.0, 0.0, 0.0],
|
||||
[-150.0, 150.0, 0.0, 0.0],
|
||||
]),
|
||||
);
|
||||
row.insert("point_count".into(), oak_node::value::NodeValue::Int(4));
|
||||
run_effect(
|
||||
&ctx,
|
||||
&effect,
|
||||
&row,
|
||||
&[("base_in".to_string(), src)],
|
||||
dst,
|
||||
(512, 512),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
let out = ctx.download(dst).unwrap();
|
||||
let soft = pixel(&out, 408 * 512 + 256)[0];
|
||||
assert!(
|
||||
soft > 0.02 && soft < 0.6,
|
||||
"just outside the feathered edge is partially visible: {soft}"
|
||||
);
|
||||
|
||||
ctx.destroy_texture(src);
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
|
||||
/// Every registered node type that ships a shader must translate (the
|
||||
/// all-shaders sweep). OCIO-stubbed shaders (`%1` markers needing the
|
||||
|
||||
Reference in New Issue
Block a user