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: f32X component (red when used as a color).
y: f32Y component (green when used as a color).
z: f32Z component (blue when used as a color).
Implementations§
Source§impl Vec3
impl Vec3
Sourcepub const fn rgb(red: u8, green: u8, blue: u8) -> Self
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}Sourcepub fn dot(self, other: Self) -> f32
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 }Sourcepub fn length_squared(self) -> f32
pub fn length_squared(self) -> f32
Squared vector length.
Sourcepub fn normalize(self) -> Self
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 }Sourcepub fn refract(self, normal: Self, eta: f32) -> Option<Self>
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.
Sourcepub const fn max_component(self) -> f32
pub const fn max_component(self) -> f32
Largest component.
Sourcepub const fn clamp01(self) -> Self
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 }Sourcepub fn lerp(self, to: Self, mix: f32) -> Self
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 AddAssign for Vec3
impl AddAssign for Vec3
Source§fn add_assign(&mut self, other: Self)
fn add_assign(&mut self, other: Self)
Performs the
+= operation. Read moreimpl Copy for Vec3
Source§impl MulAssign<f32> for Vec3
impl MulAssign<f32> for Vec3
Source§fn mul_assign(&mut self, factor: f32)
fn mul_assign(&mut self, factor: f32)
Performs the
*= operation. Read moreimpl StructuralPartialEq for Vec3
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> 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
Mutably borrows from an owned value. Read more
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>
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>
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)
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)
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
impl<T> DowncastSync for T
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> ⓘ
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 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> ⓘ
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