Develop the film last, in the view transform's place
A film stock ran at order 25, after white balance and exposure, and everything after it — contrast, the curves, the colour mixer, the grading, every mask layer's tone — acted on the film's display-referred output, as though the frame had been scanned and then worked on. That was a display-referred rendering in the middle of the chain, which D19 removes (FR-DEV-3f, FR-DEV-3j). `film_sim` is now in `Stage::View` beside `view_transform`. While a stock is loaded the composer emits it in the view transform's place and not the sigmoid; otherwise the sigmoid. So every edit is a decision about the exposure the negative receives, and the film is the last thing that happens to the picture — after the detail stage too, in the view pass, which already binds the film's tables, the mask array and the grain's source position. Its per-layer settings blend there as they did in the fused pass. `op_renders` goes: a rendering is chosen, not suppressed, and the only render with no view transform is the camera-space tap. The YAML order moves to 190 so the panel reads in pipeline order; the stage, not the number, is what places it. Existing edits that combine a stock with tone or colour operations now render differently: those operations used to act on the print, and now act on the scene.
This commit is contained in:
@@ -337,18 +337,17 @@ pub trait Operation: Send + Sync {
|
||||
/// Whether this operation *is* the rendering, rather than an adjustment to
|
||||
/// one.
|
||||
///
|
||||
/// Almost everything returns `false`. An operation that returns `true`
|
||||
/// hands back display-referred linear sRGB, and in exchange the composer
|
||||
/// does not emit the camera profile's base curve — because this operation
|
||||
/// has done its job. It is handed working-space colour like any other
|
||||
/// scene-stage operation (D19).
|
||||
/// Almost everything returns `false`. A [`Stage::View`] operation that
|
||||
/// returns `true` is a complete rendering: while it is active the composer
|
||||
/// emits it in the view transform's place and not the default view
|
||||
/// transform beside it (FR-DEV-3j).
|
||||
///
|
||||
/// The reason it is a trait method and not a flag the caller sets is the
|
||||
/// one [`compose_full`] gives for deciding the output mode the same way: a
|
||||
/// caller that got it wrong would produce a shader that compiles, runs, and
|
||||
/// renders the picture twice. `film_sim` is the operation this exists for —
|
||||
/// a stock's characteristic curve does the base curve's job, from
|
||||
/// measurements, and running both is the camera's rendering of the scene
|
||||
/// a stock's characteristic curve does the view transform's job, from
|
||||
/// measurements, and running both is the default rendering of the scene
|
||||
/// followed by a film's rendering of *that*.
|
||||
fn renders(&self) -> bool {
|
||||
false
|
||||
@@ -469,7 +468,10 @@ pub enum Stage {
|
||||
/// unbounded. Nothing here clamps above 1.0 or encodes (ARCH §6.14).
|
||||
Scene,
|
||||
/// The view transform (FR-DEV-3j): after every scene operation, and the
|
||||
/// one stage allowed to map the scene to a display range.
|
||||
/// one stage allowed to map the scene to a display range. `view_transform`
|
||||
/// and `film_sim` are its two operations, and one of them runs: the film
|
||||
/// while a stock is loaded (see [`Operation::renders`]), the sigmoid
|
||||
/// otherwise.
|
||||
///
|
||||
/// Composed **whatever its state**. A neutral operation is otherwise left
|
||||
/// out, but a photograph with no view transform is a scan rather than a
|
||||
@@ -849,11 +851,6 @@ fn compose_inner(
|
||||
)
|
||||
};
|
||||
|
||||
// Whether an operation has taken over the rendering. Decided from the
|
||||
// operations for the same reason `output_mode` is: a caller that got it
|
||||
// wrong would produce a shader that compiles and renders the picture twice.
|
||||
let op_renders = ops.iter().any(|o| o.is_active() && o.renders());
|
||||
|
||||
let mut uniform_fields = String::new();
|
||||
let mut uniform_values: Vec<f32> = Vec::new();
|
||||
let mut body = String::new();
|
||||
@@ -882,8 +879,8 @@ fn compose_inner(
|
||||
|
||||
// TRACES: FR-DEV-3j
|
||||
// Whether the composer emits a view transform — which is every render but
|
||||
// the camera-space tap and one a rendering operation has taken over.
|
||||
let views = !op_renders && output_mode != OutputMode::CameraLinear;
|
||||
// the camera-space tap, whose job is the sensor's own numbers.
|
||||
let views = output_mode != OutputMode::CameraLinear;
|
||||
// And whether *this* shader is where it goes. Not the fused pass when a
|
||||
// detail stage follows: the view transform maps into a display range, and
|
||||
// a sharpener handed a display range is what D19 set out to stop. The
|
||||
@@ -940,13 +937,18 @@ fn compose_inner(
|
||||
.map(|o| o.as_ref())
|
||||
.filter(move |o| o.detail().is_none() && o.stage() == stage)
|
||||
};
|
||||
// The view operation, or a default one when the caller's chain holds
|
||||
// none — `compose(&[])` in a test, or a probe. Absent altogether where
|
||||
// nothing is to be rendered: see `views`.
|
||||
// The view operation: an active rendering — a loaded film stock — if
|
||||
// there is one, otherwise the chain's view transform, or a default one
|
||||
// when the caller's chain holds none (`compose(&[])` in a test, or a
|
||||
// probe). Decided from the operations for the same reason `output_mode`
|
||||
// is: a caller that got it wrong would produce a shader that compiles and
|
||||
// renders the picture twice. Absent altogether where nothing is to be
|
||||
// rendered: see `views`.
|
||||
let default_view = crate::ops::ViewTransform::new();
|
||||
let view: Option<&dyn Operation> = views_here.then(|| {
|
||||
point(Stage::View)
|
||||
.next()
|
||||
.find(|o| o.is_active() && o.renders())
|
||||
.or_else(|| point(Stage::View).find(|o| !o.renders()))
|
||||
.unwrap_or(&default_view as &dyn Operation)
|
||||
});
|
||||
|
||||
@@ -1190,7 +1192,7 @@ fn compose_inner(
|
||||
let rendering_tail = if views {
|
||||
String::new()
|
||||
} else {
|
||||
" // No view transform: an operation declaring `Operation::renders` has\n // mapped the scene to a display range itself, or this is the\n // camera-space tap, which stores the sensor's own numbers.\n"
|
||||
" // No view transform: this is the camera-space tap, which stores the\n // sensor's own numbers.\n"
|
||||
.to_string()
|
||||
};
|
||||
|
||||
@@ -2319,10 +2321,22 @@ mod tests {
|
||||
}));
|
||||
assert!(film.is_active(), "the fixture did not load");
|
||||
|
||||
let source = compose(&[Box::new(film) as Box<dyn Operation>]).source;
|
||||
// Listed first, and emitted last anyway: a stock is the view
|
||||
// transform (D19), so graph order does not put it among the scene
|
||||
// operations the way order 25 once did.
|
||||
let source = compose(&[
|
||||
Box::new(film) as Box<dyn Operation>,
|
||||
fake(DESC_A.clone(), 2.0, false),
|
||||
])
|
||||
.source;
|
||||
let film_at = source
|
||||
.find("---- film_sim ----")
|
||||
.expect("the operation itself must still be emitted");
|
||||
let scene_op = source.find("---- op_a ----").expect("op present");
|
||||
assert!(
|
||||
scene_op < film_at,
|
||||
"the film must see every scene operation's result"
|
||||
);
|
||||
assert!(
|
||||
!source.contains("view_sigmoid"),
|
||||
"the view transform is still being applied on top of the film"
|
||||
|
||||
Reference in New Issue
Block a user