use serde::Serialize;
use super::encoding::{Axis, CubeFace, NormalEncoding, NormalSpace, ScalarEncoding, narrow};
use super::roles::{Occluded, OutputRole};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Place {
pub output: usize,
pub lanes: u8,
}
impl Place {
pub fn first(&self) -> usize {
(self.lanes.trailing_zeros() as usize).min(3)
}
pub fn width(&self) -> u32 {
self.lanes.count_ones()
}
pub fn lane(&self, values: [f32; 4], k: usize) -> f32 {
values.get(self.first().saturating_add(k)).copied().unwrap_or(0.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Term {
pub place: Place,
pub encoding: ScalarEncoding,
}
impl Term {
pub fn decode(&self, values: [f32; 4], k: usize) -> f32 {
self.encoding.decode(self.place.lane(values, k))
}
pub fn colour(&self, values: [f32; 4]) -> [f32; 3] {
[0, 1, 2].map(|k| self.decode(values, k))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Base {
Albedo(Place),
Colour(Place),
}
impl Base {
pub fn place(&self) -> Place {
match *self {
Base::Albedo(p) | Base::Colour(p) => p,
}
}
pub fn colour(&self, values: [f32; 4]) -> [f32; 3] {
let place = self.place();
[0, 1, 2].map(|k| place.lane(values, k))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Normal {
pub place: Place,
pub encoding: NormalEncoding,
pub space: NormalSpace,
}
impl Normal {
pub fn width(encoding: NormalEncoding) -> u32 {
match encoding {
NormalEncoding::Xy | NormalEncoding::XyUnorm | NormalEncoding::Octahedral { .. } => 2,
NormalEncoding::Xyz
| NormalEncoding::XyzUnorm
| NormalEncoding::XyzBiased { .. }
| NormalEncoding::OctahedralDiamond => 3,
NormalEncoding::CubeFace { .. } => 4,
}
}
pub fn vector(&self, values: [f32; 4]) -> [f32; 3] {
let lane = |k: usize| self.place.lane(values, k);
match self.encoding {
NormalEncoding::Xyz => [lane(0), lane(1), lane(2)],
NormalEncoding::XyzUnorm => [0, 1, 2].map(|k| (lane(k) - 0.5) * 2.0),
NormalEncoding::XyzBiased { bias_bits } => {
let bias = f32::from_bits(bias_bits);
[0, 1, 2].map(|k| (lane(k) - bias) * 2.0)
}
NormalEncoding::Xy => with_z(lane(0), lane(1)),
NormalEncoding::XyUnorm => with_z(lane(0) * 2.0 - 1.0, lane(1) * 2.0 - 1.0),
NormalEncoding::Octahedral { read, .. } => unfold(read.apply(lane(0)), read.apply(lane(1))),
NormalEncoding::OctahedralDiamond => {
let (n, _) = NormalEncoding::decode_octahedral_diamond([lane(0), lane(1), lane(2)]);
n.map(narrow)
}
NormalEncoding::CubeFace { minor, .. } => {
let thirds = (lane(3) * 3.0).round();
let axis = if thirds >= 2.0 {
Axis::Z
} else if thirds >= 1.0 {
Axis::Y
} else {
Axis::X
};
let negative = lane(2) > 0.5;
let face = CubeFace::new(axis, negative);
let (sc, tc) = (
(lane(0) - minor.bias()) / minor.scale(),
(lane(1) - minor.bias()) / minor.scale(),
);
let [(_, first_sign), (_, second_sign)] = face.minor;
let depth = (1.0 - sc * sc - tc * tc).max(0.0).sqrt() * if negative { -1.0 } else { 1.0 };
let n = face.place(depth, sc * narrow(first_sign), tc * narrow(second_sign), 0.0);
normalised(n).unwrap_or(n)
}
}
}
pub fn decode(&self, values: [f32; 4]) -> Option<[f32; 3]> {
normalised(self.vector(values))
}
}
fn normalised(n: [f32; 3]) -> Option<[f32; 3]> {
let length = n.iter().map(|x| x * x).sum::<f32>().sqrt();
(length > 1e-8).then(|| n.map(|x| x / length))
}
fn with_z(x: f32, y: f32) -> [f32; 3] {
[x, y, (1.0 - x * x - y * y).max(0.0).sqrt()]
}
fn unfold(x: f32, y: f32) -> [f32; 3] {
let z = 1.0 - x.abs() - y.abs();
let sign = |v: f32| if v < 0.0 { -1.0 } else { 1.0 };
let (x, y) = if z < 0.0 {
((1.0 - y.abs()) * sign(x), (1.0 - x.abs()) * sign(y))
} else {
(x, y)
};
let n = [x, y, z];
normalised(n).unwrap_or(n)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Roughness {
Roughness(Term),
Gloss(Term),
Exponent(Term),
}
impl Roughness {
pub fn term(&self) -> Term {
match *self {
Roughness::Roughness(t) | Roughness::Gloss(t) | Roughness::Exponent(t) => t,
}
}
pub fn decode(&self, values: [f32; 4]) -> f32 {
let value = self.term().decode(values, 0);
match self {
Roughness::Roughness(_) => value,
Roughness::Gloss(_) => 1.0 - value,
Roughness::Exponent(_) => (2.0 / (value.max(0.0) + 2.0)).powf(0.25),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Emission {
Colour(Term),
OfAlbedo(Term),
}
impl Emission {
pub fn term(&self) -> Term {
match *self {
Emission::Colour(t) | Emission::OfAlbedo(t) => t,
}
}
pub fn decode(&self, values: [f32; 4], albedo: [f32; 3]) -> [f32; 3] {
match self {
Emission::Colour(t) => t.colour(values),
Emission::OfAlbedo(t) => {
let e = t.decode(values, 0);
albedo.map(|a| a * e)
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Surface {
pub base: Option<Base>,
pub normal: Option<Normal>,
pub roughness: Option<Roughness>,
pub metallic: Option<Term>,
pub reflectance: Option<Term>,
pub specular: Option<Term>,
pub emission: Option<Emission>,
pub occlusion: Option<Term>,
pub ambient_occlusion: Option<Term>,
pub refused: Vec<String>,
}
impl Surface {
pub fn read<'a>(outputs: impl IntoIterator<Item = (u8, &'a OutputRole)>) -> Surface {
let mut s = Surface::default();
let mut colour = None;
let mut refused = Vec::new();
for (output, (lanes, role)) in outputs.into_iter().enumerate() {
let place = Place { output, lanes };
let width = place.width();
let mut fits = |what: &str, wanted: &[u32]| -> bool {
if wanted.contains(&width) {
return true;
}
refused.push(format!(
"output {output}: {what} in {width} lanes ({lanes:#06b}), not {}",
wanted.iter().map(u32::to_string).collect::<Vec<_>>().join(" or ")
));
false
};
let term = |encoding: ScalarEncoding| Term { place, encoding };
let linear = term(ScalarEncoding::LINEAR);
match *role {
OutputRole::Albedo if s.base.is_none() && fits("an albedo", &[3, 4]) => {
s.base = Some(Base::Albedo(place));
}
OutputRole::Colour if colour.is_none() && fits("a finished colour", &[3, 4]) => {
colour = Some(Base::Colour(place));
}
OutputRole::Normal { encoding, space }
if s.normal.is_none() && fits("a normal", &[Normal::width(encoding)]) =>
{
s.normal = Some(Normal {
place,
encoding,
space,
});
}
OutputRole::Roughness if s.roughness.is_none() && fits("a roughness", &[1]) => {
s.roughness = Some(Roughness::Roughness(linear));
}
OutputRole::Gloss { encoding } if s.roughness.is_none() && fits("a gloss", &[1]) => {
s.roughness = Some(Roughness::Gloss(term(encoding)));
}
OutputRole::SpecularExponent { encoding }
if s.roughness.is_none() && fits("a specular exponent", &[1]) =>
{
s.roughness = Some(Roughness::Exponent(term(encoding)));
}
OutputRole::Metallic if s.metallic.is_none() && fits("a metallic", &[1]) => {
s.metallic = Some(linear);
}
OutputRole::Reflectance { encoding }
if s.reflectance.is_none() && fits("a reflectance", &[3]) =>
{
s.reflectance = Some(term(encoding));
}
OutputRole::SpecularIntensity { encoding }
if s.specular.is_none() && fits("a specular intensity", &[1]) =>
{
s.specular = Some(term(encoding));
}
OutputRole::Emission { encoding } if s.emission.is_none() && fits("an emission", &[1, 3]) => {
s.emission = Some(if width == 1 {
Emission::OfAlbedo(term(encoding))
} else {
Emission::Colour(term(encoding))
});
}
OutputRole::Occlusion {
of: Occluded::AllLight,
encoding,
} if s.occlusion.is_none() && fits("an occlusion", &[1]) => {
s.occlusion = Some(term(encoding));
}
OutputRole::Occlusion {
of: Occluded::NaturalAmbient,
encoding,
} if s.ambient_occlusion.is_none() && fits("an occlusion", &[1]) => {
s.ambient_occlusion = Some(term(encoding));
}
_ => {}
}
}
s.base = s.base.or(colour);
if s.occlusion.is_some() {
s.ambient_occlusion = None;
}
s.refused = refused;
s
}
}
pub const WGSL: &str = include_str!("surface.wgsl");
pub const ROWS: usize = 9;
pub const BASE: usize = 0;
pub const NORMAL: usize = 1;
pub const ROUGHNESS: usize = 2;
pub const METALLIC: usize = 3;
pub const REFLECTANCE: usize = 4;
pub const SPECULAR: usize = 5;
pub const EMISSION: usize = 6;
pub const OCCLUSION: usize = 7;
pub const AMBIENT_OCCLUSION: usize = 8;
pub fn normal_kind(encoding: NormalEncoding) -> (u32, [u32; 2]) {
match encoding {
NormalEncoding::Xyz => (1, [0, 0]),
NormalEncoding::XyzUnorm => (2, [0, 0]),
NormalEncoding::XyzBiased { bias_bits } => (3, [bias_bits, 0]),
NormalEncoding::Xy => (4, [0, 0]),
NormalEncoding::XyUnorm => (5, [0, 0]),
NormalEncoding::Octahedral { read, .. } => (6, [read.scale_bits, read.bias_bits]),
NormalEncoding::OctahedralDiamond => (7, [0, 0]),
NormalEncoding::CubeFace { minor, .. } => (8, [minor.scale_bits, minor.bias_bits]),
}
}
fn layout(place: Place, kind: u32, encoding: ScalarEncoding) -> u32 {
(u32::try_from(place.first()).unwrap_or(3) & 0x3)
| (place.width() & 0xf) << 4
| (kind & 0xff) << 8
| u32::from(encoding.power) << 16
| u32::from(encoding.root) << 24
}
pub fn rows(surface: &Surface, target: impl Fn(usize) -> Option<u32>) -> [[u32; 4]; ROWS] {
let mut rows = [[0u32; 4]; ROWS];
let at = |output: usize| target(output).map(|t| t.saturating_add(1));
let term_row = |t: &Term, kind: u32| -> [u32; 4] {
at(t.place.output).map_or([0; 4], |x| {
[x, layout(t.place, kind, t.encoding), t.encoding.scale_bits, 0]
})
};
if let Some(base) = &surface.base {
let (place, kind) = match *base {
Base::Albedo(p) => (p, 1),
Base::Colour(p) => (p, 2),
};
if let Some(x) = at(place.output) {
rows[BASE] = [x, layout(place, kind, ScalarEncoding::LINEAR), 0, 0];
}
}
if let Some(n) = &surface.normal {
let (kind, [a, b]) = normal_kind(n.encoding);
if let Some(x) = at(n.place.output) {
rows[NORMAL] = [x, layout(n.place, kind, ScalarEncoding::LINEAR), a, b];
}
}
if let Some(r) = &surface.roughness {
let kind = match r {
Roughness::Roughness(_) => 1,
Roughness::Gloss(_) => 2,
Roughness::Exponent(_) => 3,
};
rows[ROUGHNESS] = term_row(&r.term(), kind);
}
for (row, term) in [
(METALLIC, &surface.metallic),
(REFLECTANCE, &surface.reflectance),
(SPECULAR, &surface.specular),
(OCCLUSION, &surface.occlusion),
(AMBIENT_OCCLUSION, &surface.ambient_occlusion),
] {
if let Some(t) = term
&& let Some(slot) = rows.get_mut(row)
{
*slot = term_row(t, 1);
}
}
if let Some(e) = &surface.emission {
let kind = match e {
Emission::Colour(_) => 1,
Emission::OfAlbedo(_) => 2,
};
rows[EMISSION] = term_row(&e.term(), kind);
}
rows
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "hand-worked values")]
mod tests {
use super::*;
use crate::shader::LaneAffine;
fn close(a: [f32; 3], b: [f32; 3]) -> bool {
within(a, b, 1e-4)
}
fn within(a: [f32; 3], b: [f32; 3], tolerance: f32) -> bool {
a.iter().zip(&b).all(|(x, y)| (x - y).abs() < tolerance)
}
fn normal(encoding: NormalEncoding, lanes: u8) -> Normal {
Normal {
place: Place { output: 0, lanes },
encoding,
space: NormalSpace::World,
}
}
#[test]
fn each_normal_encoding_decodes_what_it_stores() {
let n = [0.6f32, 0.0, 0.8];
let unorm = n.map(|c| c * 0.5 + 0.5);
assert!(close(
normal(NormalEncoding::Xyz, 0b0111).vector([n[0], n[1], n[2], 9.0]),
n
));
assert!(close(
normal(NormalEncoding::XyzUnorm, 0b0111).vector([unorm[0], unorm[1], unorm[2], 0.0]),
n
));
let bias = 0.500_488_76f32;
let biased = n.map(|c| c * 0.5 + bias);
assert!(close(
normal(
NormalEncoding::XyzBiased {
bias_bits: bias.to_bits()
},
0b0111
)
.vector([biased[0], biased[1], biased[2], 0.0]),
n
));
let long = normal(NormalEncoding::Xyz, 0b0111);
assert_eq!(long.vector([0.0, 2.0, 0.0, 0.0]), [0.0, 2.0, 0.0]);
assert_eq!(long.decode([0.0, 2.0, 0.0, 0.0]), Some([0.0, 1.0, 0.0]));
assert_eq!(long.decode([0.0; 4]), None);
assert!(close(
normal(NormalEncoding::Xy, 0b1100).vector([9.0, 9.0, 0.6, 0.0]),
n
));
assert!(close(
normal(NormalEncoding::XyUnorm, 0b1100).vector([9.0, 9.0, 0.8, 0.5]),
n
));
let e = 0.6f32 / 1.4;
let octahedral = normal(
NormalEncoding::Octahedral {
stored: LaneAffine::UNORM,
read: LaneAffine::SIGNED,
},
0b0011,
);
assert!(close(octahedral.vector([e * 0.5 + 0.5, 0.5, 0.0, 0.0]), n));
let below = octahedral.vector([1.0, (1.0 - e) * 0.5 + 0.5, 0.0, 0.0]);
assert!(close(below, [0.6, 0.0, -0.8]), "{below:?}");
}
#[test]
fn the_packed_normal_encodings_decode_what_their_encoders_store() {
let n = [0.36f64, -0.48, 0.8];
let bytes = NormalEncoding::encode_octahedral_diamond(n, 5);
let lanes = bytes.map(|b| f32::from(b) / 255.0);
let diamond =
normal(NormalEncoding::OctahedralDiamond, 0b0111).decode([lanes[0], lanes[1], lanes[2], 0.0]);
assert!(within(diamond.unwrap(), n.map(narrow), 0.01), "{diamond:?}");
let (minor, payload) = (LaneAffine::UNORM, LaneAffine::new(0.25, 0.1));
for n in [[0.36f32, -0.48, 0.8], [-0.9, 0.3, 0.3162], [0.2, -0.95, -0.24]] {
let lanes = NormalEncoding::encode_cube_face(minor, payload, n, 0.5);
let back = normal(NormalEncoding::CubeFace { minor, payload }, 0b1111).decode(lanes);
assert!(close(back.unwrap(), normalised(n).unwrap()), "{n:?}: {back:?}");
}
}
#[test]
fn the_roughness_is_read_from_a_gloss_a_roughness_or_an_exponent() {
let place = Place {
output: 0,
lanes: 0b0010,
};
let term = |encoding| Term { place, encoding };
let gloss = Roughness::Gloss(term(ScalarEncoding::square_of(0.95)));
assert!((gloss.decode([0.0, 0.64, 0.0, 0.0]) - 0.24).abs() < 1e-6);
let stored = Roughness::Roughness(term(ScalarEncoding::LINEAR));
assert_eq!(stored.decode([0.0, 0.3, 0.0, 0.0]), 0.3);
let exponent = Roughness::Exponent(term(ScalarEncoding::square_root_of(512.0)));
assert_eq!(exponent.decode([0.0; 4]), 1.0);
let stored_510 = (510.0f32 / 512.0).sqrt();
assert!((exponent.decode([0.0, stored_510, 0.0, 0.0]) - 0.25).abs() < 1e-4);
}
fn roles(outputs: &[(u8, OutputRole)]) -> Surface {
Surface::read(outputs.iter().map(|(lanes, role)| (*lanes, role)))
}
#[test]
fn the_reading_takes_the_first_output_of_each_role_and_refuses_lanes_that_do_not_fit() {
let linear = ScalarEncoding::LINEAR;
let s = roles(&[
(0b1111, OutputRole::Colour),
(0b0111, OutputRole::Albedo),
(0b0111, OutputRole::Albedo),
(
0b0001,
OutputRole::Occlusion {
of: Occluded::NaturalAmbient,
encoding: linear,
},
),
(
0b0010,
OutputRole::Occlusion {
of: Occluded::AllLight,
encoding: linear,
},
),
(0b0011, OutputRole::Metallic),
(0b0001, OutputRole::Metallic),
(0b1111, OutputRole::unknown("read by nothing")),
]);
assert_eq!(
s.base,
Some(Base::Albedo(Place {
output: 1,
lanes: 0b0111
}))
);
assert_eq!(s.occlusion.map(|t| t.place.output), Some(4));
assert_eq!(s.ambient_occlusion, None);
assert_eq!(s.metallic.map(|t| t.place.output), Some(6));
let ambient = roles(&[(
0b0001,
OutputRole::Occlusion {
of: Occluded::NaturalAmbient,
encoding: linear,
},
)]);
assert_eq!(
(
ambient.occlusion,
ambient.ambient_occlusion.map(|t| t.place.output)
),
(None, Some(0))
);
assert_eq!(s.refused, ["output 5: a metallic in 2 lanes (0b0011), not 1"]);
assert_eq!(
(s.normal, s.roughness, s.reflectance, s.emission),
(None, None, None, None)
);
let forward = roles(&[(0b1111, OutputRole::Colour)]);
assert_eq!(
forward.base,
Some(Base::Colour(Place {
output: 0,
lanes: 0b1111
}))
);
let scalar = roles(&[(0b0100, OutputRole::Emission { encoding: linear })]);
assert!(matches!(scalar.emission, Some(Emission::OfAlbedo(_))));
assert_eq!(
scalar
.emission
.unwrap()
.decode([0.0, 0.0, 2.0, 0.0], [0.5, 0.25, 1.0]),
[1.0, 0.5, 2.0]
);
let colour = roles(&[(0b0111, OutputRole::Emission { encoding: linear })]);
assert_eq!(
colour.emission.unwrap().decode([0.1, 0.2, 0.3, 0.0], [1.0; 3]),
[0.1, 0.2, 0.3]
);
}
}