# mirage-engine
Mirage is an immediate-mode 3D game engine, built on `wgpu` and `winit`,
with `egui` for the UI. It supports 3D graphics with lights and shadows,
models and animation from Blender, sprites with relief lighting, custom
shaders and post effects, egui UI, sound with spatial mixing, gamepad and
keyboard input with rebinding, and save data. Games run on desktop and in
the browser, packaged for itch.io in one command.
```rust,no_run
use mirage_engine::prelude::*;
meshes! { enum Shape { Cube } }
/// Every clip this game plays, named by its file stem.
#[derive(Catalog, Hash, PartialEq, Eq, Clone)]
enum Cue {
Step,
}
impl Sounds for Cue {
fn build(&self, assets: &Assets) -> SoundData {
assets.sound("step")
}
}
struct Chase {
position: Vec3,
}
impl Game for Chase {
type Meshes = Shape;
type Sounds = Cue;
type InputActions = Key; // the prototype vocabulary: every key binds itself
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Chase>) {
if ctx.down(Key::D) {
self.position.x += 4.0 * ctx.dt().as_secs_f32();
}
if ctx.pressed(Key::D) {
ctx.play(Cue::Step);
}
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Chase>) {
ctx.light(Light::point(self.position + Vec3::Y * 2.0, Color::WHITE, 8.0).shadow());
ctx.draw(Cube.at(self.position));
ctx.ui(|ui| {
ui.label("hold D to run");
});
}
}
fn main() {
run(
// relative to the working directory
Config::new("chase").with_assets(["sounds/step.ogg"]),
|_| Ok(Chase { position: Vec3::ZERO }),
);
}
```
`cargo doc --open` is the API reference; every item documents its contract.
## Examples
`cargo run --example <name>` runs one on the desktop and
`cargo mirage serve --example <name>` in the browser, where these screenshots were
taken.
The crates.io package ships without `examples/assets`, so check out the
repository to run the examples.
### sprite-adventure
A simple two-area world: a pond with a custom water `SurfaceStyle`, a cave
through a portal in the hedgerow with torch lighting, and progress saved
across runs. The sprites are lit in 3D through relief maps, normals and
depths generated from each sprite sheet.

### animation
A 3D character with basic movement controls, an `Animator` over ten states
with clips that loop, play once or blend by walking speed. A second elf
scrubs its animation by distance. Lit by a sun, a lamp post's shadowed point
light, a spot light over the damaging red patches, and a colorful butterfly
carrying a shadowed point light.

### breakout-game
A paddle bats a ball at a grid of bricks under fixed-step physics, the ball
and its spares the only lights in the court, with sound, music and egui menus.

### isometric-board
A fixed diagonal view through `Projection::orthographic`: a click on a unit's
`Ray::hit_aabb` box selects it, a click on a tile orders it there, and a rock
is built by arithmetic per seed.

### ui-fonts
Custom fonts in the UI: `startup.set_fonts` draws the UI with them, and
clicking a station plays a text animation.

### input-lab
A gamepad panel built on the action system: every pad button, axis,
stick, mouse button, wheel delta and pointer delta is a row in the table.
Each row shows its live bindings, what it reads this frame, and buttons to
rebind by capture or reset.

### material-playground
Sky, lights, material and post chain controls on panels. Choose between
skyboxes and their lighting. Hold the right mouse button and drag to look,
`W`/`A`/`S`/`D` to move, `Space`/`Left Shift` up and down, and the wheel to
scale each move.

### post-effects
A vignette, grain and scanlines over a lit scene, each a custom `PostEffect`
with live controls, showing how a game writes its own screen-space effects.

### sound-lab
A set of sounds and spatial audio: one-shots, sustained cues, and three
placed sources you drag around the room. Walk with WASD or a stick to hear
the mix change.

### stress-preview
A field of thousands of generated rocks instanced to the horizon. Set the
instance count to see what the engine handles, with a report of what each
frame costs.

## The browser
```sh
cargo install cargo-mirage
cargo mirage serve --example breakout-game # builds, packages, serves http://127.0.0.1:8000
```
`cargo mirage web` alone writes `dist/` and the zip that itch.io accepts as an HTML
game, with the page, the asset folders and a check against itch's upload
limits.
[`PUBLISHING.md`](PUBLISHING.md) writes out the full process for a
game in its own repository, from the build through to the itch.io project
settings. Players need a browser with WebGPU: on Linux, Firefox still ships
it disabled (`dom.webgpu.enabled` in `about:config`), and Chromium-based
browsers work out of the box.
## Assets
Export a `.glb` from Blender, list it in `Config::with_assets`, and pull each
model by name inside `build()`. Paths in `with_assets` are relative to the
working directory, which is why the examples name `examples/assets/...`.
Every model is built at startup, so a missing model, texture or material is
a startup error rather than a blank draw.
The engine uses the names from Blender's outliner. Below are two example files,
as the engine reads them:
```text
examples/assets/breakout.glb
└── paddle object, the root node assets.mesh("paddle")
└── paddle mesh
├── Paddle Frame material draws as authored
└── Paddle Face material #[part("Paddle Face")]
examples/assets/elf.glb
├── Elf armature, the root node assets.mesh("Elf")
│ ├── ElfBody skinned mesh
│ │ └── elf_body_textures material
│ └── root bone hierarchy
│ └── pelvis …
└── actions one clip each
├── idle #[clip("idle")]
├── walk #[clip("walk")]
└── jog … #[clip("jog")]
```
A model is pulled by its root node's name, the object or the armature. When
two files share a root name, put the file stem in front: `props#Ship`.
Materials are named in a `#[derive(Part)]` enum so a draw can recolor them
one at a time; a material the enum does not name draws as authored. Clips
are named in a `#[derive(Clip)]` enum the same way. The paddle above:
```rust,no_run
use mirage_engine::prelude::*;
/// The paddle in `examples/assets/breakout.glb`, pulled by its root node's name.
#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
struct Paddle;
/// The material a draw can repaint, named as Blender names it.
#[derive(Part, Clone, Debug, PartialEq, Eq, Hash)]
enum PaddlePart {
#[part("Paddle Face")]
Face,
}
impl Mesh<PaddlePart> for Paddle {
fn build(&self, assets: &Assets) -> MeshData<PaddlePart> {
assets.mesh("paddle")
}
}
meshes! { enum Shape { Paddle, Cube } }
```
`tools/hello_fixture.py` is a scripted, reproducible Blender export to start
from.
Images: a spritesheet or any standalone `.png` is pulled with
`assets.texture` by its file stem; `.pixelated()` keeps its pixels crisp.
Sounds: `.ogg` files, one clip each, pulled with `assets.sound` by file
stem; `.streamed()` marks a long track to decode during playback.
Fonts: `.ttf` or `.otf` files, one font each. `startup.font("pixel-operator")`
reads one as an `egui::FontData`, and `startup.set_fonts(fonts)` makes the UI
draw in them from the first frame.
## Known limits
- Textures hold no mip chain, so a textured model far from the camera
sparkles.
## Future work, in order
1. WebGL2 fallback, only if itch players report coverage gaps.
2. Directional sound sources: a placed sound gains a facing and a cone,
the spot light's shape applied to audio, in the same shared level
arithmetic.
3. `doubleSided` import: an unculled slot. Culling is pipeline state,
so it forks pipelines the way cutout does.
4. Vertex colors, multiplied into the tint, common in low-poly found
art.
5. `KHR_materials_emissive_strength`: one factor multiplied into the
emissive lane.
6. `Color` beside egui's colors: a conversion pair between the engine's
linear channels, whose values go past `1.0`, and the sRGB-encoded
color a picker edits, where the picker holds the tone and an intensity
value scales that tone's light.
## Out of scope
Decided against, so games can rely on the boundary:
- **Physics simulation.** Arcade physics is a few lines of game code, and a
real solver (Rapier) slots into the fixed-step `tick` as ordinary game
state.
- **Inverse kinematics.**
- **Terrain systems.** A heightmap mesh can be constructed with `build()`.
Chunked streaming worlds are a different class of engine.
- **A scene graph, ECS, or editor.** Games own their state in plain Rust.
The engine draws what a frame submits.
- **Networking.**
- **Other glTF material extensions** (transmission, clearcoat, sheen,
specular). The material set is closed; simple games do not need them.
## License
`Zlib OR Apache-2.0 OR MIT`, at your option, for all three crates. The example
assets are CC0, cropped from ArMM1998's "Zelda-like tilesets and sprites"
(<https://opengameart.org/content/zelda-like-tilesets-and-sprites>); the
`-relief.png` maps beside them are derived from that art by
`tools/spritemaps`, and share its license. The animal models are CC0 from
Gobkit's "Free Animal Pack"
(<https://gobkit.itch.io/gobkit-free-animal-pack>); the chest is
CC0 from 3darknight's "3D Low Poly Chest"
(<https://3darknight.itch.io/3d-low-poly-chest>). The elf is p4ss0's
"PSX Red Elf" (<https://p4ss0.itch.io/psx-red-elf>), offered at no cost
and royalty free, bound by `tools/elf_fixture.py` to the CC0 skeleton and
clips of Quaternius's "Universal Animation Library"
(<https://quaternius.itch.io/universal-animation-library>) and weight
painted by hand in `examples/assets/elf.blend`, whose default export is
the `.glb`. The butterfly is Čestmír Dammer's ("CDmir") "Butterfly
(animated)" (<https://opengameart.org/content/butterfly-animated>), CC0,
its wings doubled by `tools/butterfly_fixture.py` in
`examples/assets/butterfly.blend`, whose default export is the `.glb`; its
texture is derived from photo documentation by Rick Hoppmann, as the
archive's own license file states. The six sky images are CC0:
`sky-clear.png`, `sky-classic.png`, `sky-dawn.png` and `sky-sinister.png`
from vladislavzh's "Retro Skyboxes Pack"
(<https://opengameart.org/content/retro-skyboxes-pack>), and
`sky-stars-lightblue.png` and `sky-stars-blue.png` from StumpyStrust's
"Space Skyboxes" (<https://opengameart.org/content/space-skyboxes-0>);
`tools/sky_fixture.py` stitches each set's six cube faces into the
equirectangular image an example loads. The example fonts are CC0 from
Jayvee Enaguas's "Pixel Operator"
(<https://fontlibrary.org/en/font/pixel-operator>), the proportional font
and the Mono font of that family. The display font is CC0 from Sora
Sagano's "Ferrum" (<https://fontlibrary.org/en/font/ferrum>). The
input-prompt glyphs are CC0 from Kenney's "Input Prompts"
(<https://kenney.nl/assets/input-prompts>), the Keyboard & Mouse font of
that pack. One model under `tests/assets` comes from elsewhere, and the
decoder's tests alone read it: `robot-expressive.glb` is the CC0
`RobotExpressive` of the three.js examples
(<https://github.com/mrdoob/three.js>). Every other file under
`tests/assets` is the repository's own, built by the `tools/*_fixture.py`
scripts.