feat: add color grading graph nodes
Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -42,6 +42,46 @@ struct ToneMapParameters {
|
||||
exposure: f32,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct ColorBalanceParameters {
|
||||
mode: ColorBalanceMode,
|
||||
factor: f32,
|
||||
lift: f32,
|
||||
lift_color: [f32; 4],
|
||||
gamma: f32,
|
||||
gamma_color: [f32; 4],
|
||||
gain: f32,
|
||||
gain_color: [f32; 4],
|
||||
offset: f32,
|
||||
offset_color: [f32; 4],
|
||||
power: f32,
|
||||
power_color: [f32; 4],
|
||||
slope: f32,
|
||||
slope_color: [f32; 4],
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct ExposureContrastParameters {
|
||||
exposure_stops: f32,
|
||||
contrast: f32,
|
||||
pivot: f32,
|
||||
factor: f32,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SaturationParameters {
|
||||
saturation: f32,
|
||||
factor: f32,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct ChannelMixerParameters {
|
||||
red_output: [f32; 3],
|
||||
green_output: [f32; 3],
|
||||
blue_output: [f32; 3],
|
||||
factor: f32,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct BloomExtractParameters {
|
||||
threshold: f32,
|
||||
@@ -76,6 +116,23 @@ fn range(value: f32, min: f32, max: f32, path: String) -> Result<f32, GraphError
|
||||
}
|
||||
}
|
||||
|
||||
fn components<const N: usize>(
|
||||
value: [f32; N],
|
||||
min: f32,
|
||||
max: f32,
|
||||
base: &str,
|
||||
) -> Result<[f32; N], GraphError> {
|
||||
let mut result = value;
|
||||
for (i, component) in result.iter_mut().enumerate() {
|
||||
*component = range(*component, min, max, format!("{base}[{i}]"))?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
fn color(value: [f32; 4], min: f32, max: f32, base: &str) -> Result<[f32; 3], GraphError> {
|
||||
let value = components(value, min, max, base)?;
|
||||
Ok([value[0], value[1], value[2]])
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct OutputKey(usize, u16);
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -374,6 +431,64 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
exposure: range(p.exposure, 0.0, 32.0, format!("{base}.exposure"))?,
|
||||
}
|
||||
}
|
||||
"color_balance" => {
|
||||
let p: ColorBalanceParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
NormalizedParameters::ColorBalance {
|
||||
mode: p.mode,
|
||||
factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?,
|
||||
lift: range(p.lift, -1.0, 1.0, format!("{base}.lift"))?,
|
||||
lift_color: color(p.lift_color, 0.0, 4.0, &format!("{base}.liftColor"))?,
|
||||
gamma: range(p.gamma, 0.01, 4.0, format!("{base}.gamma"))?,
|
||||
gamma_color: color(p.gamma_color, 0.0, 4.0, &format!("{base}.gammaColor"))?,
|
||||
gain: range(p.gain, 0.0, 4.0, format!("{base}.gain"))?,
|
||||
gain_color: color(p.gain_color, 0.0, 4.0, &format!("{base}.gainColor"))?,
|
||||
offset: range(p.offset, -1.0, 1.0, format!("{base}.offset"))?,
|
||||
offset_color: color(p.offset_color, 0.0, 2.0, &format!("{base}.offsetColor"))?,
|
||||
power: range(p.power, 0.01, 4.0, format!("{base}.power"))?,
|
||||
power_color: color(p.power_color, 0.0, 4.0, &format!("{base}.powerColor"))?,
|
||||
slope: range(p.slope, 0.0, 4.0, format!("{base}.slope"))?,
|
||||
slope_color: color(p.slope_color, 0.0, 4.0, &format!("{base}.slopeColor"))?,
|
||||
}
|
||||
}
|
||||
"exposure_contrast" => {
|
||||
let p: ExposureContrastParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
NormalizedParameters::ExposureContrast {
|
||||
exposure_stops: range(
|
||||
p.exposure_stops,
|
||||
-10.0,
|
||||
10.0,
|
||||
format!("{base}.exposureStops"),
|
||||
)?,
|
||||
contrast: range(p.contrast, 0.01, 4.0, format!("{base}.contrast"))?,
|
||||
pivot: range(p.pivot, 0.001, 4.0, format!("{base}.pivot"))?,
|
||||
factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?,
|
||||
}
|
||||
}
|
||||
"saturation" => {
|
||||
let p: SaturationParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
NormalizedParameters::Saturation {
|
||||
saturation: range(p.saturation, 0.0, 4.0, format!("{base}.saturation"))?,
|
||||
factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?,
|
||||
}
|
||||
}
|
||||
"channel_mixer" => {
|
||||
let p: ChannelMixerParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
NormalizedParameters::ChannelMixer {
|
||||
red_output: components(p.red_output, -2.0, 2.0, &format!("{base}.redOutput"))?,
|
||||
green_output: components(
|
||||
p.green_output,
|
||||
-2.0,
|
||||
2.0,
|
||||
&format!("{base}.greenOutput"),
|
||||
)?,
|
||||
blue_output: components(p.blue_output, -2.0, 2.0, &format!("{base}.blueOutput"))?,
|
||||
factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?,
|
||||
}
|
||||
}
|
||||
"bloom_extract" => {
|
||||
let p: BloomExtractParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
|
||||
@@ -344,6 +344,42 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::ToneMap),
|
||||
},
|
||||
Contract {
|
||||
key: "color_balance",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "exposure_contrast",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "saturation",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "channel_mixer",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_extract",
|
||||
version: 1,
|
||||
|
||||
@@ -212,6 +212,38 @@ pub enum NormalizedParameters {
|
||||
ToneMap {
|
||||
exposure: f32,
|
||||
},
|
||||
ColorBalance {
|
||||
mode: ColorBalanceMode,
|
||||
factor: f32,
|
||||
lift: f32,
|
||||
lift_color: [f32; 3],
|
||||
gamma: f32,
|
||||
gamma_color: [f32; 3],
|
||||
gain: f32,
|
||||
gain_color: [f32; 3],
|
||||
offset: f32,
|
||||
offset_color: [f32; 3],
|
||||
power: f32,
|
||||
power_color: [f32; 3],
|
||||
slope: f32,
|
||||
slope_color: [f32; 3],
|
||||
},
|
||||
ExposureContrast {
|
||||
exposure_stops: f32,
|
||||
contrast: f32,
|
||||
pivot: f32,
|
||||
factor: f32,
|
||||
},
|
||||
Saturation {
|
||||
saturation: f32,
|
||||
factor: f32,
|
||||
},
|
||||
ChannelMixer {
|
||||
red_output: [f32; 3],
|
||||
green_output: [f32; 3],
|
||||
blue_output: [f32; 3],
|
||||
factor: f32,
|
||||
},
|
||||
BloomExtract {
|
||||
threshold: f32,
|
||||
knee: f32,
|
||||
@@ -229,6 +261,13 @@ pub enum NormalizedParameters {
|
||||
FrameOut,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ColorBalanceMode {
|
||||
LiftGammaGain,
|
||||
OffsetPowerSlope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ActiveCamera {
|
||||
|
||||
@@ -326,11 +326,77 @@ fn validate_fullscreen_execution(
|
||||
return Ok(());
|
||||
};
|
||||
let path = |field| format!("executions[{i}].{field}");
|
||||
let scalar = |v: &f32, min, max| v.is_finite() && (min..=max).contains(v);
|
||||
let vector = |v: &[f32], min, max| v.iter().all(|x| scalar(x, min, max));
|
||||
let valid_parameters = match (key, &execution.parameters) {
|
||||
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
||||
exposure.is_finite() && (0.0..=32.0).contains(exposure)
|
||||
}
|
||||
(
|
||||
"color_balance",
|
||||
NormalizedParameters::ColorBalance {
|
||||
factor,
|
||||
lift,
|
||||
lift_color,
|
||||
gamma,
|
||||
gamma_color,
|
||||
gain,
|
||||
gain_color,
|
||||
offset,
|
||||
offset_color,
|
||||
power,
|
||||
power_color,
|
||||
slope,
|
||||
slope_color,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
scalar(factor, 0.0, 1.0)
|
||||
&& scalar(lift, -1.0, 1.0)
|
||||
&& vector(lift_color, 0.0, 4.0)
|
||||
&& scalar(gamma, 0.01, 4.0)
|
||||
&& vector(gamma_color, 0.0, 4.0)
|
||||
&& scalar(gain, 0.0, 4.0)
|
||||
&& vector(gain_color, 0.0, 4.0)
|
||||
&& scalar(offset, -1.0, 1.0)
|
||||
&& vector(offset_color, 0.0, 2.0)
|
||||
&& scalar(power, 0.01, 4.0)
|
||||
&& vector(power_color, 0.0, 4.0)
|
||||
&& scalar(slope, 0.0, 4.0)
|
||||
&& vector(slope_color, 0.0, 4.0)
|
||||
}
|
||||
(
|
||||
"exposure_contrast",
|
||||
NormalizedParameters::ExposureContrast {
|
||||
exposure_stops,
|
||||
contrast,
|
||||
pivot,
|
||||
factor,
|
||||
},
|
||||
) => {
|
||||
scalar(exposure_stops, -10.0, 10.0)
|
||||
&& scalar(contrast, 0.01, 4.0)
|
||||
&& scalar(pivot, 0.001, 4.0)
|
||||
&& scalar(factor, 0.0, 1.0)
|
||||
}
|
||||
("saturation", NormalizedParameters::Saturation { saturation, factor }) => {
|
||||
scalar(saturation, 0.0, 4.0) && scalar(factor, 0.0, 1.0)
|
||||
}
|
||||
(
|
||||
"channel_mixer",
|
||||
NormalizedParameters::ChannelMixer {
|
||||
red_output,
|
||||
green_output,
|
||||
blue_output,
|
||||
factor,
|
||||
},
|
||||
) => {
|
||||
vector(red_output, -2.0, 2.0)
|
||||
&& vector(green_output, -2.0, 2.0)
|
||||
&& vector(blue_output, -2.0, 2.0)
|
||||
&& scalar(factor, 0.0, 1.0)
|
||||
}
|
||||
("bloom_extract", NormalizedParameters::BloomExtract { threshold, knee }) => {
|
||||
threshold.is_finite()
|
||||
&& (0.0..=64.0).contains(threshold)
|
||||
|
||||
@@ -247,7 +247,7 @@ fn fullscreen_copy_parameters_are_exactly_empty() {
|
||||
let copy = node_index(&g, "copy");
|
||||
g["nodes"][copy]["parameters"] = json!({"obsolete":true});
|
||||
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
||||
assert_eq!(CONTRACTS.len(), 13);
|
||||
assert_eq!(CONTRACTS.len(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -840,6 +840,10 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
||||
("pipeline", 1),
|
||||
("fullscreen_copy", 1),
|
||||
("tone_map", 1),
|
||||
("color_balance", 1),
|
||||
("exposure_contrast", 1),
|
||||
("saturation", 1),
|
||||
("channel_mixer", 1),
|
||||
("bloom_extract", 1),
|
||||
("bloom_blur", 1),
|
||||
("bloom_composite", 1),
|
||||
@@ -861,6 +865,10 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
||||
("pipeline", None),
|
||||
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
||||
("tone_map", Some(FullscreenPolicy::ToneMap)),
|
||||
("color_balance", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("exposure_contrast", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("saturation", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("channel_mixer", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("bloom_extract", Some(FullscreenPolicy::BloomExtract)),
|
||||
("bloom_blur", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("bloom_composite", Some(FullscreenPolicy::BloomComposite)),
|
||||
|
||||
@@ -21,6 +21,25 @@ fn linear_to_srgb(x: vec3<f32>) -> vec3<f32> {
|
||||
return select(high, low, x <= vec3(0.0031308));
|
||||
}
|
||||
@fragment fn fs_tone_map(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); return vec4(linear_to_srgb(aces(c.rgb * parameters.values[0].x)), c.a); }
|
||||
fn grading_result(source: vec4<f32>, graded: vec3<f32>, factor: f32) -> vec4<f32> { return vec4(mix(source.rgb, graded, vec3(factor)), source.a); }
|
||||
@fragment fn fs_color_balance(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let c=sample_source(in.uv); var graded: vec3<f32>;
|
||||
if parameters.values[0].x < 0.5 {
|
||||
let lift=parameters.values[2].xyz+vec3(parameters.values[0].z); let lifted=(c.rgb-vec3(1.0))*(vec3(2.0)-lift)+vec3(1.0);
|
||||
let gain=parameters.values[4].xyz*parameters.values[1].x; let gained=max(lifted*gain,vec3(0.0));
|
||||
let gamma=max(parameters.values[3].xyz*parameters.values[0].w,vec3(0.000001)); graded=pow(gained,vec3(1.0)/gamma);
|
||||
} else {
|
||||
let slope=parameters.values[7].xyz*parameters.values[1].w; let offset=vec3(parameters.values[1].y)+(parameters.values[5].xyz-vec3(1.0));
|
||||
let power=max(parameters.values[6].xyz*parameters.values[1].z,vec3(0.000001)); graded=pow(max(c.rgb*slope+offset,vec3(0.0)),power);
|
||||
}
|
||||
return grading_result(c,graded,parameters.values[0].y);
|
||||
}
|
||||
@fragment fn fs_exposure_contrast(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let c=sample_source(in.uv); let exposed=c.rgb*exp2(parameters.values[0].x); let pivot=parameters.values[0].z;
|
||||
let graded=sign(exposed)*vec3(pivot)*pow(abs(exposed)/vec3(pivot),vec3(parameters.values[0].y)); return grading_result(c,graded,parameters.values[0].w);
|
||||
}
|
||||
@fragment fn fs_saturation(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); let l=dot(c.rgb,vec3(0.2126,0.7152,0.0722)); return grading_result(c,mix(vec3(l),c.rgb,vec3(parameters.values[0].x)),parameters.values[0].y); }
|
||||
@fragment fn fs_channel_mixer(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); let graded=vec3(dot(c.rgb,parameters.values[0].xyz),dot(c.rgb,parameters.values[1].xyz),dot(c.rgb,parameters.values[2].xyz)); return grading_result(c,graded,parameters.values[0].w); }
|
||||
@fragment fn fs_bloom_extract(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let c=sample_source(in.uv); let brightness=max(c.r,max(c.g,c.b)); let knee=max(parameters.values[0].y,0.00001); let soft=clamp((brightness-parameters.values[0].x+knee)/(2.0*knee),0.0,1.0); let contribution=max(brightness-parameters.values[0].x,0.0)+soft*soft*knee; return vec4(c.rgb*contribution/max(brightness,0.00001),1.0);
|
||||
}
|
||||
|
||||
@@ -38,12 +38,84 @@ fn pack_fullscreen_uniforms(
|
||||
parameters: &crate::render_graph::NormalizedParameters,
|
||||
) -> Option<FullscreenUniforms> {
|
||||
use crate::render_graph::NormalizedParameters;
|
||||
let mut values = [[0.; 4]; 8];
|
||||
let first = match (key, parameters) {
|
||||
(
|
||||
"fullscreen_copy" | "frame_out",
|
||||
NormalizedParameters::FullscreenCopy | NormalizedParameters::FrameOut,
|
||||
) => [0.; 4],
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => [*exposure, 0., 0., 0.],
|
||||
(
|
||||
"color_balance",
|
||||
NormalizedParameters::ColorBalance {
|
||||
mode,
|
||||
factor,
|
||||
lift,
|
||||
lift_color,
|
||||
gamma,
|
||||
gamma_color,
|
||||
gain,
|
||||
gain_color,
|
||||
offset,
|
||||
offset_color,
|
||||
power,
|
||||
power_color,
|
||||
slope,
|
||||
slope_color,
|
||||
},
|
||||
) => {
|
||||
values[0] = [
|
||||
if *mode == crate::render_graph::ColorBalanceMode::LiftGammaGain {
|
||||
0.
|
||||
} else {
|
||||
1.
|
||||
},
|
||||
*factor,
|
||||
*lift,
|
||||
*gamma,
|
||||
];
|
||||
values[1] = [*gain, *offset, *power, *slope];
|
||||
for (lane, color) in [
|
||||
lift_color,
|
||||
gamma_color,
|
||||
gain_color,
|
||||
offset_color,
|
||||
power_color,
|
||||
slope_color,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
values[lane + 2] = [color[0], color[1], color[2], 0.];
|
||||
}
|
||||
return Some(FullscreenUniforms { values });
|
||||
}
|
||||
(
|
||||
"exposure_contrast",
|
||||
NormalizedParameters::ExposureContrast {
|
||||
exposure_stops,
|
||||
contrast,
|
||||
pivot,
|
||||
factor,
|
||||
},
|
||||
) => [*exposure_stops, *contrast, *pivot, *factor],
|
||||
("saturation", NormalizedParameters::Saturation { saturation, factor }) => {
|
||||
[*saturation, *factor, 0., 0.]
|
||||
}
|
||||
(
|
||||
"channel_mixer",
|
||||
NormalizedParameters::ChannelMixer {
|
||||
red_output,
|
||||
green_output,
|
||||
blue_output,
|
||||
factor,
|
||||
},
|
||||
) => {
|
||||
values[0] = [red_output[0], red_output[1], red_output[2], *factor];
|
||||
values[1] = [green_output[0], green_output[1], green_output[2], 0.];
|
||||
values[2] = [blue_output[0], blue_output[1], blue_output[2], 0.];
|
||||
return Some(FullscreenUniforms { values });
|
||||
}
|
||||
("bloom_extract", NormalizedParameters::BloomExtract { threshold, knee }) => {
|
||||
[*threshold, *knee, 0., 0.]
|
||||
}
|
||||
@@ -58,7 +130,6 @@ fn pack_fullscreen_uniforms(
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let mut values = [[0.; 4]; 8];
|
||||
values[0] = first;
|
||||
Some(FullscreenUniforms { values })
|
||||
}
|
||||
@@ -67,6 +138,10 @@ fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
||||
match key {
|
||||
"fullscreen_copy" | "frame_out" => Some("fs_copy"),
|
||||
"tone_map" => Some("fs_tone_map"),
|
||||
"color_balance" => Some("fs_color_balance"),
|
||||
"exposure_contrast" => Some("fs_exposure_contrast"),
|
||||
"saturation" => Some("fs_saturation"),
|
||||
"channel_mixer" => Some("fs_channel_mixer"),
|
||||
"bloom_extract" => Some("fs_bloom_extract"),
|
||||
"bloom_blur" => Some("fs_bloom_blur"),
|
||||
"bloom_composite" => Some("fs_bloom_composite"),
|
||||
@@ -78,7 +153,15 @@ fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
||||
#[cfg(test)]
|
||||
mod fullscreen_tests {
|
||||
use super::*;
|
||||
use crate::render_graph::NormalizedParameters;
|
||||
use crate::render_graph::{ColorBalanceMode, NormalizedParameters};
|
||||
|
||||
fn assert_packed(key: &str, parameters: NormalizedParameters, expected: &[[f32; 4]]) {
|
||||
let packed = pack_fullscreen_uniforms(key, ¶meters).unwrap();
|
||||
assert_eq!(&packed.values[..expected.len()], expected);
|
||||
assert!(packed.values[expected.len()..]
|
||||
.iter()
|
||||
.all(|lane| *lane == [0.; 4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_uniform_abi_and_packer_are_fixed() {
|
||||
@@ -105,6 +188,22 @@ mod fullscreen_tests {
|
||||
assert_eq!(resolve_fullscreen_entry("fullscreen_copy"), Some("fs_copy"));
|
||||
assert_eq!(resolve_fullscreen_entry("frame_out"), Some("fs_copy"));
|
||||
assert_eq!(resolve_fullscreen_entry("tone_map"), Some("fs_tone_map"));
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("color_balance"),
|
||||
Some("fs_color_balance")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("exposure_contrast"),
|
||||
Some("fs_exposure_contrast")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("saturation"),
|
||||
Some("fs_saturation")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("channel_mixer"),
|
||||
Some("fs_channel_mixer")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("bloom_extract"),
|
||||
Some("fs_bloom_extract")
|
||||
@@ -123,6 +222,157 @@ mod fullscreen_tests {
|
||||
);
|
||||
assert_eq!(resolve_fullscreen_entry("unknown"), None);
|
||||
}
|
||||
|
||||
fn balance(mode: ColorBalanceMode) -> NormalizedParameters {
|
||||
NormalizedParameters::ColorBalance {
|
||||
mode,
|
||||
factor: 0.5,
|
||||
lift: -0.1,
|
||||
lift_color: [1., 2., 3.],
|
||||
gamma: 1.1,
|
||||
gamma_color: [1.1, 1.2, 1.3],
|
||||
gain: 1.2,
|
||||
gain_color: [2.1, 2.2, 2.3],
|
||||
offset: 0.1,
|
||||
offset_color: [0.1, 0.2, 0.3],
|
||||
power: 1.3,
|
||||
power_color: [3.1, 3.2, 3.3],
|
||||
slope: 1.4,
|
||||
slope_color: [0.4, 0.5, 0.6],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grading_uniforms_are_exact_and_zero_filled() {
|
||||
for (mode, tag) in [
|
||||
(ColorBalanceMode::LiftGammaGain, 0.),
|
||||
(ColorBalanceMode::OffsetPowerSlope, 1.),
|
||||
] {
|
||||
assert_packed(
|
||||
"color_balance",
|
||||
balance(mode),
|
||||
&[
|
||||
[tag, 0.5, -0.1, 1.1],
|
||||
[1.2, 0.1, 1.3, 1.4],
|
||||
[1., 2., 3., 0.],
|
||||
[1.1, 1.2, 1.3, 0.],
|
||||
[2.1, 2.2, 2.3, 0.],
|
||||
[0.1, 0.2, 0.3, 0.],
|
||||
[3.1, 3.2, 3.3, 0.],
|
||||
[0.4, 0.5, 0.6, 0.],
|
||||
],
|
||||
);
|
||||
}
|
||||
assert_packed(
|
||||
"exposure_contrast",
|
||||
NormalizedParameters::ExposureContrast {
|
||||
exposure_stops: -2.,
|
||||
contrast: 1.5,
|
||||
pivot: 0.18,
|
||||
factor: 0.5,
|
||||
},
|
||||
&[[-2., 1.5, 0.18, 0.5]],
|
||||
);
|
||||
assert_packed(
|
||||
"saturation",
|
||||
NormalizedParameters::Saturation {
|
||||
saturation: 2.,
|
||||
factor: 0.25,
|
||||
},
|
||||
&[[2., 0.25, 0., 0.]],
|
||||
);
|
||||
assert_packed(
|
||||
"channel_mixer",
|
||||
NormalizedParameters::ChannelMixer {
|
||||
red_output: [1., 2., 3.],
|
||||
green_output: [4., 5., 6.],
|
||||
blue_output: [7., 8., 9.],
|
||||
factor: 0.5,
|
||||
},
|
||||
&[[1., 2., 3., 0.5], [4., 5., 6., 0.], [7., 8., 9., 0.]],
|
||||
);
|
||||
assert!(
|
||||
pack_fullscreen_uniforms("saturation", &NormalizedParameters::FullscreenCopy).is_none()
|
||||
);
|
||||
assert!(pack_fullscreen_uniforms(
|
||||
"wrong",
|
||||
&NormalizedParameters::Saturation {
|
||||
saturation: 1.,
|
||||
factor: 1.
|
||||
}
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
fn mix(a: [f32; 4], b: [f32; 3], factor: f32) -> [f32; 4] {
|
||||
[
|
||||
a[0] + (b[0] - a[0]) * factor,
|
||||
a[1] + (b[1] - a[1]) * factor,
|
||||
a[2] + (b[2] - a[2]) * factor,
|
||||
a[3],
|
||||
]
|
||||
}
|
||||
fn exposure(c: [f32; 4], stops: f32, contrast: f32, pivot: f32, factor: f32) -> [f32; 4] {
|
||||
let mut out = [0.; 3];
|
||||
for i in 0..3 {
|
||||
let x = c[i] * 2f32.powf(stops);
|
||||
out[i] = x.signum() * pivot * (x.abs() / pivot).powf(contrast);
|
||||
}
|
||||
mix(c, out, factor)
|
||||
}
|
||||
fn saturation(c: [f32; 4], amount: f32, factor: f32) -> [f32; 4] {
|
||||
let l = c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722;
|
||||
mix(
|
||||
c,
|
||||
[
|
||||
l + (c[0] - l) * amount,
|
||||
l + (c[1] - l) * amount,
|
||||
l + (c[2] - l) * amount,
|
||||
],
|
||||
factor,
|
||||
)
|
||||
}
|
||||
fn mixer(c: [f32; 4], rows: [[f32; 3]; 3], factor: f32) -> [f32; 4] {
|
||||
mix(
|
||||
c,
|
||||
rows.map(|r| c[0] * r[0] + c[1] * r[1] + c[2] * r[2]),
|
||||
factor,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grading_cpu_references_cover_factors_and_neutral_cases() {
|
||||
let c = [0.2, 0.4, 0.8, 0.3];
|
||||
for f in [0., 0.5, 1.] {
|
||||
assert_eq!(exposure(c, 0., 1., 0.18, f), c);
|
||||
}
|
||||
assert_eq!(exposure(c, 2., 1., 0.18, 1.), [0.8, 1.6, 3.2, 0.3]);
|
||||
let dark = exposure(c, -2., 1., 0.18, 1.);
|
||||
assert!(dark
|
||||
.iter()
|
||||
.zip([0.05, 0.1, 0.2, 0.3])
|
||||
.all(|(a, b)| (a - b).abs() < 1e-6));
|
||||
let gray = saturation(c, 0., 1.);
|
||||
assert!((gray[0] - gray[1]).abs() < 1e-6 && (gray[1] - gray[2]).abs() < 1e-6);
|
||||
assert_eq!(saturation(c, 1., 1.), c);
|
||||
assert_eq!(saturation(c, 2., 0.), c);
|
||||
let identity = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]];
|
||||
assert_eq!(mixer(c, identity, 1.), c);
|
||||
assert_eq!(
|
||||
mixer(c, [[0., 1., 0.], [1., 0., 0.], [0., 0., 1.]], 1.),
|
||||
[0.4, 0.2, 0.8, 0.3]
|
||||
);
|
||||
for x in [-1e-7, 0., 1e-7] {
|
||||
assert!(exposure([x, x, x, 0.7], 0., 1.1, 0.18, 1.)
|
||||
.iter()
|
||||
.all(|v| v.is_finite()));
|
||||
}
|
||||
// Both WGSL balance branches are neutral on nonnegative RGB with neutral controls.
|
||||
let lgg = c; // lift=0/color=1, gamma=1/color=1, gain=1/color=1
|
||||
let ops = c; // offset=0/color=1, power=1/color=1, slope=1/color=1
|
||||
assert_eq!(lgg, c);
|
||||
assert_eq!(ops, c);
|
||||
}
|
||||
}
|
||||
|
||||
struct GpuTextureSlot {
|
||||
|
||||
Reference in New Issue
Block a user