#[repr(C)]pub struct Color {
pub red: f32,
pub green: f32,
pub blue: f32,
pub alpha: f32,
}Expand description
A red, green, blue, and opacity color, each a linear value the engine encodes to sRGB on its way to the window.
Nothing clamps a channel: a value past 1.0 is light the frame holds
and the tone map brings down, which is what an emissive material or a
bright light is written with. 0.0..=1.0 is what the window can show.
Values are linear; the engine writes sRGB-encoded values to the screen.
A color channel past 1.0 holds light no screen draws, which
FrameContext::set_bloom spreads.
Fields§
§red: f32Red.
green: f32Green.
blue: f32Blue.
alpha: f32Opacity: 1.0 is fully opaque, 0.0 is empty.
Implementations§
Source§impl Color
impl Color
Sourcepub const fn rgb(red: f32, green: f32, blue: f32) -> Self
pub const fn rgb(red: f32, green: f32, blue: f32) -> Self
An opaque color, each channel a fraction of 1.0.
Examples found in repository?
More examples
261const SUN_COLOR: Color = Color::rgb(0.92, 0.87, 0.72);
262/// The sun's direction: low enough that props cast shadows about their own
263/// length, and from over the player's left shoulder, which is where the
264/// tileset's own sprites are shaded from.
265const SUN_DIRECTION: Vec3 = Vec3::new(0.6, -0.75, 0.4);
266
267// ---------------------------------------------------------------------
268// Cave layout
269// ---------------------------------------------------------------------
270
271const CAVE_HALF_WIDTH: f32 = 4.0;
272/// Room tiling and walls toward the camera, past the wall closing it: under
273/// the bottom corners of the frame, wherever the player walks.
274const CAVE_NEAR_Z: f32 = 10.0;
275const CAVE_FAR_Z: f32 = -6.0;
276/// Walkable extent of the room at its far end: the back wall's inner face,
277/// less the player's own half-width, so the sprite stops flush against
278/// stone.
279const CAVE_WALK_FAR_Z: f32 = CAVE_FAR_Z + TILE_SIZE * 0.5 + WALKER_WIDTH * 0.5;
280/// Walkable extent at the room's near end: past the plane stepping out
281/// crosses, so the last step through the mouth is never clamped away. The
282/// near wall itself is what keeps the player from the ledge beyond.
283const CAVE_WALK_NEAR_Z: f32 = CAVE_EXIT.z + PLAYER_RADIUS;
284/// As many meters as the wall face is tiles tall, so it samples the sheet
285/// at the floor's own texels to the meter.
286const WALL_HEIGHT: f32 = 3.0;
287/// Wall height: over the camera, so it neither looks over them nor down onto
288/// their tops.
289const WALL_TOP: f32 = CAVE_CAMERA_OFFSET.y + TILE_SIZE * 0.5;
290/// Courses that takes; the last one is cut to what is left of it.
291const WALL_COURSES: i32 = (WALL_TOP / WALL_HEIGHT) as i32 + 1;
292/// The tile row the room's near end is closed along, from each side wall to
293/// the mouth in it.
294const CAVE_LIP_Z: f32 = 5.0;
295/// Height of that wall: one tile, so its top face is at the floor's own
296/// texels to the meter, and low enough that it never hides the player.
297const CAVE_LIP_HEIGHT: f32 = TILE_SIZE;
298/// The cave's own mouth position: in the near wall, its pillars half their
299/// depth past it.
300const CAVE_EXIT: Vec3 = Vec3::new(0.0, 0.0, CAVE_LIP_Z + MOUTH_PILLAR_SIZE.z * 0.5);
301/// Landing position for an entry into the cave: past the [`EXIT`] band, so
302/// the entry does not count as another step through the mouth.
303const CAVE_SPAWN: Vec3 = Vec3::new(
304 CAVE_EXIT.x,
305 0.0,
306 CAVE_EXIT.z - MOUTH_CROSSING_INSET - PORTAL_CLEARANCE,
307);
308
309const DOOR_Z: f32 = 0.0;
310/// The near face of the wall the door hangs in: past it the wall is
311/// between the camera and the player, and is drawn through.
312const DOOR_WALL_NEAR_Z: f32 = DOOR_Z + TILE_SIZE * 0.5;
313/// Alpha for that wall and its door once the player has been behind them
314/// for [`GHOST_RAMP_TICKS`]: opaque enough for stone in a dark room,
315/// translucent enough for the chamber and the player to show through.
316const GHOST_ALPHA: f32 = 0.6;
317/// The fade of a draw nothing is seen through.
318const SOLID: f32 = 1.0;
319/// Ticks the wall and door take to fade between [`SOLID`] and
320/// [`GHOST_ALPHA`].
321const GHOST_RAMP_TICKS: u32 = 6;
322/// Span either side of the player the wall is drawn through: wide enough for
323/// the sight line to the player, no wider, so no more of the light the
324/// stone holds off the chamber passes through it than that line needs.
325const GHOST_CORRIDOR_HALF: f32 = 1.5;
326/// The door's hinge: `tools/keep_fixture.py`'s box is hinged at its own
327/// local origin and spans [`DOOR_WIDTH`] along local `+X`.
328const DOOR_HINGE: Vec3 = Vec3::new(-DOOR_WIDTH * 0.5, 0.0, DOOR_Z);
329/// Width of that box, as the fixture builds it.
330const DOOR_WIDTH: f32 = 1.0;
331/// Thickness of it, as the fixture builds it.
332const DOOR_THICKNESS: f32 = 0.12;
333/// Height of it, as the fixture builds it: the stone over the doorway starts
334/// here.
335const DOOR_HEIGHT: f32 = 1.9;
336/// Half the doorway the door hangs in: one tile wide, on the room's axis.
337const DOORWAY_HALF: f32 = TILE_SIZE * 0.5;
338/// Span from that axis of the wall the door hangs in: to the side walls'
339/// inner faces.
340const DOOR_WALL_END: f32 = CAVE_HALF_WIDTH + TILE_SIZE * 0.5;
341const INTERACT_POINT: Vec3 = Vec3::new(0.0, 0.0, DOOR_Z);
342const INTERACT_RADIUS: f32 = 1.8;
343
344/// The door slab's own color, a deep red the cave's grey stone never is, so
345/// the slab reads as a door rather than more wall.
346const DOOR_COLOR: Color = Color::rgb(0.58, 0.16, 0.09);
347const DOOR_LITNESS: f32 = 0.85;
348/// Thickness of the posts and lintel framing the doorway, and how far in
349/// front of the wall face they stand, clear of z-fighting with it.
350const DOOR_FRAME_THICKNESS: f32 = 0.14;
351const DOOR_FRAME_STANDOFF: f32 = 0.03;
352/// The frame's own color, visible from the cave's own spawn well before a
353/// torch reaches the doorway.
354const DOOR_FRAME_COLOR: Color = Color::rgb(0.85, 0.55, 0.2);
355const DOOR_FRAME_LITNESS: f32 = 0.9;
356/// The frame's own light while the door is closed and within
357/// [`INTERACT_RADIUS`]: the cue that it can be opened.
358const DOOR_FRAME_GLOW: Color = Color::rgb(1.6, 0.9, 0.35);
359
360const GEM_POSITION: Vec3 = Vec3::new(0.0, 0.5, -4.6);
361const GEM_BOB_HEIGHT: f32 = 0.12;
362const GEM_SPIN_SPEED: f32 = 1.4;
363const PICKUP_RADIUS: f32 = 1.0;
364const GEM_COLOR: Color = Color::rgb(0.35, 0.95, 0.85);
365/// The gem's own light: what lights the chamber until the door opens on the
366/// torches, and gone with the gem once it is taken.
367const GEM_LIGHT_COLOR: Color = Color::rgb(0.3, 0.85, 0.78);
368/// Reach of it: short of the door wall, so what the wall casts never depends
369/// on the gem.
370const GEM_LIGHT_RANGE: f32 = 4.0;
371/// Lift over the gem, clear of the gem's own facets, which would otherwise
372/// shadow the chamber from inside it.
373const GEM_LIGHT_LIFT: f32 = 0.7;
374
375/// The two torches' position, `x, z`: flanking the doorway on the near side
376/// of the wall, the side the player arrives on.
377const TORCH_POSITIONS: [(f32, f32); 2] = [(-3.0, 0.6), (3.0, 0.6)];
378const TORCH_STAND_HEIGHT: f32 = 2.0;
379/// Thickness of a torch's post: the four texels the sprite draws it as,
380/// which is also what it blocks the player as.
381const TORCH_STAND_WIDTH: f32 = 0.25;
382/// Width of the sprite around that post: its canvas is twice the post,
383/// transparent either side.
384const TORCH_SPRITE_WIDTH: f32 = TORCH_STAND_WIDTH * 2.0;
385const TORCH_LIGHT_RANGE: f32 = 10.0;
386/// Height above its flame a torch's light is placed, and how far it is
387/// offset from the post toward the room: straight over the post, its own face
388/// turns edge-on to the light and goes dark.
389const TORCH_LIGHT_LIFT: f32 = 0.7;
390const TORCH_LIGHT_STANDOFF: f32 = 0.8;
391const TORCH_LIGHT_COLOR: Color = Color::rgb(1.0, 0.6, 0.28);
392/// Size of one cell of the flame's loop: a tile, as everything else the
393/// tilesets draw.
394const FLAME_SIZE: f32 = TILE_SIZE;
395/// Lift of the flame's center over the post's top: its own half-height, less
396/// the overlap that keeps the two from parting.
397const FLAME_LIFT: f32 = FLAME_SIZE * 0.5 - 0.1;
398/// The flame's tint, past `1.0`: an additive draw's tint scales its sampled
399/// texel, so this lifts the flame's own lit texels out of the cave's dark
400/// without a flat color added over its dark, unlit base.
401const FLAME_TINT: Color = Color::rgb(2.2, 1.5, 0.7);51const ROCK_COLOR: Color = Color::rgb(0.42, 0.4, 0.38);
52const GROUND_COLOR: Color = Color::rgb(0.16, 0.17, 0.14);
53
54const SUN_DIRECTION: Vec3 = Vec3::new(-0.35, -1.0, -0.5);
55const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
56
57/// The sky's own zenith, horizon and nadir colors, and the fraction of its
58/// own light it lands on top of the sun's, held low so the sun's shadows
59/// still read.
60const SKY_ZENITH: Color = Color::rgb(0.25, 0.4, 0.65);
61const SKY_HORIZON: Color = Color::rgb(0.75, 0.72, 0.62);
62const SKY_NADIR: Color = Color::rgb(0.12, 0.12, 0.1);82const LIGHT_TILE: Color = Color::rgb(0.80, 0.76, 0.64);
83const DARK_TILE: Color = Color::rgb(0.55, 0.50, 0.40);
84/// A reachable tile's own mark, smaller than the tile itself so the
85/// checker tone still shows around its edge.
86const REACHABLE_MARK: Color = Color::rgb(0.20, 0.85, 0.35);
87/// The fraction of a tile's own footprint the reachable mark draws at,
88/// small enough that the tile's own checker tone still shows around it.
89const REACHABLE_MARK_SCALE: f32 = 0.45;
90/// The reachable mark's own lift over the tile's surface, clear of
91/// z-fighting with it.
92const REACHABLE_MARK_LIFT: f32 = 0.01;
93/// The mark under the selected unit, its own color bright enough to read
94/// past the sprite's own tint at a distance.
95const CURRENT_MARK: Color = Color::rgb(1.0, 0.2, 0.75);
96const CURRENT_MARK_SCALE: f32 = 0.85;
97/// The mark under the unit whose turn it is while nothing is selected:
98/// smaller and dim next to [`CURRENT_MARK`], a hint rather than a claim.
99const TURN_MARK: Color = Color::rgb(0.85, 0.75, 0.15);
100const TURN_MARK_SCALE: f32 = 0.5;
101/// The tile a hover reads while a unit is selected: reachable, or blocked
102/// by the other unit standing there.
103const HOVER_REACHABLE_TILE: Color = Color::rgb(0.35, 0.75, 0.68);
104const HOVER_BLOCKED_TILE: Color = Color::rgb(0.62, 0.28, 0.26);
105const BLOCK_IDLE: Color = Color::rgb(0.32, 0.42, 0.58);
106/// `BLOCK_IDLE`, scaled toward white to mark the block unit's own turn.
107const BLOCK_TURN: Color = Color::rgb(0.42, 0.54, 0.72);
108const GROUND_COLOR: Color = Color::rgb(0.15, 0.16, 0.13);
109const ROCK_COLOR: Color = Color::rgb(0.42, 0.40, 0.38);
110const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
111
112/// The current unit's tint, close to white so the sprite's own texture
113/// still reads under it, and the light it adds on its own, low enough
114/// that the same texture still reads under its bloom too — distinct from
115/// `SELECTED_TINT`, so a hover and a selection never read the same.
116const HOVER_TINT: Color = Color::rgb(0.9, 1.15, 1.15);
117const HOVER_GLOW: Color = Color::rgb(0.02, 0.15, 0.2);
118
119/// The current unit's tint, close to white with more red where
120/// `HOVER_TINT` raises green and blue instead, so the sprite's own
121/// texture still reads under it, and the light it adds on its own,
122/// scaled down the same way `HOVER_GLOW` is — distinct from `HOVER_TINT`.
123const SELECTED_TINT: Color = Color::rgb(1.15, 0.95, 0.85);
124const SELECTED_GLOW: Color = Color::rgb(0.22, 0.11, 0.0);
125
126/// The unit whose turn it is shows this tint and glow before any hover or
127/// selection, so it reads as the one a click selects.
128const TURN_TINT: Color = Color::rgb(1.0, 1.0, 0.82);
129const TURN_GLOW: Color = Color::rgb(0.08, 0.08, 0.02);
130
131/// The fraction of the frame `set_bloom` spreads, so `HOVER_GLOW` and
132/// `SELECTED_GLOW` read as light around the current unit, not only a
133/// larger fill.
134const BLOOM: f32 = 0.35;
135
136/// The size a world-space prompt naming a click's effect reads at, in
137/// logical points.
138const PROMPT_SIZE: f32 = 15.0;
139/// Height a prompt is lifted over the tile it names, clear of the tile's
140/// own top corner under the diagonal view.
141const PROMPT_TILE_LIFT: f32 = 0.55;
142/// Height a prompt is lifted over the unit it names, past its own height.
143const PROMPT_UNIT_LIFT: f32 = 0.25;
144/// Margin a prompt's own backdrop keeps past its galley, in logical points.
145const PROMPT_PADDING: f32 = 4.0;
146/// How much dark a prompt's own backdrop puts behind its text.
147const PROMPT_BACKDROP: u8 = 190;
148const PROMPT_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
149
150/// Thirty steps a second, half the engine's default rate; movement stays
151/// smooth through `alpha()` interpolation.
152const TICK_INTERVAL: Duration = Duration::from_nanos(33_333_333);
153
154/// Faces of a cube, each a normal with its right and up axes — the same
155/// layout `mesh::Cube` builds from, shared so a rock's corners hold the
156/// same eight positions between the faces that meet there.
157const ROCK_FACES: [(Vec3, Vec3, Vec3); 6] = [
158 (Vec3::X, Vec3::NEG_Z, Vec3::Y),
159 (Vec3::NEG_X, Vec3::Z, Vec3::Y),
160 (Vec3::Y, Vec3::X, Vec3::NEG_Z),
161 (Vec3::NEG_Y, Vec3::X, Vec3::Z),
162 (Vec3::Z, Vec3::X, Vec3::Y),
163 (Vec3::NEG_Z, Vec3::NEG_X, Vec3::Y),
164];
165
166const ROCK_TRIANGLES: [u32; 6] = [0, 1, 2, 0, 2, 3];
167
168fn main() {
169 run(
170 Config::new("Mirage: isometric board")
171 .with_size(1280, 720)
172 .with_assets([SPRITE_SOURCE, CLICK_SOURCE])
173 .with_tick_interval(TICK_INTERVAL),
174 Board::init,
175 );
176}
177
178/// A rock built for its own `seed`; each value is its own mesh.
179#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
180#[catalog(Self { seed: 0 })]
181struct Rock {
182 seed: u32,
183}
184
185impl Mesh for Rock {
186 fn build(&self, _: &Assets) -> MeshData {
187 build_rock(self.seed)
188 }
189}
190
191/// The sprite unit: a quad windowed to the walk sheet's row facing right.
192#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
193struct Sprite;
194
195impl Mesh for Sprite {
196 fn build(&self, assets: &Assets) -> MeshData {
197 Quad.build(assets)
198 .with_texture(assets.texture(SPRITE_TEXTURE).pixelated())
199 }
200}
201
202// Everything else this game draws: the ground and board tiles are the
203// engine's own Plane and Cube, given their color per draw; the block unit
204// draws as a plain Cube too.
205meshes! { enum Shape { Plane, Cube, Rock, Sprite } }
206
207/// The board's own sky: a dim gradient, so the sun stays the scene's
208/// brightest light.
209#[derive(Catalog, Clone, Copy, Debug, PartialEq, Eq, Hash)]
210enum Sky {
211 Day,
212}
213
214impl Skyboxes for Sky {
215 fn build(&self, _assets: &Assets) -> SkyboxData {
216 SkyboxData::gradient(
217 Color::rgb(0.55, 0.75, 0.95),
218 Color::rgb(0.85, 0.90, 0.95),
219 Color::rgb(0.35, 0.33, 0.30),
220 )
221 .lit_by(0.3)
222 }47const FLOOR_COLOR: Color = Color::rgb(0.14, 0.14, 0.17);
48const WALL_COLOR: Color = Color::rgb(0.22, 0.24, 0.30);
49const SUN_COLOR: Color = Color::rgb(0.85, 0.85, 0.90);
50const LISTENER_COLOR: Color = Color::rgb(0.85, 0.85, 0.75);
51/// Red on the right, white on the left: the pair the engine's own pan
52/// reads, set on the ears so the sides are distinct.
53const RIGHT_EAR_COLOR: Color = Color::rgb(0.85, 0.2, 0.2);
54const LEFT_EAR_COLOR: Color = Color::rgb(0.92, 0.92, 0.88);
55const SOURCE_COLORS: [Color; 3] = [
56 Color::rgb(0.85, 0.35, 0.35),
57 Color::rgb(0.35, 0.75, 0.85),
58 Color::rgb(0.85, 0.75, 0.30),
59];
60const RANGE_COLOR: Color = Color::rgba(1.0, 1.0, 1.0, 0.35);
61/// The inner ring drawn around a source, where its level stops being full.
62const REFERENCE_COLOR: Color = Color::rgba(1.0, 0.85, 0.35, 0.5);
63
64const SKY_ZENITH: Color = Color::rgb(0.10, 0.11, 0.16);
65const SKY_HORIZON: Color = Color::rgb(0.20, 0.20, 0.24);
66const SKY_NADIR: Color = Color::rgb(0.06, 0.06, 0.08);
67/// The fraction of its own light the sky lands and reflects: dim, so the
68/// cubes' own glow and the room's light still read.
69const SKY_LIGHT: f32 = 0.2;
70
71/// Two fixed positions holding the same clip at the default instance,
72/// read only while [`SoundCheck::merge_demo`] is set.
73const MERGE_POS_A: Vec3 = Vec3::new(-4.0, SOURCE_HEIGHT, 4.5);
74const MERGE_POS_B: Vec3 = Vec3::new(4.0, SOURCE_HEIGHT, 4.5);
75const MERGE_GAIN: f32 = 0.5;
76const MERGE_COLOR_A: Color = Color::rgb(0.95, 0.55, 0.15);
77const MERGE_COLOR_B: Color = Color::rgb(0.55, 0.4, 0.85);
78
79/// Where [`Sound::Theme`] and [`Sound::ThemeDecoded`] loop from once
80/// declared: a few seconds short of the end, so a wrap seeks across most of
81/// the clip.
82const THEME_LOOP_FROM: Duration = Duration::from_secs(130);
83
84/// Count of sustains the ring declares at once: more than the engine plays,
85/// so the least loud of them hold no voice.
86const RING_COUNT: u32 = MAX_VOICES as u32 + 8;
87/// Radius of the ring they stand on.
88const RING_RADIUS: f32 = 4.6;
89/// Distance the ring's sustains hold their full level within: over half the
90/// radius, so the whole ring is heard from the middle of the room.
91const RING_REFERENCE: f32 = 2.5;
92/// Gain the loudest of them takes; each one after it is less loud, so the cut
93/// falls inside the ring.
94const RING_GAIN: f32 = 0.35;
95/// Color of the ring's cubes, taken less bright the less loud the sustain a
96/// cube stands for.
97const RING_COLOR: Color = Color::rgb(0.35, 0.75, 0.95);
98
99/// Every source this example loads, next to `index.html` on the web and
100/// under the working directory on the desktop.
101const ASSET_FILES: [&str; 9] = [
102 "examples/assets/bounce.ogg",
103 "examples/assets/break.ogg",
104 "examples/assets/serve.ogg",
105 "examples/assets/gameover.ogg",
106 "examples/assets/lost.ogg",
107 "examples/assets/win.ogg",
108 "examples/assets/click.ogg",
109 "examples/assets/music.ogg",
110 "examples/assets/menu_music.ogg",
111];
112
113fn main() {
114 run(
115 Config::new("Mirage: sound lab")
116 .with_size(1280, 720)
117 .with_assets(ASSET_FILES),
118 SoundCheck::init,
119 );
120}
121
122/// The one sky this room draws, a gradient set each frame.
123#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
124enum Sky {
125 Room,
126}
127
128impl Skyboxes for Sky {
129 fn build(&self, _assets: &Assets) -> SkyboxData {
130 match self {
131 Self::Room => {
132 SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT)
133 }
134 }
135 }
136}
137
138/// A source's reference or its range: a flat ring on the ground,
139/// unit-sized, drawn that many meters across by its scale.
140#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
141struct Ring;
142
143impl Mesh for Ring {
144 fn build(&self, _: &Assets) -> MeshData {
145 ring_outline()
146 }
147}
148
149/// The listener's facing marker: a point through `-Z`, unit-sized.
150#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
151struct Facing;
152
153impl Mesh for Facing {
154 fn build(&self, _: &Assets) -> MeshData {
155 facing_marker()
156 }
157}
158
159// Everything else this game draws is a built-in primitive, given its
160// color and placed per draw: the room's floor and walls, a cube drawn for
161// a source or the listener, and the listener's ears.
162meshes! { enum Shape { Plane, Cube, Ring, Sphere, Facing } }
163
164fn ring_outline() -> MeshData {
165 const SEGMENTS: u32 = 48;
166 const OUTER: f32 = 1.0;
167 const INNER: f32 = 0.94;
168
169 let mut vertices = Vec::with_capacity(SEGMENTS as usize * 4);
170 let mut indices = Vec::with_capacity(SEGMENTS as usize * 6);
171 for segment in 0..SEGMENTS {
172 let a0 = segment as f32 / SEGMENTS as f32 * TAU;
173 let a1 = (segment + 1) as f32 / SEGMENTS as f32 * TAU;
174 let (u0, v0) = (a0.cos(), a0.sin());
175 let (u1, v1) = (a1.cos(), a1.sin());
176 let base = vertices.len() as u32;
177 vertices.extend([
178 Vertex::new(Vec3::new(INNER * u0, 0.0, -INNER * v0), Vec3::Y, Vec2::ZERO),
179 Vertex::new(Vec3::new(OUTER * u0, 0.0, -OUTER * v0), Vec3::Y, Vec2::ZERO),
180 Vertex::new(Vec3::new(OUTER * u1, 0.0, -OUTER * v1), Vec3::Y, Vec2::ZERO),
181 Vertex::new(Vec3::new(INNER * u1, 0.0, -INNER * v1), Vec3::Y, Vec2::ZERO),
182 ]);
183 indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
184 }
185 MeshData::new(vertices, indices)
186}
187
188fn facing_marker() -> MeshData {
189 const TIP: Vec3 = Vec3::new(0.0, 0.0, -0.5);
190 const BACK: [Vec3; 4] = [
191 Vec3::new(-0.5, -0.5, 0.5),
192 Vec3::new(0.5, -0.5, 0.5),
193 Vec3::new(0.5, 0.5, 0.5),
194 Vec3::new(-0.5, 0.5, 0.5),
195 ];
196
197 let mut vertices = Vec::with_capacity(BACK.len() * 3);
198 for (corner, next) in BACK.iter().zip(BACK.iter().cycle().skip(1)) {
199 let normal = (next - corner).cross(TIP - corner).normalize();
200 vertices.extend([
201 Vertex::new(*corner, normal, Vec2::new(0.0, 1.0)),
202 Vertex::new(*next, normal, Vec2::new(1.0, 1.0)),
203 Vertex::new(TIP, normal, Vec2::new(0.5, 0.0)),
204 ]);
205 }
206 let indices = (0..vertices.len() as u32).collect();
207 MeshData::new(vertices, indices)
208}
209
210/// Every sound this game plays. [`Sound::Break`] and [`Sound::Pulse`] read the
211/// same source under two names, so sustaining one and playing the other
212/// once never share a voice; [`Sound::Theme`] and [`Sound::ThemeDecoded`] do
213/// the same for the streamed side against the decoded one, since a clip
214/// decodes one way or the other for good, once built.
215#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
216enum Sound {
217 Bounce,
218 Break,
219 Serve,
220 GameOver,
221 Lost,
222 Win,
223 Click,
224 Theme,
225 ThemeDecoded,
226 MenuTheme,
227 Pulse,
228}
229
230impl Sound {
231 /// The alternatives a one-shot play offers.
232 const ONE_SHOTS: [Sound; 7] = [
233 Sound::Bounce,
234 Sound::Break,
235 Sound::Serve,
236 Sound::GameOver,
237 Sound::Lost,
238 Sound::Win,
239 Sound::Click,
240 ];
241
242 /// The alternatives one source's sustain offers.
243 const SOURCE_CHOICES: [Sound; 9] = [
244 Sound::Bounce,
245 Sound::Break,
246 Sound::Serve,
247 Sound::GameOver,
248 Sound::Lost,
249 Sound::Win,
250 Sound::Click,
251 Sound::Theme,
252 Sound::ThemeDecoded,
253 ];
254
255 fn label(self) -> &'static str {
256 match self {
257 Sound::Bounce => "bounce",
258 Sound::Break => "break",
259 Sound::Serve => "serve",
260 Sound::GameOver => "game over",
261 Sound::Lost => "lost",
262 Sound::Win => "win",
263 Sound::Click => "click",
264 Sound::Theme => "theme (streamed)",
265 Sound::ThemeDecoded => "theme (decoded)",
266 Sound::MenuTheme => "menu theme",
267 Sound::Pulse => "pulse",
268 }
269 }
270}
271
272impl Sounds for Sound {
273 fn build(&self, assets: &Assets) -> SoundData {
274 match self {
275 Sound::Bounce => assets.sound("bounce"),
276 Sound::Break => assets.sound("break"),
277 Sound::Serve => assets.sound("serve"),
278 Sound::GameOver => assets.sound("gameover"),
279 Sound::Lost => assets.sound("lost"),
280 Sound::Win => assets.sound("win"),
281 Sound::Click => assets.sound("click"),
282 Sound::Theme => assets.sound("music").streamed(),
283 Sound::ThemeDecoded => assets.sound("music"),
284 Sound::MenuTheme => assets.sound("menu_music").streamed(),
285 Sound::Pulse => assets.sound("break"),
286 }
287 }
288}
289
290/// The one button this game reads: it holds a source down and moves it.
291#[derive(InputButtonAction, Clone, Copy, PartialEq)]
292enum Button {
293 Select,
294}
295
296impl InputButtonAction for Button {
297 fn bindings(&self) -> Vec<ButtonBinding> {
298 match self {
299 Button::Select => vec![MouseButton::Left.into()],
300 }
301 }
302}
303
304/// The listener's walk, in the ground plane.
305#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
306enum Move {
307 Walk,
308}
309
310impl InputAxis2Action for Move {
311 fn bindings(&self) -> Vec<Axis2Binding> {
312 match self {
313 Move::Walk => vec![
314 Axis2Binding::from(ButtonAxis2 {
315 left: Key::A,
316 right: Key::D,
317 down: Key::S,
318 up: Key::W,
319 }),
320 Axis2Binding::stick(Stick::Left),
321 ],
322 }
323 }
324}
325
326struct Controls;
327
328impl InputActions for Controls {
329 type Button = Button;
330 type Axis = NoInputAxes;
331 type Axis2 = Move;
332}
333
334/// One source a drag moves: a cube on the ground, playing a sustained clip
335/// with its own gain, reference, range, and pitch.
336struct Source {
337 position: Vec3,
338 sound: Sound,
339 gain: f32,
340 reference: f32,
341 range: f32,
342 pitch: f32,
343 /// Whether this source sustains at all; off keeps startup silent.
344 enabled: bool,
345}
346
347impl Source {
348 fn new(x: f32, z: f32, sound: Sound, range: f32, enabled: bool) -> Self {
349 Self {
350 position: Vec3::new(x, SOURCE_HEIGHT, z),
351 sound,
352 gain: 0.5,
353 reference: SOURCE_REFERENCE,
354 range,
355 pitch: 1.0,
356 enabled,
357 }
358 }
359
360 /// This source's sustained cue, with the loop point that seeks far
361 /// where its choice needs one.
362 fn cue(&self) -> SoundCue<Sound> {
363 let cue = self
364 .sound
365 .at(self.position)
366 .gain(self.gain)
367 .reference(self.reference)
368 .range(self.range)
369 .pitch(self.pitch);
370 match self.sound {
371 Sound::Theme | Sound::ThemeDecoded => cue.loop_from(THEME_LOOP_FROM),
372 _ => cue,
373 }
374 }
375}
376
377struct SoundCheck {
378 master_volume: f32,
379
380 picked: Sound,
381 one_shot_gain: f32,
382 one_shot_pitch: f32,
383 one_shot_fade: f32,
384 trim_start: f32,
385 trim_end: f32,
386 one_shot_loop_from: f32,
387
388 theme_on: bool,
389 menu_on: bool,
390 pulse_on: bool,
391 cue_fade: f32,
392
393 /// Sustains [`Sound::Click`] at [`MERGE_POS_A`] and [`MERGE_POS_B`]
394 /// both at the default instance: shows the merge each source's own
395 /// instance above keeps clear of.
396 merge_demo: bool,
397
398 /// Declares [`RING_COUNT`] sustains at once, more than the engine
399 /// plays, so that the cap is heard as it allocates by level.
400 ring_demo: bool,
401
402 player: Vec2,
403 player_prev: Vec2,
404 sources: [Source; 3],
405 dragging: Option<usize>,
406
407 /// Every catalog value's length, read once at startup.
408 durations: HashMap<Sound, Duration>,
409}
410
411impl SoundCheck {
412 fn init(ctx: &mut InitContext<'_, SoundCheck>) -> Result<Self, Error> {
413 let durations = ctx.durations();
414
415 let picked = Sound::Bounce;
416 let trim_end = durations.get(&picked).copied().unwrap_or_default();
417
418 Ok(Self {
419 master_volume: 1.0,
420
421 picked,
422 one_shot_gain: 1.0,
423 one_shot_pitch: 1.0,
424 one_shot_fade: SoundCue::<Sound>::DEFAULT_FADE.as_secs_f32(),
425 trim_start: 0.0,
426 trim_end: trim_end.as_secs_f32(),
427 one_shot_loop_from: 0.0,
428
429 theme_on: false,
430 menu_on: false,
431 pulse_on: false,
432 cue_fade: 1.0,
433
434 merge_demo: false,
435 ring_demo: false,
436
437 player: Vec2::ZERO,
438 player_prev: Vec2::ZERO,
439 sources: [
440 Source::new(-2.5, -2.0, Sound::Bounce, 4.0, false),
441 Source::new(2.5, -2.0, Sound::Serve, 4.0, false),
442 Source::new(0.0, 2.8, Sound::Theme, 7.0, true),
443 ],
444 dragging: None,
445
446 durations,
447 })
448 }
449
450 fn camera(player: Vec2) -> Camera {
451 let ground = Vec3::new(player.x, 0.0, player.y);
452 Camera::new(
453 View::look_at(
454 ground + Vec3::new(0.0, CHASE_UP, CHASE_BACK),
455 ground + Vec3::Y * 0.5,
456 ),
457 Projection::perspective(55.0),
458 )
459 }
460
461 fn handle_walk(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
462 self.player_prev = self.player;
463 if ctx.ui_wants_keyboard() {
464 return;
465 }
466 let walk = ctx.axis2(Move::Walk);
467 let world = Vec2::new(walk.x, -walk.y);
468 self.player = (self.player + world * WALK_SPEED * ctx.dt().as_secs_f32())
469 .clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
470 }
471
472 /// Takes hold of the source a click's ray intersects, moves it across
473 /// the floor while the button stays down, and frees it on release.
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }
509
510 fn draw_room(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
511 ctx.draw(
512 Plane
513 .at(Transform::from_scale(Vec3::new(
514 ROOM_HALF * 2.0,
515 1.0,
516 ROOM_HALF * 2.0,
517 )))
518 .material(Material::lit(FLOOR_COLOR)),
519 );
520
521 let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, ROOM_HALF);
522 for side in [-1.0, 1.0] {
523 let x = side * (ROOM_HALF - WALL_THICKNESS * 0.5);
524 ctx.draw(
525 Cube.at(Transform::from_scale_rotation_translation(
526 side_half * 2.0,
527 Quat::IDENTITY,
528 Vec3::new(x, side_half.y, 0.0),
529 ))
530 .material(Material::lit(WALL_COLOR)),
531 );
532 }
533 let end_half = Vec3::new(ROOM_HALF, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
534 for side in [-1.0, 1.0] {
535 let z = side * (ROOM_HALF - WALL_THICKNESS * 0.5);
536 ctx.draw(
537 Cube.at(Transform::from_scale_rotation_translation(
538 end_half * 2.0,
539 Quat::IDENTITY,
540 Vec3::new(0.0, end_half.y, z),
541 ))
542 .material(Material::lit(WALL_COLOR)),
543 );
544 }
545 }
546
547 fn draw_sources(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
548 for (index, source) in self.sources.iter().enumerate() {
549 let color = SOURCE_COLORS[index];
550 let picked_up = self.dragging == Some(index);
551 let scale = if picked_up { 1.3 } else { 1.0 };
552 let emissive = if source.enabled {
553 Color::rgb(color.red * 3.0, color.green * 3.0, color.blue * 3.0)
554 } else {
555 color.dimmed(0.15)
556 };
557
558 for (radius, ring_color) in [
559 (source.range, RANGE_COLOR),
560 (source.reference, REFERENCE_COLOR),
561 ] {
562 ctx.draw(
563 Ring.at(Transform::from_scale_rotation_translation(
564 Vec3::new(radius, 1.0, radius),
565 Quat::IDENTITY,
566 Vec3::new(source.position.x, 0.01, source.position.z),
567 ))
568 .material(Material::color(ring_color)),
569 );
570 }
571 ctx.draw(
572 Cube.at(Transform::from_scale_rotation_translation(
573 Vec3::splat(SOURCE_HALF * 2.0 * scale),
574 Quat::IDENTITY,
575 source.position,
576 ))
577 .material(Material::shaded(color, 0.6).emissive(emissive)),
578 );
579 }
580 }58const GROUND_COLOR: Color = Color::rgb(0.24, 0.25, 0.22);
59const SHADING_TINT: Color = Color::rgb(0.55, 0.55, 0.6);
60const RELIEF_TINT: Color = Color::rgb(0.5, 0.45, 0.35);
61const EMISSIVE_BASE: Color = Color::rgb(0.04, 0.04, 0.05);
62const EMISSIVE_GLOW: Color = Color::rgb(3.2, 2.2, 0.7);
63
64/// Texel side length of every generated map: coarse enough that each
65/// checker cell reads as a distinct part on a sphere or a cube face.
66const MAP_SIZE: UVec2 = UVec2::new(64, 64);
67
68/// Checker cell width, in texels, for the shading map.
69const SHADING_CELL: u32 = 8;
70/// The shading checker's two states: occlusion, roughness and metallic —
71/// one square low across all three, the other full across all three.
72const SHADING_LOW: [u8; 3] = [70, 40, 15];
73const SHADING_HIGH: [u8; 3] = [255, 225, 235];
74
75/// Checker cell width, in texels, for the emissive map.
76const EMISSIVE_CELL: u32 = 6;
77
78/// Wave count the relief's texture repeats across its map, and the peak
79/// slope of its surface, in height over distance.
80const BUMP_WAVES: f32 = 6.0;
81const BUMP_SLOPE: f32 = 1.15;
82
83/// `BannerCloth`'s width and height, in meters.
84const BANNER_WIDTH: f32 = 1.1;
85const BANNER_HEIGHT: f32 = 0.7;
86
87/// Columns `BannerCloth` splits into, so its wave curves smoothly.
88const BANNER_COLUMNS: u32 = 10;
89
90/// Where the pillars, the pole, the cloth and the pulsing sphere are
91/// placed, added to every one of their own positions: apart from the
92/// pairs and the reflection row, so a light's shadow has clear ground to
93/// land on.
94const OUTPOST: Vec3 = Vec3::new(-4.6, 0.0, -0.8);
95
96const PILLARS: [(Vec3, Vec3); 3] = [
97 (Vec3::new(-1.8, 0.6, -0.6), Vec3::new(0.6, 1.2, 0.6)),
98 (Vec3::new(0.4, 0.4, -1.6), Vec3::new(0.5, 0.8, 0.5)),
99 (Vec3::new(1.7, 0.9, 0.4), Vec3::new(0.55, 1.8, 0.55)),
100];
101
102const POLE_POSITION: Vec3 = Vec3::new(-2.6, 1.0, 0.4);
103const POLE_SCALE: Vec3 = Vec3::new(0.12, 2.0, 0.12);
104const BANNER_MOUNT: Vec3 = Vec3::new(-2.54, 1.55, 0.46);
105
106const FIELD_ORB_POSITION: Vec3 = Vec3::new(1.3, 1.1, 1.6);
107const FIELD_ORB_SCALE: f32 = 0.7;
108
109/// The lamp's fixed position and reach, in meters.
110const LAMP_POSITION: Vec3 = Vec3::new(2.4, 1.4, -3.0);
111const LAMP_RANGE: f32 = 5.0;
112
113/// The spotlight's fixed placement: where it is placed, which way its
114/// cone points, its reach in meters and its width in radians.
115const SPOT_POSITION: Vec3 = Vec3::new(-3.4, 3.0, 1.6);
116const SPOT_DIRECTION: Vec3 = Vec3::new(0.55, -1.0, -1.0);
117const SPOT_RANGE: f32 = 7.0;
118const SPOT_ANGLE: f32 = 0.5;
119
120/// The camera's vertical field of view, in degrees.
121const CAMERA_FOV: f32 = 46.0;
122/// Where the camera starts, and the `yaw` and the pitch, in radians, it
123/// starts turned to.
124const START_EYE: Vec3 = Vec3::new(-0.6, 2.2, 9.0);
125const START_YAW: f32 = 0.0;
126const START_PITCH: f32 = -0.15;
127/// How far short of straight up or down the pitch may turn, in radians,
128/// where a turn alone reads as nothing.
129const PITCH_LIMIT: f32 = 1.5;
130/// The `eye`'s least height above the ground plane, in meters: held above
131/// zero so a move can never take it below.
132const MIN_EYE_HEIGHT: f32 = 0.3;
133/// Radians the pointer's own motion turns the view by, per physical
134/// pixel it crosses, before the axis it reads through bounds it: a drag
135/// across the whole window turns about a quarter turn.
136const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
137/// Meters a move key covers per second, at [`Playground::speed_scale`]'s
138/// own default.
139const MOVE_SPEED: f32 = 4.0;
140/// The factor one full wheel step multiplies the move speed apart from.
141const SPEED_STEP: f32 = 1.5;
142/// The move speed's own least and most, as a factor of [`MOVE_SPEED`].
143const MIN_SPEED_SCALE: f32 = 0.2;
144const MAX_SPEED_SCALE: f32 = 5.0;
145
146/// Bloom this scene starts at, past the engine's own default: enough that
147/// [`EMISSIVE_GLOW`] and the brightest lights scatter right away.
148const START_BLOOM: f32 = 0.25;
149const START_EXPOSURE: f32 = 1.0;
150
151/// The sky a frame that keeps [`Sky::Default`] draws and is lit by: the
152/// same flat grey the engine falls back to when a frame sets none.
153const DEFAULT_SKY: Color = Color::rgb(0.1, 0.1, 0.1);
154
155/// The fraction of its own light each loaded sky lands and reflects,
156/// through [`SkyboxData::lit_by`]: the bright images fixed low, since an
157/// image read too bright under the frame's own lights at its default
158/// `1.0`; the dim images fixed more, since the scene read too dark under
159/// them at the bright images' value.
160const CLEAR_SKY_LIGHT: f32 = 0.35;
161const CLASSIC_SKY_LIGHT: f32 = 0.35;
162const DAWN_SKY_LIGHT: f32 = 0.3;
163const SINISTER_SKY_LIGHT: f32 = 0.6;
164const LIGHT_BLUE_STARS_LIGHT: f32 = 0.8;
165const BLUE_STARS_LIGHT: f32 = 0.8;
166
167/// The shading pair's plain half: a sphere given the shared shading
168/// material and no map.
169#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
170struct ShadingPlain;
171
172impl Mesh for ShadingPlain {
173 fn build(&self, assets: &Assets) -> MeshData {
174 sphere_with_material(assets, shading_material())
175 }
176}
177
178/// The shading pair's mapped half: the same sphere and material, with its
179/// shading map (occlusion, roughness and metallic) baked in.
180#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
181struct ShadingMapped;
182
183impl Mesh for ShadingMapped {
184 fn build(&self, assets: &Assets) -> MeshData {
185 sphere_with_material(assets, shading_material()).with_shading(shading_checker())
186 }
187}
188
189/// The relief pair's plain half.
190#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
191struct ReliefPlain;
192
193impl Mesh for ReliefPlain {
194 fn build(&self, assets: &Assets) -> MeshData {
195 sphere_with_material(assets, relief_material())
196 }
197}
198
199/// The relief pair's mapped half: the same sphere and material, with its
200/// relief map baked in.
201#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
202struct ReliefMapped;
203
204impl Mesh for ReliefMapped {
205 fn build(&self, assets: &Assets) -> MeshData {
206 sphere_with_material(assets, relief_material()).with_relief(relief_bumps())
207 }
208}
209
210/// The emissive pair's plain half.
211#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
212struct EmissivePlain;
213
214impl Mesh for EmissivePlain {
215 fn build(&self, assets: &Assets) -> MeshData {
216 cube_with_material(assets, emissive_material())
217 }
218}
219
220/// The emissive pair's mapped half: the same cube and material, with its
221/// emissive map baked in.
222#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
223struct EmissiveMapped;
224
225impl Mesh for EmissiveMapped {
226 fn build(&self, assets: &Assets) -> MeshData {
227 cube_with_material(assets, emissive_material()).with_emissive_map(emissive_checker())
228 }
229}
230
231/// The front sphere: a draw overrides its material new every frame, in
232/// place of a baked one.
233#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
234struct Front;
235
236impl Mesh for Front {
237 fn build(&self, assets: &Assets) -> MeshData {
238 Sphere {
239 subdivisions: SPHERE_SUBDIVISIONS,
240 }
241 .build(assets)
242 }
243}
244
245/// `BannerCloth`: a mesh split into columns along its span and placed at
246/// its `x = 0` edge, so `Banner`'s wave curves it, not a single flat
247/// quad. Its triangles are built twice: once as authored and once in the
248/// other order, with the normal turned around, so the cloth draws from
249/// both sides however its wave curves it.
250#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
251struct BannerCloth;
252
253impl Mesh for BannerCloth {
254 fn build(&self, _: &Assets) -> MeshData {
255 banner_mesh()
256 }
257}
258
259// Everything this game draws: the ground plane, each map pair's plain and
260// mapped half, the front sphere with its own live material, the built-in
261// primitives the reflection row and the pillars beside it place per draw,
262// and the displaced cloth.
263meshes! {
264 enum Shape {
265 Plane,
266 Sphere,
267 Cube,
268 ShadingPlain,
269 ShadingMapped,
270 ReliefPlain,
271 ReliefMapped,
272 EmissivePlain,
273 EmissiveMapped,
274 Front,
275 BannerCloth,
276 }
277}
278
279fn sphere_with_material(assets: &Assets, material: Material) -> MeshData {
280 Sphere {
281 subdivisions: SPHERE_SUBDIVISIONS,
282 }
283 .build(assets)
284 .with_material(material)
285}
286
287fn cube_with_material(assets: &Assets, material: Material) -> MeshData {
288 Cube.build(assets).with_material(material)
289}
290
291fn shading_material() -> Material {
292 Material::lit(SHADING_TINT).roughness(0.5).metallic(0.5)
293}
294
295fn relief_material() -> Material {
296 Material::lit(RELIEF_TINT).roughness(0.35)
297}
298
299fn emissive_material() -> Material {
300 Material::color(EMISSIVE_BASE).emissive(EMISSIVE_GLOW)
301}
302
303/// A shading map whose checker goes between low occlusion, roughness and
304/// metallic and full occlusion, roughness and metallic, so all three read
305/// apart across [`ShadingMapped`].
306fn shading_checker() -> ShadingData {
307 ShadingData::rgba8(
308 MAP_SIZE,
309 checker_pixels(MAP_SIZE, SHADING_CELL, SHADING_LOW, SHADING_HIGH),
310 )
311}
312
313/// An emissive map whose checker goes between full glow and none, so
314/// [`EMISSIVE_GLOW`] shapes across [`EmissiveMapped`] instead of casting
315/// whole.
316fn emissive_checker() -> TextureData {
317 TextureData::rgba8(
318 MAP_SIZE,
319 checker_pixels(MAP_SIZE, EMISSIVE_CELL, [0, 0, 0], [255, 255, 255]),
320 )
321}
322
323fn checker_pixels(size: UVec2, cell: u32, low: [u8; 3], high: [u8; 3]) -> Vec<u8> {
324 let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
325 for y in 0..size.y {
326 for x in 0..size.x {
327 let on = ((x / cell) + (y / cell)).is_multiple_of(2);
328 let [red, green, blue] = if on { high } else { low };
329 pixels.extend_from_slice(&[red, green, blue, u8::MAX]);
330 }
331 }
332 pixels
333}
334
335/// A relief whose normals turn across a wave that repeats over the map:
336/// each texel's slope comes from the partial derivatives of a
337/// `sin(u) * sin(v)` height field at `BUMP_SLOPE`'s peak, computed at that
338/// texel and not sampled from any other.
339fn relief_bumps() -> ReliefData {
340 let size = MAP_SIZE;
341 let turns = core::f32::consts::TAU * BUMP_WAVES;
342 let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
343 for y in 0..size.y {
344 for x in 0..size.x {
345 let u = (x as f32 + 0.5) / size.x as f32;
346 let v = (y as f32 + 0.5) / size.y as f32;
347 let slope_u = BUMP_SLOPE * (turns * u).cos() * (turns * v).sin();
348 let slope_v = BUMP_SLOPE * (turns * u).sin() * (turns * v).cos();
349 let normal = Vec3::new(-slope_u, -slope_v, 1.0).normalize();
350 let encode = |signed: f32| ((signed * 0.5 + 0.5) * 255.0).round() as u8;
351 pixels.extend_from_slice(&[encode(normal.x), encode(normal.y), encode(normal.z), 0]);
352 }
353 }
354 ReliefData::normals(size, pixels)
355}
356
357/// `BannerCloth`'s vertices and indices, built twice over: the columns as
358/// authored, facing `+Z`, and the same columns again facing `-Z`, their
359/// triangles in the other order so both draw front side out.
360fn banner_mesh() -> MeshData {
361 let mut vertices = Vec::with_capacity(((BANNER_COLUMNS + 1) * 4) as usize);
362 for normal in [Vec3::Z, Vec3::NEG_Z] {
363 for column in 0..=BANNER_COLUMNS {
364 let u = column as f32 / BANNER_COLUMNS as f32;
365 let x = u * BANNER_WIDTH;
366 for v in [0.0, 1.0] {
367 vertices.push(Vertex::new(
368 Vec3::new(x, -v * BANNER_HEIGHT, 0.0),
369 normal,
370 Vec2::new(u, v),
371 ));
372 }
373 }
374 }
375
376 let side = BANNER_COLUMNS + 1;
377 let mut indices = Vec::with_capacity((BANNER_COLUMNS * 12) as usize);
378 for column in 0..BANNER_COLUMNS {
379 let top_left = column * 2;
380 let bottom_left = top_left + 1;
381 let top_right = top_left + 2;
382 let bottom_right = top_left + 3;
383 indices.extend([
384 bottom_left,
385 bottom_right,
386 top_right,
387 bottom_left,
388 top_right,
389 top_left,
390 ]);
391
392 let back = side * 2;
393 indices.extend([
394 back + top_right,
395 back + bottom_right,
396 back + bottom_left,
397 back + top_left,
398 back + top_right,
399 back + bottom_left,
400 ]);
401 }
402
403 MeshData::new(vertices, indices)
404}
405
406/// Displaced by a wave that grows away from its `x = 0` edge; casts the
407/// shadow of where it was placed, unmoved by its own wave. Its one value
408/// is the clock its wave slides on.
409#[derive(Default, ShaderValues)]
410struct Banner {
411 time: f32,
412}
413
414impl SurfaceStyle for Banner {
415 const PASS: DrawPass = DrawPass::Opaque;
416 const DISPLACE: Option<&'static str> = Some(include_str!("material_playground_banner.wgsl"));
417}
418
419/// A surface that reads no light of the scene's own: it draws its own
420/// pulsing tint, added over what is behind it, through the color it pulses
421/// through and the clock the pulse is timed by.
422#[derive(Default, ShaderValues)]
423struct Field {
424 tint: Color,
425 time: f32,
426}
427
428impl SurfaceStyle for Field {
429 const PASS: DrawPass = DrawPass::Additive;
430 const SURFACE: Option<&'static str> = Some(include_str!("material_playground_field.wgsl"));
431}
432
433surface_styles! { enum Looks { Banner, Field } }
434
435/// A whole scene lighting choice: it names a sky and, kept with it, the
436/// sun that lights the scene, so a choice cannot leave the two apart.
437/// `Dawn`, `Noon`, `Dusk` and `Night` each pair a gradient with a sun of
438/// its own color and direction; `Clear`, `Classic`, `ImageDawn` and
439/// `Sinister` each pair a loaded image with a sun that fits it, and
440/// `LightBlueStars` and `BlueStars` pair a loaded space image with none;
441/// `Default` is the engine's own grey sky and white sun.
442///
443/// [`Skyboxes`] proves every value at startup, so it must be [`Eq`] and
444/// [`Hash`] over a fixed [`Skyboxes::catalog`] — a sky and sun a player
445/// set to any color and direction live could never meet, since `f32` is
446/// neither. This fixed, named set is the shape this file chose in its
447/// place: the side area offers it as one row, and shows the chosen sky's
448/// own light and its sun's own strength as text, read only, rather than
449/// controls a game could not build from. See this example's report for
450/// what that choice costs.
451#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
452enum Sky {
453 Dawn,
454 Noon,
455 Dusk,
456 Night,
457 Clear,
458 Classic,
459 ImageDawn,
460 Sinister,
461 LightBlueStars,
462 BlueStars,
463 Default,
464}
465
466impl Sky {
467 const ALL: [Sky; 11] = [
468 Self::Dawn,
469 Self::Noon,
470 Self::Dusk,
471 Self::Night,
472 Self::Clear,
473 Self::Classic,
474 Self::ImageDawn,
475 Self::Sinister,
476 Self::LightBlueStars,
477 Self::BlueStars,
478 Self::Default,
479 ];
480
481 fn name(self) -> &'static str {
482 match self {
483 Self::Dawn => "dawn",
484 Self::Noon => "noon",
485 Self::Dusk => "dusk",
486 Self::Night => "night",
487 Self::Clear => "clear day",
488 Self::Classic => "classic",
489 Self::ImageDawn => "dawn image",
490 Self::Sinister => "sinister night",
491 Self::LightBlueStars => "light blue stars",
492 Self::BlueStars => "blue stars",
493 Self::Default => "default",
494 }
495 }
496
497 /// The fraction of its own light this sky lands and reflects, through
498 /// [`SkyboxData::lit_by`]: fixed per choice, so a bright one does not
499 /// read too bright, and a dark one does not read too dark, under the
500 /// frame's own lights.
501 fn light(self) -> f32 {
502 match self {
503 Self::Dawn => 0.4,
504 Self::Noon => 0.5,
505 Self::Dusk => 0.35,
506 Self::Night => 0.3,
507 Self::Clear => CLEAR_SKY_LIGHT,
508 Self::Classic => CLASSIC_SKY_LIGHT,
509 Self::ImageDawn => DAWN_SKY_LIGHT,
510 Self::Sinister => SINISTER_SKY_LIGHT,
511 Self::LightBlueStars => LIGHT_BLUE_STARS_LIGHT,
512 Self::BlueStars => BLUE_STARS_LIGHT,
513 Self::Default => 1.0,
514 }
515 }
516
517 /// The sun this choice pairs with its sky: direction, color and
518 /// strength resolved together, so a choice cannot leave them apart.
519 /// `None` for the two space images, which pair with no sun at all.
520 fn sun(self) -> Option<(Vec3, Color, f32)> {
521 match self {
522 Self::Dawn => Some((
523 Vec3::new(-1.0, -0.15, 0.05),
524 Color::rgb(1.0, 0.7, 0.45),
525 1.4,
526 )),
527 Self::Noon => Some((
528 Vec3::new(-0.15, -1.0, -0.1),
529 Color::rgb(1.0, 1.0, 0.98),
530 1.6,
531 )),
532 Self::Dusk => Some((
533 Vec3::new(1.0, -0.15, 0.05),
534 Color::rgb(1.0, 0.55, 0.25),
535 1.2,
536 )),
537 Self::Night => Some((
538 Vec3::new(-0.3, -0.7, -0.6),
539 Color::rgb(0.55, 0.65, 0.85),
540 0.15,
541 )),
542 Self::Clear => Some((
543 Vec3::new(-0.2, -1.0, -0.15),
544 Color::rgb(1.0, 0.98, 0.9),
545 1.5,
546 )),
547 Self::Classic => Some((
548 Vec3::new(-0.4, -0.9, -0.2),
549 Color::rgb(1.0, 0.95, 0.85),
550 1.3,
551 )),
552 Self::ImageDawn => Some((Vec3::new(-1.0, -0.2, 0.1), Color::rgb(1.0, 0.75, 0.5), 1.1)),
553 Self::Sinister => Some((Vec3::new(0.4, -0.5, -0.7), Color::rgb(0.4, 0.5, 0.75), 0.1)),
554 Self::LightBlueStars | Self::BlueStars => None,
555 Self::Default => Some((Vec3::new(-0.4, -1.0, -0.6), Color::WHITE, 1.0)),
556 }
557 }
558
559 /// The color the sky reads under the horizon, through
560 /// [`SkyboxData::with_ground`]: the floor as lit under this choice's own
561 /// sun and [`Self::light`], so it moves with them, not only with the
562 /// image. `None` for the gradient skies and `Default`, which need no
563 /// ground, and for the two space images, which hold space below the
564 /// horizon as well.
565 fn ground(self) -> Option<Color> {
566 match self {
567 Self::Clear => Some(Color::rgb(0.501, 0.517, 0.449)),
568 Self::Classic => Some(Color::rgb(0.420, 0.405, 0.379)),
569 Self::ImageDawn => Some(Color::rgb(0.073, 0.053, 0.032)),
570 Self::Sinister => Some(Color::rgb(0.012, 0.014, 0.020)),
571 Self::Dawn
572 | Self::Noon
573 | Self::Dusk
574 | Self::Night
575 | Self::LightBlueStars
576 | Self::BlueStars
577 | Self::Default => None,
578 }
579 }
580}
581
582impl Catalog for Sky {
583 fn catalog() -> Vec<Self> {
584 Self::ALL.to_vec()
585 }
586}
587
588impl Skyboxes for Sky {
589 fn build(&self, assets: &Assets) -> SkyboxData {
590 let sky = match self {
591 Self::Dawn => SkyboxData::gradient(
592 Color::rgb(0.55, 0.55, 0.75),
593 Color::rgb(0.95, 0.6, 0.35),
594 Color::rgb(0.12, 0.08, 0.06),
595 ),
596 Self::Noon => SkyboxData::gradient(
597 Color::rgb(0.2, 0.45, 0.85),
598 Color::rgb(0.75, 0.82, 0.9),
599 Color::rgb(0.3, 0.3, 0.28),
600 ),
601 Self::Dusk => SkyboxData::gradient(
602 Color::rgb(0.18, 0.1, 0.3),
603 Color::rgb(0.85, 0.35, 0.2),
604 Color::rgb(0.03, 0.02, 0.03),
605 ),
606 Self::Night => SkyboxData::gradient(
607 Color::rgb(0.02, 0.02, 0.06),
608 Color::rgb(0.05, 0.05, 0.1),
609 Color::rgb(0.0, 0.0, 0.0),
610 ),
611 Self::Clear => assets.skybox("sky-clear"),
612 Self::Classic => assets.skybox("sky-classic"),
613 Self::ImageDawn => assets.skybox("sky-dawn"),
614 Self::Sinister => assets.skybox("sky-sinister"),
615 Self::LightBlueStars => assets.skybox("sky-stars-lightblue"),
616 Self::BlueStars => assets.skybox("sky-stars-blue"),
617 Self::Default => SkyboxData::gradient(DEFAULT_SKY, DEFAULT_SKY, DEFAULT_SKY),
618 };
619 let sky = match self.ground() {
620 Some(ground) => sky.with_ground(ground),
621 None => sky,
622 };
623
624 sky.lit_by(self.light())
625 }
626}
627
628/// `color` scaled by `strength`, the value a [`Light`] reads.
629fn scaled(color: Color, strength: f32) -> Color {
630 Color::rgb(
631 color.red * strength,
632 color.green * strength,
633 color.blue * strength,
634 )
635}
636
637/// One light's color and strength, held apart from the position that
638/// names it, plus whether it casts.
639#[derive(Clone, Copy)]
640struct Glow {
641 color: Color,
642 strength: f32,
643 shadow: bool,
644}
645
646impl Glow {
647 /// `color` scaled by `strength`, the value a [`Light`] reads.
648 fn scaled(self) -> Color {
649 scaled(self.color, self.strength)
650 }
651}
652
653/// Every key and button this game reads apart from the UI: held, `Look`
654/// turns the camera by the pointer's own motion, `Forward`/`Back`/
655/// `Left`/`Right` move it along the view and to its side, and `Up`/
656/// `Down` move it along the world's own up.
657#[derive(InputButtonAction, Clone, Copy, PartialEq)]
658enum Move {
659 Forward,
660 Back,
661 Left,
662 Right,
663 Up,
664 Down,
665 Look,
666}
667
668impl InputButtonAction for Move {
669 fn bindings(&self) -> Vec<ButtonBinding> {
670 match self {
671 Self::Forward => vec![Key::W.into()],
672 Self::Back => vec![Key::S.into()],
673 Self::Left => vec![Key::A.into()],
674 Self::Right => vec![Key::D.into()],
675 Self::Up => vec![Key::Space.into()],
676 Self::Down => vec![Key::LeftShift.into()],
677 Self::Look => vec![MouseButton::Right.into()],
678 }
679 }
680}
681
682/// The pointer's own motion, read only while [`Move::Look`] is held.
683#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
684enum Turn {
685 Look,
686}
687
688impl InputAxis2Action for Turn {
689 fn bindings(&self) -> Vec<Axis2Binding> {
690 match self {
691 Self::Look => vec![Axis2Binding::pointer().scale(LOOK_SENSITIVITY)],
692 }
693 }
694}
695
696/// How far the wheel moved this frame, read to scale the move speed.
697#[derive(InputAxisAction, Clone, Copy, PartialEq)]
698enum Speed {
699 Wheel,
700}
701
702impl InputAxisAction for Speed {
703 fn bindings(&self) -> Vec<AxisBinding> {
704 match self {
705 Self::Wheel => vec![AxisBinding::from(WheelDelta::Up).scale(4.0)],
706 }
707 }
708}
709
710struct Controls;
711
712impl InputActions for Controls {
713 type Button = Move;
714 type Axis = Speed;
715 type Axis2 = Turn;
716}
717
718struct Playground {
719 eye: Vec3,
720 yaw: f32,
721 pitch: f32,
722 speed_scale: f32,
723
724 sky: Sky,
725 sun_shadow: bool,
726
727 lamp: Glow,
728 spotlight: Glow,
729
730 front_tint: Color,
731 front_roughness: f32,
732 front_metallic: f32,
733 shading_map_on: bool,
734 relief_map_on: bool,
735 emissive_map_on: bool,
736
737 exposure: f32,
738 bloom: f32,
739}
740
741impl Playground {
742 fn init(ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
743 let _ = ctx;
744 Ok(Self {
745 eye: START_EYE,
746 yaw: START_YAW,
747 pitch: START_PITCH,
748 speed_scale: 1.0,
749
750 sky: Sky::Default,
751 sun_shadow: true,
752
753 lamp: Glow {
754 color: Color::rgb(0.9, 0.55, 0.3),
755 strength: 3.0,
756 shadow: false,
757 },
758 spotlight: Glow {
759 color: Color::rgb(0.4, 0.6, 1.0),
760 strength: 6.0,
761 shadow: true,
762 },
763
764 front_tint: Color::rgb(0.7, 0.25, 0.2),
765 front_roughness: 0.4,
766 front_metallic: 0.0,
767 shading_map_on: true,
768 relief_map_on: true,
769 emissive_map_on: true,
770
771 exposure: START_EXPOSURE,
772 bloom: START_BLOOM,
773 })
774 }
775
776 /// This frame's forward direction, from `yaw` (turning around the
777 /// world's own up) and `pitch` (turning up or down).
778 fn forward(&self) -> Vec3 {
779 Vec3::new(
780 -self.pitch.cos() * self.yaw.sin(),
781 self.pitch.sin(),
782 -self.pitch.cos() * self.yaw.cos(),
783 )
784 }
785
786 /// The camera this frame draws from: `eye` looking along `forward`.
787 fn camera(&self) -> Camera {
788 Camera::new(
789 View::look_at(self.eye, self.eye + self.forward()),
790 Projection::perspective(CAMERA_FOV),
791 )
792 }
793
794 /// A held `Move::Look` (the right mouse button) turns the camera by
795 /// the pointer's own motion, the same way it moves: dragging right
796 /// turns the view right and left turns it left, dragging down turns
797 /// it to look further down at the scene, dragging up back toward the
798 /// horizon. `W`/`A`/`S`/`D` move along the view and to its side,
799 /// `Space`/`Left Shift` up and down, and the wheel scales how far
800 /// each move goes. The `eye` is held above the ground plane wherever
801 /// it moves.
802 fn fly_camera(&mut self, ctx: &mut FrameContext<'_, Self>) {
803 if !ctx.ui_wants_pointer() && ctx.down(Move::Look) {
804 let look = ctx.axis2(Turn::Look);
805 self.yaw -= look.x;
806 self.pitch = (self.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
807 }
808
809 let wheel = ctx.axis(Speed::Wheel);
810 if !ctx.ui_wants_pointer() && wheel != 0.0 {
811 self.speed_scale =
812 (self.speed_scale * SPEED_STEP.powf(wheel)).clamp(MIN_SPEED_SCALE, MAX_SPEED_SCALE);
813 }
814
815 let forward = self.forward();
816 let right = Vec3::new(self.yaw.cos(), 0.0, -self.yaw.sin());
817 let mut move_by = Vec3::ZERO;
818 if ctx.down(Move::Forward) {
819 move_by += forward;
820 }
821 if ctx.down(Move::Back) {
822 move_by -= forward;
823 }
824 if ctx.down(Move::Right) {
825 move_by += right;
826 }
827 if ctx.down(Move::Left) {
828 move_by -= right;
829 }
830 if ctx.down(Move::Up) {
831 move_by += Vec3::Y;
832 }
833 if ctx.down(Move::Down) {
834 move_by -= Vec3::Y;
835 }
836 if move_by.length_squared() > 1.0 {
837 move_by = move_by.normalize();
838 }
839
840 self.eye += move_by * MOVE_SPEED * self.speed_scale * ctx.dt().as_secs_f32();
841 self.eye.y = self.eye.y.max(MIN_EYE_HEIGHT);
842 }
843
844 /// The material [`Front`] draws with, resolved new from its sliders
845 /// every frame — the override [`Instance::material`] takes, in place
846 /// of a baked one.
847 fn front_material(&self) -> Material {
848 Material::lit(self.front_tint)
849 .roughness(self.front_roughness)
850 .metallic(self.front_metallic)
851 }
852
853 /// Every draw this game makes: the ground, each map pair, the front
854 /// sphere, the reflection row and the pillars beside it.
855 fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
856 ctx.draw(
857 Plane
858 .at(Transform::from_scale(Vec3::new(
859 GROUND_SIZE,
860 1.0,
861 GROUND_SIZE,
862 )))
863 .material(Material::lit(GROUND_COLOR).roughness(0.9)),
864 );
865
866 Self::draw_pair(
867 ctx,
868 SHADING_Z,
869 SPHERE_RADIUS,
870 ShadingPlain.at(Vec3::ZERO).into_set(),
871 ShadingMapped.at(Vec3::ZERO).into_set(),
872 self.shading_map_on,
873 );
874 Self::draw_pair(
875 ctx,
876 RELIEF_Z,
877 SPHERE_RADIUS,
878 ReliefPlain.at(Vec3::ZERO).into_set(),
879 ReliefMapped.at(Vec3::ZERO).into_set(),
880 self.relief_map_on,
881 );
882 Self::draw_pair(
883 ctx,
884 EMISSIVE_Z,
885 CUBE_SIZE / 2.0,
886 EmissivePlain.at(Vec3::ZERO).into_set(),
887 EmissiveMapped.at(Vec3::ZERO).into_set(),
888 self.emissive_map_on,
889 );
890
891 ctx.draw(
892 Front
893 .at(Transform::from_scale_rotation_translation(
894 Vec3::splat(FRONT_SCALE),
895 Quat::IDENTITY,
896 FRONT_POSITION,
897 ))
898 .material(self.front_material()),
899 );
900
901 self.draw_reflect_row(ctx);
902 self.draw_outpost(ctx);
903 }
904
905 /// One pair at depth `z`, its centers `height` above the ground: `plain`
906 /// on the left always, and on the right `mapped` where `mapped_on` is
907 /// set, `plain` again where it is not — the same position drawing the
908 /// same base material with and without the map.
909 fn draw_pair(
910 ctx: &mut FrameContext<'_, Self>,
911 z: f32,
912 height: f32,
913 plain: Instance<Shape, Looks>,
914 mapped: Instance<Shape, Looks>,
915 mapped_on: bool,
916 ) {
917 ctx.draw(plain.clone().at(Vec3::new(-PAIR_HALF_SPACING, height, z)));
918 let right = if mapped_on { mapped } else { plain };
919 ctx.draw(right.at(Vec3::new(PAIR_HALF_SPACING, height, z)));
920 }
921
922 /// A row of built-in `Sphere` draws at rising roughness, each
923 /// `metallic(1.0)` with its tint white, so what draws is the sky's own
924 /// reflection alone.
925 fn draw_reflect_row(&self, ctx: &mut FrameContext<'_, Self>) {
926 let start = -REFLECT_ROW_SPACING * (REFLECT_ROW_COUNT as f32 - 1.0) / 2.0;
927 for index in 0..REFLECT_ROW_COUNT {
928 let x = start + index as f32 * REFLECT_ROW_SPACING;
929 let roughness = index as f32 / (REFLECT_ROW_COUNT as f32 - 1.0);
930 ctx.draw(
931 Sphere {
932 subdivisions: SPHERE_SUBDIVISIONS,
933 }
934 .at(Transform::from_scale_rotation_translation(
935 Vec3::splat(REFLECT_ROW_RADIUS * 2.0),
936 Quat::IDENTITY,
937 Vec3::new(x, REFLECT_ROW_RADIUS, REFLECT_ROW_Z),
938 ))
939 .material(
940 Material::lit(Color::WHITE)
941 .roughness(roughness)
942 .metallic(1.0),
943 ),
944 );
945 }
946 }
947
948 /// Three pillars and a pole a light can shadow, beside `Banner`'s
949 /// displaced cloth and `Field`'s pulsing sphere — [`OUTPOST`] moves the
950 /// whole group clear of the rest of the scene.
951 fn draw_outpost(&self, ctx: &mut FrameContext<'_, Self>) {
952 let clock = ctx.elapsed().as_secs_f32();
953
954 for &(position, scale) in &PILLARS {
955 ctx.draw(
956 Cube.at(Transform::from_scale_rotation_translation(
957 scale,
958 Quat::IDENTITY,
959 OUTPOST + position,
960 ))
961 .material(Material::lit(Color::rgb(0.55, 0.5, 0.45))),
962 );
963 }
964
965 ctx.draw(
966 Cube.at(Transform::from_scale_rotation_translation(
967 POLE_SCALE,
968 Quat::IDENTITY,
969 OUTPOST + POLE_POSITION,
970 ))
971 .material(Material::lit(Color::rgb(0.3, 0.24, 0.18))),
972 );
973
974 ctx.set_surface_style(Banner { time: clock });
975 ctx.draw(
976 BannerCloth
977 .at(Transform::from_translation(OUTPOST + BANNER_MOUNT))
978 .material(Material::lit(Color::rgb(0.75, 0.12, 0.12)))
979 .surface_style::<Banner>(),
980 );
981
982 ctx.set_surface_style(Field {
983 tint: Color::rgb(0.25, 0.75, 1.0),
984 time: clock,
985 });
986 ctx.draw(
987 Sphere { subdivisions: 2 }
988 .at(Transform::from_scale_rotation_translation(
989 Vec3::splat(FIELD_ORB_SCALE),
990 Quat::IDENTITY,
991 OUTPOST + FIELD_ORB_POSITION,
992 ))
993 .material(Material::color(Color::BLACK))
994 .surface_style::<Field>(),
995 );
996 }
997
998 /// This frame's sun, lamp and spotlight, each `.shadow()` where its own
999 /// flag is set. The sun's direction, color and strength come from the
1000 /// chosen [`Sky`], and is absent where the choice pairs with none; only
1001 /// its shadow flag is the player's own.
1002 fn lights(&self) -> Vec<Light> {
1003 let lamp = Light::point(LAMP_POSITION, self.lamp.scaled(), LAMP_RANGE);
1004 let spotlight = Light::spot(Spot {
1005 position: SPOT_POSITION,
1006 direction: SPOT_DIRECTION,
1007 color: self.spotlight.scaled(),
1008 range: SPOT_RANGE,
1009 angle: SPOT_ANGLE,
1010 });
1011
1012 let mut lights = vec![
1013 if self.lamp.shadow {
1014 lamp.shadow()
1015 } else {
1016 lamp
1017 },
1018 if self.spotlight.shadow {
1019 spotlight.shadow()
1020 } else {
1021 spotlight
1022 },
1023 ];
1024
1025 if let Some((direction, color, strength)) = self.sky.sun() {
1026 let sun = Light::directional(direction, scaled(color, strength));
1027 lights.push(if self.sun_shadow { sun.shadow() } else { sun });
1028 }
1029
1030 lights
1031 }
1032
1033 /// Every live knob, in a side area of fixed width so the scene stays
1034 /// visible beside it — the default sky reads as flat grey, so a
1035 /// control added straight to the bare UI layer would read as a slab
1036 /// across the window.
1037 fn controls(&mut self, ctx: &mut FrameContext<'_, Self>) {
1038 let tonemap = ctx.config().tonemap();
1039 let antialiasing = ctx.config().antialiasing();
1040 let shadow_resolution = ctx.config().shadow_resolution();
1041
1042 ctx.ui(|ui| {
1043 egui::Panel::right("controls")
1044 .resizable(false)
1045 .default_size(300.0)
1046 .show(ui, |ui| {
1047 egui::ScrollArea::vertical().show(ui, |ui| {
1048 ui.label(
1049 "right mouse button to look, W/A/S/D to move, \
1050 space/shift up and down, wheel to scale speed",
1051 );
1052 ui.separator();
1053 self.lighting_controls(ui);
1054 ui.separator();
1055 self.light_controls(ui);
1056 ui.separator();
1057 self.material_controls(ui);
1058 ui.separator();
1059 ui.label(format!(
1060 "tone map {tonemap:?} \u{b7} antialiasing {antialiasing} \u{b7} \
1061 shadow {shadow_resolution}px: set at startup, not live"
1062 ));
1063 ui.add(egui::Slider::new(&mut self.exposure, 0.1..=3.0).text("exposure"));
1064 ui.add(egui::Slider::new(&mut self.bloom, 0.0..=1.0).text("bloom"));
1065 });
1066 });
1067 });
1068 }
1069
1070 fn lighting_controls(&mut self, ui: &mut egui::Ui) {
1071 ui.heading("lighting");
1072 for choice in Sky::ALL {
1073 ui.radio_value(&mut self.sky, choice, choice.name());
1074 }
1075 ui.label(format!("sky light {:.2}: set at startup", self.sky.light()));
1076
1077 match self.sky.sun() {
1078 Some((_, color, strength)) => {
1079 color_swatch(ui, "sun", color);
1080 ui.label(format!("sun strength {strength:.2}: set by the choice"));
1081 ui.checkbox(&mut self.sun_shadow, "sun shadow");
1082 }
1083 None => {
1084 ui.label("no sun: set by the choice");
1085 }
1086 }
1087 }
1088
1089 fn light_controls(&mut self, ui: &mut egui::Ui) {
1090 ui.heading("lights");
1091 glow_controls(ui, "lamp", &mut self.lamp);
1092 glow_controls(ui, "spotlight", &mut self.spotlight);
1093 }
1094
1095 fn material_controls(&mut self, ui: &mut egui::Ui) {
1096 ui.heading("material");
1097 color_row(ui, "tint", &mut self.front_tint);
1098 ui.add(egui::Slider::new(&mut self.front_roughness, 0.0..=1.0).text("roughness"));
1099 ui.add(egui::Slider::new(&mut self.front_metallic, 0.0..=1.0).text("metallic"));
1100 ui.checkbox(
1101 &mut self.shading_map_on,
1102 "shading map: occlusion \u{b7} roughness \u{b7} metallic",
1103 );
1104 ui.checkbox(&mut self.relief_map_on, "relief map: bump normals");
1105 ui.checkbox(&mut self.emissive_map_on, "emissive map: per-texel glow");
1106 }
1107}
1108
1109/// One light's own controls: a color, its strength and a shadow flag.
1110fn glow_controls(ui: &mut egui::Ui, label: &str, glow: &mut Glow) {
1111 ui.label(label);
1112 color_row(ui, "color", &mut glow.color);
1113 ui.add(egui::Slider::new(&mut glow.strength, 0.0..=8.0).text("strength"));
1114 ui.checkbox(&mut glow.shadow, "shadow");
1115}
1116
1117/// One named control over `color`'s red, green and blue channels; alpha
1118/// stays `1.0`.
1119fn color_row(ui: &mut egui::Ui, label: &str, color: &mut Color) {
1120 let mut rgb = [color.red, color.green, color.blue];
1121 ui.horizontal(|ui| {
1122 ui.label(label);
1123 if ui.color_edit_button_rgb(&mut rgb).changed() {
1124 *color = Color::rgb(rgb[0], rgb[1], rgb[2]);
1125 }
1126 });
1127}Sourcepub const fn rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self
pub const fn rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self
A color with opacity alpha, each channel and alpha a fraction of
1.0.
Examples found in repository?
More examples
Sourcepub const fn with_alpha(self, alpha: f32) -> Self
pub const fn with_alpha(self, alpha: f32) -> Self
The same channels at opacity alpha, a fraction of 1.0.
Examples found in repository?
661 fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662 for spark in &self.sparks {
663 let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664 let fade = 1.0 - age;
665 let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666 ctx.draw(
667 Quad.at(Transform::from_scale_rotation_translation(
668 Vec3::splat(size),
669 Quat::IDENTITY,
670 spark.position,
671 ))
672 .billboard()
673 .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674 .material(
675 Material::color(spark.color.with_alpha(fade))
676 .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677 .additive(),
678 ),
679 );
680 }
681 }
682
683 /// Draws the ball's ghost trail, each ghost smaller and more transparent
684 /// than the one ahead of it; each ghost's position interpolates between
685 /// its own last two resolved ticks by the same `alpha` the ball itself
686 /// draws at, and its radius clamps to what the ball's own radius has
687 /// left over its distance from the head, so a ghost still close to the
688 /// ball never draws past its edge.
689 fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690 let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691 for i in 0..TRAIL_LEN {
692 let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693 let age = (i + 1) as f32 / TRAIL_LEN as f32;
694 let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695 let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696 .min((BALL_RADIUS - head.distance(position)).max(0.0));
697 let scale = Vec3::splat(radius * 2.0);
698 ctx.draw(
699 Sphere { subdivisions: 2 }
700 .at(Transform::from_scale_rotation_translation(
701 scale,
702 Quat::IDENTITY,
703 position,
704 ))
705 .material(
706 Material::color(BALL_GLOW.with_alpha(fade))
707 .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708 ),
709 );
710 }
711 }Sourcepub const fn dimmed(self, factor: f32) -> Self
pub const fn dimmed(self, factor: f32) -> Self
Its red, green, and blue scaled by factor, a fraction of each
channel’s own value, keeping its opacity.
Examples found in repository?
593 fn paddle_face_material(&self) -> Material {
594 let t = (self.paddle_flash / PADDLE_FLASH).clamp(0.0, 1.0);
595 let flash = PADDLE_FLASH_EMISSIVE.dimmed(t);
596 let emissive = Color::rgb(
597 PADDLE_AMBIENT_EMISSIVE.red + flash.red,
598 PADDLE_AMBIENT_EMISSIVE.green + flash.green,
599 PADDLE_AMBIENT_EMISSIVE.blue + flash.blue,
600 );
601 Material::lit(PADDLE_BASE).emissive(emissive)
602 }
603
604 fn draw_court(&self, ctx: &mut FrameContext<'_, Breakout>) {
605 ctx.draw(
606 Plane
607 .at(Transform::from_scale(Vec3::new(
608 COURT_HALF_WIDTH * 2.0,
609 1.0,
610 COURT_HALF_DEPTH * 2.0,
611 )))
612 .material(Material::lit(FLOOR_COLOR)),
613 );
614
615 let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, COURT_HALF_DEPTH);
616 for side in [-1.0, 1.0] {
617 let x = side * (COURT_HALF_WIDTH - WALL_THICKNESS * 0.5);
618 ctx.draw(
619 Cube.at(Transform::from_scale_rotation_translation(
620 side_half * 2.0,
621 Quat::IDENTITY,
622 Vec3::new(x, side_half.y, 0.0),
623 ))
624 .material(Material::lit(WALL_COLOR)),
625 );
626 }
627
628 let top_half = Vec3::new(COURT_HALF_WIDTH, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
629 ctx.draw(
630 Cube.at(Transform::from_scale_rotation_translation(
631 top_half * 2.0,
632 Quat::IDENTITY,
633 Vec3::new(0.0, top_half.y, -COURT_HALF_DEPTH + WALL_THICKNESS * 0.5),
634 ))
635 .material(Material::lit(WALL_COLOR)),
636 );
637 }
638
639 fn draw_bricks(&self, ctx: &mut FrameContext<'_, Breakout>) {
640 let scale = Vec3::new(
641 BRICK_HALF_WIDTH * 2.0,
642 BRICK_HALF_HEIGHT * 2.0,
643 BRICK_HALF_DEPTH * 2.0,
644 );
645 for brick in self.bricks.iter().filter(|brick| brick.hits_remaining > 0) {
646 let health = f32::from(brick.hits_remaining) / f32::from(BRICK_HITS);
647 let color = BRICK_ROW_COLORS[brick.row].dimmed(0.4 + 0.6 * health);
648 ctx.draw(
649 Cube.at(Transform::from_scale_rotation_translation(
650 scale,
651 Quat::IDENTITY,
652 brick.position,
653 ))
654 .material(Material::shaded(color, health)),
655 );
656 }
657 }
658
659 /// Draws the live spark burst: additive, tumbling by roll as they age,
660 /// shrinking and fading out over their lifetime.
661 fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662 for spark in &self.sparks {
663 let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664 let fade = 1.0 - age;
665 let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666 ctx.draw(
667 Quad.at(Transform::from_scale_rotation_translation(
668 Vec3::splat(size),
669 Quat::IDENTITY,
670 spark.position,
671 ))
672 .billboard()
673 .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674 .material(
675 Material::color(spark.color.with_alpha(fade))
676 .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677 .additive(),
678 ),
679 );
680 }
681 }
682
683 /// Draws the ball's ghost trail, each ghost smaller and more transparent
684 /// than the one ahead of it; each ghost's position interpolates between
685 /// its own last two resolved ticks by the same `alpha` the ball itself
686 /// draws at, and its radius clamps to what the ball's own radius has
687 /// left over its distance from the head, so a ghost still close to the
688 /// ball never draws past its edge.
689 fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690 let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691 for i in 0..TRAIL_LEN {
692 let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693 let age = (i + 1) as f32 / TRAIL_LEN as f32;
694 let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695 let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696 .min((BALL_RADIUS - head.distance(position)).max(0.0));
697 let scale = Vec3::splat(radius * 2.0);
698 ctx.draw(
699 Sphere { subdivisions: 2 }
700 .at(Transform::from_scale_rotation_translation(
701 scale,
702 Quat::IDENTITY,
703 position,
704 ))
705 .material(
706 Material::color(BALL_GLOW.with_alpha(fade))
707 .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708 ),
709 );
710 }
711 }More examples
1677 fn draw_gem(&self, ctx: &mut FrameContext<'_, Keep>) {
1678 let t = self.simulated.as_secs_f32();
1679 let bob = (t * 2.0).sin() * GEM_BOB_HEIGHT;
1680 ctx.light(
1681 Light::point(
1682 GEM_POSITION + Vec3::Y * (bob + GEM_LIGHT_LIFT),
1683 GEM_LIGHT_COLOR,
1684 GEM_LIGHT_RANGE,
1685 )
1686 .shadow(),
1687 );
1688 ctx.draw(
1689 Gem.at(Transform::from_scale_rotation_translation(
1690 Vec3::ONE,
1691 Quat::from_rotation_y(t * GEM_SPIN_SPEED),
1692 GEM_POSITION + Vec3::Y * bob,
1693 ))
1694 .material(Material::shaded(GEM_COLOR, 0.7).emissive(GEM_COLOR.dimmed(1.6))),
1695 );
1696 }547 fn draw_sources(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
548 for (index, source) in self.sources.iter().enumerate() {
549 let color = SOURCE_COLORS[index];
550 let picked_up = self.dragging == Some(index);
551 let scale = if picked_up { 1.3 } else { 1.0 };
552 let emissive = if source.enabled {
553 Color::rgb(color.red * 3.0, color.green * 3.0, color.blue * 3.0)
554 } else {
555 color.dimmed(0.15)
556 };
557
558 for (radius, ring_color) in [
559 (source.range, RANGE_COLOR),
560 (source.reference, REFERENCE_COLOR),
561 ] {
562 ctx.draw(
563 Ring.at(Transform::from_scale_rotation_translation(
564 Vec3::new(radius, 1.0, radius),
565 Quat::IDENTITY,
566 Vec3::new(source.position.x, 0.01, source.position.z),
567 ))
568 .material(Material::color(ring_color)),
569 );
570 }
571 ctx.draw(
572 Cube.at(Transform::from_scale_rotation_translation(
573 Vec3::splat(SOURCE_HALF * 2.0 * scale),
574 Quat::IDENTITY,
575 source.position,
576 ))
577 .material(Material::shaded(color, 0.6).emissive(emissive)),
578 );
579 }
580 }
581
582 /// The listener: a cube drawn from the ground up to [`EYE_HEIGHT`],
583 /// an ear pair set on ± `view`'s right, and a marker at the front that
584 /// shows its fixed `-Z` facing.
585 fn draw_listener(&self, ctx: &mut FrameContext<'_, SoundCheck>, view: View) {
586 let head = view.eye();
587 let ground = Vec3::new(head.x, 0.0, head.z);
588
589 ctx.draw(
590 Cube.at(Transform::from_scale_rotation_translation(
591 Vec3::new(LISTENER_WIDTH, head.y, LISTENER_DEPTH),
592 Quat::IDENTITY,
593 ground + Vec3::Y * head.y * 0.5,
594 ))
595 .material(Material::lit(LISTENER_COLOR)),
596 );
597
598 let right = listener_right(view) * EAR_OFFSET;
599 for (offset, color) in [(right, RIGHT_EAR_COLOR), (-right, LEFT_EAR_COLOR)] {
600 ctx.draw(
601 Sphere { subdivisions: 1 }
602 .at(Transform::from_scale_rotation_translation(
603 Vec3::splat(EAR_SIZE),
604 Quat::IDENTITY,
605 head + offset,
606 ))
607 .material(Material::lit(color)),
608 );
609 }
610
611 ctx.draw(
612 Facing
613 .at(Transform::from_scale_rotation_translation(
614 Vec3::splat(FACING_MARKER_SIZE),
615 Quat::IDENTITY,
616 head + Vec3::NEG_Z * (FACING_MARKER_SIZE * 0.5),
617 ))
618 .material(Material::lit(LISTENER_COLOR)),
619 );
620 }
621
622 fn draw_merge_markers(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
623 if !self.merge_demo {
624 return;
625 }
626 for (position, color) in [(MERGE_POS_A, MERGE_COLOR_A), (MERGE_POS_B, MERGE_COLOR_B)] {
627 ctx.draw(
628 Cube.at(Transform::from_scale_rotation_translation(
629 Vec3::splat(SOURCE_HALF * 2.0),
630 Quat::IDENTITY,
631 position,
632 ))
633 .material(Material::lit(color)),
634 );
635 }
636 }
637
638 /// Draws the ring, each cube as dim as the gain its sustain is declared
639 /// at.
640 fn draw_ring(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
641 if !self.ring_demo {
642 return;
643 }
644 for nth in 0..RING_COUNT {
645 let over = 1.0 - nth as f32 / RING_COUNT as f32;
646 ctx.draw(
647 Cube.at(Transform::from_scale_rotation_translation(
648 Vec3::splat(SOURCE_HALF),
649 Quat::IDENTITY,
650 ring_place(nth),
651 ))
652 .material(Material::lit(RING_COLOR.dimmed(over))),
653 );
654 }
655 }Trait Implementations§
impl Copy for Color
impl Pod for Color
impl StructuralPartialEq for Color
Auto Trait Implementations§
impl Freeze for Color
impl RefUnwindSafe for Color
impl Send for Color
impl Sync for Color
impl Unpin for Color
impl UnsafeUnpin for Color
impl UnwindSafe for Color
Blanket Implementations§
impl<T> AnyBitPattern for Twhere
T: Pod,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CheckedBitPattern for Twhere
T: AnyBitPattern,
impl<T> CheckedBitPattern for Twhere
T: AnyBitPattern,
Source§type Bits = T
type Bits = T
Self must have the same layout as the specified Bits except for
the possible invalid bit patterns being checked during
is_valid_bit_pattern.Source§fn is_valid_bit_pattern(_bits: &T) -> bool
fn is_valid_bit_pattern(_bits: &T) -> bool
bits
as &Self.Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
Source§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more