Connect the lens vignetting correction to the pipeline
`ops/vignetting.rs` has carried a complete descriptor, polynomial, helper and test suite without an entry in `ops/`, so it was never in `chain()`. It reached no photograph and no panel, and `Attribute::Optics` was an empty category in consequence — filtered out of the tab strip for having no rows, by a chain that had never been given its only member. Declaring it needs the one thing the operation was written against and which did not exist. `wgsl_body` reads `radius`, and the module claimed "the composer publishes `radius` in the shader prologue for exactly this reason". It did not. `sample_source` now does, in both sampling branches, beside the `source_px` it already published for the same class of caller. It is corner-normalised there, which is the part that is easy to leave out. `p` spans ±0.5·aspect, so its length at the corner is 0.5·length(aspect) — about 0.901 on a 3:2 frame, not 1. Lensfun's polynomials are fitted against a corner radius of 1, so passing `length(p)` straight in evaluates every one of them short of where it was measured, by a factor that changes with the aspect ratio. It would have read as a correction that is simply too weak, which is indistinguishable from a bad profile. Both `lens.rs` and `framing.rs` asserted the normalisation `p` does not have; corrected. `order: 5` puts the correction ahead of the tonal stages, and the ordering is load-bearing rather than tidy. Recovering a corner means dividing by an attenuation below one — about two stops for a fast prime wide open — so run after the highlights have been rolled off and clipped, the lift has nowhere to go and the corners posterise instead of brightening. `layer_chain` now drops `Optics` as well as the neighbourhood operations. A local vignetting slider would have worked, which is what makes it worth excluding: `radius` measures from the centre of the whole photograph and a mask cannot move the optical axis, so it would lay a frame-centred radial ramp across the picture and multiply it by the mask. The existing exclusion covers operations that move and do nothing; this one covers an operation that moves and does something its name does not promise. The rule both share is now written down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1074,6 +1074,23 @@ pub(crate) fn sample_source(interpolate: bool) -> &'static str {
|
||||
let source_px = uv_src * vec2<f32>(src_dims);
|
||||
// A free angle puts output pixels between source pixels. Nearest-neighbour
|
||||
// here is what makes a straightened horizon stair-step, so interpolate.
|
||||
// Distance from the optical axis, normalised so the corner is exactly 1.
|
||||
//
|
||||
// Published beside `source_px` and for the same reason: a fragment is
|
||||
// handed a colour with no way back to a coordinate, and the radial
|
||||
// corrections need one. Derived from `p` after the whole coordinate stage,
|
||||
// so it measures the *source* frame — which is what a lens profile is
|
||||
// calibrated against, and why an off-centre crop still gets the falloff
|
||||
// its corner actually had rather than one centred on the crop.
|
||||
//
|
||||
// **The division is the part that is easy to leave out.** `p` spans
|
||||
// `+/-0.5 * aspect`, so at the corner its length is `0.5 * length(aspect)`
|
||||
// -- about 0.901 on a 3:2 frame, not 1. Lensfun's polynomials are fitted
|
||||
// against a corner radius of 1, so passing `length(p)` straight in
|
||||
// evaluates every one of them short of where it was measured, and by an
|
||||
// amount that changes with the aspect ratio. It reads as a correction that
|
||||
// is simply too weak, which is indistinguishable from a bad profile.
|
||||
let radius = length(p) / (0.5 * length(aspect));
|
||||
var c = sample_bilinear(uv_src, src_dims);
|
||||
"
|
||||
} else {
|
||||
@@ -1101,6 +1118,23 @@ pub(crate) fn sample_source(interpolate: bool) -> &'static str {
|
||||
// put, and its *amount* is handled separately by how much film a pixel
|
||||
// covers -- see `dr_film::Grain`.
|
||||
let source_px = uv_src * vec2<f32>(src_dims);
|
||||
// Distance from the optical axis, normalised so the corner is exactly 1.
|
||||
//
|
||||
// Published beside `source_px` and for the same reason: a fragment is
|
||||
// handed a colour with no way back to a coordinate, and the radial
|
||||
// corrections need one. Derived from `p` after the whole coordinate stage,
|
||||
// so it measures the *source* frame — which is what a lens profile is
|
||||
// calibrated against, and why an off-centre crop still gets the falloff
|
||||
// its corner actually had rather than one centred on the crop.
|
||||
//
|
||||
// **The division is the part that is easy to leave out.** `p` spans
|
||||
// `+/-0.5 * aspect`, so at the corner its length is `0.5 * length(aspect)`
|
||||
// -- about 0.901 on a 3:2 frame, not 1. Lensfun's polynomials are fitted
|
||||
// against a corner radius of 1, so passing `length(p)` straight in
|
||||
// evaluates every one of them short of where it was measured, and by an
|
||||
// amount that changes with the aspect ratio. It reads as a correction that
|
||||
// is simply too weak, which is indistinguishable from a bad profile.
|
||||
let radius = length(p) / (0.5 * length(aspect));
|
||||
var c = textureLoad(source, coord, 0).rgb;
|
||||
"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user