pub trait Mesh<P: Part = NoParts, C: Clip = NoClips>:
Catalog
+ Hash
+ Eq
+ Clone {
// Required method
fn build(&self, assets: &Assets) -> MeshData<P, C>;
// Provided method
fn at<S: SurfaceStyles>(
self,
transform: impl Into<Transform>,
) -> Instance<Self, S> { ... }
}Expand description
A type that is a mesh: a draw is a value of it and a transform.
Values are cache keys: equal values must build the same mesh. Catalog
proves every named asset loads before the first frame. P names the
parts a draw repaints one at a time; a mesh with none leaves it at
NoParts, and one with parts implements the trait for its own
Part enum and no other. C names the clips it is posed by, the same
way, over NoClips and its own Clip enum.
Required Methods§
Provided Methods§
Sourcefn at<S: SurfaceStyles>(
self,
transform: impl Into<Transform>,
) -> Instance<Self, S>
fn at<S: SurfaceStyles>( self, transform: impl Into<Transform>, ) -> Instance<Self, S>
Places this mesh in the world, as a draw of a game whose styles are
S.
Examples found in repository?
examples/stress-preview.rs (line 398)
394 fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
395 let side = (FIELD_RADIUS + FIELD_INNER_RADIUS) * 2.2;
396 ctx.draw(
397 Plane
398 .at(Transform::from_scale(Vec3::new(side, 1.0, side)))
399 .material(Material::lit(GROUND_COLOR).roughness(0.9)),
400 );
401 }
402
403 fn draw_field(&self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
404 for entry in &self.field {
405 let yaw = if self.settings.moving && entry.moving {
406 entry.phase + elapsed * MOVING_SPEED
407 } else {
408 entry.phase
409 };
410 ctx.draw(
411 Rock { seed: entry.seed }.at(Transform::from_scale_rotation_translation(
412 Vec3::ONE,
413 Quat::from_rotation_y(yaw),
414 entry.position,
415 )),
416 );
417 }
418 }More examples
examples/flock-parallelism.rs (lines 663-667)
660 fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
661 ctx.draw(
662 Plane
663 .at(Transform::from_scale(Vec3::new(
664 GROUND_SIZE,
665 1.0,
666 GROUND_SIZE,
667 )))
668 .material(Material::lit(GROUND_COLOR).roughness(0.9)),
669 );
670 }
671
672 /// Every butterfly at its own scale, turned to face its velocity, posed
673 /// by the flap machine of its own group and tinted its own color.
674 fn draw_butterflies(&self, ctx: &mut FrameContext<'_, Self>) {
675 for (position, velocity, kind) in self.butterflies.each() {
676 let rotation = Quat::from_rotation_arc(Vec3::Z, Vec3::from(velocity).normalize());
677 ctx.draw(
678 Butterfly
679 .at(Transform::from_scale_rotation_translation(
680 Vec3::splat(BUTTERFLY_SCALE),
681 rotation,
682 Vec3::from(position),
683 ))
684 .posed(&self.flaps[usize::from(kind.flap)])
685 .material(Material::lit(TINTS[usize::from(kind.tint)])),
686 );
687 }
688 }examples/sprite-adventure.rs (lines 1241-1245)
1236 fn draw_ground(&self, ctx: &mut FrameContext<'_, Keep>) {
1237 for col in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1238 for row in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1239 ctx.draw(
1240 Ground
1241 .at(Vec3::new(
1242 col as f32 * TILE_SIZE,
1243 0.0,
1244 row as f32 * TILE_SIZE,
1245 ))
1246 .frame(ground_cell(col, row)),
1247 );
1248 }
1249 }
1250 }
1251
1252 /// Two staggered rows of bushes around the clearing, open where the path
1253 /// leaves it, drawn between the camera and the ground's edge. The rows
1254 /// running along `Z` skip their two ends, which the rows running along
1255 /// `X` already cover.
1256 fn draw_hedgerow(&self, ctx: &mut FrameContext<'_, Keep>) {
1257 for (row, half) in [HEDGE_INNER_HALF, HEDGE_OUTER_HALF].into_iter().enumerate() {
1258 let row = row as i32;
1259 // The inner row covers both corners; the outer one is half a
1260 // span in from each, backing the gaps the inner row leaves.
1261 let spans = ((2.0 * half / HEDGE_STEP).round() as i32).max(1);
1262 let span = 2.0 * half / spans as f32;
1263 let steps = spans - row;
1264 for step in 0..=steps {
1265 let along = -half + (step as f32 + 0.5 * row as f32) * span;
1266 let scale = if (step + row) % 2 == 0 { 1.0 } else { 0.8 };
1267 let (width, height) = (BUSH_WIDTH * scale, BUSH_HEIGHT * scale);
1268 // The path leaves through the rows running along `X`, so only
1269 // those two open around it.
1270 let gated = along.abs() < HEDGE_GATE_HALF;
1271 let corner = step == 0 || step == steps;
1272 let places = [
1273 (along, -half, gated),
1274 (along, half, gated),
1275 (-half, along, corner),
1276 (half, along, corner),
1277 ];
1278 for (x, z, skip) in places {
1279 if skip {
1280 continue;
1281 }
1282 ctx.draw(
1283 Bush.at(Transform::from_scale_rotation_translation(
1284 Vec3::new(width, height, width),
1285 Quat::IDENTITY,
1286 Vec3::new(x, height * 0.5, z),
1287 ))
1288 .upright(),
1289 );
1290 }
1291 }
1292 }
1293 }
1294
1295 /// The pond: a square of styled water, and the shoreline sprite laid over
1296 /// it, which rings the open middle and hides the water's own edges.
1297 fn draw_pond(&self, ctx: &mut FrameContext<'_, Keep>) {
1298 ctx.draw(
1299 Plane
1300 .at(Transform::from_scale_rotation_translation(
1301 Vec3::splat(POND_WATER_HALF * 2.0),
1302 Quat::IDENTITY,
1303 POND_CENTER,
1304 ))
1305 .material(Material::shaded(WATER_COLOR, WATER_LITNESS))
1306 .surface_style::<Water>(),
1307 );
1308 ctx.draw(
1309 Shore
1310 .at(Transform::from_scale_rotation_translation(
1311 Vec3::splat(POND_HALF * 2.0),
1312 Quat::IDENTITY,
1313 Vec3::new(POND_CENTER.x, 0.0, POND_CENTER.z),
1314 ))
1315 .frame(Sheet::new(UVec2::new(POND_CELLS, 1)).cell(POND_SHORE_CELL)),
1316 );
1317 }
1318
1319 fn draw_crates(&self, ctx: &mut FrameContext<'_, Keep>) {
1320 for &(x, z, turn) in &CRATE_POSITIONS {
1321 ctx.draw(Crate.at(Transform::from_scale_rotation_translation(
1322 Vec3::splat(CRATE_SIZE),
1323 Quat::from_rotation_y(turn),
1324 Vec3::new(x, CRATE_SIZE * 0.5, z),
1325 )));
1326 }
1327 }
1328
1329 /// The well: its rim in grey masonry, and the mouth cell laid over the
1330 /// rim's top face.
1331 fn draw_well(&self, ctx: &mut FrameContext<'_, Keep>) {
1332 let cells = Sheet::new(UVec2::new(WELL_CELLS, 1));
1333 ctx.draw(
1334 Well.at(Transform::from_scale_rotation_translation(
1335 WELL_SIZE,
1336 Quat::IDENTITY,
1337 WELL_POSITION + Vec3::Y * (WELL_SIZE.y * 0.5),
1338 ))
1339 .frame(cells.cell(WELL_RIM_CELL)),
1340 );
1341 ctx.draw(
1342 WellMouth
1343 .at(Transform::from_scale_rotation_translation(
1344 Vec3::new(WELL_SIZE.x, 1.0, WELL_SIZE.z),
1345 Quat::IDENTITY,
1346 WELL_POSITION + Vec3::Y * (WELL_SIZE.y + WELL_MOUTH_LIFT),
1347 ))
1348 .frame(cells.cell(WELL_MOUTH_CELL)),
1349 );
1350 }
1351
1352 fn draw_flora(&self, ctx: &mut FrameContext<'_, Keep>) {
1353 for &(x, z, rock) in &FLORA {
1354 let (width, height) = if rock {
1355 (ROCK_WIDTH, ROCK_HEIGHT)
1356 } else {
1357 (BUSH_WIDTH, BUSH_HEIGHT)
1358 };
1359 let standing = Transform::from_scale_rotation_translation(
1360 Vec3::new(width, height, width),
1361 Quat::IDENTITY,
1362 Vec3::new(x, height * 0.5, z),
1363 );
1364 let flora: Instance<Shape, _> = if rock {
1365 Rock.at(standing).into_set()
1366 } else {
1367 Bush.at(standing).into_set()
1368 };
1369 ctx.draw(flora.upright());
1370 }
1371 }
1372
1373 /// One stone box drawn on the ground at `at`, `size` across, sampling
1374 /// the part of the sheet `frame` covers.
1375 fn draw_stone(ctx: &mut FrameContext<'_, Keep>, at: Vec3, size: Vec3, frame: Frame) {
1376 ctx.draw(
1377 Stone
1378 .at(Transform::from_scale_rotation_translation(
1379 size,
1380 Quat::IDENTITY,
1381 at + Vec3::Y * (size.y * 0.5),
1382 ))
1383 .frame(frame),
1384 );
1385 }
1386
1387 /// Two stone pillars drawn where `mouth` blocks the player, each a
1388 /// capital over its own course of masonry, and, on the one the camera
1389 /// looks into, the lintel across their tops and the dark filling
1390 /// the opening under it.
1391 fn draw_mouth(ctx: &mut FrameContext<'_, Keep>, mouth: Mouth) {
1392 for at in mouth.pillars() {
1393 Self::draw_stone(ctx, at, MOUTH_PILLAR_SIZE, Frame::default());
1394 }
1395 if !mouth.looked_into() {
1396 return;
1397 }
1398
1399 Self::draw_stone(
1400 ctx,
1401 mouth.at + Vec3::Y * MOUTH_PILLAR_SIZE.y,
1402 MOUTH_LINTEL_SIZE,
1403 masonry(MOUTH_LINTEL_TILES),
1404 );
1405 ctx.draw(
1406 Quad.at(Transform::from_scale_rotation_translation(
1407 Vec3::new(MOUTH_PILLAR_OFFSET * 2.0, MOUTH_DARK_HEIGHT, 1.0),
1408 Quat::IDENTITY,
1409 mouth.at + Vec3::Y * (MOUTH_DARK_HEIGHT * 0.5),
1410 ))
1411 .material(Material::color(Color::BLACK)),
1412 );
1413 }
1414
1415 fn draw_cave_floor(&self, ctx: &mut FrameContext<'_, Keep>) {
1416 let half = CAVE_HALF_WIDTH as i32;
1417 let near = CAVE_NEAR_Z as i32;
1418 let far = CAVE_FAR_Z as i32;
1419 for col in -half..=half {
1420 for row in far..=near {
1421 let variant = (col * 13 + row * 7).rem_euclid(CAVE_COLUMNS as i32) as u32;
1422 ctx.draw(
1423 CaveFloor
1424 .at(Vec3::new(
1425 col as f32 * TILE_SIZE,
1426 0.0,
1427 row as f32 * TILE_SIZE,
1428 ))
1429 .frame(
1430 Sheet::new(UVec2::new(CAVE_COLUMNS, CAVE_ROWS))
1431 .cell_at(UVec2::new(variant, CAVE_FLOOR_ROW)),
1432 ),
1433 );
1434 }
1435 }
1436 }
1437
1438 /// The wall drawn at `at` over the meters `standing`, in courses
1439 /// [`WALL_HEIGHT`] tall from the floor up, each cut to the part of it the
1440 /// span leaves; its faces are picked by `seed` and its stone faded to
1441 /// `fade`, which is `1.0` wherever it is solid.
1442 fn draw_wall(
1443 ctx: &mut FrameContext<'_, Keep>,
1444 at: Vec2,
1445 standing: Range<f32>,
1446 seed: i32,
1447 fade: f32,
1448 ) {
1449 for course in 0..WALL_COURSES {
1450 let base = course as f32 * WALL_HEIGHT;
1451 let low = (standing.start - base).max(0.0);
1452 let high = (standing.end - base).min(WALL_HEIGHT);
1453 if high <= low {
1454 continue;
1455 }
1456
1457 let variant = (seed + course).rem_euclid(CAVE_COLUMNS as i32) as u32;
1458 ctx.draw(
1459 CaveWall
1460 .at(Transform::from_scale_rotation_translation(
1461 Vec3::new(TILE_SIZE, high - low, TILE_SIZE),
1462 Quat::IDENTITY,
1463 Vec3::new(at.x, base + (low + high) * 0.5, at.y),
1464 ))
1465 .frame(cave_wall_face(variant, low..high))
1466 .faded(fade),
1467 );
1468 }
1469 }
1470
1471 /// The room's two side walls and its back wall, full height, and the low
1472 /// wall closing its near end between the side walls and the mouth. The
1473 /// back wall stops short of the corners the side walls already fill, and
1474 /// the near one leaves the mouth's own tile open.
1475 fn draw_cave_walls(&self, ctx: &mut FrameContext<'_, Keep>) {
1476 let half = CAVE_HALF_WIDTH as i32 + 1;
1477 let near = CAVE_NEAR_Z as i32;
1478 let far = CAVE_FAR_Z as i32;
1479
1480 for row in far..=near {
1481 let z = row as f32 * TILE_SIZE;
1482 let west = Vec2::new(-half as f32 * TILE_SIZE, z);
1483 let east = Vec2::new(half as f32 * TILE_SIZE, z);
1484 Self::draw_wall(ctx, west, 0.0..WALL_TOP, row * 5, SOLID);
1485 Self::draw_wall(ctx, east, 0.0..WALL_TOP, row * 5 + 1, SOLID);
1486 }
1487 for col in (-half + 1)..half {
1488 let x = col as f32 * TILE_SIZE;
1489 let back = Vec2::new(x, far as f32 * TILE_SIZE);
1490 Self::draw_wall(ctx, back, 0.0..WALL_TOP, col * 5 + 2, SOLID);
1491 if col != 0 {
1492 let lip = Vec2::new(x, CAVE_LIP_Z);
1493 Self::draw_wall(ctx, lip, 0.0..CAVE_LIP_HEIGHT, col * 5 + 4, SOLID);
1494 }
1495 }
1496 }
1497
1498 /// The wall the door hangs in, run across the room between the side walls
1499 /// with one tile left open on the room's axis for the doorway and stone
1500 /// filling the column over the door. A player behind the wall is drawn
1501 /// through the stacks between them and the camera, at `seen_through`,
1502 /// faded by `ghost`; the rest of it stays solid, and keeps casting.
1503 fn draw_door_wall(ctx: &mut FrameContext<'_, Keep>, seen_through: Option<f32>, ghost: f32) {
1504 let stone = |x: f32| match seen_through {
1505 Some(at) if (x - at).abs() < GHOST_CORRIDOR_HALF => ghost_alpha(ghost),
1506 _ => SOLID,
1507 };
1508 let half = CAVE_HALF_WIDTH as i32;
1509
1510 for col in (-half..=half).filter(|&col| col != 0) {
1511 let x = col as f32 * TILE_SIZE;
1512 Self::draw_wall(
1513 ctx,
1514 Vec2::new(x, DOOR_Z),
1515 0.0..WALL_TOP,
1516 col * 5 + 3,
1517 stone(x),
1518 );
1519 }
1520 Self::draw_wall(
1521 ctx,
1522 Vec2::new(0.0, DOOR_Z),
1523 DOOR_HEIGHT..WALL_TOP,
1524 3,
1525 stone(0.0),
1526 );
1527 }
1528
1529 /// The two torches: an upright cutout post apiece, the flame's loop
1530 /// burning over its binding, and the light that flame casts.
1531 fn draw_torches(&self, ctx: &mut FrameContext<'_, Keep>) {
1532 let elapsed = self.simulated.as_secs_f32();
1533 let loop_cells = Sheet::new(UVec2::new(FLAME_CELLS, 1));
1534
1535 for (index, &(x, z)) in TORCH_POSITIONS.iter().enumerate() {
1536 let base = Vec3::new(x, 0.0, z);
1537 ctx.draw(
1538 Torch
1539 .at(Transform::from_scale_rotation_translation(
1540 Vec3::new(TORCH_SPRITE_WIDTH, TORCH_STAND_HEIGHT, 1.0),
1541 Quat::IDENTITY,
1542 base + Vec3::Y * (TORCH_STAND_HEIGHT * 0.5),
1543 ))
1544 .upright(),
1545 );
1546
1547 let phase = index as f32 * 2.1;
1548 let flicker = (elapsed * FLAME_FLICKER_SPEED + phase).sin();
1549 let flame_pos =
1550 base + Vec3::Y * (TORCH_STAND_HEIGHT + FLAME_LIFT + flicker * FLAME_BOB);
1551
1552 let light_pos = flame_pos + Vec3::new(0.0, TORCH_LIGHT_LIFT, TORCH_LIGHT_STANDOFF);
1553 ctx.light(Light::point(light_pos, TORCH_LIGHT_COLOR, TORCH_LIGHT_RANGE).shadow());
1554 // The pair burn an even share of the loop apart.
1555 let offset = index as u32 * FLAME_CELLS / TORCH_POSITIONS.len() as u32;
1556 ctx.draw(
1557 Flame
1558 .at(Transform::from_scale_rotation_translation(
1559 Vec3::splat(FLAME_SIZE),
1560 Quat::IDENTITY,
1561 flame_pos,
1562 ))
1563 .billboard()
1564 .roll(flicker * FLAME_ROLL)
1565 .frame(loop_cells.cell((elapsed * FLAME_RATE) as u32 + offset)),
1566 );
1567 }
1568 }
1569
1570 /// The door at its hinge — swung back against the wall once opened —
1571 /// drawn through alongside its wall, faded by `ghost`.
1572 fn draw_door(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1573 let fade = ghost_alpha(ghost);
1574 let swung = if self.door_opening {
1575 Quat::from_rotation_y(core::f32::consts::FRAC_PI_2)
1576 } else {
1577 Quat::IDENTITY
1578 };
1579
1580 ctx.draw(
1581 Door.at(Transform::from_rotation_translation(swung, DOOR_HINGE))
1582 .material(Material::shaded(DOOR_COLOR, DOOR_LITNESS))
1583 .faded(fade),
1584 );
1585 }
1586
1587 /// The posts and lintel framing the doorway, in a color the stone never
1588 /// is, standing clear of the wall so the opening reads as a door from
1589 /// across the chamber. Glowing of their own while the door is closed and
1590 /// within [`INTERACT_RADIUS`], the cue that it opens.
1591 fn draw_door_frame(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1592 let reachable =
1593 !self.door_opening && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1594 let material =
1595 Material::shaded(DOOR_FRAME_COLOR, DOOR_FRAME_LITNESS).emissive(if reachable {
1596 DOOR_FRAME_GLOW
1597 } else {
1598 Color::BLACK
1599 });
1600 let fade = ghost_alpha(ghost);
1601 let z = DOOR_WALL_NEAR_Z + DOOR_FRAME_STANDOFF;
1602 let jamb_height = DOOR_HEIGHT + DOOR_FRAME_THICKNESS;
1603
1604 for side in SIDES {
1605 ctx.draw(
1606 Cube.at(Transform::from_scale_rotation_translation(
1607 Vec3::new(DOOR_FRAME_THICKNESS, jamb_height, DOOR_FRAME_THICKNESS),
1608 Quat::IDENTITY,
1609 Vec3::new(
1610 side * (DOORWAY_HALF + DOOR_FRAME_THICKNESS * 0.5),
1611 jamb_height * 0.5,
1612 z,
1613 ),
1614 ))
1615 .material(material)
1616 .faded(fade),
1617 );
1618 }
1619 ctx.draw(
1620 Cube.at(Transform::from_scale_rotation_translation(
1621 Vec3::new(
1622 DOOR_WIDTH + DOOR_FRAME_THICKNESS * 2.0,
1623 DOOR_FRAME_THICKNESS,
1624 DOOR_FRAME_THICKNESS,
1625 ),
1626 Quat::IDENTITY,
1627 Vec3::new(0.0, DOOR_HEIGHT + DOOR_FRAME_THICKNESS * 0.5, z),
1628 ))
1629 .material(material)
1630 .faded(fade),
1631 );
1632 }
1633
1634 /// A world prompt over the door: what opens it while the player is
1635 /// within [`INTERACT_RADIUS`] and it is closed, and that it swings while
1636 /// it does; gone once it has swung [`DOOR_SWING_TICKS`]. Laid out and
1637 /// placed like `examples/animation.rs`'s own prompt.
1638 fn draw_door_prompt(&self, ctx: &mut FrameContext<'_, Keep>, camera: Camera) {
1639 let near = self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1640 let swinging = self.door_opening && self.swing_ticks < DOOR_SWING_TICKS;
1641 let text = if swinging {
1642 "opening"
1643 } else if near && !self.door_opening {
1644 "e opens the door"
1645 } else {
1646 return;
1647 };
1648
1649 let galley = ctx.text_layout(text, egui::FontId::proportional(DOOR_PROMPT_SIZE));
1650 let point = INTERACT_POINT + Vec3::Y * (DOOR_HEIGHT + DOOR_PROMPT_LIFT);
1651 let window_size = ctx.window_size();
1652 let pixels_per_point = ctx.pixels_per_point();
1653 let Some(pixel) = camera.pixel_of(point, window_size) else {
1654 return;
1655 };
1656
1657 ctx.ui(|ui| {
1658 let painter = ui.painter();
1659 let at = logical(pixel, pixels_per_point);
1660 let ink = galley.mesh_bounds;
1661 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
1662 let backdrop = egui::Rect::from_center_size(
1663 at,
1664 ink.size() + egui::Vec2::splat(DOOR_PROMPT_PADDING * 2.0),
1665 );
1666 painter.rect_filled(
1667 backdrop,
1668 DOOR_PROMPT_PADDING,
1669 egui::Color32::from_black_alpha(DOOR_PROMPT_BACKDROP),
1670 );
1671 painter.galley(pos, galley, DOOR_PROMPT_COLOR);
1672 });
1673 }
1674
1675 /// The gem, spinning and bobbing over the chamber's floor, and the light
1676 /// it casts over it.
1677 fn draw_gem(&self, ctx: &mut FrameContext<'_, Keep>) {
1678 let t = self.simulated.as_secs_f32();
1679 let bob = (t * 2.0).sin() * GEM_BOB_HEIGHT;
1680 ctx.light(
1681 Light::point(
1682 GEM_POSITION + Vec3::Y * (bob + GEM_LIGHT_LIFT),
1683 GEM_LIGHT_COLOR,
1684 GEM_LIGHT_RANGE,
1685 )
1686 .shadow(),
1687 );
1688 ctx.draw(
1689 Gem.at(Transform::from_scale_rotation_translation(
1690 Vec3::ONE,
1691 Quat::from_rotation_y(t * GEM_SPIN_SPEED),
1692 GEM_POSITION + Vec3::Y * bob,
1693 ))
1694 .material(Material::shaded(GEM_COLOR, 0.7).emissive(GEM_COLOR.dimmed(1.6))),
1695 );
1696 }
1697
1698 /// The player: upright so it always faces the camera about `+Y`,
1699 /// windowed to its facing's row and the walk cycle's current frame.
1700 fn draw_walker(&self, ctx: &mut FrameContext<'_, Keep>, ground: Vec3) {
1701 let step = if self.walk_ticks > 0 {
1702 (self.walk_ticks / TICKS_PER_WALK_FRAME) % WALKER_COLUMNS
1703 } else {
1704 0
1705 };
1706 let cell = Sheet::new(UVec2::new(WALKER_COLUMNS, WALKER_ROWS))
1707 .cell_at(UVec2::new(step, self.facing as u32));
1708 let size = Vec2::new(WALKER_WIDTH, WALKER_HEIGHT);
1709
1710 ctx.draw(
1711 Walker
1712 .at(Transform::from_scale_rotation_translation(
1713 size.extend(1.0),
1714 Quat::IDENTITY,
1715 ground + Vec3::Y * (WALKER_HEIGHT * 0.5),
1716 ))
1717 .upright()
1718 .frame(cell),
1719 );
1720 }examples/ui-fonts.rs (lines 672-676)
657 fn draw_station(&self, ctx: &mut FrameContext<'_, Self>, station: StationKind) {
658 let look = station.look();
659 let center = station.center();
660 let front_offset =
661 STATION_SIZE.z * 0.5 - STATION_FRONT_SIZE.z * 0.5 + STATION_FRONT_OUTWARD;
662 let front = center - Vec3::new(0.0, 0.0, front_offset);
663 for (size, position, material) in [
664 (STATION_SIZE, center, Material::lit(look.color)),
665 (
666 STATION_FRONT_SIZE,
667 front,
668 Material::color(Color::BLACK).emissive(look.glow),
669 ),
670 ] {
671 ctx.draw(
672 Cube.at(Transform::from_scale_rotation_translation(
673 size,
674 Quat::IDENTITY,
675 position,
676 ))
677 .material(material),
678 );
679 }
680 }
681
682 fn draw_bracket(&self, ctx: &mut FrameContext<'_, Self>, camera: Camera, station: StationKind) {
683 let top = station.center() + Vec3::Y * (STATION_SIZE.y * 0.5);
684 let window_size = ctx.window_size();
685 let Some(pixel) = camera.pixel_of(top, window_size) else {
686 return;
687 };
688 let at = logical(pixel, ctx.pixels_per_point());
689
690 let name = ctx.text_layout(station.look().name, egui::FontId::proportional(BODY_SIZE));
691 let (reading_text, number_text) = station.reading(self.elapsed.as_secs_f32());
692 let reading = ctx.text_layout(&reading_text, egui::FontId::monospace(BODY_SIZE));
693 let number = ctx.text_layout(
694 &number_text,
695 egui::FontId::new(NUMBER_SIZE, egui::FontFamily::Name(DISPLAY_FAMILY.into())),
696 );
697
698 ctx.ui(|ui| bracket(ui.painter(), at, name, reading, number));
699 }
700
701 /// A `Prompt` for `Trigger::Hail`, above every `StationKind` but
702 /// `hovered`: what a player presses to reach one, apart from a hover.
703 fn draw_prompts(
704 &self,
705 ctx: &mut FrameContext<'_, Self>,
706 camera: Camera,
707 hovered: Option<StationKind>,
708 ) {
709 let Some(binding) = ctx.bindings(Trigger::Hail).into_iter().next() else {
710 return;
711 };
712 let hint = prompt(&binding);
713 let glyph = ctx.text_layout(&hint.text(), egui::FontId::new(PROMPT_SIZE, hint.family()));
714 let window_size = ctx.window_size();
715 let pixels_per_point = ctx.pixels_per_point();
716
717 ctx.ui(|ui| {
718 let painter = ui.painter();
719 for station in StationKind::ALL {
720 if Some(station) == hovered {
721 continue;
722 }
723 let top = station.center() + Vec3::Y * (STATION_SIZE.y * 0.5);
724 let Some(pixel) = camera.pixel_of(top, window_size) else {
725 continue;
726 };
727 let at = logical(pixel, pixels_per_point);
728 let at = egui::pos2(at.x, at.y - PROMPT_LIFT);
729 prompt_at(painter, at, glyph.clone());
730 }
731 });
732 }
733
734 /// The title, a line and the reading, each in a font this game loaded
735 /// rather than egui's own.
736 fn panel(&self, ctx: &mut FrameContext<'_, Self>) {
737 ctx.ui(|ui| {
738 ui.label(styled(
739 "a game's own fonts",
740 egui::FontId::proportional(HEADING_SIZE),
741 ));
742 ui.label(styled(
743 "drawn in Pixel Operator, the game's proportional font",
744 egui::FontId::proportional(BODY_SIZE),
745 ));
746 ui.label(styled(
747 "the readings above each station in Pixel Operator Mono",
748 egui::FontId::monospace(BODY_SIZE),
749 ));
750 });
751 }
752
753 fn draw_dialogue(&self, ctx: &mut FrameContext<'_, Self>) {
754 let Some(dialogue) = &self.dialogue else {
755 return;
756 };
757 let whole = ctx.text_layout(
758 dialogue.current_line(),
759 egui::FontId::proportional(BODY_SIZE),
760 );
761 let size = whole.size();
762 ctx.ui(|ui| dialogue.draw(ui, size));
763 }
764
765 /// The `StationKind` under the pointer, `None` while the UI holds it.
766 fn hovered(ctx: &FrameContext<'_, Self>) -> Option<StationKind> {
767 if ctx.ui_wants_pointer() {
768 return None;
769 }
770 hit_station(
771 ctx.last_camera()
772 .ray_through(ctx.pointer(), ctx.window_size()),
773 )
774 }
775
776 /// A held [`Trigger::Hail`] turns the camera by the pointer's own
777 /// motion; the wheel zooms it.
778 fn steer(&mut self, ctx: &mut FrameContext<'_, Self>) {
779 if !ctx.ui_wants_pointer() && ctx.down(Trigger::Hail) {
780 self.orbit.turn(ctx.axis2(Turn::Look));
781 }
782 let wheel = ctx.axis(Zoom::Wheel);
783 if !ctx.ui_wants_pointer() && wheel != 0.0 {
784 self.orbit.zoom(ZOOM_STEP.powf(wheel));
785 }
786 }
787}
788
789impl Game for WatchRoom {
790 type Meshes = Shape;
791 type Sounds = NoSounds;
792 type InputActions = Controls;
793 type Skyboxes = Sky;
794 type SurfaceStyles = NoSurfaceStyles;
795 type PostEffects = NoPostEffects;
796
797 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
798 self.elapsed += ctx.dt();
799 self.orbit.yaw += AUTO_TURN_RATE * ctx.dt().as_secs_f32();
800
801 if let Some(dialogue) = &mut self.dialogue {
802 dialogue.tick();
803 }
804 if ctx.pressed(Trigger::Close) {
805 self.dialogue = None;
806 self.hailed = None;
807 }
808 if ctx.pressed(Trigger::Sheet) {
809 self.sheet_open = !self.sheet_open;
810 }
811 if ctx.pressed(Trigger::Hail) && !ctx.ui_wants_pointer() {
812 self.handle_hail(ctx);
813 }
814 }
815
816 fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
817 self.steer(ctx);
818
819 let camera = self.orbit.camera();
820 ctx.set_camera(camera);
821 ctx.set_skybox(Sky::Dusk);
822 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
823
824 ctx.draw(
825 Plane
826 .at(Transform::from_scale(Vec3::new(
827 PLATFORM_SIZE,
828 1.0,
829 PLATFORM_SIZE,
830 )))
831 .material(Material::lit(PLATFORM_COLOR)),
832 );
833 for station in StationKind::ALL {
834 self.draw_station(ctx, station);
835 }
836
837 let hovered = Self::hovered(ctx);
838 if !self.sheet_open {
839 if let Some(station) = hovered {
840 ctx.set_cursor(Cursor::Pointer);
841 self.draw_bracket(ctx, camera, station);
842 }
843 self.draw_prompts(ctx, camera, hovered);
844 }
845 if self.dialogue.is_some() {
846 self.draw_dialogue(ctx);
847 }
848 if self.sheet_open {
849 ctx.ui(sheet);
850 }
851 self.panel(ctx);
852 }examples/post-effects.rs (lines 95-99)
90 fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
91 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
92
93 ctx.draw(
94 Plane
95 .at(Transform::from_scale(Vec3::new(
96 GROUND_SIZE,
97 1.0,
98 GROUND_SIZE,
99 )))
100 .material(Material::lit(GROUND_COLOR)),
101 );
102 ctx.draw(
103 Cube.at(Transform::from_scale_rotation_translation(
104 Vec3::splat(GLOW_SIZE),
105 Quat::IDENTITY,
106 GLOW_POSITION,
107 ))
108 .material(Material::color(Color::BLACK).emissive(GLOW_COLOR)),
109 );
110 for position in SPHERE_POSITIONS {
111 ctx.draw(
112 Sphere {
113 subdivisions: SPHERE_SUBDIVISIONS,
114 }
115 .at(position)
116 .material(Material::lit(SPHERE_COLOR)),
117 );
118 }
119 }examples/breakout-game.rs (lines 607-611)
604 fn draw_court(&self, ctx: &mut FrameContext<'_, Breakout>) {
605 ctx.draw(
606 Plane
607 .at(Transform::from_scale(Vec3::new(
608 COURT_HALF_WIDTH * 2.0,
609 1.0,
610 COURT_HALF_DEPTH * 2.0,
611 )))
612 .material(Material::lit(FLOOR_COLOR)),
613 );
614
615 let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, COURT_HALF_DEPTH);
616 for side in [-1.0, 1.0] {
617 let x = side * (COURT_HALF_WIDTH - WALL_THICKNESS * 0.5);
618 ctx.draw(
619 Cube.at(Transform::from_scale_rotation_translation(
620 side_half * 2.0,
621 Quat::IDENTITY,
622 Vec3::new(x, side_half.y, 0.0),
623 ))
624 .material(Material::lit(WALL_COLOR)),
625 );
626 }
627
628 let top_half = Vec3::new(COURT_HALF_WIDTH, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
629 ctx.draw(
630 Cube.at(Transform::from_scale_rotation_translation(
631 top_half * 2.0,
632 Quat::IDENTITY,
633 Vec3::new(0.0, top_half.y, -COURT_HALF_DEPTH + WALL_THICKNESS * 0.5),
634 ))
635 .material(Material::lit(WALL_COLOR)),
636 );
637 }
638
639 fn draw_bricks(&self, ctx: &mut FrameContext<'_, Breakout>) {
640 let scale = Vec3::new(
641 BRICK_HALF_WIDTH * 2.0,
642 BRICK_HALF_HEIGHT * 2.0,
643 BRICK_HALF_DEPTH * 2.0,
644 );
645 for brick in self.bricks.iter().filter(|brick| brick.hits_remaining > 0) {
646 let health = f32::from(brick.hits_remaining) / f32::from(BRICK_HITS);
647 let color = BRICK_ROW_COLORS[brick.row].dimmed(0.4 + 0.6 * health);
648 ctx.draw(
649 Cube.at(Transform::from_scale_rotation_translation(
650 scale,
651 Quat::IDENTITY,
652 brick.position,
653 ))
654 .material(Material::shaded(color, health)),
655 );
656 }
657 }
658
659 /// Draws the live spark burst: additive, tumbling by roll as they age,
660 /// shrinking and fading out over their lifetime.
661 fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662 for spark in &self.sparks {
663 let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664 let fade = 1.0 - age;
665 let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666 ctx.draw(
667 Quad.at(Transform::from_scale_rotation_translation(
668 Vec3::splat(size),
669 Quat::IDENTITY,
670 spark.position,
671 ))
672 .billboard()
673 .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674 .material(
675 Material::color(spark.color.with_alpha(fade))
676 .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677 .additive(),
678 ),
679 );
680 }
681 }
682
683 /// Draws the ball's ghost trail, each ghost smaller and more transparent
684 /// than the one ahead of it; each ghost's position interpolates between
685 /// its own last two resolved ticks by the same `alpha` the ball itself
686 /// draws at, and its radius clamps to what the ball's own radius has
687 /// left over its distance from the head, so a ghost still close to the
688 /// ball never draws past its edge.
689 fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690 let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691 for i in 0..TRAIL_LEN {
692 let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693 let age = (i + 1) as f32 / TRAIL_LEN as f32;
694 let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695 let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696 .min((BALL_RADIUS - head.distance(position)).max(0.0));
697 let scale = Vec3::splat(radius * 2.0);
698 ctx.draw(
699 Sphere { subdivisions: 2 }
700 .at(Transform::from_scale_rotation_translation(
701 scale,
702 Quat::IDENTITY,
703 position,
704 ))
705 .material(
706 Material::color(BALL_GLOW.with_alpha(fade))
707 .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708 ),
709 );
710 }
711 }
712
713 /// Draws one held ball for every life past the one in play, set in a
714 /// row alongside the paddle's own path.
715 fn draw_lives(&self, ctx: &mut FrameContext<'_, Breakout>) {
716 let held_lives = self.lives.saturating_sub(1);
717 for slot in 0..held_lives {
718 let z = PADDLE_Z + (slot + 1) as f32 * LIFE_ROW_SPACING;
719 ctx.draw(
720 Sphere { subdivisions: 2 }
721 .at(Transform::from_scale_rotation_translation(
722 Vec3::splat(BALL_RADIUS * 2.0),
723 Quat::IDENTITY,
724 Vec3::new(LIFE_ROW_X, BALL_RADIUS, z),
725 ))
726 .material(
727 Material::color(BALL_GLOW)
728 .emissive(BALL_EMISSIVE)
729 .additive(),
730 ),
731 );
732 }
733 }
734
735 fn overlay(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
736 let bricks_left = self
737 .bricks
738 .iter()
739 .filter(|brick| brick.hits_remaining > 0)
740 .count();
741 // Read before `ctx.ui` so a rebind changes what the hint reads this
742 // frame too.
743 let move_hint = bindings_text(ctx.bindings(Move::Paddle));
744 let pause_hint = bindings_text(ctx.bindings(Button::Pause));
745 let serve_hint = bindings_text(ctx.bindings(Button::Serve));
746 ctx.ui(|ui| {
747 ui.horizontal(|ui| {
748 ui.label(egui::RichText::new(format!("score {}", self.score)).size(32.0));
749 ui.label(format!("{bricks_left} bricks left"));
750 });
751 ui.label(format!("move: {move_hint} · {pause_hint} to pause"));
752 if self.phase == Phase::Serving {
753 ui.label(format!("{serve_hint} to serve"));
754 }
755 });
756
757 match self.phase {
758 Phase::Serving | Phase::Playing if self.paused => self.menu(ctx, "paused", false),
759 Phase::Won => self.menu(ctx, "you win", true),
760 Phase::Lost => self.menu(ctx, "game over", true),
761 _ => {}
762 }
763 }
764
765 fn menu(&mut self, ctx: &mut FrameContext<'_, Breakout>, title: &str, over: bool) {
766 let mut clicked = false;
767 let mut quit = false;
768
769 // `ctx.ui` cannot borrow `ctx`, so anything the controls list needs is
770 // read first and applied after.
771 let buttons: Vec<(Button, String)> = Button::all()
772 .into_iter()
773 .map(|action| (action, bindings_text(ctx.bindings(action))))
774 .collect();
775 let axes: Vec<(Move, String)> = Move::all()
776 .into_iter()
777 .map(|action| (action, bindings_text(ctx.bindings(action))))
778 .collect();
779 let listening = self.listening;
780 let actuated_button = (!ctx.ui_wants_keyboard())
781 .then(|| ctx.actuated_button())
782 .flatten();
783 let actuated_axis = (!ctx.ui_wants_keyboard())
784 .then(|| ctx.actuated_axis())
785 .flatten();
786 let mut reset = None;
787
788 ctx.ui(|ui| {
789 egui::Window::new(title)
790 .collapsible(false)
791 .resizable(false)
792 .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
793 .show(ui.ctx(), |ui| {
794 if over {
795 ui.label(format!("score {}", self.score));
796 }
797 if !over {
798 ui.add(
799 egui::Slider::new(&mut self.master_volume, 0.0..=1.0).text("volume"),
800 );
801 if ui.button("resume").clicked() {
802 self.paused = false;
803 clicked = true;
804 }
805 ui.separator();
806 ui.heading("controls");
807 for (action, text) in &buttons {
808 controls_row(
809 ui,
810 action.name(),
811 text,
812 listening == Some(Listening::Button(*action)),
813 &mut self.listening,
814 Listening::Button(*action),
815 &mut reset,
816 );
817 }
818 for (action, text) in &axes {
819 controls_row(
820 ui,
821 action.name(),
822 text,
823 listening == Some(Listening::Move(*action)),
824 &mut self.listening,
825 Listening::Move(*action),
826 &mut reset,
827 );
828 }
829 }
830 if ui.button("restart").clicked() {
831 self.restart();
832 clicked = true;
833 }
834 if ui.button("quit").clicked() {
835 quit = true;
836 }
837 });
838 });
839
840 match (self.listening, actuated_button, actuated_axis) {
841 (Some(Listening::Button(action)), Some(binding), _) => {
842 ctx.rebind(action, vec![binding]);
843 self.listening = None;
844 }
845 (Some(Listening::Move(action)), _, Some(binding)) => {
846 ctx.rebind(action, vec![binding]);
847 self.listening = None;
848 }
849 _ => {}
850 }
851 match reset {
852 Some(Listening::Button(action)) => ctx.rebind(action, action.bindings()),
853 Some(Listening::Move(action)) => ctx.rebind(action, action.bindings()),
854 None => {}
855 }
856
857 if clicked {
858 ctx.play(Sound::Click);
859 }
860 if quit {
861 ctx.close();
862 }
863 }
864
865 /// Sustains both tracks every frame, and the gain goes to whichever the
866 /// game calls for: gameplay music while a round is live, serving
867 /// included, and menu music whenever a menu covers it.
868 ///
869 /// Each fades in over [`MUSIC_CROSSFADE`] and slides every later gain
870 /// over it, which is the crossfade itself; the one at no gain costs no
871 /// voice while its playback goes on under the other.
872 fn sustain_music(&self, ctx: &mut FrameContext<'_, Breakout>) {
873 let playing = !self.paused && matches!(self.phase, Phase::Serving | Phase::Playing);
874 let gain = |wanted: bool| match wanted {
875 true => MUSIC_GAIN,
876 false => 0.0,
877 };
878
879 ctx.sustain(
880 Sound::Music
881 .gain(gain(playing))
882 .fade(MUSIC_CROSSFADE)
883 .glide(MUSIC_CROSSFADE)
884 .loop_from(MUSIC_LOOP_FROM),
885 );
886 ctx.sustain(
887 Sound::MenuMusic
888 .gain(gain(!playing))
889 .fade(MUSIC_CROSSFADE)
890 .glide(MUSIC_CROSSFADE)
891 .loop_from(MENU_MUSIC_LOOP_FROM),
892 );
893 }
894}
895
896/// One action's name, its live bindings, a rebind control that starts
897/// listening for a new one, and a reset to its defaults; cancel is a
898/// button rather than Escape, since Escape is itself a binding a listen
899/// could capture.
900fn controls_row(
901 ui: &mut egui::Ui,
902 name: &str,
903 bindings: &str,
904 listening: bool,
905 target: &mut Option<Listening>,
906 action: Listening,
907 reset: &mut Option<Listening>,
908) {
909 ui.horizontal(|ui| {
910 ui.label(format!("{name}: {bindings}"));
911 if listening {
912 ui.label("listening");
913 if ui.button("cancel").clicked() {
914 *target = None;
915 }
916 } else if ui.button("rebind").clicked() {
917 *target = Some(action);
918 }
919 if ui.button("reset").clicked() {
920 *reset = Some(action);
921 }
922 });
923}
924
925/// The controls-menu text for a live binding list: each alternative,
926/// separated, in the order the player can use them.
927fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
928 bindings
929 .iter()
930 .map(ToString::to_string)
931 .collect::<Vec<_>>()
932 .join(", ")
933}
934
935fn spawn_bricks() -> Vec<Brick> {
936 let cell = BRICK_HALF_WIDTH * 2.0 + BRICK_GAP;
937 let row_span = BRICK_HALF_DEPTH * 2.0 + BRICK_ROW_GAP;
938 let grid_width = cell * BRICK_COLUMNS as f32 - BRICK_GAP;
939 let start_x = -grid_width * 0.5 + BRICK_HALF_WIDTH;
940 let start_z = -COURT_HALF_DEPTH + WALL_THICKNESS + BRICK_HALF_DEPTH + 0.6;
941
942 (0..BRICK_ROWS)
943 .flat_map(|row| {
944 (0..BRICK_COLUMNS).map(move |column| Brick {
945 row,
946 position: Vec3::new(
947 start_x + column as f32 * cell,
948 BRICK_HALF_HEIGHT,
949 start_z + row as f32 * row_span,
950 ),
951 hits_remaining: BRICK_HITS,
952 })
953 })
954 .collect()
955}
956
957impl Game for Breakout {
958 type Meshes = Shape;
959 type Sounds = Sound;
960 type InputActions = Controls;
961 type Skyboxes = NoSkyboxes;
962 type SurfaceStyles = NoSurfaceStyles;
963 type PostEffects = NoPostEffects;
964
965 fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966 if self.paused {
967 return;
968 }
969
970 let dt = ctx.dt().as_secs_f32();
971 self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972 self.brick_flash = (self.brick_flash - dt).max(0.0);
973 self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974 self.step_sparks(dt);
975
976 // Decay runs before the end-screen return below, so the last pulse and
977 // burst do not stay on screen.
978 if matches!(self.phase, Phase::Won | Phase::Lost) {
979 return;
980 }
981
982 let axis = if ctx.ui_wants_keyboard() {
983 0.0
984 } else {
985 ctx.axis(Move::Paddle)
986 };
987 self.step_paddle(axis, dt);
988
989 match self.phase {
990 Phase::Serving => self.hold_ball(ctx),
991 _ => self.step_ball(ctx, dt),
992 }
993 }
994
995 fn frame(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
996 if matches!(self.phase, Phase::Serving | Phase::Playing) && ctx.pressed(Button::Pause) {
997 self.paused = !self.paused;
998 }
999
1000 ctx.set_volume(self.master_volume);
1001 self.sustain_music(ctx);
1002
1003 ctx.set_camera(Self::camera());
1004
1005 let brick_pulse = (self.brick_flash / BRICK_FLASH).clamp(0.0, 1.0);
1006 ctx.set_bloom((BLOOM_BASE + brick_pulse * BLOOM_PULSE_PEAK).clamp(0.0, 1.0));
1007
1008 let life_lost_t = (self.life_lost_flash / LIFE_LOST_FLASH).clamp(0.0, 1.0);
1009 ctx.set_exposure((1.0 - life_lost_t * EXPOSURE_DIP_DEPTH).clamp(0.0, 1.0));
1010
1011 // The tick moves nothing behind a menu, so a frame there draws the last
1012 // step whole rather than interpolating from the one before.
1013 let alpha = match self.phase {
1014 Phase::Serving | Phase::Playing if !self.paused => ctx.alpha(),
1015 _ => 1.0,
1016 };
1017 let paddle_x = self.paddle_prev_x.lerp(self.paddle_x, alpha);
1018 let ball_pos = self.ball_prev.lerp(self.ball_pos, alpha);
1019
1020 ctx.light(Light::point(ball_pos, BALL_GLOW, BALL_LIGHT_RANGE).shadow());
1021
1022 self.draw_court(ctx);
1023 self.draw_bricks(ctx);
1024 self.draw_sparks(ctx);
1025 self.draw_lives(ctx);
1026
1027 ctx.draw(
1028 Paddle
1029 .at(Transform::from_translation(Vec3::new(
1030 paddle_x,
1031 PADDLE_HALF_HEIGHT,
1032 PADDLE_Z,
1033 )))
1034 .material_of(PaddlePart::Face, self.paddle_face_material()),
1035 );
1036
1037 self.draw_trail(ctx, alpha);
1038 ctx.draw(
1039 Sphere { subdivisions: 2 }
1040 .at(Transform::from_scale_rotation_translation(
1041 Vec3::splat(BALL_RADIUS * 2.0),
1042 Quat::IDENTITY,
1043 ball_pos,
1044 ))
1045 .material(
1046 Material::color(BALL_GLOW)
1047 .emissive(BALL_EMISSIVE)
1048 .additive(),
1049 ),
1050 );
1051
1052 self.overlay(ctx);
1053 }Additional examples can be found in:
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".