use core::time::Duration;
use std::str::FromStr;
use argh::FromArgs;
use bevy::{
asset::RenderAssetUsages,
color::palettes::basic::*,
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
window::{PresentMode, WindowResolution},
winit::WinitSettings,
};
use rand::{seq::IndexedRandom, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
const CUBES_PER_SECOND: u32 = 10000;
const GRAVITY: f32 = -9.8;
const MAX_VELOCITY: f32 = 10.;
const CUBE_SCALE: f32 = 1.0;
const CUBE_TEXTURE_SIZE: usize = 256;
const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5;
const VOLUME_WIDTH: usize = 50;
const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32);
#[derive(Resource)]
struct BevyCounter {
pub count: usize,
pub color: Color,
}
#[derive(Component)]
struct Cube {
velocity: Vec3,
}
#[derive(FromArgs, Resource)]
struct Args {
#[argh(switch)]
benchmark: bool,
#[argh(option, default = "0")]
per_wave: usize,
#[argh(option, default = "0")]
waves: usize,
#[argh(switch)]
vary_per_instance: bool,
#[argh(option, default = "1")]
material_texture_count: usize,
#[argh(option, default = "AlphaMode::Opaque")]
alpha_mode: AlphaMode,
}
#[derive(Default, Clone)]
enum AlphaMode {
#[default]
Opaque,
Blend,
AlphaMask,
}
impl FromStr for AlphaMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"opaque" => Ok(Self::Opaque),
"blend" => Ok(Self::Blend),
"alpha_mask" => Ok(Self::AlphaMask),
_ => Err(format!(
"Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
)),
}
}
}
const FIXED_TIMESTEP: f32 = 0.2;
fn main() {
#[cfg(not(target_arch = "wasm32"))]
let args: Args = argh::from_env();
#[cfg(target_arch = "wasm32")]
let args = Args::from_args(&[], &[]).unwrap();
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "BevyMark 3D".into(),
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings::continuous())
.insert_resource(args)
.insert_resource(BevyCounter {
count: 0,
color: Color::WHITE,
})
.add_systems(Startup, setup)
.add_systems(FixedUpdate, scheduled_spawner)
.add_systems(
Update,
(
mouse_handler,
movement_system,
collision_system,
counter_system,
),
)
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
FIXED_TIMESTEP,
)))
.run();
}
#[derive(Resource)]
struct CubeScheduled {
waves: usize,
per_wave: usize,
}
fn scheduled_spawner(
mut commands: Commands,
args: Res<Args>,
mut scheduled: ResMut<CubeScheduled>,
mut counter: ResMut<BevyCounter>,
cube_resources: ResMut<CubeResources>,
) {
if scheduled.waves > 0 {
let cube_resources = cube_resources.into_inner();
spawn_cubes(
&mut commands,
args.into_inner(),
&mut counter,
scheduled.per_wave,
cube_resources,
None,
scheduled.waves - 1,
);
scheduled.waves -= 1;
}
}
#[derive(Resource)]
struct CubeResources {
_textures: Vec<Handle<Image>>,
materials: Vec<Handle<StandardMaterial>>,
cube_mesh: Handle<Mesh>,
color_rng: ChaCha8Rng,
material_rng: ChaCha8Rng,
velocity_rng: ChaCha8Rng,
transform_rng: ChaCha8Rng,
}
#[derive(Component)]
struct StatsText;
fn setup(
mut commands: Commands,
args: Res<Args>,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
material_assets: ResMut<Assets<StandardMaterial>>,
images: ResMut<Assets<Image>>,
counter: ResMut<BevyCounter>,
) {
let args = args.into_inner();
let images = images.into_inner();
let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
if args.material_texture_count > 0 {
textures.push(asset_server.load("branding/icon.png"));
}
init_textures(&mut textures, args, images);
let material_assets = material_assets.into_inner();
let materials = init_materials(args, &textures, material_assets);
let mut cube_resources = CubeResources {
_textures: textures,
materials,
cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))),
color_rng: ChaCha8Rng::seed_from_u64(42),
material_rng: ChaCha8Rng::seed_from_u64(12),
velocity_rng: ChaCha8Rng::seed_from_u64(97),
transform_rng: ChaCha8Rng::seed_from_u64(26),
};
let font = TextFont {
font_size: FontSize::Px(40.0),
..Default::default()
};
commands.spawn((
Camera3d::default(),
Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
DirectionalLight {
illuminance: 10000.0,
shadow_maps_enabled: false,
..default()
},
Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
Node {
position_type: PositionType::Absolute,
padding: UiRect::all(px(5)),
..default()
},
BackgroundColor(Color::BLACK.with_alpha(0.75)),
GlobalZIndex(i32::MAX),
children![(
Text::default(),
StatsText,
children![
(
TextSpan::new("Cube Count: "),
font.clone(),
TextColor(LIME.into()),
),
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
(
TextSpan::new("\nFPS (raw): "),
font.clone(),
TextColor(LIME.into()),
),
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
(
TextSpan::new("\nFPS (SMA): "),
font.clone(),
TextColor(LIME.into()),
),
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
(
TextSpan::new("\nFPS (EMA): "),
font.clone(),
TextColor(LIME.into()),
),
(TextSpan::new(""), font.clone(), TextColor(AQUA.into()))
]
)],
));
let mut scheduled = CubeScheduled {
per_wave: args.per_wave,
waves: args.waves,
};
if args.benchmark {
let counter = counter.into_inner();
for wave in (0..scheduled.waves).rev() {
spawn_cubes(
&mut commands,
args,
counter,
scheduled.per_wave,
&mut cube_resources,
Some(wave),
wave,
);
}
scheduled.waves = 0;
}
commands.insert_resource(cube_resources);
commands.insert_resource(scheduled);
}
fn mouse_handler(
mut commands: Commands,
args: Res<Args>,
time: Res<Time>,
mouse_button_input: Res<ButtonInput<MouseButton>>,
cube_resources: ResMut<CubeResources>,
mut counter: ResMut<BevyCounter>,
mut rng: Local<Option<ChaCha8Rng>>,
mut wave: Local<usize>,
) {
if rng.is_none() {
*rng = Some(ChaCha8Rng::seed_from_u64(42));
}
let rng = rng.as_mut().unwrap();
if mouse_button_input.just_released(MouseButton::Left) {
counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
}
if mouse_button_input.pressed(MouseButton::Left) {
let spawn_count = (CUBES_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
spawn_cubes(
&mut commands,
args.into_inner(),
&mut counter,
spawn_count,
cube_resources.into_inner(),
None,
*wave,
);
*wave += 1;
}
}
fn cube_velocity_transform(
mut translation: Vec3,
velocity_rng: &mut ChaCha8Rng,
waves: Option<usize>,
dt: f32,
) -> (Transform, Vec3) {
let mut velocity = Vec3::new(0., 0., MAX_VELOCITY * velocity_rng.random::<f32>());
if let Some(waves) = waves {
for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
step_movement(&mut translation, &mut velocity, dt);
handle_collision(&translation, &mut velocity);
}
}
(Transform::from_translation(translation), velocity)
}
const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
fn spawn_cubes(
commands: &mut Commands,
args: &Args,
counter: &mut BevyCounter,
spawn_count: usize,
cube_resources: &mut CubeResources,
waves_to_simulate: Option<usize>,
wave: usize,
) {
let batch_material = cube_resources.materials[wave % cube_resources.materials.len()].clone();
let spawn_y = VOLUME_SIZE.y / 2.0 - HALF_CUBE_SIZE;
let spawn_z = -VOLUME_SIZE.z / 2.0 + HALF_CUBE_SIZE;
let batch = (0..spawn_count)
.map(|_| {
let spawn_pos = Vec3::new(
(cube_resources.transform_rng.random::<f32>() - 0.5) * VOLUME_SIZE.x,
spawn_y,
spawn_z,
);
let (transform, velocity) = cube_velocity_transform(
spawn_pos,
&mut cube_resources.velocity_rng,
waves_to_simulate,
FIXED_DELTA_TIME,
);
let material = if args.vary_per_instance {
cube_resources
.materials
.choose(&mut cube_resources.material_rng)
.unwrap()
.clone()
} else {
batch_material.clone()
};
(
Mesh3d(cube_resources.cube_mesh.clone()),
MeshMaterial3d(material),
transform,
Cube { velocity },
)
})
.collect::<Vec<_>>();
commands.spawn_batch(batch);
counter.count += spawn_count;
counter.color = Color::linear_rgb(
cube_resources.color_rng.random(),
cube_resources.color_rng.random(),
cube_resources.color_rng.random(),
);
}
fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
translation.x += velocity.x * dt;
translation.y += velocity.y * dt;
translation.z += velocity.z * dt;
velocity.y += GRAVITY * dt;
}
fn movement_system(
args: Res<Args>,
time: Res<Time>,
mut cube_query: Query<(&mut Cube, &mut Transform)>,
) {
let dt = if args.benchmark {
FIXED_DELTA_TIME
} else {
time.delta_secs()
};
for (mut cube, mut transform) in &mut cube_query {
step_movement(&mut transform.translation, &mut cube.velocity, dt);
}
}
fn handle_collision(translation: &Vec3, velocity: &mut Vec3) {
if (velocity.x > 0. && translation.x + HALF_CUBE_SIZE > VOLUME_SIZE.x / 2.0)
|| (velocity.x <= 0. && translation.x - HALF_CUBE_SIZE < -VOLUME_SIZE.x / 2.0)
{
velocity.x = -velocity.x;
}
if (velocity.z > 0. && translation.z + HALF_CUBE_SIZE > VOLUME_SIZE.z / 2.0)
|| (velocity.z <= 0. && translation.z - HALF_CUBE_SIZE < -VOLUME_SIZE.z / 2.0)
{
velocity.z = -velocity.z;
}
let velocity_y = velocity.y;
if velocity_y < 0. && translation.y - HALF_CUBE_SIZE < -VOLUME_SIZE.y / 2.0 {
velocity.y = -velocity_y;
}
if translation.y + HALF_CUBE_SIZE > VOLUME_SIZE.y / 2.0 && velocity_y > 0.0 {
velocity.y = 0.0;
}
}
fn collision_system(mut cube_query: Query<(&mut Cube, &Transform)>) {
cube_query.par_iter_mut().for_each(|(mut cube, transform)| {
handle_collision(&transform.translation, &mut cube.velocity);
});
}
fn counter_system(
diagnostics: Res<DiagnosticsStore>,
counter: Res<BevyCounter>,
query: Single<Entity, With<StatsText>>,
mut writer: TextUiWriter,
) {
let text = *query;
if counter.is_changed() {
*writer.text(text, 2) = counter.count.to_string();
}
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
if let Some(raw) = fps.value() {
*writer.text(text, 4) = format!("{raw:.2}");
}
if let Some(sma) = fps.average() {
*writer.text(text, 6) = format!("{sma:.2}");
}
if let Some(ema) = fps.smoothed() {
*writer.text(text, 8) = format!("{ema:.2}");
}
};
}
fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
while textures.len() < args.material_texture_count {
let pixel = [
color_rng.random(),
color_rng.random(),
color_rng.random(),
255,
];
textures.push(images.add(Image::new_fill(
Extent3d {
width: CUBE_TEXTURE_SIZE as u32,
height: CUBE_TEXTURE_SIZE as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&pixel,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
)));
}
}
fn init_materials(
args: &Args,
textures: &[Handle<Image>],
assets: &mut Assets<StandardMaterial>,
) -> Vec<Handle<StandardMaterial>> {
let mut capacity = if args.vary_per_instance {
args.per_wave * args.waves
} else {
args.material_texture_count.max(args.waves)
};
if !args.benchmark {
capacity = capacity.max(256);
}
capacity = capacity.max(1);
let alpha_mode = match args.alpha_mode {
AlphaMode::Opaque => bevy::prelude::AlphaMode::Opaque,
AlphaMode::Blend => bevy::prelude::AlphaMode::Blend,
AlphaMode::AlphaMask => bevy::prelude::AlphaMode::Mask(0.5),
};
let mut materials = Vec::with_capacity(capacity);
materials.push(assets.add(StandardMaterial {
base_color: Color::WHITE,
base_color_texture: textures.first().cloned(),
alpha_mode,
..default()
}));
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
materials.extend(
std::iter::repeat_with(|| {
assets.add(StandardMaterial {
base_color: Color::linear_rgb(
color_rng.random(),
color_rng.random(),
color_rng.random(),
),
base_color_texture: textures.choose(&mut texture_rng).cloned(),
alpha_mode,
..default()
})
})
.take(capacity - materials.len()),
);
materials
}