Correct converging verticals with a keystone in framing

There was no perspective transform anywhere in the pipeline: framing
offered a ±45° straighten, quarter turns and flips, and a building shot
looking up kept its leaning walls.

Framing gains a vertical and a horizontal keystone (-100..100). They are
parameters of framing rather than a new stage, so they carry its Compose
attribute, persist in the sidecar under framing, and are withheld from a
default paste exactly as the crop is. In the prologue the keystone runs
after the crop and the straightening and before the stored orientation
and the lens warp, so "vertical" is the photograph's displayed height and
the lens still sees its whole frame.

The map takes the output frame onto a trapezoid inside the source, built
as a homography from four corners and uploaded as three columns in the
framing uniform block (which grows from two vec4s to five). A keystone on
its own therefore never exposes an empty corner and leaves any crop valid.
Combined with a straightening angle the empty area is a pulled-back
quadrilateral the closed-form inscribed rectangle cannot describe, so
max_inscribed_crop searches for the largest centred rectangle whose
corners all have a source pixel behind them. source_at and output_at
apply the same map, so masks, gradients and spot handles follow it.
This commit is contained in:
2026-09-24 22:13:11 -04:00
parent ff89a4fa21
commit 5a500118ae
6 changed files with 786 additions and 36 deletions
+5 -1
View File
@@ -801,7 +801,11 @@ fn compose_inner(
\x20 // angle as sin/cos — a trig call per pixel would recompute a\n\
\x20 // value that is constant across the dispatch.\n\
\x20 crop_rect: vec4<f32>,\n\
\x20 framing_angle: vec4<f32>,\n",
\x20 framing_angle: vec4<f32>,\n\
\x20 // The perspective map (FR-DEV-20) by columns, `.w` unused.\n\
\x20 keystone_c0: vec4<f32>,\n\
\x20 keystone_c1: vec4<f32>,\n\
\x20 keystone_c2: vec4<f32>,\n",
);
uniform_values.extend_from_slice(&framing.uniforms());