Skip to main content

Vec3

Struct Vec3 

Source
pub struct Vec3 {
    pub x: f32,
    pub y: f32,
    pub z: f32,
}
Expand description

A 3-component f32 vector used for points, directions, and linear colors.

Fields§

§x: f32

X component (red when used as a color).

§y: f32

Y component (green when used as a color).

§z: f32

Z component (blue when used as a color).

Implementations§

Source§

impl Vec3

Source

pub const ONE: Self

The vector with every component set to one.

Source

pub const ZERO: Self

The zero vector.

Source

pub const fn splat(value: f32) -> Self

Creates a vector with every component set to value.

Source

pub const fn rgb(red: u8, green: u8, blue: u8) -> Self

Decodes an sRGB byte triple into linear-light components.

Examples found in repository?
examples/chat/welcome.rs (line 129)
128const DISK_STOPS: [Vec3; 4] = [
129	Vec3::rgb(56, 189, 248),
130	Vec3::rgb(129, 140, 248),
131	Vec3::rgb(192, 132, 252),
132	Vec3::rgb(56, 189, 248),
133];
134const BACKGROUND: Vec3 = Vec3::rgb(11, 14, 21);
135const WHITE: Vec3 = Vec3::rgb(248, 250, 252);
136const INK: Vec3 = Vec3::rgb(3, 4, 7);
137
138type LogoGrid = [[Option<(char, Color)>; LOGO_COLS]; LOGO_ROWS];
139
140/// The animated welcome screen: a retained full-viewport frame, the
141/// pre-built title chip, and the pointer-orbit camera. [`crate::run_welcome`]
142/// drives it until the user resumes into the chat demo.
143pub struct Welcome {
144	frame:          Frame,
145	title:          Str,
146	/// Detected glyph tier for the card chrome.
147	charset:        Charset,
148	camera:         (f32, f32),
149	camera_target:  (f32, f32),
150	last_elapsed:   f32,
151	/// Top-left cell of the logo as last drawn; anchors pointer mapping.
152	logo_origin:    (u16, u16),
153	logo:           LogoGrid,
154	logo_at:        Option<Duration>,
155	backdrop_frame: Frame,
156	backdrop_at:    Option<Duration>,
157	/// Eclipse backdrop program and its reusable half-block render target.
158	backdrop:       Eclipse,
159	surface:        Surface,
160	/// Last pointer cell reported by the host loop.
161	pointer:        Option<(u16, u16)>,
162	/// Eased hover amount driving the border glow.
163	hover:          Tween<f32>,
164}
165
166impl Welcome {
167	pub fn new(charset: Charset) -> Self {
168		Self {
169			charset,
170			frame: Frame::new(Size::new(0, 0)),
171			title: fmts!(" {} omp v{} ", charset.icon(Icon::Omp), env!("CARGO_PKG_VERSION")),
172			camera: (0.0, 0.0),
173			camera_target: (0.0, 0.0),
174			last_elapsed: 0.0,
175			logo_origin: (0, 0),
176			logo: [[None; LOGO_COLS]; LOGO_ROWS],
177			logo_at: None,
178			backdrop_frame: Frame::new(Size::new(0, 0)),
179			backdrop_at: None,
180			backdrop: Eclipse::default(),
181			surface: Surface::new(),
182			pointer: None,
183			hover: Tween::settled(0.0),
184		}
185	}
186
187	/// Records the pointer (0-based cells) for the hover zone and retargets
188	/// the camera: the pointer's offset from the logo center maps to camera
189	/// lift and a full half-turn of yaw in each direction, matching the
190	/// prototype.
191	pub fn point_at(&mut self, column: u16, row: u16) {
192		self.pointer = Some((column, row));
193		self.logo_at = None;
194		let center_x = f32::from(self.logo_origin.0) + LOGO_COLS as f32 / 2.0;
195		let center_y = f32::from(self.logo_origin.1) + LOGO_ROWS as f32 / 2.0;
196		let horizontal = ((f32::from(column) - center_x) / (LOGO_COLS as f32 / 2.0)).clamp(-1.0, 1.0);
197		let vertical = ((f32::from(row) - center_y) / (LOGO_ROWS as f32 / 2.0)).clamp(-1.0, 1.0);
198		self.camera_target = (-vertical * 0.42, -horizontal * PI);
199	}
200
201	/// Paints the card centered in `viewport` at `elapsed` since boot and
202	/// returns the full-viewport frame (no stable rows, everything damaged).
203	pub fn render(&mut self, viewport: Size, elapsed: Duration) -> &Frame {
204		if self.frame.size() != viewport {
205			self.frame = Frame::new(viewport);
206			self.backdrop_frame = Frame::new(viewport);
207			self.backdrop_at = None;
208		}
209		let clock = elapsed;
210		let elapsed = elapsed.as_secs_f32();
211		// Exponential pointer chase, frame-rate independent (~100ms lag).
212		let delta = (elapsed - self.last_elapsed).max(0.0);
213		self.last_elapsed = elapsed;
214		let response = 1.0 - (-delta * 10.0).exp();
215		self.camera.0 += (self.camera_target.0 - self.camera.0) * response;
216		self.camera.1 += (self.camera_target.1 - self.camera.1) * response;
217		self.draw_backdrop(viewport, clock, elapsed);
218
219		let logo_interval = ambient_interval(clock, LOGO_IDLE_INTERVAL);
220		if self
221			.logo_at
222			.is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= logo_interval)
223		{
224			self.logo = logo_cells(elapsed, self.camera);
225			self.logo_at = Some(clock);
226		}
227		let cols = if viewport.width >= CARD_COLS && viewport.height >= CARD_ROWS {
228			Some(CARD_COLS)
229		} else if viewport.width >= SMOL_COLS && viewport.height >= CARD_ROWS {
230			Some(SMOL_COLS)
231		} else {
232			None
233		};
234		let Some(cols) = cols else {
235			let left = viewport.width.saturating_sub(LOGO_COLS as u16) / 2;
236			let top = viewport.height.saturating_sub(LOGO_ROWS as u16) / 2;
237			self.logo_origin = (left, top);
238			blit_logo(&mut self.frame, &self.logo, left, top, PLATE);
239			return &self.frame;
240		};
241
242		let left = (viewport.width - cols) / 2;
243		let top = (viewport.height - CARD_ROWS) / 2;
244		let hovered = self.pointer.is_some_and(|(x, y)| {
245			(left..left + cols).contains(&x) && (top..top + CARD_ROWS).contains(&y)
246		});
247		self
248			.hover
249			.retarget(clock, if hovered { 1.0 } else { 0.0 }, HOVER_EASE, Easing::EaseOut);
250		let hover = self.hover.sample(clock).clamp(0.0, 1.0);
251		self.draw_card(cols, left, top, elapsed, hover);
252		&self.frame
253	}
254
255	/// Paints the eclipse across the whole viewport, resolving out of
256	/// black over the first [`BACKDROP_FADE`] seconds of boot.
257	fn draw_backdrop(&mut self, viewport: Size, clock: Duration, elapsed: f32) {
258		let interval = ambient_interval(clock, BACKDROP_IDLE_INTERVAL);
259		if self
260			.backdrop_at
261			.is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= interval)
262		{
263			let fade = smooth((elapsed / BACKDROP_FADE).clamp(0.0, 1.0));
264			self
265				.backdrop_frame
266				.fill(Rect::new(0, 0, viewport.width, viewport.height), Style::default());
267			let frame = &mut self.backdrop_frame;
268			let mut buffer = [0_u8; 4];
269			let dim = |color: Color| Color::Rgb(0, 0, 0).lerp(color, fade);
270			self.surface.render(
271				&mut self.backdrop,
272				clock,
273				viewport.width,
274				viewport.height,
275				|x, y, glyph, fg, bg| {
276					let style = Style::new().fg(dim(fg));
277					let style = match bg {
278						Some(bg) => style.bg(dim(bg)),
279						None => style,
280					};
281					frame.put(x, y, glyph.encode_utf8(&mut buffer), style);
282				},
283			);
284			self.backdrop_at = Some(clock);
285		}
286		self.frame.clone_from(&self.backdrop_frame);
287	}
288
289	fn draw_card(&mut self, cols: u16, left: u16, top: u16, elapsed: f32, hover: f32) {
290		let full = cols == CARD_COLS;
291		let logo_left = if full {
292			left + 3
293		} else {
294			left + (cols - LOGO_COLS as u16) / 2
295		};
296		self.logo_origin = (logo_left, top + 2);
297		// Pointer-tracking border glow: the brand gradient sampled by angle
298		// around the card center (the disk's own palette), strongest near
299		// the pointer, scaled by the eased hover amount.
300		let pointer = self.pointer;
301		let center =
302			(f32::from(left) + f32::from(cols) / 2.0, f32::from(top) + f32::from(CARD_ROWS) / 2.0);
303		let edge_at = move |x: u16, y: u16| -> Style {
304			let Some((px, py)) = pointer.filter(|_| hover > 0.02) else {
305				return on_card(CARD_BORDER);
306			};
307			let dx = (f32::from(x) - f32::from(px)) * 0.5;
308			let dy = f32::from(y) - f32::from(py);
309			let glow = hover * (-(dx * dx + dy * dy) / 34.0).exp();
310			if glow < 0.02 {
311				return on_card(CARD_BORDER);
312			}
313			let angle = (f32::from(y) - center.1).atan2((f32::from(x) - center.0) * 0.5);
314			let brand = vec3_color(gradient(angle - elapsed * 0.5));
315			on_card(CARD_BORDER.lerp(brand, glow))
316		};
317		let frame = &mut self.frame;
318		frame.fill(Rect::new(left, top, cols, CARD_ROWS), on_card(TEXT));
319
320		let right = left + cols - 1;
321		let bottom = top + CARD_ROWS - 1;
322		let divider = bottom - 2;
323		let (tl, tr, bl, br, horizontal, vertical) = self.charset.border(Border::Round);
324		let grid = self.charset.grid();
325		let mut glyph = [0_u8; 4];
326		frame.put(left, top, tl.encode_utf8(&mut glyph), edge_at(left, top));
327		frame.put(right, top, tr.encode_utf8(&mut glyph), edge_at(right, top));
328		frame.put(left, divider, grid.middle.0.encode_utf8(&mut glyph), edge_at(left, divider));
329		frame.put(right, divider, grid.middle.2.encode_utf8(&mut glyph), edge_at(right, divider));
330		frame.put(left, bottom, bl.encode_utf8(&mut glyph), edge_at(left, bottom));
331		frame.put(right, bottom, br.encode_utf8(&mut glyph), edge_at(right, bottom));
332		for x in left + 1..right {
333			frame.put(x, top, horizontal.encode_utf8(&mut glyph), edge_at(x, top));
334			frame.put(x, divider, horizontal.encode_utf8(&mut glyph), edge_at(x, divider));
335			frame.put(x, bottom, horizontal.encode_utf8(&mut glyph), edge_at(x, bottom));
336		}
337		for y in top + 1..bottom {
338			if y != divider {
339				frame.put(left, y, vertical.encode_utf8(&mut glyph), edge_at(left, y));
340				frame.put(right, y, vertical.encode_utf8(&mut glyph), edge_at(right, y));
341			}
342		}
343
344		frame.put(left + 2, top, self.title.as_str(), on_card(TEXT_STRONG));
345		if full {
346			frame.put(left + 34, top, " SESSION ORBIT ", on_card(FAINT));
347			draw_dust(frame, left, top, elapsed);
348			draw_sessions(frame, left, top, self.charset);
349			draw_beam(frame, left, top, elapsed);
350		}
351
352		blit_logo(frame, &self.logo, logo_left, top + 2, CARD_BG);
353
354		let footer = divider + 1;
355		frame.fill(Rect::new(left + 1, footer, cols - 2, 1), on_footer(TEXT));
356		if full {
357			frame.put(left + 3, divider, " SHORTCUTS ", on_card(FAINT));
358			let dot = fmts!(" {} ", self.charset.icon(Icon::Enabled));
359			let x = frame.put(left + cols - 21, top, &dot, on_card(GREEN));
360			frame.put(x, top, "rust-analyzer ", on_card(MUTED));
361			draw_full_hints(frame, left, footer);
362		} else {
363			draw_smol_hints(frame, left, cols, footer);
364		}
365	}
366}
367
368impl Default for Welcome {
369	fn default() -> Self {
370		Self::new(Charset::NerdFont)
371	}
372}
373
374fn draw_dust(frame: &mut Frame, left: u16, top: u16, elapsed: f32) {
375	for &(x, y, offset) in &DUST {
376		let pulse = 0.5 + 0.5 * (elapsed * 1.4 + offset).sin();
377		let color = FAINT.lerp(CYAN, pulse * 0.28);
378		frame.put(left + x, top + y, "·", on_card(color));
379	}
380	frame.put(left + 1, top + 7, HORIZON, on_card(FAINT.lerp(INDIGO, 0.16)));
381	frame.put(left + 14, top + 1, "+Z", on_card(FAINT));
382}
383
384fn draw_beam(frame: &mut Frame, left: u16, top: u16, elapsed: f32) {
385	let phase = (elapsed * 9.0) as usize % BEAM.len();
386	for (index, &(x, y, glyph)) in BEAM.iter().enumerate() {
387		let direct = index.abs_diff(phase);
388		let distance = direct.min(BEAM.len() - direct);
389		let color = match distance {
390			0 => TEXT_STRONG,
391			1 => CYAN,
392			_ => FAINT.lerp(INDIGO, 0.34),
393		};
394		frame.put(left + x, top + y, glyph, on_card(color));
395	}
396}
397
398fn draw_sessions(frame: &mut Frame, left: u16, top: u16, charset: Charset) {
399	let (_, _, _, _, _, vertical) = charset.border(Border::Round);
400	let mut glyph = [0_u8; 4];
401	let panel_x = left + 36;
402	frame.put(panel_x, top + 2, "RECENT SESSIONS", on_card(MUTED));
403	frame.put(left + CARD_COLS - 14, top + 2, "4 / LOCAL", on_card(FAINT));
404	for y in top + 4..=top + 10 {
405		frame.put(panel_x, y, vertical.encode_utf8(&mut glyph), on_card(FAINT.lerp(INDIGO, 0.18)));
406	}
407	for (index, (label, age)) in SESSIONS.iter().enumerate() {
408		let y = top + 4 + index as u16 * 2;
409		if index == 0 {
410			frame.fill(Rect::new(panel_x - 2, y, CARD_COLS - 35, 1), on_selected(TEXT));
411			frame.put(panel_x - 2, y, charset.rail(), on_selected(GREEN));
412			frame.put(panel_x, y, charset.radio(true), on_selected(GREEN));
413			frame.put(panel_x + 2, y, age, on_selected(GREEN));
414			frame.put(panel_x + 7, y, label, on_selected(TEXT_STRONG));
415		} else {
416			frame.put(panel_x, y, charset.radio(false), on_card(FAINT));
417			frame.put(panel_x + 2, y, age, on_card(FAINT));
418			frame.put(panel_x + 7, y, label, on_card(MUTED));
419		}
420	}
421}
422
423fn draw_full_hints(frame: &mut Frame, left: u16, y: u16) {
424	frame.put(left + 3, y, "#", on_footer(CYAN));
425	frame.put(left + 5, y, "actions", on_footer(MUTED));
426	frame.put(left + 14, y, "/", on_footer(GREEN));
427	frame.put(left + 16, y, "commands", on_footer(MUTED));
428	frame.put(left + 27, y, "!", on_footer(AMBER));
429	frame.put(left + 29, y, "shell", on_footer(MUTED));
430	frame.put(left + 37, y, "$", on_footer(VIOLET));
431	frame.put(left + 39, y, "python", on_footer(MUTED));
432	frame.put(left + CARD_COLS - 23, y, "↑↓ move", on_footer(FAINT));
433	frame.put(left + CARD_COLS - 13, y, "↵ resume", on_footer(TEXT_STRONG));
434}
435
436fn draw_smol_hints(frame: &mut Frame, left: u16, cols: u16, y: u16) {
437	frame.put(left + 3, y, "#", on_footer(CYAN).bold());
438	frame.put(left + 5, y, "/", on_footer(CYAN).bold());
439	frame.put(left + 7, y, "!", on_footer(AMBER).bold());
440	frame.put(left + 9, y, "$", on_footer(GREEN).bold());
441	frame.put(left + cols - 14, y, "enter", on_footer(FAINT));
442	frame.put(left + cols - 8, y, "resume", on_footer(TEXT_STRONG).bold());
443}
444
445fn blit_logo(frame: &mut Frame, logo: &LogoGrid, left: u16, top: u16, background: Color) {
446	let mut buffer = [0_u8; 4];
447	for (row, cells) in logo.iter().enumerate() {
448		for (column, cell) in cells.iter().enumerate() {
449			let Some((glyph, color)) = cell else { continue };
450			let style = Style::new().fg(*color).bg(background);
451			frame.put(left + column as u16, top + row as u16, glyph.encode_utf8(&mut buffer), style);
452		}
453	}
454}
455
456const fn on_card(fg: Color) -> Style {
457	Style::new().fg(fg).bg(CARD_BG)
458}
459
460const fn on_footer(fg: Color) -> Style {
461	Style::new().fg(fg).bg(FOOTER_BG)
462}
463
464const fn on_selected(fg: Color) -> Style {
465	Style::new().fg(fg).bg(SELECTED_BG)
466}
467
468// ── raytraced logo ───────────────────────────────────────────────────────────
469
470fn smooth(edge: f32) -> f32 {
471	edge * edge * (3.0 - 2.0 * edge)
472}
473
474fn ease_in(progress: f32) -> f32 {
475	let clamped = progress.clamp(0.0, 1.0);
476	clamped * clamped
477}
478
479/// Opaque platter → glass blend, driven by boot time.
480fn color_mix(elapsed: f32) -> f32 {
481	ease_in((elapsed - 0.18) / 0.58)
482}
483
484/// One decaying full camera orbit as the logo reveals.
485fn reveal_orbit(elapsed: f32) -> f32 {
486	let progress = ((elapsed - 0.18) / 0.64).clamp(0.0, 1.0);
487	TAU * (1.0 - (1.0 - progress).powi(3))
488}
489
490/// Disk spin: quadratic ramp into a constant angular speed.
491fn disk_rotation(elapsed: f32) -> f32 {
492	let spinning = (elapsed - 0.18).max(0.0);
493	let ramp = 0.40;
494	let angular_speed = TAU / 2.4;
495	if spinning < ramp {
496		angular_speed * spinning * spinning / (2.0 * ramp)
497	} else {
498		angular_speed * (spinning - ramp / 2.0)
499	}
500}
501
502/// Samples the brand gradient by angle around the disk.
503fn gradient(angle: f32) -> Vec3 {
504	let position = (angle / TAU + 0.5).rem_euclid(1.0) * 3.0;
505	let index = (position as usize).min(2);
506	DISK_STOPS[index].lerp(DISK_STOPS[index + 1], position - index as f32)
507}
508
509/// Encodes a linear-light color for the terminal.
510fn vec3_color(color: Vec3) -> Color {
511	Color::from(color)
512}
513
514/// Three soft light shafts crossing the stage in the sun's plane.
515fn sun_rays(x: f32, z: f32) -> f32 {
516	let length = 0.55_f32.hypot(0.38);
517	let (hx, hz) = (-0.55 / length, 0.38 / length);
518	let across = x * -hz + z * hx;
519	let along = x * hx + z * hz;
520	let rays = (-((across + 0.56) / 0.075).powi(2)).exp()
521		+ (-((across + 0.04) / 0.055).powi(2)).exp()
522		+ (-((across - 0.47) / 0.09).powi(2)).exp();
523	let envelope = (-((along + 0.20) / 2.45).powi(4)).exp();
524	(rays * envelope).clamp(0.0, 1.0)
525}
526
527fn sphere_depth(origin: Vec3, direction: Vec3, center: Vec3, radius: f32) -> f32 {
528	let offset = origin - center;
529	let projection = offset.dot(direction);
530	let discriminant = projection * projection - (offset.dot(offset) - radius * radius);
531	let root = -projection - discriminant.max(0.0).sqrt();
532	if discriminant >= 0.0 && root > 0.0 {
533		root
534	} else {
535		f32::INFINITY
536	}
537}
538
539/// Central axis: a capped cylinder with a slightly bulged top sphere.
540fn axis_hit(origin: Vec3, direction: Vec3) -> (f32, bool, Vec3) {
541	let quadratic = direction.x * direction.x + direction.z * direction.z;
542	let linear = 2.0 * (origin.x * direction.x + origin.z * direction.z);
543	let constant = origin.x * origin.x + origin.z * origin.z - AXIS_RADIUS * AXIS_RADIUS;
544	let discriminant = linear * linear - 4.0 * quadratic * constant;
545	let root = (-linear - discriminant.max(0.0).sqrt()) / (2.0 * quadratic).max(1e-8);
546	let hit_y = origin.y + direction.y * root;
547	let cylinder = if discriminant >= 0.0 && root > 0.0 && (AXIS_BOTTOM..=AXIS_TOP).contains(&hit_y)
548	{
549		root
550	} else {
551		f32::INFINITY
552	};
553	let cap_center = vec3(0.0, AXIS_TOP, 0.0);
554	let cap = sphere_depth(origin, direction, cap_center, AXIS_RADIUS * 1.75);
555	let depth = cylinder.min(cap);
556	if !depth.is_finite() {
557		return (f32::INFINITY, false, vec3(0.0, 1.0, 0.0));
558	}
559	let point = origin + direction * depth;
560	let normal = if cap < cylinder {
561		(point - cap_center).normalize()
562	} else {
563		vec3(point.x, 0.0, point.z).normalize()
564	};
565	(depth, true, normal)
566}
567
568/// Terminal display lift in linear light so braille remains legible on the
569/// dark card.
570fn tone(color: Vec3) -> Vec3 {
571	color * 1.28 + Vec3::rgb(4, 4, 4)
572}
Source

pub fn dot(self, other: Self) -> f32

Dot product.

Examples found in repository?
examples/chat/welcome.rs (line 529)
527fn sphere_depth(origin: Vec3, direction: Vec3, center: Vec3, radius: f32) -> f32 {
528	let offset = origin - center;
529	let projection = offset.dot(direction);
530	let discriminant = projection * projection - (offset.dot(offset) - radius * radius);
531	let root = -projection - discriminant.max(0.0).sqrt();
532	if discriminant >= 0.0 && root > 0.0 {
533		root
534	} else {
535		f32::INFINITY
536	}
537}
538
539/// Central axis: a capped cylinder with a slightly bulged top sphere.
540fn axis_hit(origin: Vec3, direction: Vec3) -> (f32, bool, Vec3) {
541	let quadratic = direction.x * direction.x + direction.z * direction.z;
542	let linear = 2.0 * (origin.x * direction.x + origin.z * direction.z);
543	let constant = origin.x * origin.x + origin.z * origin.z - AXIS_RADIUS * AXIS_RADIUS;
544	let discriminant = linear * linear - 4.0 * quadratic * constant;
545	let root = (-linear - discriminant.max(0.0).sqrt()) / (2.0 * quadratic).max(1e-8);
546	let hit_y = origin.y + direction.y * root;
547	let cylinder = if discriminant >= 0.0 && root > 0.0 && (AXIS_BOTTOM..=AXIS_TOP).contains(&hit_y)
548	{
549		root
550	} else {
551		f32::INFINITY
552	};
553	let cap_center = vec3(0.0, AXIS_TOP, 0.0);
554	let cap = sphere_depth(origin, direction, cap_center, AXIS_RADIUS * 1.75);
555	let depth = cylinder.min(cap);
556	if !depth.is_finite() {
557		return (f32::INFINITY, false, vec3(0.0, 1.0, 0.0));
558	}
559	let point = origin + direction * depth;
560	let normal = if cap < cylinder {
561		(point - cap_center).normalize()
562	} else {
563		vec3(point.x, 0.0, point.z).normalize()
564	};
565	(depth, true, normal)
566}
567
568/// Terminal display lift in linear light so braille remains legible on the
569/// dark card.
570fn tone(color: Vec3) -> Vec3 {
571	color * 1.28 + Vec3::rgb(4, 4, 4)
572}
573
574/// Per-frame scene state shared by every ray: sun direction, disk spin,
575/// glass transition, and the pointer camera offsets.
576struct Platter {
577	sun:        Vec3,
578	angle:      f32,
579	transition: f32,
580	/// Smoothed pointer camera from [`Welcome`]: (lift, yaw).
581	pointer:    (f32, f32),
582}
583
584impl Platter {
585	fn new(pointer: (f32, f32)) -> Self {
586		Self { sun: vec3(-0.55, 1.0, 0.38).normalize(), angle: 0.0, transition: 0.0, pointer }
587	}
588
589	/// Floor glow: light shafts, the disk's shadow, tinted transmission
590	/// through the glass, and a soft rim reflection.
591	fn ground(&self, origin: Vec3, direction: Vec3) -> (Vec3, f32) {
592		if direction.y >= 0.0 {
593			return (BACKGROUND, 0.0);
594		}
595		let depth = (FLOOR_Y - origin.y) / direction.y;
596		if depth <= 0.0 {
597			return (BACKGROUND, 0.0);
598		}
599		let floor = origin + direction * depth;
600		let stage =
601			(-0.42 * ((floor.x.abs() / 2.30).powi(4) + ((floor.z + 0.15).abs() / 1.70).powi(4))).exp();
602
603		let sun_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / self.sun.y;
604		let sunlit = floor + self.sun * sun_depth;
605		let shadow_radius = sunlit.x.hypot(sunlit.z);
606		let occlusion = smooth(((DISK_RADIUS + 0.08 - shadow_radius) / 0.16).clamp(0.0, 1.0));
607		let rays = sun_rays(floor.x, floor.z);
608
609		let neutral_alpha = stage * rays * (1.0 - occlusion) * 0.36;
610		let neutral = (BACKGROUND + vec3(0.36, 0.33, 0.27) * (stage * rays)).clamp01();
611		let mut color = neutral * neutral_alpha + BACKGROUND * (1.0 - neutral_alpha);
612		let mut alpha = neutral_alpha;
613
614		let transmission_alpha =
615			stage * sun_rays(sunlit.x, sunlit.z) * occlusion * self.transition * 0.64;
616		let transmission =
617			(BACKGROUND + gradient(sunlit.z.atan2(sunlit.x) - self.angle) * 0.86).clamp01();
618		color = transmission * transmission_alpha + color * (1.0 - transmission_alpha);
619		alpha = transmission_alpha + alpha * (1.0 - transmission_alpha);
620
621		let mirrored = vec3(direction.x, -direction.y, direction.z);
622		let reflected_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / mirrored.y.max(1e-6);
623		let reflected = floor + mirrored * reflected_depth;
624		let reflected_radius = reflected.x.hypot(reflected.z);
625		if reflected_radius <= DISK_RADIUS {
626			let edge = smooth(((DISK_RADIUS - reflected_radius) / 0.13).clamp(0.0, 1.0));
627			let grazing = (1.0 + direction.y).clamp(0.0, 1.0);
628			let reflection_alpha = self.transition * edge * (0.22 + 0.28 * grazing);
629			let reflection = gradient(reflected.z.atan2(reflected.x) - self.angle);
630			color = reflection * reflection_alpha + color * (1.0 - reflection_alpha);
631			alpha = reflection_alpha + alpha * (1.0 - reflection_alpha);
632		}
633		(color, alpha)
634	}
635}
636
637impl Trace for Platter {
638	fn advance(&mut self, now: Duration) -> Camera {
639		let elapsed = now.as_secs_f32();
640		self.angle = disk_rotation(elapsed);
641		self.transition = color_mix(elapsed);
642		Camera {
643			target:   vec3(0.0, 0.08, 0.0),
644			yaw:      self.pointer.1 + reveal_orbit(elapsed) + (elapsed * 0.31).sin() * 0.018,
645			pitch:    CAMERA_PITCH,
646			distance: CAMERA_DISTANCE,
647			lift:     self.pointer.0.clamp(-0.42, 0.42) + (elapsed * 0.55).sin() * 0.014,
648			focal:    CAMERA_FOCAL,
649		}
650	}
651
652	/// Shades one sample: returns the color composited over the card
653	/// background and the coverage used for braille dot thresholds.
654	fn shade(&self, ray: Ray) -> (Vec3, f32) {
655		let Ray { origin, dir: direction } = ray;
656
657		// Top surface of the disk.
658		let mut disk_depth = f32::INFINITY;
659		let mut disk_point = Vec3::ZERO;
660		let mut disk_radial = 0.0;
661		if direction.y < 0.0 {
662			let depth = (DISK_Y + DISK_HALF_THICKNESS - origin.y) / direction.y;
663			if depth > 0.0 {
664				let point = origin + direction * depth;
665				let radial = point.x.hypot(point.z);
666				if radial <= DISK_RADIUS {
667					disk_depth = depth;
668					disk_point = point;
669					disk_radial = radial;
670				}
671			}
672		}
673		let disk_visible = disk_depth.is_finite();
674		let (axis_depth, axis_visible, axis_normal) = axis_hit(origin, direction);
675
676		let (ground_color, ground_alpha) = self.ground(origin, direction);
677		let mut color = ground_color * ground_alpha + BACKGROUND * (1.0 - ground_alpha);
678		let mut alpha = ground_alpha;
679
680		let view = direction * -1.0;
681		let halfway = if disk_visible || axis_visible {
682			(self.sun + view).normalize()
683		} else {
684			Vec3::ZERO
685		};
686		let axis_color = if axis_visible {
687			let diffuse = axis_normal.dot(self.sun).max(0.0);
688			let specular = axis_normal.dot(halfway).max(0.0).powi(44);
689			(WHITE * (0.32 + 0.68 * diffuse + 0.48 * specular)).clamp01()
690		} else {
691			Vec3::ZERO
692		};
693
694		if axis_visible && axis_depth > disk_depth {
695			color = axis_color;
696			alpha = 1.0;
697		}
698
699		if disk_visible {
700			let diffuse = self.sun.y.max(0.0);
701			let specular = halfway.y.max(0.0).powi(72);
702			let fresnel = (1.0 - view.y.max(0.0)).powi(4);
703			let material_angle = disk_point.z.atan2(disk_point.x) - self.angle;
704			let rim = smooth(((disk_radial - (DISK_RADIUS - 0.075)) / 0.055).clamp(0.0, 1.0));
705			let streak_angle = (material_angle - 0.32 + PI).rem_euclid(TAU) - PI;
706			let streak = (-(streak_angle / 0.19).powi(2)).exp();
707			let incident = sun_rays(disk_point.x, disk_point.z);
708
709			let opaque =
710				(WHITE * (0.30 + 0.27 * diffuse + 0.36 * incident + 0.20 * specular)).clamp01();
711			let glass = (gradient(material_angle) * (0.34 + 0.70 * diffuse)
712				+ WHITE * (0.72 * specular + 0.16 * fresnel + 0.20 * streak))
713				.clamp01();
714			let border_strength = rim * self.transition;
715			let mut disk_color = opaque
716				.lerp(glass, self.transition)
717				.lerp(WHITE, border_strength);
718
719			// Orbiting index marker punched into the surface.
720			let marker_x = 0.52 * self.angle.cos();
721			let marker_z = 0.52 * self.angle.sin();
722			let marker_distance = (disk_point.x - marker_x).hypot(disk_point.z - marker_z);
723			let marker = smooth(((0.10 - marker_distance) / 0.035).clamp(0.0, 1.0));
724			disk_color = disk_color.lerp(INK, marker);
725
726			let disk_alpha = (1.0 - self.transition * (1.0 - DISK_GLASS_OPACITY))
727				.max(border_strength * 0.96)
728				.max(marker * 0.98);
729			color = disk_color * disk_alpha + color * (1.0 - disk_alpha);
730			alpha = disk_alpha + alpha * (1.0 - disk_alpha);
731		}
732
733		if axis_visible && axis_depth <= disk_depth {
734			color = axis_color;
735			alpha = 1.0;
736		}
737		(tone(color), alpha)
738	}
Source

pub fn length_squared(self) -> f32

Squared vector length.

Source

pub fn length(self) -> f32

Vector length.

Source

pub fn cross(self, other: Self) -> Self

Cross product.

Source

pub fn normalize(self) -> Self

Unit-length copy; near-zero vectors stay finite.

Examples found in repository?
examples/chat/welcome.rs (line 561)
540fn axis_hit(origin: Vec3, direction: Vec3) -> (f32, bool, Vec3) {
541	let quadratic = direction.x * direction.x + direction.z * direction.z;
542	let linear = 2.0 * (origin.x * direction.x + origin.z * direction.z);
543	let constant = origin.x * origin.x + origin.z * origin.z - AXIS_RADIUS * AXIS_RADIUS;
544	let discriminant = linear * linear - 4.0 * quadratic * constant;
545	let root = (-linear - discriminant.max(0.0).sqrt()) / (2.0 * quadratic).max(1e-8);
546	let hit_y = origin.y + direction.y * root;
547	let cylinder = if discriminant >= 0.0 && root > 0.0 && (AXIS_BOTTOM..=AXIS_TOP).contains(&hit_y)
548	{
549		root
550	} else {
551		f32::INFINITY
552	};
553	let cap_center = vec3(0.0, AXIS_TOP, 0.0);
554	let cap = sphere_depth(origin, direction, cap_center, AXIS_RADIUS * 1.75);
555	let depth = cylinder.min(cap);
556	if !depth.is_finite() {
557		return (f32::INFINITY, false, vec3(0.0, 1.0, 0.0));
558	}
559	let point = origin + direction * depth;
560	let normal = if cap < cylinder {
561		(point - cap_center).normalize()
562	} else {
563		vec3(point.x, 0.0, point.z).normalize()
564	};
565	(depth, true, normal)
566}
567
568/// Terminal display lift in linear light so braille remains legible on the
569/// dark card.
570fn tone(color: Vec3) -> Vec3 {
571	color * 1.28 + Vec3::rgb(4, 4, 4)
572}
573
574/// Per-frame scene state shared by every ray: sun direction, disk spin,
575/// glass transition, and the pointer camera offsets.
576struct Platter {
577	sun:        Vec3,
578	angle:      f32,
579	transition: f32,
580	/// Smoothed pointer camera from [`Welcome`]: (lift, yaw).
581	pointer:    (f32, f32),
582}
583
584impl Platter {
585	fn new(pointer: (f32, f32)) -> Self {
586		Self { sun: vec3(-0.55, 1.0, 0.38).normalize(), angle: 0.0, transition: 0.0, pointer }
587	}
588
589	/// Floor glow: light shafts, the disk's shadow, tinted transmission
590	/// through the glass, and a soft rim reflection.
591	fn ground(&self, origin: Vec3, direction: Vec3) -> (Vec3, f32) {
592		if direction.y >= 0.0 {
593			return (BACKGROUND, 0.0);
594		}
595		let depth = (FLOOR_Y - origin.y) / direction.y;
596		if depth <= 0.0 {
597			return (BACKGROUND, 0.0);
598		}
599		let floor = origin + direction * depth;
600		let stage =
601			(-0.42 * ((floor.x.abs() / 2.30).powi(4) + ((floor.z + 0.15).abs() / 1.70).powi(4))).exp();
602
603		let sun_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / self.sun.y;
604		let sunlit = floor + self.sun * sun_depth;
605		let shadow_radius = sunlit.x.hypot(sunlit.z);
606		let occlusion = smooth(((DISK_RADIUS + 0.08 - shadow_radius) / 0.16).clamp(0.0, 1.0));
607		let rays = sun_rays(floor.x, floor.z);
608
609		let neutral_alpha = stage * rays * (1.0 - occlusion) * 0.36;
610		let neutral = (BACKGROUND + vec3(0.36, 0.33, 0.27) * (stage * rays)).clamp01();
611		let mut color = neutral * neutral_alpha + BACKGROUND * (1.0 - neutral_alpha);
612		let mut alpha = neutral_alpha;
613
614		let transmission_alpha =
615			stage * sun_rays(sunlit.x, sunlit.z) * occlusion * self.transition * 0.64;
616		let transmission =
617			(BACKGROUND + gradient(sunlit.z.atan2(sunlit.x) - self.angle) * 0.86).clamp01();
618		color = transmission * transmission_alpha + color * (1.0 - transmission_alpha);
619		alpha = transmission_alpha + alpha * (1.0 - transmission_alpha);
620
621		let mirrored = vec3(direction.x, -direction.y, direction.z);
622		let reflected_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / mirrored.y.max(1e-6);
623		let reflected = floor + mirrored * reflected_depth;
624		let reflected_radius = reflected.x.hypot(reflected.z);
625		if reflected_radius <= DISK_RADIUS {
626			let edge = smooth(((DISK_RADIUS - reflected_radius) / 0.13).clamp(0.0, 1.0));
627			let grazing = (1.0 + direction.y).clamp(0.0, 1.0);
628			let reflection_alpha = self.transition * edge * (0.22 + 0.28 * grazing);
629			let reflection = gradient(reflected.z.atan2(reflected.x) - self.angle);
630			color = reflection * reflection_alpha + color * (1.0 - reflection_alpha);
631			alpha = reflection_alpha + alpha * (1.0 - reflection_alpha);
632		}
633		(color, alpha)
634	}
635}
636
637impl Trace for Platter {
638	fn advance(&mut self, now: Duration) -> Camera {
639		let elapsed = now.as_secs_f32();
640		self.angle = disk_rotation(elapsed);
641		self.transition = color_mix(elapsed);
642		Camera {
643			target:   vec3(0.0, 0.08, 0.0),
644			yaw:      self.pointer.1 + reveal_orbit(elapsed) + (elapsed * 0.31).sin() * 0.018,
645			pitch:    CAMERA_PITCH,
646			distance: CAMERA_DISTANCE,
647			lift:     self.pointer.0.clamp(-0.42, 0.42) + (elapsed * 0.55).sin() * 0.014,
648			focal:    CAMERA_FOCAL,
649		}
650	}
651
652	/// Shades one sample: returns the color composited over the card
653	/// background and the coverage used for braille dot thresholds.
654	fn shade(&self, ray: Ray) -> (Vec3, f32) {
655		let Ray { origin, dir: direction } = ray;
656
657		// Top surface of the disk.
658		let mut disk_depth = f32::INFINITY;
659		let mut disk_point = Vec3::ZERO;
660		let mut disk_radial = 0.0;
661		if direction.y < 0.0 {
662			let depth = (DISK_Y + DISK_HALF_THICKNESS - origin.y) / direction.y;
663			if depth > 0.0 {
664				let point = origin + direction * depth;
665				let radial = point.x.hypot(point.z);
666				if radial <= DISK_RADIUS {
667					disk_depth = depth;
668					disk_point = point;
669					disk_radial = radial;
670				}
671			}
672		}
673		let disk_visible = disk_depth.is_finite();
674		let (axis_depth, axis_visible, axis_normal) = axis_hit(origin, direction);
675
676		let (ground_color, ground_alpha) = self.ground(origin, direction);
677		let mut color = ground_color * ground_alpha + BACKGROUND * (1.0 - ground_alpha);
678		let mut alpha = ground_alpha;
679
680		let view = direction * -1.0;
681		let halfway = if disk_visible || axis_visible {
682			(self.sun + view).normalize()
683		} else {
684			Vec3::ZERO
685		};
686		let axis_color = if axis_visible {
687			let diffuse = axis_normal.dot(self.sun).max(0.0);
688			let specular = axis_normal.dot(halfway).max(0.0).powi(44);
689			(WHITE * (0.32 + 0.68 * diffuse + 0.48 * specular)).clamp01()
690		} else {
691			Vec3::ZERO
692		};
693
694		if axis_visible && axis_depth > disk_depth {
695			color = axis_color;
696			alpha = 1.0;
697		}
698
699		if disk_visible {
700			let diffuse = self.sun.y.max(0.0);
701			let specular = halfway.y.max(0.0).powi(72);
702			let fresnel = (1.0 - view.y.max(0.0)).powi(4);
703			let material_angle = disk_point.z.atan2(disk_point.x) - self.angle;
704			let rim = smooth(((disk_radial - (DISK_RADIUS - 0.075)) / 0.055).clamp(0.0, 1.0));
705			let streak_angle = (material_angle - 0.32 + PI).rem_euclid(TAU) - PI;
706			let streak = (-(streak_angle / 0.19).powi(2)).exp();
707			let incident = sun_rays(disk_point.x, disk_point.z);
708
709			let opaque =
710				(WHITE * (0.30 + 0.27 * diffuse + 0.36 * incident + 0.20 * specular)).clamp01();
711			let glass = (gradient(material_angle) * (0.34 + 0.70 * diffuse)
712				+ WHITE * (0.72 * specular + 0.16 * fresnel + 0.20 * streak))
713				.clamp01();
714			let border_strength = rim * self.transition;
715			let mut disk_color = opaque
716				.lerp(glass, self.transition)
717				.lerp(WHITE, border_strength);
718
719			// Orbiting index marker punched into the surface.
720			let marker_x = 0.52 * self.angle.cos();
721			let marker_z = 0.52 * self.angle.sin();
722			let marker_distance = (disk_point.x - marker_x).hypot(disk_point.z - marker_z);
723			let marker = smooth(((0.10 - marker_distance) / 0.035).clamp(0.0, 1.0));
724			disk_color = disk_color.lerp(INK, marker);
725
726			let disk_alpha = (1.0 - self.transition * (1.0 - DISK_GLASS_OPACITY))
727				.max(border_strength * 0.96)
728				.max(marker * 0.98);
729			color = disk_color * disk_alpha + color * (1.0 - disk_alpha);
730			alpha = disk_alpha + alpha * (1.0 - disk_alpha);
731		}
732
733		if axis_visible && axis_depth <= disk_depth {
734			color = axis_color;
735			alpha = 1.0;
736		}
737		(tone(color), alpha)
738	}
Source

pub fn reflect(self, normal: Self) -> Self

Reflection of this direction around normal.

Source

pub fn refract(self, normal: Self, eta: f32) -> Option<Self>

Refraction through normal at the incident/transmitted IOR ratio.

Returns None when total internal reflection prevents transmission.

Source

pub const fn max_component(self) -> f32

Largest component.

Source

pub const fn is_finite(self) -> bool

Whether every component is finite.

Source

pub const fn clamp01(self) -> Self

Componentwise clamp to 0..=1.

Examples found in repository?
examples/chat/welcome.rs (line 610)
591	fn ground(&self, origin: Vec3, direction: Vec3) -> (Vec3, f32) {
592		if direction.y >= 0.0 {
593			return (BACKGROUND, 0.0);
594		}
595		let depth = (FLOOR_Y - origin.y) / direction.y;
596		if depth <= 0.0 {
597			return (BACKGROUND, 0.0);
598		}
599		let floor = origin + direction * depth;
600		let stage =
601			(-0.42 * ((floor.x.abs() / 2.30).powi(4) + ((floor.z + 0.15).abs() / 1.70).powi(4))).exp();
602
603		let sun_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / self.sun.y;
604		let sunlit = floor + self.sun * sun_depth;
605		let shadow_radius = sunlit.x.hypot(sunlit.z);
606		let occlusion = smooth(((DISK_RADIUS + 0.08 - shadow_radius) / 0.16).clamp(0.0, 1.0));
607		let rays = sun_rays(floor.x, floor.z);
608
609		let neutral_alpha = stage * rays * (1.0 - occlusion) * 0.36;
610		let neutral = (BACKGROUND + vec3(0.36, 0.33, 0.27) * (stage * rays)).clamp01();
611		let mut color = neutral * neutral_alpha + BACKGROUND * (1.0 - neutral_alpha);
612		let mut alpha = neutral_alpha;
613
614		let transmission_alpha =
615			stage * sun_rays(sunlit.x, sunlit.z) * occlusion * self.transition * 0.64;
616		let transmission =
617			(BACKGROUND + gradient(sunlit.z.atan2(sunlit.x) - self.angle) * 0.86).clamp01();
618		color = transmission * transmission_alpha + color * (1.0 - transmission_alpha);
619		alpha = transmission_alpha + alpha * (1.0 - transmission_alpha);
620
621		let mirrored = vec3(direction.x, -direction.y, direction.z);
622		let reflected_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / mirrored.y.max(1e-6);
623		let reflected = floor + mirrored * reflected_depth;
624		let reflected_radius = reflected.x.hypot(reflected.z);
625		if reflected_radius <= DISK_RADIUS {
626			let edge = smooth(((DISK_RADIUS - reflected_radius) / 0.13).clamp(0.0, 1.0));
627			let grazing = (1.0 + direction.y).clamp(0.0, 1.0);
628			let reflection_alpha = self.transition * edge * (0.22 + 0.28 * grazing);
629			let reflection = gradient(reflected.z.atan2(reflected.x) - self.angle);
630			color = reflection * reflection_alpha + color * (1.0 - reflection_alpha);
631			alpha = reflection_alpha + alpha * (1.0 - reflection_alpha);
632		}
633		(color, alpha)
634	}
635}
636
637impl Trace for Platter {
638	fn advance(&mut self, now: Duration) -> Camera {
639		let elapsed = now.as_secs_f32();
640		self.angle = disk_rotation(elapsed);
641		self.transition = color_mix(elapsed);
642		Camera {
643			target:   vec3(0.0, 0.08, 0.0),
644			yaw:      self.pointer.1 + reveal_orbit(elapsed) + (elapsed * 0.31).sin() * 0.018,
645			pitch:    CAMERA_PITCH,
646			distance: CAMERA_DISTANCE,
647			lift:     self.pointer.0.clamp(-0.42, 0.42) + (elapsed * 0.55).sin() * 0.014,
648			focal:    CAMERA_FOCAL,
649		}
650	}
651
652	/// Shades one sample: returns the color composited over the card
653	/// background and the coverage used for braille dot thresholds.
654	fn shade(&self, ray: Ray) -> (Vec3, f32) {
655		let Ray { origin, dir: direction } = ray;
656
657		// Top surface of the disk.
658		let mut disk_depth = f32::INFINITY;
659		let mut disk_point = Vec3::ZERO;
660		let mut disk_radial = 0.0;
661		if direction.y < 0.0 {
662			let depth = (DISK_Y + DISK_HALF_THICKNESS - origin.y) / direction.y;
663			if depth > 0.0 {
664				let point = origin + direction * depth;
665				let radial = point.x.hypot(point.z);
666				if radial <= DISK_RADIUS {
667					disk_depth = depth;
668					disk_point = point;
669					disk_radial = radial;
670				}
671			}
672		}
673		let disk_visible = disk_depth.is_finite();
674		let (axis_depth, axis_visible, axis_normal) = axis_hit(origin, direction);
675
676		let (ground_color, ground_alpha) = self.ground(origin, direction);
677		let mut color = ground_color * ground_alpha + BACKGROUND * (1.0 - ground_alpha);
678		let mut alpha = ground_alpha;
679
680		let view = direction * -1.0;
681		let halfway = if disk_visible || axis_visible {
682			(self.sun + view).normalize()
683		} else {
684			Vec3::ZERO
685		};
686		let axis_color = if axis_visible {
687			let diffuse = axis_normal.dot(self.sun).max(0.0);
688			let specular = axis_normal.dot(halfway).max(0.0).powi(44);
689			(WHITE * (0.32 + 0.68 * diffuse + 0.48 * specular)).clamp01()
690		} else {
691			Vec3::ZERO
692		};
693
694		if axis_visible && axis_depth > disk_depth {
695			color = axis_color;
696			alpha = 1.0;
697		}
698
699		if disk_visible {
700			let diffuse = self.sun.y.max(0.0);
701			let specular = halfway.y.max(0.0).powi(72);
702			let fresnel = (1.0 - view.y.max(0.0)).powi(4);
703			let material_angle = disk_point.z.atan2(disk_point.x) - self.angle;
704			let rim = smooth(((disk_radial - (DISK_RADIUS - 0.075)) / 0.055).clamp(0.0, 1.0));
705			let streak_angle = (material_angle - 0.32 + PI).rem_euclid(TAU) - PI;
706			let streak = (-(streak_angle / 0.19).powi(2)).exp();
707			let incident = sun_rays(disk_point.x, disk_point.z);
708
709			let opaque =
710				(WHITE * (0.30 + 0.27 * diffuse + 0.36 * incident + 0.20 * specular)).clamp01();
711			let glass = (gradient(material_angle) * (0.34 + 0.70 * diffuse)
712				+ WHITE * (0.72 * specular + 0.16 * fresnel + 0.20 * streak))
713				.clamp01();
714			let border_strength = rim * self.transition;
715			let mut disk_color = opaque
716				.lerp(glass, self.transition)
717				.lerp(WHITE, border_strength);
718
719			// Orbiting index marker punched into the surface.
720			let marker_x = 0.52 * self.angle.cos();
721			let marker_z = 0.52 * self.angle.sin();
722			let marker_distance = (disk_point.x - marker_x).hypot(disk_point.z - marker_z);
723			let marker = smooth(((0.10 - marker_distance) / 0.035).clamp(0.0, 1.0));
724			disk_color = disk_color.lerp(INK, marker);
725
726			let disk_alpha = (1.0 - self.transition * (1.0 - DISK_GLASS_OPACITY))
727				.max(border_strength * 0.96)
728				.max(marker * 0.98);
729			color = disk_color * disk_alpha + color * (1.0 - disk_alpha);
730			alpha = disk_alpha + alpha * (1.0 - disk_alpha);
731		}
732
733		if axis_visible && axis_depth <= disk_depth {
734			color = axis_color;
735			alpha = 1.0;
736		}
737		(tone(color), alpha)
738	}
Source

pub fn lerp(self, to: Self, mix: f32) -> Self

Linear interpolation toward to by mix (0 = self, 1 = to).

Examples found in repository?
examples/chat/welcome.rs (line 506)
503fn gradient(angle: f32) -> Vec3 {
504	let position = (angle / TAU + 0.5).rem_euclid(1.0) * 3.0;
505	let index = (position as usize).min(2);
506	DISK_STOPS[index].lerp(DISK_STOPS[index + 1], position - index as f32)
507}
508
509/// Encodes a linear-light color for the terminal.
510fn vec3_color(color: Vec3) -> Color {
511	Color::from(color)
512}
513
514/// Three soft light shafts crossing the stage in the sun's plane.
515fn sun_rays(x: f32, z: f32) -> f32 {
516	let length = 0.55_f32.hypot(0.38);
517	let (hx, hz) = (-0.55 / length, 0.38 / length);
518	let across = x * -hz + z * hx;
519	let along = x * hx + z * hz;
520	let rays = (-((across + 0.56) / 0.075).powi(2)).exp()
521		+ (-((across + 0.04) / 0.055).powi(2)).exp()
522		+ (-((across - 0.47) / 0.09).powi(2)).exp();
523	let envelope = (-((along + 0.20) / 2.45).powi(4)).exp();
524	(rays * envelope).clamp(0.0, 1.0)
525}
526
527fn sphere_depth(origin: Vec3, direction: Vec3, center: Vec3, radius: f32) -> f32 {
528	let offset = origin - center;
529	let projection = offset.dot(direction);
530	let discriminant = projection * projection - (offset.dot(offset) - radius * radius);
531	let root = -projection - discriminant.max(0.0).sqrt();
532	if discriminant >= 0.0 && root > 0.0 {
533		root
534	} else {
535		f32::INFINITY
536	}
537}
538
539/// Central axis: a capped cylinder with a slightly bulged top sphere.
540fn axis_hit(origin: Vec3, direction: Vec3) -> (f32, bool, Vec3) {
541	let quadratic = direction.x * direction.x + direction.z * direction.z;
542	let linear = 2.0 * (origin.x * direction.x + origin.z * direction.z);
543	let constant = origin.x * origin.x + origin.z * origin.z - AXIS_RADIUS * AXIS_RADIUS;
544	let discriminant = linear * linear - 4.0 * quadratic * constant;
545	let root = (-linear - discriminant.max(0.0).sqrt()) / (2.0 * quadratic).max(1e-8);
546	let hit_y = origin.y + direction.y * root;
547	let cylinder = if discriminant >= 0.0 && root > 0.0 && (AXIS_BOTTOM..=AXIS_TOP).contains(&hit_y)
548	{
549		root
550	} else {
551		f32::INFINITY
552	};
553	let cap_center = vec3(0.0, AXIS_TOP, 0.0);
554	let cap = sphere_depth(origin, direction, cap_center, AXIS_RADIUS * 1.75);
555	let depth = cylinder.min(cap);
556	if !depth.is_finite() {
557		return (f32::INFINITY, false, vec3(0.0, 1.0, 0.0));
558	}
559	let point = origin + direction * depth;
560	let normal = if cap < cylinder {
561		(point - cap_center).normalize()
562	} else {
563		vec3(point.x, 0.0, point.z).normalize()
564	};
565	(depth, true, normal)
566}
567
568/// Terminal display lift in linear light so braille remains legible on the
569/// dark card.
570fn tone(color: Vec3) -> Vec3 {
571	color * 1.28 + Vec3::rgb(4, 4, 4)
572}
573
574/// Per-frame scene state shared by every ray: sun direction, disk spin,
575/// glass transition, and the pointer camera offsets.
576struct Platter {
577	sun:        Vec3,
578	angle:      f32,
579	transition: f32,
580	/// Smoothed pointer camera from [`Welcome`]: (lift, yaw).
581	pointer:    (f32, f32),
582}
583
584impl Platter {
585	fn new(pointer: (f32, f32)) -> Self {
586		Self { sun: vec3(-0.55, 1.0, 0.38).normalize(), angle: 0.0, transition: 0.0, pointer }
587	}
588
589	/// Floor glow: light shafts, the disk's shadow, tinted transmission
590	/// through the glass, and a soft rim reflection.
591	fn ground(&self, origin: Vec3, direction: Vec3) -> (Vec3, f32) {
592		if direction.y >= 0.0 {
593			return (BACKGROUND, 0.0);
594		}
595		let depth = (FLOOR_Y - origin.y) / direction.y;
596		if depth <= 0.0 {
597			return (BACKGROUND, 0.0);
598		}
599		let floor = origin + direction * depth;
600		let stage =
601			(-0.42 * ((floor.x.abs() / 2.30).powi(4) + ((floor.z + 0.15).abs() / 1.70).powi(4))).exp();
602
603		let sun_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / self.sun.y;
604		let sunlit = floor + self.sun * sun_depth;
605		let shadow_radius = sunlit.x.hypot(sunlit.z);
606		let occlusion = smooth(((DISK_RADIUS + 0.08 - shadow_radius) / 0.16).clamp(0.0, 1.0));
607		let rays = sun_rays(floor.x, floor.z);
608
609		let neutral_alpha = stage * rays * (1.0 - occlusion) * 0.36;
610		let neutral = (BACKGROUND + vec3(0.36, 0.33, 0.27) * (stage * rays)).clamp01();
611		let mut color = neutral * neutral_alpha + BACKGROUND * (1.0 - neutral_alpha);
612		let mut alpha = neutral_alpha;
613
614		let transmission_alpha =
615			stage * sun_rays(sunlit.x, sunlit.z) * occlusion * self.transition * 0.64;
616		let transmission =
617			(BACKGROUND + gradient(sunlit.z.atan2(sunlit.x) - self.angle) * 0.86).clamp01();
618		color = transmission * transmission_alpha + color * (1.0 - transmission_alpha);
619		alpha = transmission_alpha + alpha * (1.0 - transmission_alpha);
620
621		let mirrored = vec3(direction.x, -direction.y, direction.z);
622		let reflected_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / mirrored.y.max(1e-6);
623		let reflected = floor + mirrored * reflected_depth;
624		let reflected_radius = reflected.x.hypot(reflected.z);
625		if reflected_radius <= DISK_RADIUS {
626			let edge = smooth(((DISK_RADIUS - reflected_radius) / 0.13).clamp(0.0, 1.0));
627			let grazing = (1.0 + direction.y).clamp(0.0, 1.0);
628			let reflection_alpha = self.transition * edge * (0.22 + 0.28 * grazing);
629			let reflection = gradient(reflected.z.atan2(reflected.x) - self.angle);
630			color = reflection * reflection_alpha + color * (1.0 - reflection_alpha);
631			alpha = reflection_alpha + alpha * (1.0 - reflection_alpha);
632		}
633		(color, alpha)
634	}
635}
636
637impl Trace for Platter {
638	fn advance(&mut self, now: Duration) -> Camera {
639		let elapsed = now.as_secs_f32();
640		self.angle = disk_rotation(elapsed);
641		self.transition = color_mix(elapsed);
642		Camera {
643			target:   vec3(0.0, 0.08, 0.0),
644			yaw:      self.pointer.1 + reveal_orbit(elapsed) + (elapsed * 0.31).sin() * 0.018,
645			pitch:    CAMERA_PITCH,
646			distance: CAMERA_DISTANCE,
647			lift:     self.pointer.0.clamp(-0.42, 0.42) + (elapsed * 0.55).sin() * 0.014,
648			focal:    CAMERA_FOCAL,
649		}
650	}
651
652	/// Shades one sample: returns the color composited over the card
653	/// background and the coverage used for braille dot thresholds.
654	fn shade(&self, ray: Ray) -> (Vec3, f32) {
655		let Ray { origin, dir: direction } = ray;
656
657		// Top surface of the disk.
658		let mut disk_depth = f32::INFINITY;
659		let mut disk_point = Vec3::ZERO;
660		let mut disk_radial = 0.0;
661		if direction.y < 0.0 {
662			let depth = (DISK_Y + DISK_HALF_THICKNESS - origin.y) / direction.y;
663			if depth > 0.0 {
664				let point = origin + direction * depth;
665				let radial = point.x.hypot(point.z);
666				if radial <= DISK_RADIUS {
667					disk_depth = depth;
668					disk_point = point;
669					disk_radial = radial;
670				}
671			}
672		}
673		let disk_visible = disk_depth.is_finite();
674		let (axis_depth, axis_visible, axis_normal) = axis_hit(origin, direction);
675
676		let (ground_color, ground_alpha) = self.ground(origin, direction);
677		let mut color = ground_color * ground_alpha + BACKGROUND * (1.0 - ground_alpha);
678		let mut alpha = ground_alpha;
679
680		let view = direction * -1.0;
681		let halfway = if disk_visible || axis_visible {
682			(self.sun + view).normalize()
683		} else {
684			Vec3::ZERO
685		};
686		let axis_color = if axis_visible {
687			let diffuse = axis_normal.dot(self.sun).max(0.0);
688			let specular = axis_normal.dot(halfway).max(0.0).powi(44);
689			(WHITE * (0.32 + 0.68 * diffuse + 0.48 * specular)).clamp01()
690		} else {
691			Vec3::ZERO
692		};
693
694		if axis_visible && axis_depth > disk_depth {
695			color = axis_color;
696			alpha = 1.0;
697		}
698
699		if disk_visible {
700			let diffuse = self.sun.y.max(0.0);
701			let specular = halfway.y.max(0.0).powi(72);
702			let fresnel = (1.0 - view.y.max(0.0)).powi(4);
703			let material_angle = disk_point.z.atan2(disk_point.x) - self.angle;
704			let rim = smooth(((disk_radial - (DISK_RADIUS - 0.075)) / 0.055).clamp(0.0, 1.0));
705			let streak_angle = (material_angle - 0.32 + PI).rem_euclid(TAU) - PI;
706			let streak = (-(streak_angle / 0.19).powi(2)).exp();
707			let incident = sun_rays(disk_point.x, disk_point.z);
708
709			let opaque =
710				(WHITE * (0.30 + 0.27 * diffuse + 0.36 * incident + 0.20 * specular)).clamp01();
711			let glass = (gradient(material_angle) * (0.34 + 0.70 * diffuse)
712				+ WHITE * (0.72 * specular + 0.16 * fresnel + 0.20 * streak))
713				.clamp01();
714			let border_strength = rim * self.transition;
715			let mut disk_color = opaque
716				.lerp(glass, self.transition)
717				.lerp(WHITE, border_strength);
718
719			// Orbiting index marker punched into the surface.
720			let marker_x = 0.52 * self.angle.cos();
721			let marker_z = 0.52 * self.angle.sin();
722			let marker_distance = (disk_point.x - marker_x).hypot(disk_point.z - marker_z);
723			let marker = smooth(((0.10 - marker_distance) / 0.035).clamp(0.0, 1.0));
724			disk_color = disk_color.lerp(INK, marker);
725
726			let disk_alpha = (1.0 - self.transition * (1.0 - DISK_GLASS_OPACITY))
727				.max(border_strength * 0.96)
728				.max(marker * 0.98);
729			color = disk_color * disk_alpha + color * (1.0 - disk_alpha);
730			alpha = disk_alpha + alpha * (1.0 - disk_alpha);
731		}
732
733		if axis_visible && axis_depth <= disk_depth {
734			color = axis_color;
735			alpha = 1.0;
736		}
737		(tone(color), alpha)
738	}

Trait Implementations§

Source§

impl Add for Vec3

Source§

type Output = Vec3

The resulting type after applying the + operator.
Source§

fn add(self, other: Self) -> Self

Performs the + operation. Read more
Source§

impl AddAssign for Vec3

Source§

fn add_assign(&mut self, other: Self)

Performs the += operation. Read more
Source§

impl Clone for Vec3

Source§

fn clone(&self) -> Vec3

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for Vec3

Source§

impl Debug for Vec3

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Vec3

Source§

fn default() -> Vec3

Returns the “default value” for a type. Read more
Source§

impl Div<f32> for Vec3

Source§

type Output = Vec3

The resulting type after applying the / operator.
Source§

fn div(self, divisor: f32) -> Self

Performs the / operation. Read more
Source§

impl From<Vec3> for Color

Encodes linear-light components through the sRGB transfer function.

Source§

fn from(color: Vec3) -> Self

Converts to this type from the input type.
Source§

impl Mul for Vec3

Source§

type Output = Vec3

The resulting type after applying the * operator.
Source§

fn mul(self, other: Self) -> Self

Performs the * operation. Read more
Source§

impl Mul<Vec3> for f32

Source§

type Output = Vec3

The resulting type after applying the * operator.
Source§

fn mul(self, vector: Vec3) -> Vec3

Performs the * operation. Read more
Source§

impl Mul<f32> for Vec3

Source§

type Output = Vec3

The resulting type after applying the * operator.
Source§

fn mul(self, factor: f32) -> Self

Performs the * operation. Read more
Source§

impl MulAssign<f32> for Vec3

Source§

fn mul_assign(&mut self, factor: f32)

Performs the *= operation. Read more
Source§

impl Neg for Vec3

Source§

type Output = Vec3

The resulting type after applying the - operator.
Source§

fn neg(self) -> Self

Performs the unary - operation. Read more
Source§

impl PartialEq for Vec3

Source§

fn eq(&self, other: &Vec3) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for Vec3

Source§

impl Sub for Vec3

Source§

type Output = Vec3

The resulting type after applying the - operator.
Source§

fn sub(self, other: Self) -> Self

Performs the - operation. Read more

Auto Trait Implementations§

§

impl Freeze for Vec3

§

impl RefUnwindSafe for Vec3

§

impl Send for Vec3

§

impl Sync for Vec3

§

impl Unpin for Vec3

§

impl UnsafeUnpin for Vec3

§

impl UnwindSafe for Vec3

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &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
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.