Mark a pixel the lens correction pushed off the sensor with alpha 0 in the camera-space tap
The fused shader stored black with alpha 1 for a pixel whose source coordinate left the frame, and the merge's warp averaged it in like any other: a dark, badly interpolated fringe along every frame's edge, visible as a seam wherever a frame ended and, later, as the edge the border fill continued. The display keeps its opaque black; CameraLinear stores alpha 0 and the warp weights each sample by the alpha it interpolated, dropping a sample that has none.
This commit is contained in:
@@ -68,8 +68,8 @@ fn to_direction(u: f32, v: f32) -> vec3<f32> {
|
|||||||
return vec3<f32>(sin(theta) * cos(phi), sin(phi), cos(theta) * cos(phi));
|
return vec3<f32>(sin(theta) * cos(phi), sin(phi), cos(theta) * cos(phi));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(x: i32, y: i32) -> vec3<f32> {
|
fn load(x: i32, y: i32) -> vec4<f32> {
|
||||||
return textureLoad(tile, vec2<i32>(x, y), 0).rgb;
|
return textureLoad(tile, vec2<i32>(x, y), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@compute @workgroup_size(8, 8, 1)
|
@compute @workgroup_size(8, 8, 1)
|
||||||
@@ -110,11 +110,25 @@ fn warp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||||||
let y1 = min(y0 + 1, i32(p.tile_size.y) - 1);
|
let y1 = min(y0 + 1, i32(p.tile_size.y) - 1);
|
||||||
let fx = tx - f32(x0);
|
let fx = tx - f32(x0);
|
||||||
let fy = ty - f32(y0);
|
let fy = ty - f32(y0);
|
||||||
let top = mix(load(x0, y0), load(x1, y0), fx);
|
// The four texels, with their alpha: the tap writes alpha 0 where the
|
||||||
let bot = mix(load(x0, y1), load(x1, y1), fx);
|
// lens correction found no source pixel, and a sample that touches one
|
||||||
let rgb = mix(top, bot, fy) * p.gain;
|
// of those is a partial pixel — down-weighted by exactly how much of
|
||||||
|
// it is missing, and dropped when all of it is.
|
||||||
|
let s00 = load(x0, y0);
|
||||||
|
let s10 = load(x1, y0);
|
||||||
|
let s01 = load(x0, y1);
|
||||||
|
let s11 = load(x1, y1);
|
||||||
|
let top = mix(s00, s10, fx);
|
||||||
|
let bot = mix(s01, s11, fx);
|
||||||
|
let s = mix(top, bot, fy);
|
||||||
|
if (s.a <= 0.001) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Colour is the alpha-weighted mean of the texels that exist.
|
||||||
|
let rgb = s.rgb / s.a * p.gain;
|
||||||
|
let wa = w * s.a;
|
||||||
let i = gid.y * p.chunk_size.x + gid.x;
|
let i = gid.y * p.chunk_size.x + gid.x;
|
||||||
acc[i] = acc[i] + vec4<f32>(rgb * w, w);
|
acc[i] = acc[i] + vec4<f32>(rgb * wa, wa);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve: the accumulated chunk to sixteen-bit samples.
|
// Resolve: the accumulated chunk to sixteen-bit samples.
|
||||||
|
|||||||
@@ -857,12 +857,22 @@ fn compose_inner(
|
|||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TRACES: FR-MRG-2
|
||||||
|
// A pixel whose source coordinate leaves the frame — the corners a lens
|
||||||
|
// correction pulls in — is stored black. For the display that is the
|
||||||
|
// right picture; for a merge it is a pixel that does not exist and must
|
||||||
|
// not be averaged in as if it did, so the camera-space tap marks it
|
||||||
|
// with alpha 0 and the warp reads the alpha as validity.
|
||||||
|
let void_alpha = match output_mode {
|
||||||
|
OutputMode::CameraLinear => "0.0",
|
||||||
|
_ => "1.0",
|
||||||
|
};
|
||||||
let prologue = format!(
|
let prologue = format!(
|
||||||
"{}{}{}\n{}",
|
"{}{}{}\n{}",
|
||||||
framing.wgsl_prologue(),
|
framing.wgsl_prologue(),
|
||||||
channel_positions,
|
channel_positions,
|
||||||
warp.body,
|
warp.body,
|
||||||
sample_source(interpolate, warp.splits_channels)
|
sample_source(interpolate, warp.splits_channels).replace("VOID_ALPHA", void_alpha)
|
||||||
);
|
);
|
||||||
let sampler_helper = if interpolate { BILINEAR_HELPER } else { "" };
|
let sampler_helper = if interpolate { BILINEAR_HELPER } else { "" };
|
||||||
|
|
||||||
@@ -1237,7 +1247,7 @@ pub(crate) fn sample_source(interpolate: bool, splits_channels: bool) -> &'stati
|
|||||||
// remove. `sample_bilinear` clamps its own texel indices, so red and blue
|
// remove. `sample_bilinear` clamps its own texel indices, so red and blue
|
||||||
// land on the edge pixel rather than out of bounds.
|
// land on the edge pixel rather than out of bounds.
|
||||||
if (any(uv_src < vec2<f32>(0.0)) || any(uv_src >= vec2<f32>(1.0))) {
|
if (any(uv_src < vec2<f32>(0.0)) || any(uv_src >= vec2<f32>(1.0))) {
|
||||||
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(0.0, 0.0, 0.0, 1.0));
|
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(0.0, 0.0, 0.0, VOID_ALPHA));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,7 +1275,7 @@ pub(crate) fn sample_source(interpolate: bool, splits_channels: bool) -> &'stati
|
|||||||
// corners; render them black rather than clamping, which would smear an
|
// corners; render them black rather than clamping, which would smear an
|
||||||
// edge pixel across them.
|
// edge pixel across them.
|
||||||
if (any(uv_src < vec2<f32>(0.0)) || any(uv_src >= vec2<f32>(1.0))) {
|
if (any(uv_src < vec2<f32>(0.0)) || any(uv_src >= vec2<f32>(1.0))) {
|
||||||
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(0.0, 0.0, 0.0, 1.0));
|
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(0.0, 0.0, 0.0, VOID_ALPHA));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1308,7 +1318,7 @@ pub(crate) fn sample_source(interpolate: bool, splits_channels: bool) -> &'stati
|
|||||||
// transformed at all. Render it black rather than clamping, which would
|
// transformed at all. Render it black rather than clamping, which would
|
||||||
// smear an edge pixel across the gap.
|
// smear an edge pixel across the gap.
|
||||||
if (any(uv_src < vec2<f32>(0.0)) || any(uv_src >= vec2<f32>(1.0))) {
|
if (any(uv_src < vec2<f32>(0.0)) || any(uv_src >= vec2<f32>(1.0))) {
|
||||||
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(0.0, 0.0, 0.0, 1.0));
|
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(0.0, 0.0, 0.0, VOID_ALPHA));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2109,6 +2119,35 @@ mod tests {
|
|||||||
assert!(convert < clip, "the clip must come after the conversion");
|
assert!(convert < clip, "the clip must come after the conversion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_camera_space_tap_marks_a_pixel_off_the_sensor_with_alpha_zero() {
|
||||||
|
// TRACES: FR-MRG-2
|
||||||
|
// A lens correction pulls the corners in, and the pixels it leaves
|
||||||
|
// behind have no source. The display stores them black and opaque;
|
||||||
|
// the tap stores them black and *transparent*, so a merge can tell
|
||||||
|
// "nothing here" from "black here" and never averages the fringe in.
|
||||||
|
let tap = compose_camera_linear(
|
||||||
|
&[],
|
||||||
|
dr_types::Orientation::default(),
|
||||||
|
crate::framing::CropRect::default(),
|
||||||
|
)
|
||||||
|
.source;
|
||||||
|
assert!(
|
||||||
|
tap.contains("vec4<f32>(0.0, 0.0, 0.0, 0.0)"),
|
||||||
|
"the tap must store alpha 0 off the sensor:\n{tap}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!tap.contains("VOID_ALPHA"),
|
||||||
|
"the placeholder must be substituted:\n{tap}"
|
||||||
|
);
|
||||||
|
let display = compose(&[]).source;
|
||||||
|
assert!(
|
||||||
|
!display.contains("vec4<f32>(0.0, 0.0, 0.0, 0.0)"),
|
||||||
|
"the display keeps its opaque black:\n{display}"
|
||||||
|
);
|
||||||
|
assert!(!display.contains("VOID_ALPHA"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_generated_matrix_is_the_one_the_profile_writer_will_use() {
|
fn the_generated_matrix_is_the_one_the_profile_writer_will_use() {
|
||||||
// The shader encodes the pixels and `dr-export` describes them, from
|
// The shader encodes the pixels and `dr-export` describes them, from
|
||||||
|
|||||||
Reference in New Issue
Block a user