Skip to main content

Mesh

Trait Mesh 

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

Source

fn build(&self, assets: &Assets) -> MeshData<P, C>

Builds this value’s mesh, the first time a draw needs it.

Must not read a file or the network; loaded data is in assets.

Provided Methods§

Source

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
Hide additional 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    }

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§