map_scatter 0.2.0

Rule-based object scattering library with field-graph evaluation and sampling
Documentation

map_scatter

License: MIT or Apache 2.0 Docs Crate Build Status

Rule-based object scattering library with field-graph evaluation and sampling.

logo

Overview

map_scatter is a fast, composable scattering library for games and tools. You define:

  • Scalar fields as a directed acyclic graph (DAG), including textures and distance-field utilities.
  • Candidate positions via a selection of samplers (Poisson disk, jittered grid, Halton, best-candidate, clustered, and more).
  • A multi-layer plan describing which “kinds” can be placed, their probabilities, and gating rules.

At runtime, map_scatter evaluates the field graph over chunked grids with caching, selects valid placements according to your strategy, and optionally produces overlay textures for downstream layers.

Highlights

  • Field graph authoring and compilation into an efficient program
  • Chunked evaluation with raster caching for speed
  • Multiple sampling strategies for candidate generation
  • Per-layer selection strategies (weighted random, highest probability)
  • Optional overlay generation to feed subsequent layers
  • Event stream for inspection, logging, and tooling

Architecture

For a high-level architecture overview, see ARCHITECTURE.md.

Status

This crate is actively developed. The core APIs are designed to be practical and composable for real projects. Feedback and contributions are welcome.

Quick Start

Add the dependency:

[dependencies]
map_scatter = "0.2"
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(
        "layer_grass",
        vec![grass],
        JitterGridSampling::new(0.35, 5.0), // jitter, cell_size
    )
    // Optional: produce an overlay mask to reuse in later layers (name: "mask_layer_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 mut 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, &mut 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 mut 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, &mut 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());
}

Performance Notes

  • Chunked evaluation: Keeps working sets small and cache-friendly.
  • Raster cell size and chunk extent control performance/quality trade-offs.
  • Field programs are cached and reused per (Kind, Chunk).
  • Overlays are generated only when configured on the layer.

API Tips

  • Bring common types into scope with:
    use map_scatter::prelude::*;
    
  • Start simple: one kind with a constant Probability field, then introduce gates/overlays.
  • Tune RunConfig:
    • chunk_extent: larger chunks reduce overhead but can increase evaluation cost
    • raster_cell_size: smaller cells improve accuracy at higher cost
    • grid_halo: extra cells for filters/EDT at chunk borders
  • Overlays: bridge layers by enabling with_overlay, then refer to the registered texture mask_<layer_id> in subsequent field graphs.

Compatibility

  • 2D domains (Vec2 positions); usable for 3D by feeding height/slope textures and augmenting the 2D placement with a height component in your engine
  • No engine lock-in; pair with your renderer/tooling of choice
  • Integrates well with tracing for diagnostics
  • Use rand RNGs; examples commonly use StdRng

Benchmarks

Some micro-benchmarks are included:

cargo bench -p map_scatter

Roadmap

  • Additional field nodes and utilities
  • Advanced distance-field ops and compositors
  • More sampling controls and distributions
  • Higher-level authoring ergonomics

Contributions and ideas are welcome—please open issues or PRs.

License

map_scatter is dual-licensed under either:

at your option.

Links