use bevy::asset::RenderAssetUsages;
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use bevy::render::view::screenshot::{save_to_disk, Screenshot};
use plumesplat::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PlumeSplatPlugin)
.add_systems(Startup, setup)
.add_systems(Update, (rotate_camera, auto_screenshot))
.run();
}
#[derive(Resource)]
struct ScreenshotTimer {
timer: Timer,
taken: bool,
frames_after_screenshot: u32,
}
impl Default for ScreenshotTimer {
fn default() -> Self {
Self {
timer: Timer::from_seconds(3.0, TimerMode::Once),
taken: false,
frames_after_screenshot: 0,
}
}
}
fn auto_screenshot(
mut commands: Commands,
time: Res<Time>,
mut screenshot_timer: Option<ResMut<ScreenshotTimer>>,
) {
if screenshot_timer.is_none() {
commands.insert_resource(ScreenshotTimer::default());
return;
}
let timer = screenshot_timer.as_mut().unwrap();
timer.timer.tick(time.delta());
if timer.timer.is_finished() && !timer.taken {
timer.taken = true;
let path = "./screenshot.png";
info!("Taking screenshot: {}", path);
commands
.spawn(Screenshot::primary_window())
.observe(save_to_disk(path.to_string()));
}
if timer.taken {
timer.frames_after_screenshot += 1;
if timer.frames_after_screenshot > 30 {
info!("Screenshot saved, exiting...");
std::process::exit(0);
}
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
asset_server: Res<AssetServer>,
) {
let terrain_mesh = create_terrain_mesh(128, 128, 10.0, 2.0);
let mesh_handle = meshes.add(terrain_mesh);
let grass = MaterialLayer::new(asset_server.load("textures/raw/Grass004_1K-PNG_Color.png"))
.with_normal(asset_server.load("textures/raw/Grass004_1K-PNG_NormalGL.png"));
let dirt = MaterialLayer::new(asset_server.load("textures/raw/Ground054_1K-PNG_Color.png"))
.with_normal(asset_server.load("textures/raw/Ground054_1K-PNG_NormalGL.png"));
let rock = MaterialLayer::new(asset_server.load("textures/raw/Rock051_1K-PNG_Color.png"))
.with_normal(asset_server.load("textures/raw/Rock051_1K-PNG_NormalGL.png"));
let snow = MaterialLayer::new(asset_server.load("textures/raw/snow_diff.png"));
let pending_material = SplatMaterialBuilder::new()
.add_layer(grass)
.add_layer(dirt)
.add_layer(rock)
.add_layer(snow)
.with_uv_scale(0.5)
.with_triplanar_sharpness(4.0)
.with_blend_offset(0.1)
.with_blend_exponent(2.0)
.build();
commands.spawn((
Mesh3d(mesh_handle),
pending_material,
Transform::from_xyz(0.0, 0.0, 0.0),
));
commands.spawn((
DirectionalLight {
illuminance: 15000.0,
shadows_enabled: true,
..default()
},
Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.6, 0.4, 0.0)),
));
commands.spawn(AmbientLight {
color: Color::WHITE,
brightness: 200.0,
..default()
});
commands.spawn((
Camera3d::default(),
Transform::from_xyz(15.0, 10.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),
CameraController { angle: 0.0 },
Msaa::Sample4,
));
info!("PlumeSplat basic example running!");
info!("Terrain uses 4 materials: grass, dirt, rock, snow");
info!("Materials blend based on height and slope.");
info!("Using builder API for automatic texture array creation!");
}
#[derive(Component)]
struct CameraController {
angle: f32,
}
fn rotate_camera(time: Res<Time>, mut query: Query<(&mut Transform, &mut CameraController)>) {
for (mut transform, mut controller) in query.iter_mut() {
controller.angle += time.delta_secs() * 0.2;
let radius = 20.0;
let height = 12.0;
transform.translation = Vec3::new(
controller.angle.cos() * radius,
height,
controller.angle.sin() * radius,
);
transform.look_at(Vec3::ZERO, Vec3::Y);
}
}
fn create_terrain_mesh(width: u32, height: u32, size: f32, max_height: f32) -> Mesh {
let mut positions: Vec<[f32; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut uvs: Vec<[f32; 2]> = Vec::new();
let mut material_indices: Vec<u32> = Vec::new();
let mut material_weights: Vec<u32> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let half_size = size / 2.0;
let step_x = size / (width - 1) as f32;
let step_z = size / (height - 1) as f32;
for z in 0..height {
for x in 0..width {
let px = -half_size + x as f32 * step_x;
let pz = -half_size + z as f32 * step_z;
let h = heightmap(px, pz, max_height);
positions.push([px, h, pz]);
uvs.push([
x as f32 / (width - 1) as f32,
z as f32 / (height - 1) as f32,
]);
normals.push([0.0, 1.0, 0.0]);
let material_data = compute_material_blend(h, max_height);
material_indices.push(material_data.packed_indices());
material_weights.push(material_data.packed_weights());
}
}
for z in 0..height {
for x in 0..width {
let idx = (z * width + x) as usize;
let px = positions[idx][0];
let pz = positions[idx][2];
let eps = step_x * 0.5;
let h_left = heightmap(px - eps, pz, max_height);
let h_right = heightmap(px + eps, pz, max_height);
let h_back = heightmap(px, pz - eps, max_height);
let h_front = heightmap(px, pz + eps, max_height);
let normal = Vec3::new(h_left - h_right, 2.0 * eps, h_back - h_front).normalize();
normals[idx] = normal.to_array();
let slope = 1.0 - normal.y; let h = positions[idx][1];
let material_data = compute_material_blend_with_slope(h, max_height, slope);
material_indices[idx] = material_data.packed_indices();
material_weights[idx] = material_data.packed_weights();
}
}
for z in 0..(height - 1) {
for x in 0..(width - 1) {
let top_left = z * width + x;
let top_right = top_left + 1;
let bottom_left = (z + 1) * width + x;
let bottom_right = bottom_left + 1;
if (x + z) % 2 == 0 {
indices.push(top_left);
indices.push(bottom_left);
indices.push(top_right);
indices.push(top_right);
indices.push(bottom_left);
indices.push(bottom_right);
} else {
indices.push(top_left);
indices.push(bottom_left);
indices.push(bottom_right);
indices.push(top_left);
indices.push(bottom_right);
indices.push(top_right);
}
}
}
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
mesh.insert_attribute(ATTRIBUTE_MATERIAL_INDICES, material_indices);
mesh.insert_attribute(ATTRIBUTE_MATERIAL_WEIGHTS, material_weights);
mesh.insert_indices(Indices::U32(indices));
mesh
}
fn heightmap(x: f32, z: f32, max_height: f32) -> f32 {
let freq1 = 0.3;
let freq2 = 0.7;
let freq3 = 1.5;
let h1 = (x * freq1).sin() * (z * freq1).cos();
let h2 = (x * freq2 + 1.0).cos() * (z * freq2 - 0.5).sin() * 0.5;
let h3 = (x * freq3).sin() * (z * freq3).sin() * 0.25;
((h1 + h2 + h3) * 0.5 + 0.5) * max_height
}
fn compute_material_blend(height: f32, max_height: f32) -> MaterialVertex {
let normalized_height = height / max_height;
let grass = smooth_band(normalized_height, 0.0, 0.35, 0.1);
let dirt = smooth_band(normalized_height, 0.25, 0.55, 0.1);
let rock = smooth_band(normalized_height, 0.45, 0.85, 0.15);
let snow = smooth_band(normalized_height, 0.7, 1.0, 0.1);
MaterialVertex::blend4([0, 1, 2, 3], [grass, dirt, rock, snow])
}
fn compute_material_blend_with_slope(height: f32, max_height: f32, slope: f32) -> MaterialVertex {
let normalized_height = height / max_height;
let grass = smooth_band(normalized_height, 0.0, 0.4, 0.2);
let dirt = smooth_band(normalized_height, 0.2, 0.6, 0.2);
let rock = smooth_band(normalized_height, 0.4, 0.9, 0.25);
let snow = smooth_band(normalized_height, 0.65, 1.0, 0.2);
let slope_factor = (slope * 3.0).clamp(0.0, 1.0);
let rock = rock + slope_factor * 0.5;
let grass = grass * (1.0 - slope_factor * 0.7);
let snow = snow * (1.0 - slope_factor * 0.5);
MaterialVertex::blend4([0, 1, 2, 3], [grass, dirt, rock, snow])
}
fn smooth_band(value: f32, start: f32, end: f32, fade: f32) -> f32 {
let fade_in = smoothstep(start - fade, start + fade, value);
let fade_out = 1.0 - smoothstep(end - fade, end + fade, value);
fade_in * fade_out
}
fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}