# bevy_symbios
Bevy integration for the [Symbios](https://crates.io/crates/symbios) L-System ecosystem.
Converts L-System skeletons into Bevy meshes and physics colliders for procedural plant generation, organic structures, and generative art.
## Features
- **Mesh Generation**: Smooth tube meshes from skeleton strands using parallel transport
- **Multi-Material Support**: Separate meshes per `u16` material ID for palette-driven PBR (bark, leaves, etc.)
- **Vertex Colors**: Per-vertex RGBA colors from skeleton data
- **UV Mapping**: Arc-length parameterized UVs with aspect-ratio preservation
- **Mesh Caching**: Optional fingerprint-keyed `MeshCache` to avoid re-meshing identical L-systems
- **Procedural Materials**: 23 procedural texture generators (Leaf, Twig, Bark, Brick, Plank, …) plus Grid/Noise/Checker previews
- **OBJ + GLB Export**: Pure data conversion for tooling / asset pipelines
- **Egui Editor** (optional): Drop-in `material_palette_editor` widget for live PBR + per-texture-config editing
- **Physics Colliders** (optional): Compound capsule colliders for Avian3D physics
- **Robot Spawning** (optional): Spawn articulated rigid-body robots from `symbios-robot` blueprints
- **Asset Loaders** (optional): `.lsys` grammars and `.matpalette.json` palettes loaded through `AssetServer` (hot-reload friendly)
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
bevy_symbios = "0.4"
```
Feature flags:
| `physics` | | `ColliderGenerator`, `PositionedCollider` (Avian3D) |
| `egui` | | `ui::material_palette_editor` (via `bevy_egui`) |
| `robot` | `physics` | `spawn_robot`, `SpawnedRobot`, `ImuSensor`, `TouchSensor` |
| `asset-loader` | | `LSystemAssetPlugin`, `.lsys` + `.matpalette.json` loaders |
```toml
[dependencies]
bevy_symbios = { version = "0.4", features = ["physics", "egui", "asset-loader"] }
```
## Usage
### Basic Mesh Generation
```rust
use bevy::prelude::*;
use bevy_symbios::{LSystemMeshBuilder, symbios_turtle_3d::Skeleton};
fn spawn_tree(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
skeleton: Skeleton,
) {
// Convert skeleton to meshes (one per material ID)
let mesh_map = LSystemMeshBuilder::new()
.with_resolution(12) // Vertices around tube circumference
.build(&skeleton);
// Define a material palette: each material ID maps to PBR properties
let palette: Vec<StandardMaterial> = vec![
StandardMaterial { // ID 0: Bark
base_color: Color::WHITE, // Tinted by vertex colors
perceptual_roughness: 0.9,
metallic: 0.0,
..default()
},
StandardMaterial { // ID 1: Leaves
base_color: Color::WHITE,
perceptual_roughness: 0.6,
metallic: 0.1,
..default()
},
];
// Spawn each material's mesh with its palette entry
for (material_id, mesh) in mesh_map {
let mat = palette
.get(material_id as usize)
.cloned()
.unwrap_or_default();
commands.spawn((
Mesh3d(meshes.add(mesh)),
MeshMaterial3d(materials.add(mat)),
));
}
}
```
### Multi-Material Workflow
The material system separates **PBR surface properties** from **local color variation**:
- **Material ID** (`SkeletonPoint::material_id`) — Selects a palette entry that defines
surface properties like roughness, metallic, and emissive. Each unique ID produces a
separate mesh, so different Bevy `StandardMaterial`s can be applied per group.
- **Vertex Colors** (`SkeletonPoint::color`) — Baked into mesh vertices as `ATTRIBUTE_COLOR`.
These provide per-vertex tinting (e.g. darker bark at branch bases, lighter tips on
leaves) without needing additional materials or textures.
Set `base_color: Color::WHITE` on your palette materials so vertex colors pass through
unmodified. Any non-white base color will multiply with the vertex color.
### Physics Colliders
Generate a compound capsule collider for physics simulation (requires `physics` feature):
```rust
use bevy::prelude::*;
use bevy_symbios::{ColliderGenerator, symbios_turtle_3d::Skeleton};
fn spawn_with_collider(
mut commands: Commands,
skeleton: Skeleton,
) {
// Generate a single compound collider, filtering out thin branches
if let Some(collider) = ColliderGenerator::new()
.with_min_radius(0.05) // Ignore twigs thinner than 5cm
.build(&skeleton)
{
commands.spawn((Transform::default(), collider));
}
}
```
### Working with Symbios
This crate works with skeletons from the [symbios-turtle-3d](https://crates.io/crates/symbios-turtle-3d) interpreter:
```rust
use symbios::System;
use symbios_turtle_3d::{TurtleConfig, TurtleInterpreter};
use bevy_symbios::LSystemMeshBuilder;
// Parse and derive an L-System
let mut sys = System::new();
sys.set_axiom("F").unwrap();
sys.add_rule("p1: F -> F[+F]F[-F]F").unwrap();
sys.derive(4).unwrap();
// Interpret derived state as a 3D skeleton
let mut interpreter = TurtleInterpreter::new(TurtleConfig::default());
interpreter.populate_standard_symbols(&sys.interner);
let skeleton = interpreter.build_skeleton(&sys.state);
// Now use LSystemMeshBuilder to create meshes
let meshes = LSystemMeshBuilder::new()
.with_resolution(8)
.build(&skeleton);
```
### Material Palette System
The `materials` module provides a palette-first PBR workflow with live editing support
driven by an explicit-trigger observer:
```rust
use bevy::prelude::*;
use bevy_symbios::materials::{
MaterialSettingsMap, MaterialPalette, MaterialSettingsChanged,
setup_material_assets, on_material_settings_changed, apply_foliage_textures,
};
// In your app setup:
app.init_resource::<MaterialSettingsMap>()
.add_systems(Startup, setup_material_assets)
.add_observer(on_material_settings_changed)
.add_systems(Update, apply_foliage_textures);
// After editing MaterialSettingsMap, ask the observer to re-apply the palette:
fn on_edit(mut commands: Commands /* , ... */) {
commands.trigger(MaterialSettingsChanged);
}
```
`MaterialSettingsMap` holds editable settings per material ID (base color, emission,
roughness, metallic, texture type, UV scale). The default seeds three entries (IDs 0/1/2)
but the map accepts any `u16` key. `on_material_settings_changed` is an **observer** —
nothing happens until you `commands.trigger(MaterialSettingsChanged)` after mutating the
map, which keeps re-application off the hot path and lets the UI decide when to commit
in-progress edits. For procedural foliage configs the observer spawns an async
generation task; `apply_foliage_textures` drains those tasks each frame and applies the
resulting handles to the `MaterialPalette`.
### Material Palette Editor (requires `egui` feature)
```toml
bevy_symbios = { version = "0.4", features = ["egui"] }
```
```rust
use bevy_symbios::ui::material_palette_editor;
// Inside an egui panel — returns true when a property changed in a way that
// warrants texture regeneration. Pair with `commands.trigger(MaterialSettingsChanged)`.
let regen = material_palette_editor(ui, &mut material_settings.settings);
```
The editor exposes PBR sliders plus a **Texture parameters** subsection that dispatches
to the per-variant editor for any of the 23 `bevy_symbios_texture` generators (Leaf,
Twig, Bark, Window, StainedGlass, IronGrille, Ground, Rock, Brick, Plank, Shingle,
Stucco, Concrete, Metal, Pavers, Ashlar, Cobblestone, Thatch, Marble, Corrugated,
Asphalt, Wainscoting, Encaustic).
### Mesh Caching
For scenes that re-spawn the same L-system many times (props, tile-based foliage,
deterministic regrowth) use `MeshCache` to avoid re-meshing identical skeletons:
```rust
use bevy::prelude::*;
use bevy_symbios::{LSystemMeshBuilder, MeshCache};
#[derive(Resource, Default)]
struct Cache(MeshCache);
fn spawn_cached(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut cache: ResMut<Cache>,
skeleton: symbios_turtle_3d::Skeleton,
) {
let handles = LSystemMeshBuilder::new()
.with_resolution(12)
.build_cached(&skeleton, &mut cache.0, &mut meshes);
for (mat_id, mesh_handle) in handles {
commands.spawn(Mesh3d(mesh_handle));
let _ = mat_id; // look up material from your palette
}
}
```
`build_cached` fingerprints the skeleton (positions, rotations, radii, colors, material
IDs, UV scales) plus the builder's resolution. A matching fingerprint returns the cached
`Handle<Mesh>` map; otherwise it builds, inserts, and bumps the miss counter. Inspect
`cache.hits()` / `cache.misses()` for instrumentation, or use the lower-level
`MeshCache::get_or_insert_with` with `compute_skeleton_fingerprint` if you need to drive
caching outside `build_cached`. The cache does not LRU-evict — call `clear()`
periodically in long-running scenes that generate many unique skeletons.
### Robot Spawning (requires `robot` feature)
```rust
use bevy::prelude::*;
use bevy_symbios::{spawn_robot, materials::MaterialPalette};
use symbios_robot::RobotBlueprint;
fn spawn(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
palette: Res<MaterialPalette>,
blueprint: Res<MyBlueprint>,
) {
let spawned = spawn_robot(
&mut commands,
&blueprint.0,
&palette,
&mut meshes,
Transform::from_xyz(0.0, 1.0, 0.0),
);
// spawned.modules / .joints / .sensors are returned for parenting or tagging.
}
# #[derive(Resource)] struct MyBlueprint(RobotBlueprint);
```
Module shapes (`Box`, `Cylinder`, `Sphere`, `Capsule`) are converted to both a Bevy
mesh and an Avian3D collider. `JointType::Fixed`, `Hinge`, `Ball`, and `Prismatic` map
to the corresponding Avian joints; `Screw` is approximated as `Fixed` with a warning
since Avian3D has no helical constraint. Hinge and prismatic joints pick the
`AxisLimit` whose `axis` aligns with the drive axis and install a motor with that
entry's effort/velocity. `IMU` and `Touch` sensors attach `ImuSensor` / `TouchSensor`
marker components.
### Asset Loaders (requires `asset-loader` feature)
Load `.lsys` grammars and `.matpalette.json` files through Bevy's `AssetServer`:
```rust
use bevy::prelude::*;
use bevy_symbios::loader::{LSystemAssetPlugin, LSystemSource, MaterialSettingsSource};
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(LSystemAssetPlugin)
.add_systems(Startup, |mut commands: Commands, assets: Res<AssetServer>| {
commands.insert_resource(Handles {
grammar: assets.load("example_tree.lsys"),
palette: assets.load("example_palette.matpalette.json"),
});
})
.run();
# #[derive(Resource)] struct Handles { grammar: Handle<LSystemSource>, palette: Handle<MaterialSettingsSource> }
```
The plugin registers `LSystemSource` (wrapping `symbios::System`) and
`MaterialSettingsSource` (wrapping `HashMap<u16, MaterialSettings>`) plus their loaders.
For runtime hot-reload, enable Bevy's `file_watcher` feature in your consuming crate
and listen for `AssetEvent::Modified` — see [`examples/hot_reload.rs`](examples/hot_reload.rs)
and the [`assets/`](assets/) sample files.
For one-shot parsing without the asset pipeline, the pure parsers are also exported:
`loader::parse_lsys_source(&str)` and `loader::parse_material_settings(&[u8])`.
### Export
The `export` module converts meshes to OBJ and GLB (binary glTF) formats:
```rust
use bevy_symbios::export::{mesh_to_obj, meshes_to_obj, meshes_to_glb, ExportFormat};
// OBJ string from a single mesh (vertex_offset=0 for standalone)
let obj_string = mesh_to_obj(&mesh, "tree", 0);
// Combined OBJ from all material buckets
let obj_combined = meshes_to_obj(&mesh_map, "tree");
// GLB binary with embedded PBR materials
let glb_bytes = meshes_to_glb(&mesh_map, &material_settings_map.settings);
```
`ExportFormat::{Obj, Glb}` (with `name()` / `extension()` helpers) is provided for
UI / CLI selection. The GLB writer emits `POSITION`, `NORMAL`, `COLOR_0`, and indexed
primitives with one PBR material per bucket; vertex tangents and UVs are not yet
included in the GLB output.
## API Reference
### `LSystemMeshBuilder`
| `new()` | Create builder with default resolution (8) |
| `with_resolution(n)` | Set vertices per ring (clamped to 3..=128) |
| `build(&skeleton)` | Convert to `HashMap<u16, Mesh>` (one mesh per material ID) |
| `build_cached(&skeleton, &mut cache, meshes)`| Cache-aware variant returning `HashMap<u16, Handle<Mesh>>`; see [`MeshCache`] |
### `MeshCache`
| `new()` / `default()` | Empty cache |
| `len()` / `is_empty()` | Entry count |
| `contains(&skeleton, resolution)` | Probe a fingerprint without bumping counters |
| `hits()` / `misses()` / `reset_stats()` | Cumulative counters since construction or last reset |
| `clear()` | Drop all cached entries (counters preserved) |
| `get_or_insert_with(fingerprint, build)` | Lookup-or-build by explicit fingerprint |
| `compute_skeleton_fingerprint(&skel, res)` | Free function — derive the same fingerprint `build_cached` uses |
### `ColliderGenerator` (requires `physics` feature)
| `new()` | Create generator with no filtering |
| `with_min_radius(r)` | Skip segments thinner than `r` |
| `build(&skeleton)` | Generate `Option<Collider>` (single compound collider) |
| `build_parts(&skeleton)` | Generate `Vec<PositionedCollider>` (individual segments) |
### `PositionedCollider`
| `transform` | `Transform` | World-space position and rotation |
| `collider` | `Collider` | Avian3D capsule (or sphere for short segments) |
| `radius` | `f32` | Average segment radius |
| `length` | `f32` | Segment length |
## Mesh Attributes
Generated meshes include:
| `POSITION` | Vertex positions |
| `NORMAL` | Smooth normals |
| `COLOR` | RGBA vertex colors for local tinting (`SkeletonPoint::color`) |
| `UV_0` | Texture coordinates (U: around tube, V: along strand, scaled by `uv_scale`) |
| `TANGENT` | Tangent vectors (auto-generated for normal mapping) |
## License
MIT