mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation

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.

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.

the player beside the pond, the portal beyond

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.

the elf dancing in front of the lamp post, the butterfly and its light above, a damaging red patch

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.

the ball at the bricks, one of them broken and scattering sparks

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.

the selected unit, its reachable tiles marked, a hovered tile named

ui-fonts

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

a station's name and a line typing out in the game's custom fonts

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.

the South button held under Space, the LeftX bar and the left stick dot at full deflection under D

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.

the loaded sky image over the scene, reflected in a sphere at roughness zero

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.

the three passes over two spheres and a glowing cube

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.

the sound panels over the room, one sustained cue checked

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.

a field of instanced cones drawn to the horizon

The browser

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

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:

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.