use bevy::{
asset::RenderAssetUsages, ecs::system::RunSystemOnce, input::mouse::MouseMotion, mesh::Indices,
platform::collections::HashMap, prelude::*, render::render_resource::PrimitiveTopology,
};
use bevy_egui::{
EguiContext, EguiPlugin,
egui::{self, Ui},
};
use bevy_inspector_egui::bevy_inspector;
use hexx::*;
use rand::{Rng, rng};
use storage::HexagonalMap;
#[derive(Debug, Resource)]
struct HexInfo {
pub layout: HexLayout,
pub mesh_entity: Entity,
pub mesh_handle: Handle<Mesh>,
pub material_handle: Handle<StandardMaterial>,
}
#[derive(Debug, Resource)]
struct BuilderParams {
pub max_height: f32,
pub range: u32,
pub scale: Vec3,
pub fill_holes: bool,
pub sides_options: Option<FaceOptions>,
pub caps_options: Option<FaceOptions>,
}
pub fn main() {
App::new()
.init_resource::<BuilderParams>()
.insert_resource(AmbientLight {
brightness: 500.0,
..default()
})
.add_plugins(DefaultPlugins)
.add_plugins(EguiPlugin::default())
.add_plugins(bevy_inspector_egui::DefaultInspectorConfigPlugin)
.add_systems(Startup, setup)
.add_systems(Update, (show_ui, animate, gizmos))
.run();
}
fn face_opts(ui: &mut Ui, options: &mut Option<FaceOptions>, world: &mut World) {
if options.is_none() {
if ui.button("Enable").clicked() {
*options = Some(FaceOptions::new());
}
} else if ui.button("Disable").clicked() {
*options = None;
}
let Some(opts) = options else {
return;
};
ui.scope(|ui| {
ui.label("UV");
bevy_inspector::ui_for_value(&mut opts.uv, ui, world);
ui.horizontal(|ui| {
ui.label("Insetting");
if opts.insetting.is_none() {
if ui.button("Enable").clicked() {
opts.insetting = Some(InsetOptions {
keep_inner_face: true,
scale: 0.2,
mode: InsetScaleMode::default(),
})
}
} else if ui.button("Disable").clicked() {
opts.insetting = None;
}
});
ui.scope(|ui| {
if let Some(inset) = &mut opts.insetting {
bevy_inspector::ui_for_value(inset, ui, world);
}
});
});
}
fn show_ui(world: &mut World) {
let mut regenerate = false;
world.resource_scope(|world, mut params: Mut<BuilderParams>| {
let Ok(egui_context) = world.query::<&mut EguiContext>().single(world) else {
return;
};
let mut egui_context = egui_context.clone();
egui::SidePanel::left("Mesh settings").show(egui_context.get_mut(), |ui| {
ui.heading("Global");
egui::Grid::new("Grid").num_columns(2).show(ui, |ui| {
ui.label("Range");
ui.add(egui::DragValue::new(&mut params.range).range(1..=10));
ui.end_row();
ui.label("Max Height");
ui.add(egui::DragValue::new(&mut params.max_height).range(1.0..=50.0));
ui.end_row();
ui.label("Scale");
bevy_inspector::ui_for_value(&mut params.scale, ui, world);
ui.end_row();
ui.label("Fill Holes");
ui.add(egui::Checkbox::without_text(&mut params.fill_holes));
ui.end_row();
});
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
ui.heading("Caps Options");
ui.push_id("Caps", |ui| {
face_opts(ui, &mut params.caps_options, world);
});
ui.separator();
ui.heading("Sides Options");
ui.push_id("Sides", |ui| {
face_opts(ui, &mut params.sides_options, world);
});
});
ui.add_space(10.0);
ui.vertical_centered_justified(|ui| {
regenerate = ui.button("Generate").clicked();
});
});
});
world.resource_scope(|world, mut materials: Mut<Assets<StandardMaterial>>| {
let Ok(egui_context) = world.query::<&mut EguiContext>().single(world) else {
return;
};
let mut egui_context = egui_context.clone();
let ctx = egui_context.get_mut();
let rect = ctx.screen_rect().with_min_x(250.0);
egui::Window::new("Visuals")
.constrain_to(rect)
.show(ctx, |ui| {
ui.collapsing("Ambient Light", |ui| {
bevy_inspector::ui_for_resource::<AmbientLight>(world, ui);
});
let info = world.resource::<HexInfo>();
let mat = materials.get_mut(&info.material_handle).unwrap();
ui.collapsing("Material", |ui| {
ui.horizontal(|ui| {
ui.label("Base Color");
bevy_inspector::ui_for_value(&mut mat.base_color, ui, world);
});
ui.horizontal(|ui| {
ui.label("Double sided");
bevy_inspector::ui_for_value(&mut mat.double_sided, ui, world);
});
match &mut mat.cull_mode {
Some(_) => {
if ui.button("No Culling").clicked() {
mat.cull_mode = None
}
}
None => {
if ui.button("Cull back faces").clicked() {
mat.cull_mode = Some(bevy::render::render_resource::Face::Back);
}
}
}
});
});
});
if regenerate {
world.run_system_once(generate).unwrap();
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
let texture = asset_server.load("uv_checker.png");
let transform = Transform::from_xyz(10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y);
commands.spawn((Camera3d::default(), transform));
let transform = Transform::from_xyz(20.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y);
commands.spawn((DirectionalLight::default(), transform));
let layout = HexLayout::default();
let mesh = HeightMapMeshBuilder::new(&layout, &HashMap::new()).build();
let mesh_handle = meshes.add(compute_mesh(mesh));
let material_handle = materials.add(StandardMaterial {
base_color_texture: Some(texture),
cull_mode: None,
double_sided: true,
..default()
});
let mesh_entity = commands
.spawn((
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(material_handle.clone()),
))
.id();
commands.insert_resource(HexInfo {
layout,
mesh_entity,
mesh_handle,
material_handle,
});
}
fn animate(
info: Res<HexInfo>,
mut transforms: Query<&mut Transform>,
mut motion_evr: MessageReader<MouseMotion>,
buttons: Res<ButtonInput<MouseButton>>,
time: Res<Time>,
) {
for event in motion_evr.read() {
if buttons.pressed(MouseButton::Left) {
let mut transform = transforms.get_mut(info.mesh_entity).unwrap();
let axis = Vec3::new(event.delta.y, event.delta.x, 0.0).normalize();
let angle = event.delta.length() * time.delta_secs();
let quaternion = Quat::from_axis_angle(axis, angle);
transform.rotate(quaternion);
}
}
}
fn gizmos(
mut draw: Gizmos,
info: Res<HexInfo>,
transforms: Query<&Transform>,
params: Res<BuilderParams>,
) {
let transform = transforms.get(info.mesh_entity).unwrap();
let mut transform = *transform;
transform.scale.y += params.max_height / 2.0;
transform.scale.x += info.layout.scale.x;
transform.scale.z += info.layout.scale.y;
transform.scale *= params.scale;
draw.axes(transform, 1.0);
}
fn generate(params: Res<BuilderParams>, info: Res<HexInfo>, mut meshes: ResMut<Assets<Mesh>>) {
let mut rng = rng();
let map = HexagonalMap::new(Hex::ZERO, params.range, |_| {
rng.random_range(0.0..=params.max_height)
});
let mut new_mesh = HeightMapMeshBuilder::new(&info.layout, &map)
.with_offset(Vec3::NEG_Y * params.max_height / 2.0 * params.scale.y)
.with_scale(params.scale)
.with_height_range(0.0..=params.max_height)
.without_top_face()
.without_sides();
if let Some(opts) = params.caps_options {
new_mesh = new_mesh.with_cap_options(opts);
}
if let Some(opts) = params.sides_options {
new_mesh = new_mesh.with_side_options(opts);
}
if params.fill_holes {
new_mesh = new_mesh.with_default_height(0.0);
}
let new_mesh = compute_mesh(new_mesh.build());
let mesh = meshes.get_mut(&info.mesh_handle).unwrap();
*mesh = new_mesh;
}
fn compute_mesh(mesh_info: MeshInfo) -> Mesh {
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, mesh_info.vertices)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, mesh_info.normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, mesh_info.uvs)
.with_inserted_indices(Indices::U16(mesh_info.indices))
}
impl Default for BuilderParams {
fn default() -> Self {
Self {
max_height: 3.0,
range: 3,
sides_options: Some(FaceOptions::new()),
caps_options: Some(FaceOptions::new()),
scale: Vec3::ONE,
fill_holes: false,
}
}
}