| Crates.io | plumesplat |
| lib.rs | plumesplat |
| version | 0.1.1 |
| created_at | 2026-01-16 19:44:40.530073+00 |
| updated_at | 2026-01-16 19:45:15.599038+00 |
| description | Advanced terrain splatting for Bevy with support for 256+ materials using texture arrays |
| homepage | |
| repository | https://github.com/drewridley/plumesplat |
| max_upload_size | |
| id | 2049156 |
| size | 289,532 |
Advanced terrain splatting for Bevy with support for up to 256 materials in a single draw call.
PlumeSplat is a high-performance terrain material blending library for the Bevy game engine. It extends Bevy's StandardMaterial with powerful texture array splatting, enabling complex multi-material terrains with full PBR support.

StandardMaterial for proper lighting, shadows, and reflectionsAdd PlumeSplat to your Cargo.toml:
[dependencies]
plumesplat = "0.1"
bevy = "0.18"
The easiest way to use PlumeSplat is with the builder API. Simply define your material layers with individual textures, and PlumeSplat automatically combines them into optimized texture arrays when they finish loading:
use bevy::prelude::*;
use plumesplat::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PlumeSplatPlugin)
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
asset_server: Res<AssetServer>,
) {
// Define material layers - each with its own textures
let grass = MaterialLayer::new(asset_server.load("grass_albedo.png"))
.with_normal(asset_server.load("grass_normal.png"));
let rock = MaterialLayer::new(asset_server.load("rock_albedo.png"))
.with_normal(asset_server.load("rock_normal.png"))
.with_pbr(asset_server.load("rock_pbr.png"));
// Build the material - textures are combined automatically!
let pending_material = SplatMaterialBuilder::new()
.add_layer(grass)
.add_layer(rock)
.with_uv_scale(2.0)
.with_triplanar_sharpness(4.0)
.build();
// Spawn with mesh - material auto-creates when textures load
commands.spawn((
Mesh3d(meshes.add(create_terrain_mesh())),
pending_material,
));
}
MaterialLayer instances with individual texture files.build() to get a PendingSplatMaterial componentPlumeSplatMaterialEach MaterialLayer represents one material in your terrain:
// Minimal: just albedo (color) texture
let grass = MaterialLayer::new(asset_server.load("grass.png"));
// With normal map for surface detail
let dirt = MaterialLayer::new(asset_server.load("dirt.png"))
.with_normal(asset_server.load("dirt_normal.png"));
// Full PBR: albedo + normal + packed PBR texture
let rock = MaterialLayer::new(asset_server.load("rock.png"))
.with_normal(asset_server.load("rock_normal.png"))
.with_pbr(asset_server.load("rock_pbr.png"));
The PBR texture packs multiple channels:
let pending = SplatMaterialBuilder::new()
.add_layer(grass)
.add_layer(dirt)
.add_layer(rock)
// UV scale for triplanar mapping (higher = more tiling)
.with_uv_scale(2.0)
// Triplanar blend sharpness (higher = sharper transitions between projections)
.with_triplanar_sharpness(4.0)
// Height-based blending strength (0.0 = disabled)
.with_height_blending(0.5)
// Blend offset for sharper material transitions
.with_blend_offset(0.1)
// Blend exponent (higher = sharper boundaries)
.with_blend_exponent(2.0)
// Custom base material settings
.with_base_material(StandardMaterial {
perceptual_roughness: 0.8,
..default()
})
.build();
| Setting | Default | Range | Description |
|---|---|---|---|
uv_scale |
1.0 | 0.1+ | Controls texture tiling density |
triplanar_sharpness |
4.0 | 1.0-16.0 | Sharpness of triplanar UV transitions |
height_blend_sharpness |
0.0 | 0.0+ | Height-based blending (0 = disabled) |
blend_offset |
0.0 | 0.0-0.5 | Subtracts from weights for sharper transitions |
blend_exponent |
1.0 | 1.0-8.0 | Power applied to weights (higher = sharper) |
Meshes must include custom vertex attributes to specify which materials to use at each vertex:
use plumesplat::prelude::*;
// Single material (no blending)
let vertex = MaterialVertex::single(0); // 100% material index 0
// Blend two materials
let vertex = MaterialVertex::blend2(0, 1, 0.5); // 50% each
// Blend three materials
let vertex = MaterialVertex::blend3(0, 1, 2, [0.5, 0.3, 0.2]);
// Blend four materials (maximum per vertex)
let vertex = MaterialVertex::blend4([0, 1, 2, 3], [0.4, 0.3, 0.2, 0.1]);
// Add attributes to your mesh
let (indices, weights) = encode_material_data(&material_vertices);
mesh.insert_attribute(ATTRIBUTE_MATERIAL_INDICES, indices);
mesh.insert_attribute(ATTRIBUTE_MATERIAL_WEIGHTS, weights);
For advanced use cases, you can bypass the builder and create materials directly with pre-combined texture arrays:
use plumesplat::prelude::*;
fn setup(
mut commands: Commands,
mut materials: ResMut<Assets<PlumeSplatMaterial>>,
asset_server: Res<AssetServer>,
) {
// Load pre-stacked texture arrays (layers stacked vertically)
let albedo_array = asset_server.load("terrain_albedo_array.png");
let normal_array = asset_server.load("terrain_normal_array.png");
let material = PlumeSplatMaterial {
base: StandardMaterial {
perceptual_roughness: 0.8,
..default()
},
extension: PlumeSplatExtension::new(albedo_array)
.with_normal_array(normal_array)
.with_uv_scale(2.0),
};
commands.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(materials.add(material)),
));
}
When using direct creation, textures must be pre-stacked vertically:
+------------------+
| Material 0 | <- Grass (512x512)
+------------------+
| Material 1 | <- Dirt (512x512)
+------------------+
| Material 2 | <- Rock (512x512)
+------------------+
| Material 3 | <- Snow (512x512)
+------------------+
= 512x2048 total
Run the included example:
cargo run --example basic
This demonstrates:
Each vertex stores:
u32u32This allows any vertex to blend up to 4 materials from a palette of 256.
Instead of traditional UV mapping, PlumeSplat projects textures from three orthogonal directions (X, Y, Z) and blends them based on the surface normal. This eliminates UV seams and works on any geometry.
To prevent visible repetition patterns, PlumeSplat uses hash-based random offsets when sampling textures. This breaks up the regular grid pattern that would otherwise be visible on large terrains.
When using the builder API, the plugin:
PendingSplatMaterial components| PlumeSplat | Bevy |
|---|---|
| 0.1.x | 0.18 |
PlumeSplat is dual-licensed under either:
at your option.
Contributions are welcome! Please feel free to submit a Pull Request.