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,
|
exposure: f32,
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[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)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct BloomExtractParameters {
|
struct BloomExtractParameters {
|
||||||
threshold: f32,
|
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)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct OutputKey(usize, u16);
|
struct OutputKey(usize, u16);
|
||||||
#[derive(Clone, Copy)]
|
#[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"))?,
|
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" => {
|
"bloom_extract" => {
|
||||||
let p: BloomExtractParameters =
|
let p: BloomExtractParameters =
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
|
|||||||
@@ -344,6 +344,42 @@ pub static CONTRACTS: &[Contract] = &[
|
|||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
fullscreen_policy: Some(FullscreenPolicy::ToneMap),
|
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 {
|
Contract {
|
||||||
key: "bloom_extract",
|
key: "bloom_extract",
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|||||||
@@ -212,6 +212,38 @@ pub enum NormalizedParameters {
|
|||||||
ToneMap {
|
ToneMap {
|
||||||
exposure: f32,
|
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 {
|
BloomExtract {
|
||||||
threshold: f32,
|
threshold: f32,
|
||||||
knee: f32,
|
knee: f32,
|
||||||
@@ -229,6 +261,13 @@ pub enum NormalizedParameters {
|
|||||||
FrameOut,
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ActiveCamera {
|
pub enum ActiveCamera {
|
||||||
|
|||||||
@@ -326,11 +326,77 @@ fn validate_fullscreen_execution(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let path = |field| format!("executions[{i}].{field}");
|
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) {
|
let valid_parameters = match (key, &execution.parameters) {
|
||||||
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
||||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
||||||
exposure.is_finite() && (0.0..=32.0).contains(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 }) => {
|
("bloom_extract", NormalizedParameters::BloomExtract { threshold, knee }) => {
|
||||||
threshold.is_finite()
|
threshold.is_finite()
|
||||||
&& (0.0..=64.0).contains(threshold)
|
&& (0.0..=64.0).contains(threshold)
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ fn fullscreen_copy_parameters_are_exactly_empty() {
|
|||||||
let copy = node_index(&g, "copy");
|
let copy = node_index(&g, "copy");
|
||||||
g["nodes"][copy]["parameters"] = json!({"obsolete":true});
|
g["nodes"][copy]["parameters"] = json!({"obsolete":true});
|
||||||
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
||||||
assert_eq!(CONTRACTS.len(), 13);
|
assert_eq!(CONTRACTS.len(), 17);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -840,6 +840,10 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
|||||||
("pipeline", 1),
|
("pipeline", 1),
|
||||||
("fullscreen_copy", 1),
|
("fullscreen_copy", 1),
|
||||||
("tone_map", 1),
|
("tone_map", 1),
|
||||||
|
("color_balance", 1),
|
||||||
|
("exposure_contrast", 1),
|
||||||
|
("saturation", 1),
|
||||||
|
("channel_mixer", 1),
|
||||||
("bloom_extract", 1),
|
("bloom_extract", 1),
|
||||||
("bloom_blur", 1),
|
("bloom_blur", 1),
|
||||||
("bloom_composite", 1),
|
("bloom_composite", 1),
|
||||||
@@ -861,6 +865,10 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
|||||||
("pipeline", None),
|
("pipeline", None),
|
||||||
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
||||||
("tone_map", Some(FullscreenPolicy::ToneMap)),
|
("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_extract", Some(FullscreenPolicy::BloomExtract)),
|
||||||
("bloom_blur", Some(FullscreenPolicy::HdrSameExtent)),
|
("bloom_blur", Some(FullscreenPolicy::HdrSameExtent)),
|
||||||
("bloom_composite", Some(FullscreenPolicy::BloomComposite)),
|
("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));
|
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); }
|
@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> {
|
@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);
|
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,
|
parameters: &crate::render_graph::NormalizedParameters,
|
||||||
) -> Option<FullscreenUniforms> {
|
) -> Option<FullscreenUniforms> {
|
||||||
use crate::render_graph::NormalizedParameters;
|
use crate::render_graph::NormalizedParameters;
|
||||||
|
let mut values = [[0.; 4]; 8];
|
||||||
let first = match (key, parameters) {
|
let first = match (key, parameters) {
|
||||||
(
|
(
|
||||||
"fullscreen_copy" | "frame_out",
|
"fullscreen_copy" | "frame_out",
|
||||||
NormalizedParameters::FullscreenCopy | NormalizedParameters::FrameOut,
|
NormalizedParameters::FullscreenCopy | NormalizedParameters::FrameOut,
|
||||||
) => [0.; 4],
|
) => [0.; 4],
|
||||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => [*exposure, 0., 0., 0.],
|
("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 }) => {
|
("bloom_extract", NormalizedParameters::BloomExtract { threshold, knee }) => {
|
||||||
[*threshold, *knee, 0., 0.]
|
[*threshold, *knee, 0., 0.]
|
||||||
}
|
}
|
||||||
@@ -58,7 +130,6 @@ fn pack_fullscreen_uniforms(
|
|||||||
}
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let mut values = [[0.; 4]; 8];
|
|
||||||
values[0] = first;
|
values[0] = first;
|
||||||
Some(FullscreenUniforms { values })
|
Some(FullscreenUniforms { values })
|
||||||
}
|
}
|
||||||
@@ -67,6 +138,10 @@ fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
|||||||
match key {
|
match key {
|
||||||
"fullscreen_copy" | "frame_out" => Some("fs_copy"),
|
"fullscreen_copy" | "frame_out" => Some("fs_copy"),
|
||||||
"tone_map" => Some("fs_tone_map"),
|
"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_extract" => Some("fs_bloom_extract"),
|
||||||
"bloom_blur" => Some("fs_bloom_blur"),
|
"bloom_blur" => Some("fs_bloom_blur"),
|
||||||
"bloom_composite" => Some("fs_bloom_composite"),
|
"bloom_composite" => Some("fs_bloom_composite"),
|
||||||
@@ -78,7 +153,15 @@ fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod fullscreen_tests {
|
mod fullscreen_tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn fullscreen_uniform_abi_and_packer_are_fixed() {
|
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("fullscreen_copy"), Some("fs_copy"));
|
||||||
assert_eq!(resolve_fullscreen_entry("frame_out"), 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("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!(
|
assert_eq!(
|
||||||
resolve_fullscreen_entry("bloom_extract"),
|
resolve_fullscreen_entry("bloom_extract"),
|
||||||
Some("fs_bloom_extract")
|
Some("fs_bloom_extract")
|
||||||
@@ -123,6 +222,157 @@ mod fullscreen_tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(resolve_fullscreen_entry("unknown"), None);
|
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 {
|
struct GpuTextureSlot {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@
|
|||||||
<option value="hdr">HDR Fullscreen</option>
|
<option value="hdr">HDR Fullscreen</option>
|
||||||
<option value="culling">GPU frustum culling</option>
|
<option value="culling">GPU frustum culling</option>
|
||||||
<option value="tone">Tone map</option>
|
<option value="tone">Tone map</option>
|
||||||
|
<option value="grading">Grading</option>
|
||||||
<option value="edges">Edges</option>
|
<option value="edges">Edges</option>
|
||||||
<option value="bloom">Bloom</option>
|
<option value="bloom">Bloom</option>
|
||||||
<option value="combined">Combined</option>
|
<option value="combined">Combined</option>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const GRAPH_ID = "authored_gpu_culling";
|
export const GRAPH_ID = "authored_gpu_culling";
|
||||||
export const CATALOG_VERSION = 4;
|
export const CATALOG_VERSION = 5;
|
||||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||||
const i = (type, required = true, authoringType) => ({
|
const i = (type, required = true, authoringType) => ({
|
||||||
accepted: typeof type === "string" ? exact(type) : type,
|
accepted: typeof type === "string" ? exact(type) : type,
|
||||||
@@ -130,6 +130,32 @@ export const semanticCatalog = Object.freeze({
|
|||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: { exposure: 1 },
|
parameters: { exposure: 1 },
|
||||||
},
|
},
|
||||||
|
color_balance: {
|
||||||
|
version: 1,
|
||||||
|
execution: "render",
|
||||||
|
inputs: { source: i("texture"), colorTarget: i("texture") },
|
||||||
|
outputs: { color: o("texture") },
|
||||||
|
parameters: {
|
||||||
|
mode: "lift_gamma_gain", factor: 1,
|
||||||
|
lift: 0, liftColor: [1, 1, 1, 1], gamma: 1, gammaColor: [1, 1, 1, 1], gain: 1, gainColor: [1, 1, 1, 1],
|
||||||
|
offset: 0, offsetColor: [1, 1, 1, 1], power: 1, powerColor: [1, 1, 1, 1], slope: 1, slopeColor: [1, 1, 1, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exposure_contrast: {
|
||||||
|
version: 1, execution: "render",
|
||||||
|
inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") },
|
||||||
|
parameters: { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 },
|
||||||
|
},
|
||||||
|
saturation: {
|
||||||
|
version: 1, execution: "render",
|
||||||
|
inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") },
|
||||||
|
parameters: { saturation: 1, factor: 1 },
|
||||||
|
},
|
||||||
|
channel_mixer: {
|
||||||
|
version: 1, execution: "render",
|
||||||
|
inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") },
|
||||||
|
parameters: { redOutput: [1, 0, 0], greenOutput: [0, 1, 0], blueOutput: [0, 0, 1], factor: 1 },
|
||||||
|
},
|
||||||
bloom_extract: {
|
bloom_extract: {
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: "render",
|
execution: "render",
|
||||||
@@ -280,12 +306,13 @@ const boolean = (value) => ({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: tagged("boolean", value),
|
default: tagged("boolean", value),
|
||||||
});
|
});
|
||||||
const color = (value) => ({
|
const color = (value, minimum = 0, maximum = 1) => ({
|
||||||
type: "color",
|
type: "color",
|
||||||
default: tagged("color", value),
|
default: tagged("color", value),
|
||||||
minimum: 0,
|
minimum,
|
||||||
maximum: 1,
|
maximum,
|
||||||
});
|
});
|
||||||
|
const vector = (value, minimum, maximum) => ({ type: "vector", default: tagged("vector", value), minimum, maximum });
|
||||||
const json = (value) => ({ type: "json", default: tagged("json", value) });
|
const json = (value) => ({ type: "json", default: tagged("json", value) });
|
||||||
const parameterSchemas = {
|
const parameterSchemas = {
|
||||||
texture: {
|
texture: {
|
||||||
@@ -357,6 +384,14 @@ const parameterSchemas = {
|
|||||||
},
|
},
|
||||||
fullscreen_copy: {},
|
fullscreen_copy: {},
|
||||||
tone_map: { exposure: number(1, 0, 32) },
|
tone_map: { exposure: number(1, 0, 32) },
|
||||||
|
color_balance: {
|
||||||
|
mode: enumeration("lift_gamma_gain", ["lift_gamma_gain", "offset_power_slope"]), factor: number(1, 0, 1),
|
||||||
|
lift: number(0, -1, 1), liftColor: color([1, 1, 1, 1], 0, 4), gamma: number(1, 0.01, 4), gammaColor: color([1, 1, 1, 1], 0, 4), gain: number(1, 0, 4), gainColor: color([1, 1, 1, 1], 0, 4),
|
||||||
|
offset: number(0, -1, 1), offsetColor: color([1, 1, 1, 1], 0, 2), power: number(1, 0.01, 4), powerColor: color([1, 1, 1, 1], 0, 4), slope: number(1, 0, 4), slopeColor: color([1, 1, 1, 1], 0, 4),
|
||||||
|
},
|
||||||
|
exposure_contrast: { exposureStops: number(0, -10, 10), contrast: number(1, 0.01, 4), pivot: number(0.18, 0.001, 4), factor: number(1, 0, 1) },
|
||||||
|
saturation: { saturation: number(1, 0, 4), factor: number(1, 0, 1) },
|
||||||
|
channel_mixer: { redOutput: vector([1, 0, 0], -2, 2), greenOutput: vector([0, 1, 0], -2, 2), blueOutput: vector([0, 0, 1], -2, 2), factor: number(1, 0, 1) },
|
||||||
bloom_extract: { threshold: number(1, 0, 64), knee: number(0.5, 0, 1) },
|
bloom_extract: { threshold: number(1, 0, 64), knee: number(0.5, 0, 1) },
|
||||||
bloom_blur: {
|
bloom_blur: {
|
||||||
direction: enumeration("horizontal", ["horizontal", "vertical"]),
|
direction: enumeration("horizontal", ["horizontal", "vertical"]),
|
||||||
@@ -425,6 +460,17 @@ export const nodeDefinitions = Object.fromEntries(
|
|||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
nodeDefinitions.color_balance.ui = [
|
||||||
|
{ kind: "parameter", parameter: "mode" },
|
||||||
|
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||||
|
{ title: "Lift", scalar: "lift", color: "liftColor" }, { title: "Gamma", scalar: "gamma", color: "gammaColor" }, { title: "Gain", scalar: "gain", color: "gainColor" },
|
||||||
|
], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } },
|
||||||
|
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||||
|
{ title: "Offset", scalar: "offset", color: "offsetColor" }, { title: "Power", scalar: "power", color: "powerColor" }, { title: "Slope", scalar: "slope", color: "slopeColor" },
|
||||||
|
], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } },
|
||||||
|
{ kind: "parameter", parameter: "factor" },
|
||||||
|
{ kind: "socket", socket: "source" }, { kind: "socket", socket: "colorTarget" }, { kind: "socket", socket: "color" },
|
||||||
|
];
|
||||||
export const fxNodeComposition = Object.freeze({
|
export const fxNodeComposition = Object.freeze({
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
id: "yawn.render-graph",
|
id: "yawn.render-graph",
|
||||||
|
|||||||
@@ -75,4 +75,13 @@ export const tone = postPreset("preset_tone", "tone");
|
|||||||
export const edges = postPreset("preset_edges", "edges");
|
export const edges = postPreset("preset_edges", "edges");
|
||||||
export const bloom = postPreset("preset_bloom", "bloom");
|
export const bloom = postPreset("preset_bloom", "bloom");
|
||||||
export const combined = postPreset("preset_combined", "combined");
|
export const combined = postPreset("preset_combined", "combined");
|
||||||
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, edges, bloom, combined });
|
export const grading = graph("preset_grading", [
|
||||||
|
node("balance_hdr", "texture", texture("rgba16_float")), node("exposure_hdr", "texture", texture("rgba16_float")), node("saturation_hdr", "texture", texture("rgba16_float")), node("mixer_hdr", "texture", texture("rgba16_float")), node("ldr", "texture", texture("rgba8_unorm")),
|
||||||
|
...scene("hdr"),
|
||||||
|
node("balance", "color_balance", { mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1], gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1], offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1], slope: 1, slopeColor: [1,1,1,1] }, { source: input("pbr_double", "color"), colorTarget: input("balance_hdr", "texture") }),
|
||||||
|
node("exposure", "exposure_contrast", { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 }, { source: input("balance", "color"), colorTarget: input("exposure_hdr", "texture") }),
|
||||||
|
node("saturation", "saturation", { saturation: 1, factor: 1 }, { source: input("exposure", "color"), colorTarget: input("saturation_hdr", "texture") }),
|
||||||
|
node("mixer", "channel_mixer", { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 }, { source: input("saturation", "color"), colorTarget: input("mixer_hdr", "texture") }),
|
||||||
|
node("tone", "tone_map", { exposure: 1 }, { source: input("mixer", "color"), colorTarget: input("ldr", "texture") }), node("frame_out", "frame_out", {}, { color: input("tone", "color") }),
|
||||||
|
]);
|
||||||
|
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, grading, edges, bloom, combined });
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
spawnRequestedNode,
|
spawnRequestedNode,
|
||||||
} from "../static/render-graph/node-spawn.js";
|
} from "../static/render-graph/node-spawn.js";
|
||||||
|
|
||||||
test("add-node model contains all 13 catalog types in application groups", () => {
|
test("add-node model contains all 17 catalog types in application groups", () => {
|
||||||
assert.equal(addNodeItems.length, 13);
|
assert.equal(addNodeItems.length, 17);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
[...new Set(addNodeItems.map((item) => item.group))],
|
[...new Set(addNodeItems.map((item) => item.group))],
|
||||||
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
||||||
);
|
);
|
||||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 13);
|
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 17);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
searchAddNodeItems("tone render").map((item) => item.typeId),
|
searchAddNodeItems("tone render").map((item) => item.typeId),
|
||||||
["tone_map"],
|
["tone_map"],
|
||||||
@@ -40,7 +40,7 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("all 13 types spawn with exact position, current version and generated ID", async () => {
|
test("all 17 types spawn with exact position, current version and generated ID", async () => {
|
||||||
let revision = 5,
|
let revision = 5,
|
||||||
expectedType;
|
expectedType;
|
||||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ test("production render graph composition passes fxnode's public validator", asy
|
|||||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||||
);
|
);
|
||||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||||
assert.equal(fxNodeComposition.version, 4);
|
assert.equal(fxNodeComposition.version, 5);
|
||||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 13);
|
assert.equal(Object.keys(fxNodeComposition.nodes).length, 17);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Object.values(fxNodeComposition.nodes).every(
|
Object.values(fxNodeComposition.nodes).every(
|
||||||
(definition) => definition.migrations.length === 0,
|
(definition) => definition.migrations.length === 0,
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
|||||||
"pipeline",
|
"pipeline",
|
||||||
"fullscreen_copy",
|
"fullscreen_copy",
|
||||||
"tone_map",
|
"tone_map",
|
||||||
|
"color_balance",
|
||||||
|
"exposure_contrast",
|
||||||
|
"saturation",
|
||||||
|
"channel_mixer",
|
||||||
"bloom_extract",
|
"bloom_extract",
|
||||||
"bloom_blur",
|
"bloom_blur",
|
||||||
"bloom_composite",
|
"bloom_composite",
|
||||||
@@ -130,7 +134,7 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
|||||||
Object.keys(contract.parameters).sort(),
|
Object.keys(contract.parameters).sort(),
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
assert.equal(CATALOG_VERSION, 4);
|
assert.equal(CATALOG_VERSION, 5);
|
||||||
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
|
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
|
||||||
pipeline: {
|
pipeline: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -175,6 +179,26 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
|||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
assert.equal(nodeDefinitions.mesh_query.sockets.isVisible.showValue, true);
|
assert.equal(nodeDefinitions.mesh_query.sockets.isVisible.showValue, true);
|
||||||
|
|
||||||
|
assert.deepEqual(nodeDefinitions.color_balance.ui.slice(0, 4), [
|
||||||
|
{ kind: "parameter", parameter: "mode" },
|
||||||
|
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||||
|
{ title: "Lift", scalar: "lift", color: "liftColor" },
|
||||||
|
{ title: "Gamma", scalar: "gamma", color: "gammaColor" },
|
||||||
|
{ title: "Gain", scalar: "gain", color: "gainColor" },
|
||||||
|
], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } },
|
||||||
|
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||||
|
{ title: "Offset", scalar: "offset", color: "offsetColor" },
|
||||||
|
{ title: "Power", scalar: "power", color: "powerColor" },
|
||||||
|
{ title: "Slope", scalar: "slope", color: "slopeColor" },
|
||||||
|
], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } },
|
||||||
|
{ kind: "parameter", parameter: "factor" },
|
||||||
|
]);
|
||||||
|
for (const name of ["liftColor", "gammaColor", "gainColor", "offsetColor", "powerColor", "slopeColor"])
|
||||||
|
assert.deepEqual(nodeDefinitions.color_balance.parameters[name].default, { kind: "color", value: [1, 1, 1, 1] });
|
||||||
|
assert.deepEqual(nodeDefinitions.channel_mixer.parameters.redOutput, {
|
||||||
|
type: "vector", default: { kind: "vector", value: [1, 0, 0] }, minimum: -2, maximum: 2,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("adapter validates and exactly lowers canonical pipeline controls and blur direction", () => {
|
test("adapter validates and exactly lowers canonical pipeline controls and blur direction", () => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const order = [
|
|||||||
"hdr",
|
"hdr",
|
||||||
"culling",
|
"culling",
|
||||||
"tone",
|
"tone",
|
||||||
|
"grading",
|
||||||
"edges",
|
"edges",
|
||||||
"bloom",
|
"bloom",
|
||||||
"combined",
|
"combined",
|
||||||
@@ -72,6 +73,12 @@ const sequences = {
|
|||||||
["tone", "tone_map"],
|
["tone", "tone_map"],
|
||||||
["frame_out", "frame_out"],
|
["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
|
grading: [
|
||||||
|
["balance_hdr", "texture"], ["exposure_hdr", "texture"], ["saturation_hdr", "texture"], ["mixer_hdr", "texture"], ["ldr", "texture"],
|
||||||
|
["hdr", "texture"], ["depth", "texture"], ["mesh", "mesh"], ["query", "mesh_query"], ["registry", "pipeline_registry"],
|
||||||
|
["ground", "pipeline"], ["pbr", "pipeline"], ["pbr_double", "pipeline"], ["balance", "color_balance"], ["exposure", "exposure_contrast"],
|
||||||
|
["saturation", "saturation"], ["mixer", "channel_mixer"], ["tone", "tone_map"], ["frame_out", "frame_out"],
|
||||||
|
],
|
||||||
edges: [
|
edges: [
|
||||||
["ldr", "texture"],
|
["ldr", "texture"],
|
||||||
["edge_hdr", "texture"],
|
["edge_hdr", "texture"],
|
||||||
@@ -143,6 +150,7 @@ test("presets have the exact canonical pipeline identities, schemas, and node se
|
|||||||
"preset_hdr_fullscreen",
|
"preset_hdr_fullscreen",
|
||||||
"preset_gpu_culling",
|
"preset_gpu_culling",
|
||||||
"preset_tone",
|
"preset_tone",
|
||||||
|
"preset_grading",
|
||||||
"preset_edges",
|
"preset_edges",
|
||||||
"preset_bloom",
|
"preset_bloom",
|
||||||
"preset_combined",
|
"preset_combined",
|
||||||
@@ -175,6 +183,16 @@ test("presets have the exact canonical pipeline identities, schemas, and node se
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const grading = presets.grading;
|
||||||
|
assert.deepEqual(grading.nodes.find((n) => n.id === "balance").parameters, {
|
||||||
|
mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1],
|
||||||
|
gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1],
|
||||||
|
offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1],
|
||||||
|
slope: 1, slopeColor: [1,1,1,1],
|
||||||
|
});
|
||||||
|
assert.deepEqual(grading.nodes.find((n) => n.id === "exposure").parameters, { exposureStops: 0, contrast: 1, pivot: .18, factor: 1 });
|
||||||
|
assert.deepEqual(grading.nodes.find((n) => n.id === "saturation").parameters, { saturation: 1, factor: 1 });
|
||||||
|
assert.deepEqual(grading.nodes.find((n) => n.id === "mixer").parameters, { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("presets preserve common mesh, texture, query, pipeline, culling and post wiring", () => {
|
test("presets preserve common mesh, texture, query, pipeline, culling and post wiring", () => {
|
||||||
@@ -264,12 +282,13 @@ test("presets preserve common mesh, texture, query, pipeline, culling and post w
|
|||||||
node: "cull",
|
node: "cull",
|
||||||
socket: "isFrustumCulled",
|
socket: "isFrustumCulled",
|
||||||
});
|
});
|
||||||
for (const name of ["tone", "edges", "bloom", "combined"])
|
for (const name of ["tone", "grading", "edges", "bloom", "combined"])
|
||||||
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
||||||
const finalSource = {
|
const finalSource = {
|
||||||
hdr: "pbr_double",
|
hdr: "pbr_double",
|
||||||
culling: "pbr_double",
|
culling: "pbr_double",
|
||||||
tone: "tone",
|
tone: "tone",
|
||||||
|
grading: "tone",
|
||||||
edges: "tone",
|
edges: "tone",
|
||||||
bloom: "tone",
|
bloom: "tone",
|
||||||
combined: "tone",
|
combined: "tone",
|
||||||
|
|||||||
Reference in New Issue
Block a user