| Crates.io | map_scatter |
| lib.rs | map_scatter |
| version | 0.4.1 |
| created_at | 2025-08-28 18:21:06.560288+00 |
| updated_at | 2026-01-17 20:17:36.354205+00 |
| description | Rule-based object scattering library with field-graph evaluation and sampling |
| homepage | |
| repository | https://github.com/morgenthum/map_scatter |
| max_upload_size | |
| id | 1814424 |
| size | 1,403,866 |
Rule-based object scattering library with field-graph evaluation and sampling.

map_scatter helps you fill 2D worlds (or a 2D projection of 3D) with lots of small things - vegetation, props, resources, decals, spawn points - quickly and reproducibly.
Three parts:
Field graphs compile to a program and run in chunks with caching for stable results.
Links:
Examples: https://github.com/morgenthum/map_scatter/blob/main/crates/map_scatter_examples/README.md
Architecture: ./ARCHITECTURE.md
Use map_scatter when you need to place many small items in a 2D domain (or a 2D projection of 3D) and still control:
Examples:
See the example crate for curated demos you can run locally.
For a high-level architecture overview, see ARCHITECTURE.md.
Active development; API may evolve between minor releases.
Add the dependency:
[dependencies]
map_scatter = "0.3"
rand = "0.9"
glam = { version = "0.30", features = ["mint"] }
mint = "0.5"
Hello, scatter:
use glam::Vec2;
use rand::{SeedableRng, rngs::StdRng};
use map_scatter::prelude::*;
fn main() {
// 1) Author a field graph for a "kind"
// Here, we tag a constant=1.0 as the Probability field (always placeable).
let mut spec = FieldGraphSpec::default();
spec.add_with_semantics(
"probability",
NodeSpec::constant(1.0),
FieldSemantics::Probability,
);
let grass = Kind::new("grass", spec);
// 2) Build a layer using a sampling strategy (e.g., jittered grid)
let layer = Layer::new_with(
"grass",
vec![grass],
JitterGridSampling::new(0.35, 5.0), // jitter, cell_size
)
// Optional: produce an overlay mask to reuse in later layers (name: "mask_grass")
.with_overlay((256, 256), 3);
// 3) Assemble a plan (one or more layers)
let plan = Plan::new().with_layer(layer);
// 4) Prepare runtime
let cache = FieldProgramCache::new();
let textures = TextureRegistry::new(); // Register textures as needed
let cfg = RunConfig::new(Vec2::new(100.0, 100.0))
.with_chunk_extent(32.0)
.with_raster_cell_size(1.0)
.with_grid_halo(2);
// 5) Run
let mut rng = StdRng::seed_from_u64(42);
let mut runner = ScatterRunner::new(cfg, &textures, &cache);
let result = runner.run(&plan, &mut rng);
println!(
"Placed {} instances (evaluated: {}, rejected: {}).",
result.placements.len(),
result.positions_evaluated,
result.positions_rejected
);
}
Observing events:
use rand::{SeedableRng, rngs::StdRng};
use map_scatter::prelude::*;
fn run_with_events(plan: &Plan) {
let cache = FieldProgramCache::new();
let textures = TextureRegistry::new();
let cfg = RunConfig::new(glam::Vec2::new(64.0, 64.0));
let mut rng = StdRng::seed_from_u64(7);
let mut runner = ScatterRunner::new(cfg, &textures, &cache);
// Capture events for inspection (warnings, per-position evaluations, overlays, etc.)
let mut sink = VecSink::new();
let result = runner.run_with_events(plan, &mut rng, &mut sink);
for event in sink.into_inner() {
match event {
ScatterEvent::PlacementMade { placement, .. } => {
println!("Placed '{}' at {:?}", placement.kind_id, placement.position);
}
ScatterEvent::Warning { context, message } => {
eprintln!("[WARN] {context}: {message}");
}
_ => {}
}
}
println!("Total placed: {}", result.placements.len());
}
use map_scatter::prelude::*;
RunConfig:
domain_center: shift the evaluated window in world space (useful for streaming chunks)chunk_extent: larger chunks reduce overhead but can increase evaluation costraster_cell_size: smaller cells improve accuracy at higher costgrid_halo: extra cells for filters/EDT at chunk borderswith_overlay, then refer to the registered texture mask_<layer_id> in subsequent field graphs.tracing for diagnosticsrand RNGs; examples commonly use StdRngSome micro-benchmarks are included:
cargo bench -p map_scatter