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:
@@ -1,22 +1,25 @@
|
||||
//! TRACES: FR-DEV-3f
|
||||
//! Film simulation — the stock renders the picture.
|
||||
//!
|
||||
//! # Why this one replaces the base curve
|
||||
//! # Why this one is the view transform
|
||||
//!
|
||||
//! [`crate::ops`]' other nodes adjust a picture. This one *makes* it. The base
|
||||
//! curve exists because sensor data is scene-referred and nothing anybody looks
|
||||
//! at is (FR-DEV-3e); a film stock's characteristic curve does the same job,
|
||||
//! from measurements, with a toe and a shoulder that were coated onto acetate
|
||||
//! rather than drawn. Running both renders the image twice — the camera's
|
||||
//! JPEG-ish rendering, and then a film's rendering of that — which is not what
|
||||
//! [`crate::ops`]' other nodes adjust a picture. This one *makes* it. The view
|
||||
//! transform exists because sensor data is scene-referred and nothing anybody
|
||||
//! looks at is (FR-DEV-3j); a film stock's characteristic curve does the same
|
||||
//! job, from measurements, with a toe and a shoulder that were coated onto
|
||||
//! acetate rather than drawn. Running both renders the image twice — the
|
||||
//! default rendering, and then a film's rendering of that — which is not what
|
||||
//! either is for and looks like neither.
|
||||
//!
|
||||
//! So this node declares [`Operation::renders`], and the composer answers by
|
||||
//! emitting neither the base curve nor the camera matrix. Both jobs move here:
|
||||
//! the fragment takes camera RGB, converts it to linear sRGB itself with the
|
||||
//! matrix already in the uniform block, and returns linear sRGB. That is a
|
||||
//! contract worth stating plainly, because a node that got half of it wrong
|
||||
//! would produce a picture that renders perfectly and is wrong everywhere.
|
||||
//! So this node is in [`Stage::View`] and declares [`Operation::renders`]: when
|
||||
//! a stock is loaded the composer puts it at the end of the chain in place of
|
||||
//! the default sigmoid (D19). It is handed working-space colour — linear sRGB
|
||||
//! primaries, scene-referred, after every other operation and after the detail
|
||||
//! stage — and returns display-referred linear sRGB for the output transform.
|
||||
//! Before D19 it ran at order 25, after exposure and before everything else,
|
||||
//! and the operations below it acted on its output. They now act on the scene
|
||||
//! it is shown: an edit is a decision about the exposure the negative
|
||||
//! receives, and the film is the last thing that happens to the picture.
|
||||
//!
|
||||
//! # Why the tables are not parameters
|
||||
//!
|
||||
@@ -34,7 +37,7 @@
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||
use crate::operation::{Operation, Uniform};
|
||||
use crate::operation::{Operation, Stage, Uniform};
|
||||
|
||||
pub const ID: OpId = OpId("film_sim");
|
||||
pub const EXPOSURE: ParamId = ParamId("exposure");
|
||||
@@ -304,11 +307,17 @@ impl Operation for FilmSim {
|
||||
self.tables.is_some()
|
||||
}
|
||||
|
||||
/// This node renders; the camera's own rendering must not also run.
|
||||
/// This node renders; the default view transform must not also run.
|
||||
fn renders(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3f | FR-DEV-3j
|
||||
/// The view transform's place, at the end of the chain (D19).
|
||||
fn stage(&self) -> Stage {
|
||||
Stage::View
|
||||
}
|
||||
|
||||
fn set_film_tables(&mut self, tables: Option<&FilmTables>) {
|
||||
self.set_tables(tables.cloned());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user