A Bevy-powered tilemap editor that uses bevy_egui for its in-app UI. The app runs entirely on the CPU/GPU of the user's machine—no external services are required.
- Ensure you have the Rust toolchain installed (Rust 1.75+ recommended for current Bevy releases).
- Run the editor with:
cargo run
- The app starts with default lighting and plugins defined in
src/main.rsand renders immediately without extra setup.
- Application bootstrap —
src/main.rswires Bevy's default plugins with the UI, texture, camera, controls, editor, runtime, and debug inspector plugins, then adds a directional light and grid rendering each frame. - Camera controls —
src/controls.rshandles WASD panning and mouse-wheel zoom for the orthographic camera while respecting Egui focus. - Editing state & tools —
src/editor.rsdefinesEditorState, the current tool selection (paint vs. ramp rotation), map data, hover gizmos, and the per-frame systems that rebuild meshes when the map changes. - UI & file operations —
src/ui.rsbuilds the toolbar, texture palette, and file dialogs for save/load/export usingrfd::AsyncFileDialogand Bevy's async task pool. - Runtime rendering —
src/runtime.rscreates the live terrain entity, regenerates the combined mesh fromEditorState, writes splat maps for texture blending, and keeps materials hidden until all assets load. - Core data types —
src/types.rsmodels tiles, ramps, tile types, and map dimensions, including helpers for indexing and constants for tile sizing.
We welcome contributions! A concise workflow:
- Fork or branch from
workand create a feature branch for your change. - Make focused commits with clear messages.
- Run
cargo fmtandcargo clippy -- -D warningsto keep formatting and lints clean. - Use
cargo runto manually test interactions (painting, ramp rotation, loading/saving) before opening a PR. - Open a pull request describing the change and any relevant repro steps.
Use the checklist placeholders below when recording upcoming tasks:
- Allow adding new tiles (not just editing existing ones) so the overall map can grow or shrink.
- Enable importing custom textures without modifying source code.
- Support user/org-specific utilities (e.g., scenario triggers, spawn points, neutrals, or other hooks).
- The splatmap is limited to four channels (
Rgba8Unorm), so painting with more than four textures can produce unexpected blends. Extending this to more layers is a stretch goal that will require reworking texture allocation across the map. The current splatmap generation lives insrc/terrain.rsundersplatmapfunctions and is bound insrc/runtime.rswhen the terrain mesh is refreshed. - Roughness maps are loaded and sampled in the terrain shader, but their results have not been fully verified yet. See the roughness accumulation paths in
assets/shaders/terrain_pbr_extension.wgslfor the current implementation. - Only a single cliff texture (wall layer) is supported right now. Extending wall variety is planned once a preferred approach is chosen.
- Generation — The CPU builds the
Rgba8Unormsplatmap from the map grid insrc/terrain.rs(splatmap::createandsplatmap::write), assigning one channel perTileTypeindex. The runtime registers the resulting texture handle insrc/runtime.rsso the renderer can sample it when rebuilding terrain meshes. - Sampling in the shader — The fragment shader converts world space to splat UVs in
world_to_splat_uv, samples the splat texture (textureSampleLevel) around each tile to derive normalized weights, and falls back to vertex UVs if no weights are present. This logic lives inassets/shaders/terrain_pbr_extension.wgslnear the weight normalization loop (see the section whereweightsis divided byweight_total). - Applying layers and cliffs — The same shader triplanar-samples base color, normals, and roughness for each weighted layer. Cliff handling happens later in the file around the computation of
cliff_weight: when cliffs are enabled it useswall_layer_indexfor the cliff sample; otherwise it reuses the top layer. Blending between cliff, top, and optional bottom layers is done in that block before the final PBR lighting call.
- Textures are registered during startup in
src/texture/registry.rs; new terrain materials should be added there so both the editor preview and runtime renderer can access them. - Grid rendering runs each frame via
grid_visual::draw_gridfromsrc/grid_visual.rs, controlled by theshow_gridflag inEditorState. - The default map dimensions and tile defaults live in
TileMap::newinsidesrc/types.rs, which initializes a 64×64 grid with grass floor tiles.