use mirage_engine::prelude::*;
const GROUND_SIZE: f32 = 18.0;
const PAIR_HALF_SPACING: f32 = 1.0;
const SHADING_Z: f32 = 3.4;
const RELIEF_Z: f32 = 1.8;
const EMISSIVE_Z: f32 = 0.2;
const SPHERE_RADIUS: f32 = 0.5;
const CUBE_SIZE: f32 = 0.85;
const FRONT_POSITION: Vec3 = Vec3::new(0.0, 0.7, -1.6);
const FRONT_SCALE: f32 = 1.4;
const SPHERE_SUBDIVISIONS: u32 = 3;
const REFLECT_ROW_COUNT: usize = 5;
const REFLECT_ROW_SPACING: f32 = 1.5;
const REFLECT_ROW_Z: f32 = -1.6;
const REFLECT_ROW_RADIUS: f32 = 0.55;
const GROUND_COLOR: Color = Color::rgb(0.24, 0.25, 0.22);
const SHADING_TINT: Color = Color::rgb(0.55, 0.55, 0.6);
const RELIEF_TINT: Color = Color::rgb(0.5, 0.45, 0.35);
const EMISSIVE_BASE: Color = Color::rgb(0.04, 0.04, 0.05);
const EMISSIVE_GLOW: Color = Color::rgb(3.2, 2.2, 0.7);
const MAP_SIZE: UVec2 = UVec2::new(64, 64);
const SHADING_CELL: u32 = 8;
const SHADING_LOW: [u8; 3] = [70, 40, 15];
const SHADING_HIGH: [u8; 3] = [255, 225, 235];
const EMISSIVE_CELL: u32 = 6;
const BUMP_WAVES: f32 = 6.0;
const BUMP_SLOPE: f32 = 1.15;
const BANNER_WIDTH: f32 = 1.1;
const BANNER_HEIGHT: f32 = 0.7;
const BANNER_COLUMNS: u32 = 10;
const OUTPOST: Vec3 = Vec3::new(-4.6, 0.0, -0.8);
const PILLARS: [(Vec3, Vec3); 3] = [
(Vec3::new(-1.8, 0.6, -0.6), Vec3::new(0.6, 1.2, 0.6)),
(Vec3::new(0.4, 0.4, -1.6), Vec3::new(0.5, 0.8, 0.5)),
(Vec3::new(1.7, 0.9, 0.4), Vec3::new(0.55, 1.8, 0.55)),
];
const POLE_POSITION: Vec3 = Vec3::new(-2.6, 1.0, 0.4);
const POLE_SCALE: Vec3 = Vec3::new(0.12, 2.0, 0.12);
const BANNER_MOUNT: Vec3 = Vec3::new(-2.54, 1.55, 0.46);
const FIELD_ORB_POSITION: Vec3 = Vec3::new(1.3, 1.1, 1.6);
const FIELD_ORB_SCALE: f32 = 0.7;
const LAMP_POSITION: Vec3 = Vec3::new(2.4, 1.4, -3.0);
const LAMP_RANGE: f32 = 5.0;
const SPOT_POSITION: Vec3 = Vec3::new(-3.4, 3.0, 1.6);
const SPOT_DIRECTION: Vec3 = Vec3::new(0.55, -1.0, -1.0);
const SPOT_RANGE: f32 = 7.0;
const SPOT_ANGLE: f32 = 0.5;
const CAMERA_FOV: f32 = 46.0;
const START_EYE: Vec3 = Vec3::new(-0.6, 2.2, 9.0);
const START_YAW: f32 = 0.0;
const START_PITCH: f32 = -0.15;
const PITCH_LIMIT: f32 = 1.5;
const MIN_EYE_HEIGHT: f32 = 0.3;
const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
const MOVE_SPEED: f32 = 4.0;
const SPEED_STEP: f32 = 1.5;
const MIN_SPEED_SCALE: f32 = 0.2;
const MAX_SPEED_SCALE: f32 = 5.0;
const START_BLOOM: f32 = 0.25;
const START_EXPOSURE: f32 = 1.0;
const DEFAULT_SKY: Color = Color::rgb(0.1, 0.1, 0.1);
const CLEAR_SKY_LIGHT: f32 = 0.35;
const CLASSIC_SKY_LIGHT: f32 = 0.35;
const DAWN_SKY_LIGHT: f32 = 0.3;
const SINISTER_SKY_LIGHT: f32 = 0.6;
const LIGHT_BLUE_STARS_LIGHT: f32 = 0.8;
const BLUE_STARS_LIGHT: f32 = 0.8;
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct ShadingPlain;
impl Mesh for ShadingPlain {
fn build(&self, assets: &Assets) -> MeshData {
sphere_with_material(assets, shading_material())
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct ShadingMapped;
impl Mesh for ShadingMapped {
fn build(&self, assets: &Assets) -> MeshData {
sphere_with_material(assets, shading_material()).with_shading(shading_checker())
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct ReliefPlain;
impl Mesh for ReliefPlain {
fn build(&self, assets: &Assets) -> MeshData {
sphere_with_material(assets, relief_material())
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct ReliefMapped;
impl Mesh for ReliefMapped {
fn build(&self, assets: &Assets) -> MeshData {
sphere_with_material(assets, relief_material()).with_relief(relief_bumps())
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct EmissivePlain;
impl Mesh for EmissivePlain {
fn build(&self, assets: &Assets) -> MeshData {
cube_with_material(assets, emissive_material())
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct EmissiveMapped;
impl Mesh for EmissiveMapped {
fn build(&self, assets: &Assets) -> MeshData {
cube_with_material(assets, emissive_material()).with_emissive_map(emissive_checker())
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct Front;
impl Mesh for Front {
fn build(&self, assets: &Assets) -> MeshData {
Sphere {
subdivisions: SPHERE_SUBDIVISIONS,
}
.build(assets)
}
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
struct BannerCloth;
impl Mesh for BannerCloth {
fn build(&self, _: &Assets) -> MeshData {
banner_mesh()
}
}
meshes! {
enum Shape {
Plane,
Sphere,
Cube,
ShadingPlain,
ShadingMapped,
ReliefPlain,
ReliefMapped,
EmissivePlain,
EmissiveMapped,
Front,
BannerCloth,
}
}
fn sphere_with_material(assets: &Assets, material: Material) -> MeshData {
Sphere {
subdivisions: SPHERE_SUBDIVISIONS,
}
.build(assets)
.with_material(material)
}
fn cube_with_material(assets: &Assets, material: Material) -> MeshData {
Cube.build(assets).with_material(material)
}
fn shading_material() -> Material {
Material::lit(SHADING_TINT).roughness(0.5).metallic(0.5)
}
fn relief_material() -> Material {
Material::lit(RELIEF_TINT).roughness(0.35)
}
fn emissive_material() -> Material {
Material::color(EMISSIVE_BASE).emissive(EMISSIVE_GLOW)
}
fn shading_checker() -> ShadingData {
ShadingData::rgba8(
MAP_SIZE,
checker_pixels(MAP_SIZE, SHADING_CELL, SHADING_LOW, SHADING_HIGH),
)
}
fn emissive_checker() -> TextureData {
TextureData::rgba8(
MAP_SIZE,
checker_pixels(MAP_SIZE, EMISSIVE_CELL, [0, 0, 0], [255, 255, 255]),
)
}
fn checker_pixels(size: UVec2, cell: u32, low: [u8; 3], high: [u8; 3]) -> Vec<u8> {
let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
for y in 0..size.y {
for x in 0..size.x {
let on = ((x / cell) + (y / cell)).is_multiple_of(2);
let [red, green, blue] = if on { high } else { low };
pixels.extend_from_slice(&[red, green, blue, u8::MAX]);
}
}
pixels
}
fn relief_bumps() -> ReliefData {
let size = MAP_SIZE;
let turns = core::f32::consts::TAU * BUMP_WAVES;
let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
for y in 0..size.y {
for x in 0..size.x {
let u = (x as f32 + 0.5) / size.x as f32;
let v = (y as f32 + 0.5) / size.y as f32;
let slope_u = BUMP_SLOPE * (turns * u).cos() * (turns * v).sin();
let slope_v = BUMP_SLOPE * (turns * u).sin() * (turns * v).cos();
let normal = Vec3::new(-slope_u, -slope_v, 1.0).normalize();
let encode = |signed: f32| ((signed * 0.5 + 0.5) * 255.0).round() as u8;
pixels.extend_from_slice(&[encode(normal.x), encode(normal.y), encode(normal.z), 0]);
}
}
ReliefData::normals(size, pixels)
}
fn banner_mesh() -> MeshData {
let mut vertices = Vec::with_capacity(((BANNER_COLUMNS + 1) * 4) as usize);
for normal in [Vec3::Z, Vec3::NEG_Z] {
for column in 0..=BANNER_COLUMNS {
let u = column as f32 / BANNER_COLUMNS as f32;
let x = u * BANNER_WIDTH;
for v in [0.0, 1.0] {
vertices.push(Vertex::new(
Vec3::new(x, -v * BANNER_HEIGHT, 0.0),
normal,
Vec2::new(u, v),
));
}
}
}
let side = BANNER_COLUMNS + 1;
let mut indices = Vec::with_capacity((BANNER_COLUMNS * 12) as usize);
for column in 0..BANNER_COLUMNS {
let top_left = column * 2;
let bottom_left = top_left + 1;
let top_right = top_left + 2;
let bottom_right = top_left + 3;
indices.extend([
bottom_left,
bottom_right,
top_right,
bottom_left,
top_right,
top_left,
]);
let back = side * 2;
indices.extend([
back + top_right,
back + bottom_right,
back + bottom_left,
back + top_left,
back + top_right,
back + bottom_left,
]);
}
MeshData::new(vertices, indices)
}
#[derive(Default, ShaderValues)]
struct Banner {
time: f32,
}
impl SurfaceStyle for Banner {
const PASS: DrawPass = DrawPass::Opaque;
const DISPLACE: Option<&'static str> = Some(include_str!("material_playground_banner.wgsl"));
}
#[derive(Default, ShaderValues)]
struct Field {
tint: Color,
time: f32,
}
impl SurfaceStyle for Field {
const PASS: DrawPass = DrawPass::Additive;
const SURFACE: Option<&'static str> = Some(include_str!("material_playground_field.wgsl"));
}
surface_styles! { enum Looks { Banner, Field } }
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Sky {
Dawn,
Noon,
Dusk,
Night,
Clear,
Classic,
ImageDawn,
Sinister,
LightBlueStars,
BlueStars,
Default,
}
impl Sky {
const ALL: [Sky; 11] = [
Self::Dawn,
Self::Noon,
Self::Dusk,
Self::Night,
Self::Clear,
Self::Classic,
Self::ImageDawn,
Self::Sinister,
Self::LightBlueStars,
Self::BlueStars,
Self::Default,
];
fn name(self) -> &'static str {
match self {
Self::Dawn => "dawn",
Self::Noon => "noon",
Self::Dusk => "dusk",
Self::Night => "night",
Self::Clear => "clear day",
Self::Classic => "classic",
Self::ImageDawn => "dawn image",
Self::Sinister => "sinister night",
Self::LightBlueStars => "light blue stars",
Self::BlueStars => "blue stars",
Self::Default => "default",
}
}
fn light(self) -> f32 {
match self {
Self::Dawn => 0.4,
Self::Noon => 0.5,
Self::Dusk => 0.35,
Self::Night => 0.3,
Self::Clear => CLEAR_SKY_LIGHT,
Self::Classic => CLASSIC_SKY_LIGHT,
Self::ImageDawn => DAWN_SKY_LIGHT,
Self::Sinister => SINISTER_SKY_LIGHT,
Self::LightBlueStars => LIGHT_BLUE_STARS_LIGHT,
Self::BlueStars => BLUE_STARS_LIGHT,
Self::Default => 1.0,
}
}
fn sun(self) -> Option<(Vec3, Color, f32)> {
match self {
Self::Dawn => Some((
Vec3::new(-1.0, -0.15, 0.05),
Color::rgb(1.0, 0.7, 0.45),
1.4,
)),
Self::Noon => Some((
Vec3::new(-0.15, -1.0, -0.1),
Color::rgb(1.0, 1.0, 0.98),
1.6,
)),
Self::Dusk => Some((
Vec3::new(1.0, -0.15, 0.05),
Color::rgb(1.0, 0.55, 0.25),
1.2,
)),
Self::Night => Some((
Vec3::new(-0.3, -0.7, -0.6),
Color::rgb(0.55, 0.65, 0.85),
0.15,
)),
Self::Clear => Some((
Vec3::new(-0.2, -1.0, -0.15),
Color::rgb(1.0, 0.98, 0.9),
1.5,
)),
Self::Classic => Some((
Vec3::new(-0.4, -0.9, -0.2),
Color::rgb(1.0, 0.95, 0.85),
1.3,
)),
Self::ImageDawn => Some((Vec3::new(-1.0, -0.2, 0.1), Color::rgb(1.0, 0.75, 0.5), 1.1)),
Self::Sinister => Some((Vec3::new(0.4, -0.5, -0.7), Color::rgb(0.4, 0.5, 0.75), 0.1)),
Self::LightBlueStars | Self::BlueStars => None,
Self::Default => Some((Vec3::new(-0.4, -1.0, -0.6), Color::WHITE, 1.0)),
}
}
fn ground(self) -> Option<Color> {
match self {
Self::Clear => Some(Color::rgb(0.501, 0.517, 0.449)),
Self::Classic => Some(Color::rgb(0.420, 0.405, 0.379)),
Self::ImageDawn => Some(Color::rgb(0.073, 0.053, 0.032)),
Self::Sinister => Some(Color::rgb(0.012, 0.014, 0.020)),
Self::Dawn
| Self::Noon
| Self::Dusk
| Self::Night
| Self::LightBlueStars
| Self::BlueStars
| Self::Default => None,
}
}
}
impl Catalog for Sky {
fn catalog() -> Vec<Self> {
Self::ALL.to_vec()
}
}
impl Skyboxes for Sky {
fn build(&self, assets: &Assets) -> SkyboxData {
let sky = match self {
Self::Dawn => SkyboxData::gradient(
Color::rgb(0.55, 0.55, 0.75),
Color::rgb(0.95, 0.6, 0.35),
Color::rgb(0.12, 0.08, 0.06),
),
Self::Noon => SkyboxData::gradient(
Color::rgb(0.2, 0.45, 0.85),
Color::rgb(0.75, 0.82, 0.9),
Color::rgb(0.3, 0.3, 0.28),
),
Self::Dusk => SkyboxData::gradient(
Color::rgb(0.18, 0.1, 0.3),
Color::rgb(0.85, 0.35, 0.2),
Color::rgb(0.03, 0.02, 0.03),
),
Self::Night => SkyboxData::gradient(
Color::rgb(0.02, 0.02, 0.06),
Color::rgb(0.05, 0.05, 0.1),
Color::rgb(0.0, 0.0, 0.0),
),
Self::Clear => assets.skybox("sky-clear"),
Self::Classic => assets.skybox("sky-classic"),
Self::ImageDawn => assets.skybox("sky-dawn"),
Self::Sinister => assets.skybox("sky-sinister"),
Self::LightBlueStars => assets.skybox("sky-stars-lightblue"),
Self::BlueStars => assets.skybox("sky-stars-blue"),
Self::Default => SkyboxData::gradient(DEFAULT_SKY, DEFAULT_SKY, DEFAULT_SKY),
};
let sky = match self.ground() {
Some(ground) => sky.with_ground(ground),
None => sky,
};
sky.lit_by(self.light())
}
}
fn scaled(color: Color, strength: f32) -> Color {
Color::rgb(
color.red * strength,
color.green * strength,
color.blue * strength,
)
}
#[derive(Clone, Copy)]
struct Glow {
color: Color,
strength: f32,
shadow: bool,
}
impl Glow {
fn scaled(self) -> Color {
scaled(self.color, self.strength)
}
}
#[derive(InputButtonAction, Clone, Copy, PartialEq)]
enum Move {
Forward,
Back,
Left,
Right,
Up,
Down,
Look,
}
impl InputButtonAction for Move {
fn bindings(&self) -> Vec<ButtonBinding> {
match self {
Self::Forward => vec![Key::W.into()],
Self::Back => vec![Key::S.into()],
Self::Left => vec![Key::A.into()],
Self::Right => vec![Key::D.into()],
Self::Up => vec![Key::Space.into()],
Self::Down => vec![Key::LeftShift.into()],
Self::Look => vec![MouseButton::Right.into()],
}
}
}
#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
enum Turn {
Look,
}
impl InputAxis2Action for Turn {
fn bindings(&self) -> Vec<Axis2Binding> {
match self {
Self::Look => vec![Axis2Binding::pointer().scale(LOOK_SENSITIVITY)],
}
}
}
#[derive(InputAxisAction, Clone, Copy, PartialEq)]
enum Speed {
Wheel,
}
impl InputAxisAction for Speed {
fn bindings(&self) -> Vec<AxisBinding> {
match self {
Self::Wheel => vec![AxisBinding::from(WheelDelta::Up).scale(4.0)],
}
}
}
struct Controls;
impl InputActions for Controls {
type Button = Move;
type Axis = Speed;
type Axis2 = Turn;
}
struct Playground {
eye: Vec3,
yaw: f32,
pitch: f32,
speed_scale: f32,
sky: Sky,
sun_shadow: bool,
lamp: Glow,
spotlight: Glow,
front_tint: Color,
front_roughness: f32,
front_metallic: f32,
shading_map_on: bool,
relief_map_on: bool,
emissive_map_on: bool,
exposure: f32,
bloom: f32,
}
impl Playground {
fn init(ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
let _ = ctx;
Ok(Self {
eye: START_EYE,
yaw: START_YAW,
pitch: START_PITCH,
speed_scale: 1.0,
sky: Sky::Default,
sun_shadow: true,
lamp: Glow {
color: Color::rgb(0.9, 0.55, 0.3),
strength: 3.0,
shadow: false,
},
spotlight: Glow {
color: Color::rgb(0.4, 0.6, 1.0),
strength: 6.0,
shadow: true,
},
front_tint: Color::rgb(0.7, 0.25, 0.2),
front_roughness: 0.4,
front_metallic: 0.0,
shading_map_on: true,
relief_map_on: true,
emissive_map_on: true,
exposure: START_EXPOSURE,
bloom: START_BLOOM,
})
}
fn forward(&self) -> Vec3 {
Vec3::new(
-self.pitch.cos() * self.yaw.sin(),
self.pitch.sin(),
-self.pitch.cos() * self.yaw.cos(),
)
}
fn camera(&self) -> Camera {
Camera::new(
View::look_at(self.eye, self.eye + self.forward()),
Projection::perspective(CAMERA_FOV),
)
}
fn fly_camera(&mut self, ctx: &mut FrameContext<'_, Self>) {
if !ctx.ui_wants_pointer() && ctx.down(Move::Look) {
let look = ctx.axis2(Turn::Look);
self.yaw -= look.x;
self.pitch = (self.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
}
let wheel = ctx.axis(Speed::Wheel);
if !ctx.ui_wants_pointer() && wheel != 0.0 {
self.speed_scale =
(self.speed_scale * SPEED_STEP.powf(wheel)).clamp(MIN_SPEED_SCALE, MAX_SPEED_SCALE);
}
let forward = self.forward();
let right = Vec3::new(self.yaw.cos(), 0.0, -self.yaw.sin());
let mut move_by = Vec3::ZERO;
if ctx.down(Move::Forward) {
move_by += forward;
}
if ctx.down(Move::Back) {
move_by -= forward;
}
if ctx.down(Move::Right) {
move_by += right;
}
if ctx.down(Move::Left) {
move_by -= right;
}
if ctx.down(Move::Up) {
move_by += Vec3::Y;
}
if ctx.down(Move::Down) {
move_by -= Vec3::Y;
}
if move_by.length_squared() > 1.0 {
move_by = move_by.normalize();
}
self.eye += move_by * MOVE_SPEED * self.speed_scale * ctx.dt().as_secs_f32();
self.eye.y = self.eye.y.max(MIN_EYE_HEIGHT);
}
fn front_material(&self) -> Material {
Material::lit(self.front_tint)
.roughness(self.front_roughness)
.metallic(self.front_metallic)
}
fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
ctx.draw(
Plane
.at(Transform::from_scale(Vec3::new(
GROUND_SIZE,
1.0,
GROUND_SIZE,
)))
.material(Material::lit(GROUND_COLOR).roughness(0.9)),
);
Self::draw_pair(
ctx,
SHADING_Z,
SPHERE_RADIUS,
ShadingPlain.at(Vec3::ZERO).into_set(),
ShadingMapped.at(Vec3::ZERO).into_set(),
self.shading_map_on,
);
Self::draw_pair(
ctx,
RELIEF_Z,
SPHERE_RADIUS,
ReliefPlain.at(Vec3::ZERO).into_set(),
ReliefMapped.at(Vec3::ZERO).into_set(),
self.relief_map_on,
);
Self::draw_pair(
ctx,
EMISSIVE_Z,
CUBE_SIZE / 2.0,
EmissivePlain.at(Vec3::ZERO).into_set(),
EmissiveMapped.at(Vec3::ZERO).into_set(),
self.emissive_map_on,
);
ctx.draw(
Front
.at(Transform::from_scale_rotation_translation(
Vec3::splat(FRONT_SCALE),
Quat::IDENTITY,
FRONT_POSITION,
))
.material(self.front_material()),
);
self.draw_reflect_row(ctx);
self.draw_outpost(ctx);
}
fn draw_pair(
ctx: &mut FrameContext<'_, Self>,
z: f32,
height: f32,
plain: Instance<Shape, Looks>,
mapped: Instance<Shape, Looks>,
mapped_on: bool,
) {
ctx.draw(plain.clone().at(Vec3::new(-PAIR_HALF_SPACING, height, z)));
let right = if mapped_on { mapped } else { plain };
ctx.draw(right.at(Vec3::new(PAIR_HALF_SPACING, height, z)));
}
fn draw_reflect_row(&self, ctx: &mut FrameContext<'_, Self>) {
let start = -REFLECT_ROW_SPACING * (REFLECT_ROW_COUNT as f32 - 1.0) / 2.0;
for index in 0..REFLECT_ROW_COUNT {
let x = start + index as f32 * REFLECT_ROW_SPACING;
let roughness = index as f32 / (REFLECT_ROW_COUNT as f32 - 1.0);
ctx.draw(
Sphere {
subdivisions: SPHERE_SUBDIVISIONS,
}
.at(Transform::from_scale_rotation_translation(
Vec3::splat(REFLECT_ROW_RADIUS * 2.0),
Quat::IDENTITY,
Vec3::new(x, REFLECT_ROW_RADIUS, REFLECT_ROW_Z),
))
.material(
Material::lit(Color::WHITE)
.roughness(roughness)
.metallic(1.0),
),
);
}
}
fn draw_outpost(&self, ctx: &mut FrameContext<'_, Self>) {
let clock = ctx.elapsed().as_secs_f32();
for &(position, scale) in &PILLARS {
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
scale,
Quat::IDENTITY,
OUTPOST + position,
))
.material(Material::lit(Color::rgb(0.55, 0.5, 0.45))),
);
}
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
POLE_SCALE,
Quat::IDENTITY,
OUTPOST + POLE_POSITION,
))
.material(Material::lit(Color::rgb(0.3, 0.24, 0.18))),
);
ctx.set_surface_style(Banner { time: clock });
ctx.draw(
BannerCloth
.at(Transform::from_translation(OUTPOST + BANNER_MOUNT))
.material(Material::lit(Color::rgb(0.75, 0.12, 0.12)))
.surface_style::<Banner>(),
);
ctx.set_surface_style(Field {
tint: Color::rgb(0.25, 0.75, 1.0),
time: clock,
});
ctx.draw(
Sphere { subdivisions: 2 }
.at(Transform::from_scale_rotation_translation(
Vec3::splat(FIELD_ORB_SCALE),
Quat::IDENTITY,
OUTPOST + FIELD_ORB_POSITION,
))
.material(Material::color(Color::BLACK))
.surface_style::<Field>(),
);
}
fn lights(&self) -> Vec<Light> {
let lamp = Light::point(LAMP_POSITION, self.lamp.scaled(), LAMP_RANGE);
let spotlight = Light::spot(Spot {
position: SPOT_POSITION,
direction: SPOT_DIRECTION,
color: self.spotlight.scaled(),
range: SPOT_RANGE,
angle: SPOT_ANGLE,
});
let mut lights = vec![
if self.lamp.shadow {
lamp.shadow()
} else {
lamp
},
if self.spotlight.shadow {
spotlight.shadow()
} else {
spotlight
},
];
if let Some((direction, color, strength)) = self.sky.sun() {
let sun = Light::directional(direction, scaled(color, strength));
lights.push(if self.sun_shadow { sun.shadow() } else { sun });
}
lights
}
fn controls(&mut self, ctx: &mut FrameContext<'_, Self>) {
let tonemap = ctx.config().tonemap();
let antialiasing = ctx.config().antialiasing();
let shadow_resolution = ctx.config().shadow_resolution();
ctx.ui(|ui| {
egui::Panel::right("controls")
.resizable(false)
.default_size(300.0)
.show(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label(
"right mouse button to look, W/A/S/D to move, \
space/shift up and down, wheel to scale speed",
);
ui.separator();
self.lighting_controls(ui);
ui.separator();
self.light_controls(ui);
ui.separator();
self.material_controls(ui);
ui.separator();
ui.label(format!(
"tone map {tonemap:?} \u{b7} antialiasing {antialiasing} \u{b7} \
shadow {shadow_resolution}px: set at startup, not live"
));
ui.add(egui::Slider::new(&mut self.exposure, 0.1..=3.0).text("exposure"));
ui.add(egui::Slider::new(&mut self.bloom, 0.0..=1.0).text("bloom"));
});
});
});
}
fn lighting_controls(&mut self, ui: &mut egui::Ui) {
ui.heading("lighting");
for choice in Sky::ALL {
ui.radio_value(&mut self.sky, choice, choice.name());
}
ui.label(format!("sky light {:.2}: set at startup", self.sky.light()));
match self.sky.sun() {
Some((_, color, strength)) => {
color_swatch(ui, "sun", color);
ui.label(format!("sun strength {strength:.2}: set by the choice"));
ui.checkbox(&mut self.sun_shadow, "sun shadow");
}
None => {
ui.label("no sun: set by the choice");
}
}
}
fn light_controls(&mut self, ui: &mut egui::Ui) {
ui.heading("lights");
glow_controls(ui, "lamp", &mut self.lamp);
glow_controls(ui, "spotlight", &mut self.spotlight);
}
fn material_controls(&mut self, ui: &mut egui::Ui) {
ui.heading("material");
color_row(ui, "tint", &mut self.front_tint);
ui.add(egui::Slider::new(&mut self.front_roughness, 0.0..=1.0).text("roughness"));
ui.add(egui::Slider::new(&mut self.front_metallic, 0.0..=1.0).text("metallic"));
ui.checkbox(
&mut self.shading_map_on,
"shading map: occlusion \u{b7} roughness \u{b7} metallic",
);
ui.checkbox(&mut self.relief_map_on, "relief map: bump normals");
ui.checkbox(&mut self.emissive_map_on, "emissive map: per-texel glow");
}
}
fn glow_controls(ui: &mut egui::Ui, label: &str, glow: &mut Glow) {
ui.label(label);
color_row(ui, "color", &mut glow.color);
ui.add(egui::Slider::new(&mut glow.strength, 0.0..=8.0).text("strength"));
ui.checkbox(&mut glow.shadow, "shadow");
}
fn color_row(ui: &mut egui::Ui, label: &str, color: &mut Color) {
let mut rgb = [color.red, color.green, color.blue];
ui.horizontal(|ui| {
ui.label(label);
if ui.color_edit_button_rgb(&mut rgb).changed() {
*color = Color::rgb(rgb[0], rgb[1], rgb[2]);
}
});
}
fn color_swatch(ui: &mut egui::Ui, label: &str, color: Color) {
let mut rgb = [color.red, color.green, color.blue];
ui.horizontal(|ui| {
ui.label(label);
ui.add_enabled_ui(false, |ui| {
ui.color_edit_button_rgb(&mut rgb);
});
});
}
impl Game for Playground {
type Meshes = Shape;
type Sounds = NoSounds;
type InputActions = Controls;
type Skyboxes = Sky;
type SurfaceStyles = Looks;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.fly_camera(ctx);
ctx.set_camera(self.camera());
ctx.set_skybox(self.sky);
for light in self.lights() {
ctx.light(light);
}
ctx.set_exposure(self.exposure);
ctx.set_bloom(self.bloom);
self.draw_scene(ctx);
self.controls(ctx);
}
}
fn main() {
let config = Config::new("Mirage: material playground")
.with_size(1280, 720)
.with_assets([
"examples/assets/sky-clear.png",
"examples/assets/sky-classic.png",
"examples/assets/sky-dawn.png",
"examples/assets/sky-sinister.png",
"examples/assets/sky-stars-lightblue.png",
"examples/assets/sky-stars-blue.png",
]);
run(config, Playground::init);
}