Develop a mask layer's film on its own settings
A layer offered the film's sliders and they moved nothing: its copy of the node was never given the stock, so it stayed inactive. Film now works in a layer the way the other adjustments do, as offsets to the photograph's settings, but blended as settings rather than as results, since a film is a rendering and cross-fading two developments is not what a region on a pushed film looks like. - dr-film bakes no slider. Exposure is a gain in the shader; push interpolates the stock's measured processes, one curve row each; the print is split at the paper's log exposure, so print exposure is an addition between two lookups and exact at any setting. The enlarger stays balanced at the photograph's exposure. - film_sim reads all four settings as uniforms, format one-hot over a grain count per format, so every uniform is linear in what it does. - Operation::blends_settings lets the composer average each overlapping layer's uniforms with the global ones by mask weight, the global setting taking whatever weight the layers leave, and run the fragment once. Three layers at full weight give the mean of their settings. - The stock picker is hidden on a layer. Only the photograph's exposure re-solves the print balance; push, print exposure and format need no rebake at all now.
This commit is contained in:
@@ -307,6 +307,32 @@ pub trait Operation: Send + Sync {
|
||||
/// shape it should take.
|
||||
fn set_film_tables(&mut self, _tables: Option<&crate::ops::film_sim::FilmTables>) {}
|
||||
|
||||
/// TRACES: FR-DEV-3f
|
||||
/// The stock's tables this operation holds, for a mask layer's copy of it.
|
||||
///
|
||||
/// The other half of [`Self::set_film_tables`]. A layer holds offsets,
|
||||
/// never a stock of its own — the photograph is made on one film — so the
|
||||
/// composer hands the global operation's tables to the layer's combined
|
||||
/// copy through this pair.
|
||||
fn film_tables(&self) -> Option<&crate::ops::film_sim::FilmTables> {
|
||||
None
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3 | FR-DEV-3f
|
||||
/// Whether a mask layer's version of this operation is blended as
|
||||
/// *settings* rather than as a result.
|
||||
///
|
||||
/// Default `false`: each layer's version runs on the same input as the
|
||||
/// global one and the results are blended by weight, which is right for an
|
||||
/// adjustment. An operation that answers `true` has every uniform linear
|
||||
/// in what it controls, so the composer can blend the uniforms instead —
|
||||
/// the weighted average of every overlapping layer's settings, the global
|
||||
/// setting taking whatever weight the layers leave — and run the fragment
|
||||
/// once. See `local_settings_block`.
|
||||
fn blends_settings(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3e | FR-DEV-3f
|
||||
/// Whether this operation *is* the rendering, rather than an adjustment to
|
||||
/// one.
|
||||
@@ -884,10 +910,11 @@ fn compose_inner(
|
||||
let prefix = sanitise(id);
|
||||
|
||||
let mut fragment = String::new();
|
||||
let mut op_uniforms = Vec::new();
|
||||
if op.is_active() {
|
||||
// Each op's uniforms are prefixed, so two operations may both
|
||||
// declare a field called `amount` without colliding.
|
||||
let op_uniforms = op.uniforms();
|
||||
op_uniforms = op.uniforms();
|
||||
if !op_uniforms.is_empty() {
|
||||
let _ = writeln!(uniform_fields, " // {id}");
|
||||
}
|
||||
@@ -901,16 +928,25 @@ fn compose_inner(
|
||||
helpers.push(*h);
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite bare uniform names to their prefixed struct fields, so a
|
||||
// fragment is written without knowing about any other operation.
|
||||
fragment = op.wgsl_body();
|
||||
for u in &op_uniforms {
|
||||
fragment = rewrite_uniform(&fragment, u.name, &format!("u.{prefix}_{}", u.name));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = writeln!(body, "\n // ---- {id} ----");
|
||||
if op.blends_settings() && !local.is_empty() {
|
||||
// TRACES: FR-DEV-3f
|
||||
body.push_str(&local_settings_block(
|
||||
&fragment,
|
||||
&prefix,
|
||||
&op_uniforms,
|
||||
&local,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
// Rewrite bare uniform names to their prefixed struct fields, so a
|
||||
// fragment is written without knowing about any other operation.
|
||||
for u in &op_uniforms {
|
||||
fragment = rewrite_uniform(&fragment, u.name, &format!("u.{prefix}_{}", u.name));
|
||||
}
|
||||
body.push_str(&local_block(&fragment, &local));
|
||||
}
|
||||
|
||||
@@ -1283,6 +1319,65 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3f
|
||||
/// One operation's block when its layers are blended as *settings*: every
|
||||
/// uniform the weighted average of the global value and each overlapping
|
||||
/// layer's, and then the fragment once.
|
||||
///
|
||||
/// The weights: each layer's mask weight `w_i`, and the global setting
|
||||
/// whatever the layers leave, `w_g = max(0, 1 − Σ w_i)`. So
|
||||
///
|
||||
/// ```text
|
||||
/// p = (w_g·p_g + Σ w_i·p_i) / (w_g + Σ w_i)
|
||||
/// ```
|
||||
///
|
||||
/// — one layer at weight `w` is `(1 − w)·p_g + w·p_1`, exactly the blend a
|
||||
/// layer has always had, and three layers overlapping at full weight are the
|
||||
/// plain mean of their three settings rather than one piled on another. A
|
||||
/// layer's `p_i` is its combined setting (the global one plus its offset), so
|
||||
/// the average is of what each layer *asks for*.
|
||||
///
|
||||
/// Only layers that move this operation take part. One that left it alone is
|
||||
/// not voting for the global setting; it is not voting.
|
||||
///
|
||||
/// `fragment` is the operation's body with bare uniform names, as
|
||||
/// `wgsl_body` wrote it: they are rewritten here to the blended values.
|
||||
fn local_settings_block(
|
||||
fragment: &str,
|
||||
prefix: &str,
|
||||
uniforms: &[Uniform],
|
||||
local: &[&crate::mask::LocalOp],
|
||||
) -> String {
|
||||
let mut out = String::new();
|
||||
let _ = writeln!(out, " {{");
|
||||
let weights: Vec<String> = local.iter().map(|l| format!("mask_w{}", l.slot)).collect();
|
||||
let _ = writeln!(out, " let set_w = {};", weights.join(" + "));
|
||||
let _ = writeln!(out, " let set_g = max(1.0 - set_w, 0.0);");
|
||||
// Never zero: the global weight is one wherever no layer reaches.
|
||||
let _ = writeln!(out, " let set_n = max(set_g + set_w, 1e-6);");
|
||||
let mut body = fragment.to_string();
|
||||
for u in uniforms {
|
||||
let mut sum = format!("set_g * u.{prefix}_{}", u.name);
|
||||
for l in local {
|
||||
let _ = write!(
|
||||
sum,
|
||||
" + mask_w{slot} * u.mask{slot}_{prefix}_{name}",
|
||||
slot = l.slot,
|
||||
name = u.name
|
||||
);
|
||||
}
|
||||
let _ = writeln!(out, " let set_{} = ({sum}) / set_n;", u.name);
|
||||
body = rewrite_uniform(&body, u.name, &format!("set_{}", u.name));
|
||||
}
|
||||
let _ = writeln!(out, " {{");
|
||||
for line in body.lines() {
|
||||
let _ = writeln!(out, " {line}");
|
||||
}
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out, " }}");
|
||||
out
|
||||
}
|
||||
|
||||
/// One operation's block: its global fragment, and each layer's version of it
|
||||
/// blended in by that layer's weight.
|
||||
///
|
||||
@@ -2158,7 +2253,9 @@ mod tests {
|
||||
lut: vec![[0.5, 0.5, 0.5]; 32 * 32 * 32],
|
||||
density_max: 3.0,
|
||||
lut_size: 32,
|
||||
grain_particles: [0.0; 3],
|
||||
grain_particles: [[0.0; 3]; crate::ops::film_sim::FORMAT_COUNT],
|
||||
push_stations: vec![0.0],
|
||||
paper: None,
|
||||
grain_density_max: [3.0; 3],
|
||||
grain_uniformity: 0.97,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user