use super::*;
const SPLIT: UVec2 = UVec2::new(2, 1);
const WHOLE: [u8; 4] = [u8::MAX; 4];
const TURNED: u8 = 218;
const STRAIGHT: u8 = 128;
const FROM_ABOVE: Vec3 = Vec3::new(0.0, -1.0, -1.0);
const FROM_BELOW: Vec3 = Vec3::new(0.0, 1.0, -1.0);
const FROM_THE_RIGHT: Vec3 = Vec3::new(-1.0, 0.0, -1.0);
const FROM_THE_LEFT: Vec3 = Vec3::new(1.0, 0.0, -1.0);
meshes! { enum MappedSet { Mapped } }
meshes! { enum PerTexelSet { PerTexel } }
fn shading(right: [u8; 4]) -> ShadingData {
ShadingData::rgba8(SPLIT, [WHOLE, right].concat())
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum PerTexel {
Bare,
Roughness,
Emissive,
}
impl Catalog for PerTexel {
fn catalog() -> Vec<Self> {
vec![Self::Bare, Self::Roughness, Self::Emissive]
}
}
impl Mesh for PerTexel {
fn build(&self, assets: &Assets) -> MeshData {
let square = Quad
.build(assets)
.with_texture(TextureData::rgba8(UVec2::ONE, WHOLE.to_vec()).pixelated());
match self {
Self::Bare => square,
Self::Roughness => square.with_shading(shading([u8::MAX, 0, u8::MAX, u8::MAX])),
Self::Emissive => square.with_emissive_map(TextureData::rgba8(
SPLIT,
[WHOLE, [0, 0, 0, u8::MAX]].concat(),
)),
}
}
}
struct Split {
mesh: PerTexel,
material: Material,
lit: bool,
bloom: f32,
}
impl Split {
fn dark(mesh: PerTexel, material: Material) -> Self {
Self {
mesh,
material,
lit: false,
bloom: 0.0,
}
}
}
impl Game for Split {
type Meshes = PerTexelSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(Camera::new(
View::look_at(Vec3::Z, Vec3::ZERO),
Projection::orthographic(1.0),
));
ctx.set_bloom(self.bloom);
let color = if self.lit { DIM } else { Color::BLACK };
ctx.light(Light::directional(Vec3::NEG_Z, color));
ctx.draw(self.mesh.at(Vec3::ZERO).material(self.material));
}
}
fn split(square: Split) -> Option<Vec<u8>> {
sized(raw("headless per-texel"), UVec2::splat(WIDE), square)
}
fn half(pixels: &[u8], half: u32) -> [u8; 4] {
wide_pixel(pixels, WIDE / 4 + half * WIDE / 2, WIDE / 2)
}
#[test]
fn a_shading_maps_roughness_splits_one_slots_highlight() {
let sheen = |mesh| {
split(Split {
lit: true,
..Split::dark(mesh, Material::lit(DIM))
})
};
let Some(mapped) = sheen(PerTexel::Roughness) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(bare) = sheen(PerTexel::Bare) else {
return;
};
assert_eq!(
half(&mapped, 0),
half(&bare, 0),
"the half the map leaves whole draws at the material's own roughness"
);
assert!(
half(&mapped, 1)[0] > half(&bare, 1)[0],
"and the half it sharpens takes a highlight the other half has none of, got {:?} \
against {:?}",
half(&mapped, 1),
half(&bare, 1)
);
}
#[test]
fn an_emissive_map_casts_the_materials_light_in_its_own_shape_and_blooms_in_it() {
let glowing = |bloom| {
split(Split {
bloom,
..Split::dark(
PerTexel::Emissive,
Material::lit(Color::BLACK).emissive(Color::rgb(1.0, 0.0, 0.0)),
)
})
};
let Some(plain) = glowing(0.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(bloomed) = glowing(0.5) else {
return;
};
let dropped = half(&plain, 1);
assert_eq!(
half(&plain, 0),
[u8::MAX, dropped[1], dropped[2], u8::MAX],
"the half the map keeps casts the material's own light over what the \
surface reflects of the sky"
);
assert!(
dropped[0] > 0 && dropped[0] == dropped[1] && dropped[1] == dropped[2],
"and the half it drops casts none, leaving one grey reflection of the \
sky, got {dropped:?}"
);
assert!(
half(&bloomed, 1)[0] > 0,
"the chain spreads that light into the half that cast none"
);
assert!(
half(&bloomed, 1)[0] < half(&bloomed, 0)[0],
"which stays darker than the half it came from, got {:?} against {:?}",
half(&bloomed, 1),
half(&bloomed, 0)
);
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum Mapped {
Flat,
Up,
Across,
}
impl Catalog for Mapped {
fn catalog() -> Vec<Self> {
vec![Self::Flat, Self::Up, Self::Across]
}
}
impl Mesh for Mapped {
fn build(&self, assets: &Assets) -> MeshData {
let normals =
|across, up, out| ReliefData::normals(UVec2::ONE, vec![across, up, out, u8::MAX]);
Quad.build(assets).with_relief(match self {
Self::Flat => normals(STRAIGHT, STRAIGHT, u8::MAX),
Self::Up => normals(STRAIGHT, TURNED, TURNED),
Self::Across => normals(TURNED, STRAIGHT, TURNED),
})
}
}
struct Relit {
mesh: Mapped,
sun: Vec3,
faced: bool,
}
impl Game for Relit {
type Meshes = MappedSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(Camera::new(
View::look_at(Vec3::Z, Vec3::ZERO),
Projection::orthographic(1.0),
));
ctx.light(Light::directional(self.sun, Color::WHITE));
let drawn = self
.mesh
.at(Vec3::ZERO)
.material(Material::lit(Color::WHITE));
ctx.draw(if self.faced { drawn.upright() } else { drawn });
}
}
#[test]
fn a_placed_draws_relief_turns_the_light_its_texels_take() {
let lit = |mesh, sun| {
center(Relit {
mesh,
sun,
faced: false,
})
};
let Some(above) = lit(Mapped::Up, FROM_ABOVE) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some(below), Some(flat_above), Some(flat_below)) = (
lit(Mapped::Up, FROM_BELOW),
lit(Mapped::Flat, FROM_ABOVE),
lit(Mapped::Flat, FROM_BELOW),
) else {
return;
};
assert!(
flat_above[0].abs_diff(flat_below[0]) <= 1,
"a texel facing straight out of the square takes the two suns alike, got \
{flat_above:?} against {flat_below:?}"
);
assert!(
above[0] > flat_above[0],
"one turned up takes more of the sun over the view, got {above:?} against \
{flat_above:?}"
);
assert!(
below[0] < flat_below[0],
"and less of the one under it, got {below:?} against {flat_below:?}"
);
}
#[test]
fn a_relief_texel_turned_up_the_map_takes_more_of_a_sun_over_it_than_a_flat_one() {
let lit = |mesh, faced| {
center(Relit {
mesh,
sun: FROM_ABOVE,
faced,
})
};
let Some(placed) = lit(Mapped::Up, false) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some(placed_flat), Some(faced), Some(faced_flat)) = (
lit(Mapped::Flat, false),
lit(Mapped::Up, true),
lit(Mapped::Flat, true),
) else {
return;
};
assert!(
placed[0] > placed_flat[0],
"the derivative basis reads the map's own up, got {placed:?} against {placed_flat:?}"
);
assert!(
faced[0] > faced_flat[0],
"and so do the axes the camera turned, got {faced:?} against {faced_flat:?}"
);
}
#[test]
fn a_relief_texel_turned_across_the_map_takes_more_of_a_sun_on_the_cameras_right_than_its_left() {
let lit = |sun, faced| {
center(Relit {
mesh: Mapped::Across,
sun,
faced,
})
};
let Some(faced_right) = lit(FROM_THE_RIGHT, true) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some(faced_left), Some(placed_right), Some(placed_left)) = (
lit(FROM_THE_LEFT, true),
lit(FROM_THE_RIGHT, false),
lit(FROM_THE_LEFT, false),
) else {
return;
};
assert!(
faced_right[0] > faced_left[0],
"the axes the camera turned read the map's own across, got {faced_right:?} against \
{faced_left:?}"
);
assert!(
placed_right[0] > placed_left[0],
"and so does the derivative basis, got {placed_right:?} against {placed_left:?}"
);
}