nodes: pivot transform rotation/scale around the frame center
The transform shader sampled in a top-left-origin pixel space while Olive's transform semantics (and every other node) are center-origin: rotation swung the image around the top-left corner, pushing it partly off-frame - reading exactly like an unwanted zoom. Match the C++ transform.vert projection: position (0,0) is the frame center and rotation/scale pivot around the anchor, so rotation and scale stay independent user controls. GPU tests pin the 90-degree landing spot (no smearing) and the 2x scale centroid (stays centered).
This commit is contained in:
@@ -24,9 +24,12 @@
|
||||
/// The transform fragment shader: samples the input through the inverse
|
||||
/// of the node's pixel-space transform (`transform_in` carries the
|
||||
/// CPU-inverted matrix; `resolution_in` is auto-filled by the runner).
|
||||
/// The C++ path transforms vertices (`ove_mvpmat` in transform.vert);
|
||||
/// an affine transform is equivalently applied fragment-side by
|
||||
/// inverse-mapping the sample position — the fixed fullscreen vertex
|
||||
/// Pixel space is CENTER-origin (C++ `transform.vert` transforms the
|
||||
/// quad in the frame-centered projection): position (0,0) is the frame
|
||||
/// center and rotation/scale pivot around the anchor — NOT around the
|
||||
/// top-left corner, which is what a top-left-origin pixel space would
|
||||
/// pivot on (a corner pivot swings the image off-frame as it rotates,
|
||||
/// reading exactly like an unwanted zoom). The fixed fullscreen vertex
|
||||
/// stage stays unchanged.
|
||||
const TRANSFORM_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform mat4 transform_in;
|
||||
@@ -36,8 +39,9 @@ in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main(void) {
|
||||
vec2 px = ove_texcoord * resolution_in;
|
||||
vec2 src = (transform_in * vec4(px, 0.0, 1.0)).xy;
|
||||
vec2 half_res = resolution_in * 0.5;
|
||||
vec2 px = ove_texcoord * resolution_in - half_res;
|
||||
vec2 src = (transform_in * vec4(px, 0.0, 1.0)).xy + half_res;
|
||||
frag_color = texture(tex_in, src / resolution_in);
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -2935,6 +2935,86 @@ mod tests {
|
||||
assert_eq!(pixel_at(&out, 0, 0), [0.0, 0.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
/// Transform rotation pivots around the FRAME CENTER (the C++
|
||||
/// center-origin pixel space), not the top-left corner: a 90° turn
|
||||
/// moves a pixel sitting 2px right of center to 2px below center,
|
||||
/// with no scaling or smearing (the turn is lossless).
|
||||
#[test]
|
||||
fn gpu_transform_rotates_around_the_frame_center() {
|
||||
if oak_core::backend::GpuContext::shared().is_none() {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
}
|
||||
// 8x8 black frame with one white pixel at (6, 4) — center+(2, 0).
|
||||
let mut frame = generate_frame(Rational::new(0, 1), (8, 8), PixelFormat::F32).unwrap();
|
||||
let at = (4 * 8 + 6) * 16;
|
||||
for (c, v) in [1.0f32, 1.0, 1.0, 1.0].iter().enumerate() {
|
||||
frame.data[at + c * 4..at + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
let mut inputs = NodeValueRow::new();
|
||||
inputs.insert("tex_in".into(), texture_value(Texture::wrap_frame(frame)));
|
||||
inputs.insert("rot_in".into(), NodeValue::Float(90.0));
|
||||
|
||||
let out = eval_node_row("org.olivevideoeditor.Olive.transform", inputs, None);
|
||||
assert_eq!(pixel_at(&out, 6, 4), [0.0, 0.0, 0.0, 0.0], "source spot vacated");
|
||||
assert_eq!(
|
||||
pixel_at(&out, 3, 6),
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
"90° around (4,4) maps texel center (6.5,4.5) -> (3.5,6.5)"
|
||||
);
|
||||
let lit = out
|
||||
.data
|
||||
.chunks_exact(16)
|
||||
.filter(|px| f32::from_le_bytes(px[12..16].try_into().unwrap()) > 0.01)
|
||||
.count();
|
||||
assert_eq!(lit, 1, "a pure rotation neither scales nor smears: {lit} lit pixels");
|
||||
}
|
||||
|
||||
/// Transform scale pivots around the frame center too: a center 2x2
|
||||
/// block at 2x uniform scale grows into the surrounding 4x4 (a
|
||||
/// corner pivot would drag it toward the bottom-right instead).
|
||||
#[test]
|
||||
fn gpu_transform_scales_around_the_frame_center() {
|
||||
if oak_core::backend::GpuContext::shared().is_none() {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
}
|
||||
// 8x8 black frame with a white 2x2 block at texels (3..4, 3..4).
|
||||
let mut frame = generate_frame(Rational::new(0, 1), (8, 8), PixelFormat::F32).unwrap();
|
||||
for (x, y) in [(3usize, 3usize), (4, 3), (3, 4), (4, 4)] {
|
||||
let at = (y * 8 + x) * 16;
|
||||
for (c, v) in [1.0f32, 1.0, 1.0, 1.0].iter().enumerate() {
|
||||
frame.data[at + c * 4..at + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
let mut inputs = NodeValueRow::new();
|
||||
inputs.insert("tex_in".into(), texture_value(Texture::wrap_frame(frame)));
|
||||
inputs.insert("scale_in".into(), NodeValue::Vec2([2.0, 2.0]));
|
||||
|
||||
let out = eval_node_row("org.olivevideoeditor.Olive.transform", inputs, None);
|
||||
// Pivot check via the alpha distribution: the block [3,5] scaled
|
||||
// 2x around (4,4) grows symmetrically to [2,6] — the centroid
|
||||
// stays at the frame center. A corner pivot would drag the block
|
||||
// to [6,10], shifting the centroid off-center and clipping the
|
||||
// block against the frame edge.
|
||||
let mut total = 0.0f32;
|
||||
let mut cx = 0.0f32;
|
||||
let mut cy = 0.0f32;
|
||||
for y in 0..8usize {
|
||||
for x in 0..8usize {
|
||||
let a = pixel_at(&out, x, y)[3];
|
||||
total += a;
|
||||
cx += (x as f32 + 0.5) * a;
|
||||
cy += (y as f32 + 0.5) * a;
|
||||
}
|
||||
}
|
||||
let (cx, cy) = (cx / total, cy / total);
|
||||
assert!(
|
||||
(cx - 4.0).abs() < 0.2 && (cy - 4.0).abs() < 0.2,
|
||||
"the block grows around the frame center, centroid ({cx}, {cy})"
|
||||
);
|
||||
}
|
||||
|
||||
/// Shape generator over the real GPU path: a centered 8x8 rectangle
|
||||
/// on a 16x16 frame fills exactly the middle block (pixel centers
|
||||
/// with texcoord in [0.25, 0.75)), everything outside stays
|
||||
|
||||
Reference in New Issue
Block a user