Apply a camera profile's HueSatMap and LookTable after exposure
The second half of D20: a camera_profile scene operation at order 25 that converts working colour into linear ProPhoto, runs the DNG SDK's HSV lookup through the HueSatMap and then the LookTable, and converts back. Hue and saturation do not change under the uniform gains before it, so a 2.5-D HueSatMap gives the same answer as straight after the matrix, and the look sees the photographer's exposure as it does in the SDK. Two departures for scene-referred values: value is not clamped on the way out, and a colour outside ProPhoto passes through. The operation holds only the switch (on by default) and a look strength of 0-200 %. It is composed while the switch is on — a new Operation::composes() separates "does something" from "moved from the defaults", so an untouched raw renders through its profile and still writes nothing. The tables come from the source: dr-gpu uploads the ones DemosaicedImage carries into a storage buffer at @binding(8), whose two-entry header tells the fragment whether there is anything to apply, and binds a header of zeros for every other source. apply_reference is the lookup on the CPU. The GPU test holds the shader to it over 256 colours, through synthetic tables strong enough that a wrong index shows, and through the library's real Adobe Standard tables when the 6D DNG is present.
This commit is contained in:
@@ -270,6 +270,19 @@ pub trait Operation: Send + Sync {
|
||||
/// what is actually used.
|
||||
fn is_active(&self) -> bool;
|
||||
|
||||
/// TRACES: FR-DEV-3e
|
||||
/// Whether the composer emits this operation's fragment.
|
||||
///
|
||||
/// Default: exactly when it [`Self::is_active`]. The exception is an
|
||||
/// operation that *is* part of the rendering at its defaults — the
|
||||
/// camera profile, which an untouched raw is rendered through (D20) —
|
||||
/// where "moved from the defaults" and "does something" come apart. Such
|
||||
/// an operation keeps `is_active` meaning the former, so a sidecar still
|
||||
/// stores nothing for it, and answers this with the latter.
|
||||
fn composes(&self) -> bool {
|
||||
self.is_active()
|
||||
}
|
||||
|
||||
/// The WGSL body of this operation's transform.
|
||||
///
|
||||
/// Receives `c` (a `vec3<f32>` of linear RGB) and must produce the
|
||||
@@ -843,7 +856,7 @@ fn compose_inner(
|
||||
let active: Vec<&dyn Operation> = ops
|
||||
.iter()
|
||||
.map(|o| o.as_ref())
|
||||
.filter(|o| o.is_active() && o.detail().is_none())
|
||||
.filter(|o| o.composes() && o.detail().is_none())
|
||||
.collect();
|
||||
|
||||
// Whether a detail stage follows. If one does, this pass stops short of
|
||||
@@ -1040,7 +1053,7 @@ fn compose_inner(
|
||||
let local: Vec<&crate::mask::LocalOp> = layers.ops.iter().filter(|l| l.op == id).collect();
|
||||
// The global side of the blend. A view operation always has one; see
|
||||
// `Stage::View`.
|
||||
let global = op.is_active() || op.stage() == Stage::View;
|
||||
let global = op.composes() || op.stage() == Stage::View;
|
||||
if !global && local.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -1333,6 +1346,11 @@ struct Params {{
|
||||
// bound to 1x1 placeholders whenever the flags say not to touch them.
|
||||
@group(0) @binding(6) var sampled: texture_2d<f32>;
|
||||
@group(0) @binding(7) var sample_out: texture_storage_2d<rgba16float, write>;
|
||||
// The source's camera profile tables (FR-DEV-3e, D20): a two-entry header,
|
||||
// then the entries (`ops::camera_profile::profile_buffer`). Declared
|
||||
// unconditionally like the masks, and bound to a header of zeros — no
|
||||
// tables — for every source without a profile.
|
||||
@group(0) @binding(8) var<storage, read> profile_table: array<vec4<f32>>;
|
||||
|
||||
{WINDOW_HELPER}{sampler_helper}{helper_src}{encode_output}
|
||||
// Display-encoded sRGB back to linear, for sources that arrive that way.
|
||||
|
||||
Reference in New Issue
Block a user